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’) |
|
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流程如下:
从上述流程,我们可以看到,除了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/