Plugin - Seaven/incubator-doris GitHub Wiki

[TOC]

MySQL-Plugin

参考mysql 8.0 代码&文档(https://dev.mysql.com/doc/refman/8.0/en/plugin-api.html)

MySQL提供了一套通用插件API来扩展MySQL功能,支持服务端(MySQL-service)插件和客户端(MySQL-Client)插件,通过插件框架,MySQL可以动态(运行时)加载插件使用。

Plugin类型

目前MySQL 8.0中涉及的插件种类有以下几类:

  • 全文索引插件(Full-Text Parser Plugins)

可以定义不同的分词算法,例如”我的名字叫李明“,在创建全文索引时,默认的分词结果为”我/的/名字/叫、李明“,在使用全文索引的插件后,实现新的分词算法,分词结果为”我的名字/叫/李明“

  • 后台任务插件(Daemon Plugins)

相当于启动一个MySQL的子进程,处理Plugin实现的任务,如:定期一个数据备份,定期做一次表删除

  • INFORMATION_SCHEMA插件(INFORMATION_SCHEMA Plugins)

主要用于扩展MySQL的INFORMATION_SCHEMA库,增加元数据等功能,例如InnoDB利用该插件提供事务表、锁信息

  • 半同步插件(Semisynchronous Replication Plugins)

MySQL数据同步功能默认是异步的,可以通过半同步插件实现事务的半同步

  • 权限插件(Authentication Plugins)

MySQL的权限插件通常有Service端和Client端协同实现,例如:Client端要求用户提供特定的校验信息(证书,秘钥...),完成后发送给Service端做权限校验

  • 审计插件(Audit Plugins)

审计相关功能,例如:记录慢查询SQL到日志,DDL SQL发送消息通知

  • 查询重写插件(Query Rewrite Plugins)

查询重写插件可以在MySQL执行SQL之前,根据规则将传入的SQL重写成目标SQL后执行,例如:将delete user where userid = 1 重写为 update user set disable = 1 where 1 执行

  • 存储引擎插件(Storage Engine Plugins) 》 提供单机innodb,myisam,memory等等引擎都是以plugin方式插入到mysql中的

  • 密码验证插件(Password-Validation Plugins)

主要用于密码相关功能,例如:设置密码时拒绝弱密码或者检查密码强度

  • Protocol-Trace插件(Protocol Trace Plugins)

用于追踪MySQl Client和Service的通信流程

  • 密钥插件(Keyring Plugins)

##Data Struct

plugin.h是MySQL Plugin的基础接口,不同种类的插件实现时除了plugin.h以外,还有部分其他对应的头文件,MySQL中一个plugin的基本定义:

//include/mysql/plugin.h

/*
  Plugin description structure.
*/
struct st_mysql_plugin {
  // MySQL插件类型,上述几类
  int type;           /* the plugin type (a MYSQL_XXX_PLUGIN value)   */
  // plugin核心执行,每种plugin的description不同,可能会包括实际plugin work的方法
  void *info;         /* pointer to type-specific plugin descriptor   */
  
  const char *name;   /* plugin name                                  */
  const char *author; /* plugin author (for I_S.PLUGINS)              */
  const char *descr;  /* general descriptive text (for I_S.PLUGINS)   */
  int license;        /* the plugin license (PLUGIN_LICENSE_XXX)      */
  
  // 框架通用方法,install/加载时调用
  /** Function to invoke when plugin is loaded. */
  int (*init)(MYSQL_PLUGIN);
  
  // 框架通用方法,uninstall时调用
  /** Function to invoke when plugin is uninstalled. */
  int (*check_uninstall)(MYSQL_PLUGIN);
  
  // 框架通用方法,uninstall时,mysql shutdown时调用
  /** Function to invoke when plugin is unloaded. */
  int (*deinit)(MYSQL_PLUGIN);
  
  unsigned int version; /* plugin version (for I_S.PLUGINS)             */
 
  // show status语句时,暴露插件状态/数据用
  SHOW_VAR *status_vars; 
  
  // SHOW VARIABLES语句时,暴露插件状态/数据用,同时也可以通过vars变量控制plugin参数
  SYS_VAR **system_vars;
  
  void *__reserved1;   /* reserved for dependency checking             */
  unsigned long flags; /* flags for plugin */
};

除此外,在sql_plugin_ref.h中有个重要的数据结构,内部使用plugin时,plugin相关的上下文数据(handler....)会记录在这里

/* A handle of a plugin */

struct st_plugin_int {
  LEX_STRING name{nullptr, 0};
  st_mysql_plugin *plugin{nullptr};
  st_plugin_dl *plugin_dl{nullptr};
  uint state{0};
  uint ref_count{0};             /* number of threads using the plugin */
  void *data{nullptr};           /* plugin type specific, e.g. handlerton */
  MEM_ROOT mem_root;             /* memory for dynamic plugin structures */
  sys_var *system_vars{nullptr}; /* server variables for this plugin */
  enum_plugin_load_option load_option{
      PLUGIN_OFF}; /* OFF, ON, FORCE, F+PERMANENT */
};

Demo

实现一个Plugin Demo流程:

  1. plugin代码实现(可以使用MySQL内置的数据结构,访问MySQL表等)
  2. 和对应版本的MySQL的版本编译产出plugin.so
  3. 将plugin.so copy到mysql指定的plugin dir下
  4. 通过install plugin或者启动mysql时通过--plugin-load加载plugin

这里贴一个注册information_schema表的demo

#include "table.h"
#include "sql_class.h"
#include "sql_show.h"

#include "mysql/plugin.h"

#define MYSQL_DYNAMIC_PLUGIN

static ST_FIELD_INFO simple_table_fields[] =
{
    {"NAME", 10, MYSQL_TYPE_STRING, 0, 0, 0, 0},
    {"VALUE", 6, MYSQL_TYPE_LONG, 0, MY_I_S_UNSIGNED, 0, 0},
    {0, 0, MYSQL_TYPE_NULL, 0, 0, 0, 0}
};

static int simple_fill_table(THD* thd, TABLE_LIST* tables, Item* cond)
{
    TABLE* table = tables->table;

    table->field[0]->store("seaven", 6, system_charset_info);
    table->field[1]->store(1);

    if (schema_table_store_record(thd, table)) {
        return 1;
    }

    table->field[0]->store("knight", 6, system_charset_info);
    table->field[1]->store(2);

    if (schema_table_store_record(thd, table)) {
        return 1;
    }

    return 0;
}

static struct st_mysql_information_schema simple_table_info = {
    MYSQL_INFORMATION_SCHEMA_INTERFACE_VERSION
};


static int simple_table_init(void *ptr) {
    ST_SCHEMA_TABLE *schema_table = (ST_SCHEMA_TABLE* )ptr;

    schema_table->fields_info = simple_table_fields;
    schema_table->fill_table = simple_fill_table;

    return 0;
}

mysql_declare_plugin(is_example)
{
    MYSQL_INFORMATION_SCHEMA_PLUGIN,
    &simple_table_info,
    "SIMPLE_I_S_TABLE",
    "Seaven",
    "Simple INFORMATION SCHEMA table",
    PLUGIN_LICENSE_GPL,
    simple_table_init,
    NULL,
    0x1610,
    NULL,
    NULL,
    NULL,
    0
} 
mysql_declare_plugin_end;

Components

Plugin涉及的功能点:

  • SQL Statements:

    • INSTALL PLUGIN:INSTALL PLUGIN plugin_name SONAME 'shared_library_name'
    • UNINSTALL PLUGIN:UNINSTALL PLUGIN plugin_name
    • SHOW PLUGIN
  • Command Line Options & System variables:

    • --plugin-load
    • --plugin-dir
  • Related tables(metadata):

    • INFORMATION_SCHEMA.PLUGINS
    • mysql.plugin

Progress

plugin相关核心操作主要在sql_plugin.h、sql_plugin.cc中实现。 MySQL在全局会维护一个Plugin Map,存储Plugin的实例。

install

INSTALL PLUGIN plugin_name SONAME 'shared_library_name'

Plugin加载的方式有三种:

  • mysqld启动时自动加载(内置plugin,meta中保存的plugin)
  • mysqld启动时通过--plugin_load指定加载
  • 通过install plugin语句加载

前两种加载的方式涉及到的接口如下,具体的实现基本上也是和install plugin的方式一样,都是一样

// sql_plugin.h
extern bool plugin_register_early_plugins(int *argc, char **argv, int flags);
extern bool plugin_register_builtin_and_init_core_se(int *argc, char **argv);
extern bool plugin_register_dynamic_and_init_all(int *argc, char **argv,
                                                 int init_flags);

涉及sql_plugin.cc中plugin加载方法

static bool mysql_install_plugin(THD *thd, const LEX_STRING *name,
                                 const LEX_STRING *dl);
static bool plugin_load_list(MEM_ROOT *tmp_root, int *argc, char **argv,
                             const char *list, bool load_early);;
static void plugin_load(MEM_ROOT *tmp_root, int *argc, char **argv);
static bool plugin_add(MEM_ROOT *tmp_root, const LEX_STRING *name,
                       const LEX_STRING *dl, int *argc, char **argv, int report,
                       bool load_early);

plugin加载流程:

  1. 检查plugin是否已经加载过
  2. dlopen打开so库
  3. 检查plugin ce version
  4. 检查service版本(?没理解)
  5. 查找plugin声明(一个plugin.cc中可以定义多个plugin,参考innodb:ha_innodb.c)
  6. 检查不同类型plugin的版本要求
  7. plugin参数初始化
  8. 调用plugin的init方法初始化
  9. 注册到全局plugin列表
  1. load & - load plugin方法中,plugin list从mysql内置的plugin列表和mysql.plugin表读取
  2. install plugin方法中,完成plugin注册逻辑后,会将plugin信息回写到mysql.plugin
  3. install方法中,会阻止将install plugin写入到binlog中
  4. 对于某些特定种类plugin,初始化方法为MySQL内置(例如,storage engine的plugin初始化方法会将handlerton存储在plugin的上下文中)

uninstall

UNINSTALL PLUGIN plugin_name

  1. 检查plugin状态
  2. 检查plugin引用计数
  3. 从元数据(mysql.plugs、information_schema.plugin)中删除plugin
  4. 调用plugin的deinit方法
  5. 从全局plugin列表中删除

对于某些特定种类plugin,卸载方法为MySQL内置

Usage

这里用innodb作为例子,叙述store engine plugind流程。不同种类的plugin在mysql中使用比较随意,没有固定的接口和调用流程。

  1. SQL解析找到table
  2. 通过table查找frm(元数据),通过frm确定storage engine
  3. 通过storage engine在全局plugin中获取对应的plugin
  4. 将的plugin的data转换为handlerton,后续操作交由具体引擎的handlerton处理
  5. .......

其他

  • UDF
  • MySQL同样支持UDF功能,也是install一个so,但是UDF和Plugin还是有区别,UDF的上下文在SQL执行流程中,主要为了实现自定义逻辑(数据类型转换、聚合)的算子,而Plugin的上下文在MySQL中,主要为了扩展MySQL功能,不会侵入到具体的SQL执行流程中(SQL重写Plugin工作在SQL执行之前)
  • UDF的覆盖范围可能只是某一个DB,Table的,但是Plugin则是覆盖整个MySQL的工作流程
  • 调用

MySQL每次调用plugin时,只会根据plugin name确定&调用一个plugin,不会出现一次调用多个plugin(或者调用一组plugin,或者制作一个plugin链调用)的情况。

ES-Plugin

相比较于MySQL,ES作为分布式系统的方式可能会更适合Doris

ES的Plugin安装后,必须重启每个节点,插件才能生效

Management

// install
sudo bin/elasticsearch-plugin install [plugin_name]
sudo bin/elasticsearch-plugin install file:///path/to/plugin.zip
sudo bin/elasticsearch-plugin install http://some.domain/path/to/plugin.zip

// uninstall
sudo bin/elasticsearch-plugin remove [pluginname]

##Demo

plugin-descriptor.properties

ES的plugin必须包括一个plugin-descriptor.properties,内部描述了plugin的classname,version,jar等信息

# Elasticsearch plugin descriptor file
# This file must exist as 'plugin-descriptor.properties' inside a plugin.
#
### example plugin for "foo"
#
# foo.zip <-- zip file for the plugin, with this structure:
# |____   <arbitrary name1>.jar <-- classes, resources, dependencies
# |____   <arbitrary nameN>.jar <-- any number of jars
# |____   plugin-descriptor.properties <-- example contents below:
#
# classname=foo.bar.BazPlugin
# description=My cool plugin
# version=6.0
# elasticsearch.version=6.0
# java.version=1.8
#
### mandatory elements for all plugins:
#
# 'description': simple summary of the plugin
description=${description}
#
# 'version': plugin's version
version=${version}
#
# 'name': the plugin name
name=${name}
#
# 'classname': the name of the class to load, fully-qualified.
classname=${classname}
#
# 'java.version': version of java the code is built against
# use the system property java.specification.version
# version string must be a sequence of nonnegative decimal integers
# separated by "."'s and may have leading zeros
java.version=${javaVersion}
#
# 'elasticsearch.version': version of elasticsearch compiled against
elasticsearch.version=${elasticsearchVersion}
### optional elements for plugins:
#
#  'extended.plugins': other plugins this plugin extends through SPI
extended.plugins=${extendedPlugins}
#
# 'has.native.controller': whether or not the plugin has a native controller
has.native.controller=${hasNativeController}

interface

一个ES plugin的实现类,必须要继承plugin,针对不同的功能实现不同的接口类:

  • ActionPlugin
  • AnalysisPlugin
  • EnginePlugin
  • .......(具体可以参考ES代码, 种类很多很多)

这里贴一个简单的Plugin定义,在通过GET/POST访问/_cat/example时,plugin将会返回一条“Hello from Cat Example action”,代码可以参考这里

public class ExampleRestHandlerPlugin extends Plugin implements ActionPlugin {

    @Override
    public List<RestHandler> getRestHandlers(final Settings settings,
                                             final RestController restController,
                                             final ClusterSettings clusterSettings,
                                             final IndexScopedSettings indexScopedSettings,
                                             final SettingsFilter settingsFilter,
                                             final IndexNameExpressionResolver indexNameExpressionResolver,
                                             final Supplier<DiscoveryNodes> nodesInCluster) {

        return singletonList(new ExampleCatAction(restController));
    }
}

Components

install

ES并未提供热插拔的功能,所以在ES的install只会校验&安装plugin

  1. 下载plugin zip(如果是http)
  2. 解压zip
  3. 读取zip中的plugin-descriptor.properties,获取plugin信息
  4. plugin信息完整性校验(name,version等)
  5. plugin jar文件校验
  6. 复制plugin的bin、config文件夹到es的plugin文件夹

真正加载plugin的流程,在ES启动时: elasticsearch -> Bootstrap.setup() -> new Node() -> new PluginService() -> classLoad加载plugin

usage

ES在初始化完成PluginService后,会将plugin注册进es不同的模块中,由各个模块自行调用。

例如上述的demo plugin,在PluginService初始化完成后,将Action类别的plugin注册进ActionModule中。最终ActionModule产生Http Dispatcher,在NetworkModule中被调用。

相比较与Mysql,ES的plugin调用逻辑更清晰一点。

汇总

| x | MySQL | ES | |--|--|--|--| |分布式系统|否|是| |语法支持|本地so|本地,文件系统,http| |安装|本地支持|每个node必须自行安装| |插件状态管理|元数据| 没有元数据注册,安装XPackPlugin插件支持| |热插拔|支持|否| |版本管理|版本更新最好重新编译|更新es需要重新安装|

#举个栗子

MySQL: ddl-rewrite Plugin

举个栗子,安装一个MySQL的DDL重写Plugin,Plugin的实现可以从MySQL源码中plugin/ddl_rewriter/下找到。 rewrite Plugin的作用是在MySQL接受create table语句时,会删除ENCRYPTION(InnoDB通过该子句指定页级别的数据加密),DATA DIRECTORY和INDEX DIRECTORY子句(MyISAM可以通过这两个子句指定数据文件和索引文件的存储地址)。

install

  1. 将ddl_rewrite Plugin编译后产出的so文件copy到Mysql的plugin目录下。
  2. 执行SQL安装Plugin:

INSTALL PLUGIN ddl_rewriter SONAME 'ddl_rewriter.so';

3 . 安装完成后可以通过SQL查询Plugin状态:

SELECT PLUGIN_NAME, PLUGIN_STATUS, PLUGIN_TYPE FROM INFORMATION_SCHEMA.PLUGINS WHERE PLUGIN_NAME LIKE '%rewrite%';

可以看到执行结果:

+--------------+---------------+-------------+
| PLUGIN_NAME  | PLUGIN_STATUS | PLUGIN_TYPE |
+--------------+---------------+-------------+
| ddl_rewriter | ACTIVE        | AUDIT       |
+--------------+---------------+-------------+

ACTIVE的状态表示Plugin已经完成安装了,至于PluginType这里是AUDIT,是由于DDL_rewrite是通过MySQL的Audit类Plugin实现的。

usage

rewrite Plugin的作用是在执行create table语句时,会删除ENCRYPTION,DATA DIRECTORY和INDEX DIRECTORY子句。

当执行一条create table SQL:

CREATE TABLE t (i INT) DATA DIRECTORY '/var/mysql/data';

通过MYSQL的SHOW WARNING可以看到ddl_rewrite会将create table的DATA DIRECTORY删除。实际执行的SQL为:

CREATE TABLE t (i INT) ;

mysql> CREATE TABLE t (i INT) DATA DIRECTORY '/var/mysql/data';
Query OK, 0 rows affected, 1 warning (0.03 sec)

mysql> SHOW WARNINGS\G
*************************** 1. row ***************************
  Level: Note
   Code: 1105
Message: Query 'CREATE TABLE t (i INT) DATA DIRECTORY '/var/mysql/data''
         rewritten to 'CREATE TABLE t (i INT) ' by a query rewrite plugin
1 row in set (0.00 sec)