MySQL: 初试Memcached Plugin

Memcached Plugin对我而言是一个基本没触摸过的领域,因此本文会以一个“小白”的视角,开始一步步学习,从如何使用和源码实现的角度进行阐述。

关于Memcached的好处就不多说了,跳过语法解析和优化器,直达Innodb层,支持k-v操作,也就是所谓的NOSQL。同时你依然可以通过SQL来操作Innodb表。

官方文档列出的一大堆优点,见:http://dev.mysql.com/doc/refman/5.7/en/innodb-memcached-benefits.html

注意本文是我学习过程的记录,因此可能阅读起来比较不太通顺,但尝鲜的朋友可以根据前面几节step by step玩一下memcached。由于我之前完全没接触过memcached,所以相关的主要命令也一个个尝试了,并总结在本文中。

1.配置Memcached Plugin

a.编译

cmake参数需要加上: -DWITH_INNODB_MEMCACHED=ON,会产生两个so文件:

libmemcached.so: MySQL层,daemon plugin,用于接受用户请求

innodb_engine.so:Innodb的API插件

 

b.安装配置表

mysql -S $socket -uroot < $INSTALL_DIR/share/innodb_memcached_config.sql

这会创建一个名为innodb_memcache的库并建立如下描述的几张配置表:

cache_policies表:

Field Type Desc
policy_name(PK) varchar(40)
get_policy enum(‘innodb_only’,’cache_only’,’caching’,’disabled’)
  • Innodb_only:通过innodb获取数据,修改落Innodb;
  • Cache_only:通过memcached缓存和操作数据,不落盘,和传统memcached类似;
  • Caching:先查memcached,如果没有对应的Key,再查询innodb,可能GET拿到过期数据
set_policy enum(‘innodb_only’,’cache_only’,’caching’,’disabled’) 同上
delete_policy enum(‘innodb_only’,’cache_only’,’caching’,’disabled’) 同上
flush_policy enum(‘innodb_only’,’cache_only’,’caching’,’disabled’) 同上

 

通常我们全部使用Innodb_only来配置GET/SET/DELETE/FLUSH操作

config_options表:

Field Type Desc
Name(PK) varchar(50) 配置项名,包括Separator: 用于分割字符串table_map_delimiter:用于分割schema名和表名,例如:@@t1.some_key
value varchar(50)

 

containers表:用于定义每个表的key-value映射的表信息

Field Type Desc
Name(PK) varchar(50)
db_schema varchar(250) 库名
db_table varchar(250) 表名
key_columns varchar(250) 作为key的列名,必须是非空的CHAR 或 VARCHAR;最多不超过250个字符
value_columns varchar(250) 作为value的列名,可以选择多个列,分隔符隔开,例如col1|col2|col3分割符的选择由表config_options定义VALUE必须映射到CHAR、VARCHAR、或者BLOB列,没有长度限制
flags varchar(250) 选择一个作为flag的列,是为诸如incr, prepend这样的操作提供的区分标记;例如对于Key->(v1,v2,v3)这样的映射关系,如果你只想在一个列上做递增操作,就可以通过flags列来标识选择操作哪个列映射到的列至少为4个字节的整数
cas_column varchar(250) 用于存储memcached 的cas操作(compare and swap)映射的列必须至少为64bit的整数BIGINT该列也会标识value发生的变化
expire_time_column varchar(250) 用于存储失效时间值的列映射到至少4字节的整数
unique_idx_name_on_key varchar(250) 作为key的列对应索引名,

 

c.安装插件

root@(none) 11:59:18>install plugin daemon_memcached soname “libmemcached.so”;

Query OK, 0 rows affected (0.00 sec)

d.配置端口

在配置文件中指定,或者启动mysqld时启动,注意这是只读参数,例如

daemon_memcached_option=’-p13407′

然后重启。

其他几个相关参数默认即可,例如:

daemon_memcached_engine_lib_name: 默认值为innodb_engine.so

daemon_memcached_engine_lib_path:默认为NULL,表示PLUGIN目录,我们保持为默认值即可;

 e.验证安装是否正常

$telnet 127.0.0.1  13407

Trying 127.0.0.1…

Connected to 127.0.0.1.

Escape character is ‘^]’.

set a11 10 0 9

123456789

STORED

get a11

VALUE a11 10 9

123456789

END

quit

Connection closed by foreign host

 

2.简单使用

例如,我们创建一个简单的表:

root@innodb_memcache 05:24:50>show create table test.t1\G

*************************** 1. row ***************************

Table: t1

Create Table: CREATE TABLE `t1` (

`pk` varchar(20) NOT NULL,

`val1` int(11) DEFAULT NULL,

`val2` int(11) DEFAULT NULL,

`c3` bigint(20) DEFAULT NULL,

`c4` bigint(20) DEFAULT NULL,

`c5` bigint(20) DEFAULT NULL,

PRIMARY KEY (`pk`)

) ENGINE=InnoDB DEFAULT CHARSET=utf8

1 row in set (0.00 sec)

 

root@test 05:36:02>insert into t1(pk, val1, val2) values (‘pk1’, 1, 2),(‘pk2’, 2, 3),(‘pk3’, 3, 4);

Query OK, 3 rows affected (0.00 sec)

 

向container中插入一条记录:

root@innodb_memcache 05:25:50>select * from containers where name like ‘tt1’;

+——+———–+———-+————-+—————+——-+————+——————–+————————+

| name | db_schema | db_table | key_columns | value_columns | flags | cas_column | expire_time_column | unique_idx_name_on_key |

+——+———–+———-+————-+—————+——-+————+——————–+————————+

| tt1  | test      | t1       | pk          | val1|val2     | c3    | c4         | c5                 | PRIMARY                |

+——+———–+———-+————-+—————+——-+————+——————–+————————+

 

Telnet简单操作:

telnet 127.0.0.1  13407

几种操作类型:

操作名 描述
get 根据key值读取数据或者设置操作的表设置当前操作的表为test.t1get @@tt1VALUE @@tt1 0 7

test/t1

END

 

查询数据

get pk1

VALUE pk1 0 3

1|2

END

get pk2

VALUE pk2 0 3

2|3

END

get pk1 pk2

We temporarily don’t support multiple get option.  //不支持一次查询多个键值

 

gets 与get类似,但会多返回一个数字,以标识数据是否发生了变化,例如:Session 1:gets pk3VALUE pk3 0 3 0

3|4

END

 

Session 2:

set pk3 0 0 3

4|5

STORED

 

Seesion1:

gets pk3

VALUE pk3 0 3 12

4|5

END

 

这个值来自于表的cas_column的值,通过SQL我们也可以看到c4被更新为12

select * from t1 where pk = ‘pk3’;

+—–+——+——+——+——+——+

| pk  | val1 | val2 | c3   | c4   | c5   |

+—–+——+——+——+——+——+

| pk3 |    4 |    5 |    0 |   12 |    0 |

+—–+——+——+——+——+——+

1 row in set (0.00 sec)

set 写入或更新数据, key值存在表示更新数据,不存在则插入telnet的命令格式为:set  <key> <flags> <exptime> <bytes><bytes长度的字符串>

 

set pk4 0 0 4

9|10

STORED           //插入数据

get pk4

VALUE pk4 0 4

9|10

END

set pk4 0 0 5

12|13

STORED          //更新数据

get pk4

VALUE pk4 0 5

12|13

 

replace 只有在数据存在时进行更新命令格式与上述类似
add 只有在数据不存在时进行插入命令格式与set类似,如下例:add pk4 0 0 48|10

NOT_STORED      // pk4已经存在了

add pk5 0 0 5

10|11

STORED           //成功插入pk5

 

delete 删除操作,根据key值删除,如下例:get pk4VALUE pk4 0 512|13

END

delete pk4

DELETED

get pk4

END

incr 对整数进行递增操作,例如:
decr 对整数进行递减操作,与incr类似
flush_all 删除表上所有记录,相当于truncate table
append 在现有的缓存数据后添加数据
prepend 在现有的缓存数据前添加数据
cas 需要和gets配合使用,只有当和gets返回的最后一个数字匹配时,才进行操作存储,也就是所谓compare and swap,例如:gets pk3VALUE pk3 0 3 124|5

END

cas pk3 0 0 5 11     //当前为12,指定11则更新失败

1|111

EXISTS      //失败

cas pk3 0 0 5 12

1|111

STORED         //成功

gets pk3

VALUE pk3 0 5 13      //cas值从12更新成13

1|111

END

stats 查看当前的memcached状态信息

上述操作遵循已有的memcached协议。在源代码目录下plugin/innodb_memcached/daemon_memcached/doc有相关的协议文档

3.daemon plugin

MySQL以插件的形式在memcached外面加了一层封装,这样就可以通过已有的PLUGIN机制,将memcached动态的加载到MySQL进程空间中。

相关代码定义在文件目录plugin/innodb_memcached下:

memcached_mysql.cc文件声明插件的定义,daemon plugin顾名思义,就是以起一个守护线程的方式提供服务,插件初始化入口函数为daemon_memcached_plugin_init

 a. 初始化阶段

首先初始化配置项,存储到mysql_memcached_context:: memcached_conf中,包括以下几个配置项:

daemon_memcached_enable_binlog
daemon_memcached_engine_lib_name 默认为innodb_engine.so
daemon_memcached_engine_lib_path innodb_engine.so的位置
daemon_memcached_option 字符串指定,例如端口号等,另外还包含大量的选项,对应存储到结构体settings中,如下:”a:”  /* access mask for unix socket */”p:”  /* TCP port number to listen on */”s:”  /* unix socket path to listen on */

“U:”  /* UDP port number to listen on */

“m:”  /* max memory to use for items in megabytes */

“M”   /* return error on memory exhausted */

“c:”  /* max simultaneous connections */

“k”   /* lock down all paged memory */

“hi”  /* help, licence info */

“r”   /* maximize core file limit */

“v”   /* verbose */

“d”   /* daemon mode */

“l:”  /* interface to listen on */

“u:”  /* user identity to run as */

“P:”  /* save PID in file */

“f:”  /* factor? */

“n:”  /* minimum space allocated for key+value+flags */

“t:”  /* threads */

“D:”  /* prefix delimiter? */

“L”   /* Large memory pages */

“R:”  /* max requests per event */

“C”   /* Disable use of CAS */

“b:”  /* backlog queue limit */

“B:”  /* Binding protocol */

“I:”  /* Max item size */

“S”   /* Sasl ON */

“E:”  /* Engine to load */

“e:”  /* Engine options */

“q”   /* Disallow detailed stats */

“X:”  /* Load extension */

daemon_memcached_r_batch_size 控制每读N次commit一次
daemon_memcached_w_batch_size 控制每写N次commit一次

注意这几个参数都是只读参数,因为是在创建线程之前初始化的。初始化完配置后,随后创建线程,进入函数daemon_memcached_main流程如下:

memcache_main

从上述流程,我们可以看到,除了daemon线程,innodb memcached engine也会创建一个独立的后台线程,来为开启的事务定期提交。

当完成一系列初始化后,daemon线程进入函数event_base_loop开始监听请求。

从代码结构来看,memcached定义了比较完善的接口,因此理论上开发者可以针对接口进行开发,就可以实现不同引擎的memcached。

 

b.处理用户请求

当接受到新请求时,会进入:

event_base_loop

|–> event_process_active

.      |–>event_handler

.             | –>conn_parse_cmd

.                    |–>try_read_command|

.                           –>process_command

在process_command函数中,根据不同的操作类型,调用相应的函数:

get, bget , gets process_get_command
Add, set, replace, prepend, append, cas process_update_command
delete process_delete_command
Incr, decr process_arithmetic_command
bind process_bind_command
stats process_stat
flush_all settings.engine.v1->flush
Version out_string(c, “VERSION ” VERSION)
quit conn_set_state(c, conn_closing);
verbosity process_verbosity_command

 

所有的命令都是根据字符串匹配来做选择的。

这里我们稍微说明下常见操作的相关堆栈。

b1)GET操作

process_get_command

|–> innodb_get

在函数innodb_get中:

(1)如果配置了可以从memcached的default engine中读取数据,则先从其中读,如果没有,再从innodb层读。根据cache策略配置表来决定:

typedef enum meta_cache_opt {

META_CACHE_OPT_INNODB = 1,      /*!< Use InnoDB Memcached Engine only */

META_CACHE_OPT_DEFAULT,         /*!< Use Default Memcached Engine

only */

META_CACHE_OPT_MIX,             /*!< Use both, first use default

memcached engine */

META_CACHE_OPT_DISABLE,         /*!< This operation is disabled */

META_CACHE_NUM_OPT              /*!< Number of options */

} meta_cache_opt_t;

 

对我们而言,最常用的当然是innodb_only了,这里我们不讨论memcached本身的缓存策略,只考虑innodb。

(2)检查是否需要重新map表,例如当前session修改了操作的表

err_ret = check_key_name_for_map_switch(handle, cookie, key, &key_len);

(3)确定加锁模式:

        lock_mode = (innodb_eng->trx_level == IB_TRX_SERIALIZABLE

&& innodb_eng->read_batch_size == 1)

? IB_LOCK_S

: IB_LOCK_NONE;

(4)初始化session cursor及事务

        conn_data = innodb_conn_init(innodb_eng, cookie, CONN_MODE_READ,

lock_mode, false, NULL);

memcached 使用connection cookie来标识一个连接,会话的信息被维护在innodb_conn_data_t中,感觉有点类似mysql的THD。

 

在innodb_conn_init中,会做事务开启,或者打开cursor之类的操作,相当于在真正查询/DML操作之前的准备工作。

这里不得不提的一个问题是,在早期版本的memcached中,每次操作都需要分配事务对象,这会严重影响到性能。而MySQL本身是可以重用事务对象的。在比较新的版本中,这个bug被fix掉了,Memcached也可以重用事务对象,从而大大提升了性能。

 

事务开启后,打开表,并设置cursor,对应函数innodb_api_begin

 

(5) 查询记录

        err = innodb_api_search(conn_data, &crsr, key + nkey – key_len,

key_len, result, NULL, true);

 

对应堆栈:

innodb_get

|–> innodb_api_search–> ib_cursor_moveto –> row_search_for_mysql

……

b2)SET操作

对于SET操作由于需要交互两次,因此处理逻辑的调用栈为:

(1)执行(例如set pk1 0 0 5)

调用process_command–> process_update_command

(2)输入value值

检索记录堆栈:

event_handler–> conn_nread–> complete_nread–> complete_nread_ascii–> complete_update_ascii

|–> innodb_store

.             |–> innodb_api_store

.             .      |–> innodb_api_search       //检索记录

.             .      .      |–> row_search_for_mysql

.             .      |–> innodb_api_update        //更新记录

.             .      .      |–> ib_cursor_update_row

.             .      .             |–> ib_execute_update_query_graph

.             .      .                    |–> ib_update_row_with_lock_retry

.             .      .                           |–> row_upd_step

.             |–> innodb_api_cursor_reset

.                    |–> innodb_reset_conn

.                           |–> handler_binlog_commit     //记录binlog

.                                  |–> MYSQL_BIN_LOG::commit

.                                         |–> MYSQL_BIN_LOG::ordered_commit

 

开启了Binlog需要打开如下参数:

daemon_memcached_enable_binlog=1

innodb_api_enable_binlog=1

 

b3)DELETE操作

类似的堆栈如下:

process_delete_command

|–> innodb_remove

.      |–> innodb_api_delete

.      .      |–> innodb_api_search   //根据key检索记录

.      .      |–> ib_cursor_delete_row  //删除记录

.      .             |–> ib_delete_row

.      .                    |–> ib_execute_update_query_graph

.      .                           |–> ib_update_row_with_lock_retry

.      .                                  |–> row_upd_step

.      |–> innodb_api_cursor_reset

.             |–> innodb_reset_conn

.                    |–> handler_binlog_commit  //记录binlog

 

 

c.Memcached 与DDL

MySQL本身使用MDL锁来解决DDL和DML/SELECT的冲突,而Memcached跨过了Server层,如果需要对操作加MDL锁,需要打开选项:innodb_api_enable_mdl (或者开启binlog)

当daemon_memcached_r_batch_size 或者daemon_memcached_w_batch_size 大于1时,SET/GET都会去获取MDL锁;

当daemon_memcached_w_batch_size=1时,并且需要记录binlog时,在commit阶段需要持有意向排他的MDL锁,以处理和GLOBAL READ LOCK的MDL锁逻辑。

 

我们以GET操作为例,对应堆栈为:

process_get_command

|–>innodb_get

.      |–>innodb_conn_init

.             |–>innodb_api_begin

.                    |–>handler_open_table

.                           |–>open_table

 

MySQL 5.7的改进(持续更新)

#从5.7.3版本开始支持key值为整数

#fix bug #70712. 重用事务对象(5.6也fix了)

#通过Memcached发送的查询,作为只读事务,能够利用最新的Innodb事务系统改进

#对memcached本身的代码改进,一个是内存分配的问题,之前总是从memcached的代码中分配内存,存在锁开销,现在修改成使用线程私有内存来存储和发送结果集;另外一个问题是统计信息搜集锁thread_stats->mutex,现改成原子操作

#开始缓存”searched tuple”,这样无需为每个查询分配tuple

 

参考:

1.官方文档:http://dev.mysql.com/doc/refman/5.7/en/innodb-memcached.html

2.MySQL 5.7.5源代码

3.http://mysqlserverteam.com/mysql-5-7-3-deep-dive-into-1mil-qps-with-innodb-memcached/

 

原创文章,转载请注明: 转载自Simple Life

本文链接地址: MySQL: 初试Memcached Plugin

Post Footer automatically generated by wp-posturl plugin for wordpress.


Comments

Leave a Reply

Your email address will not be published. Name and email are required


Current month ye@r day *