HandlerSocket调研
概述
背景
在前MySQL核心开发人员Yoshinori Matsunobu宣传handlerSocket之后,最近这个插件颇受关注 。它的核心思想很简单,在profiling后发现对于简单的主键查询,SQL层的消耗很大。在数据集较小,能够在内存中存放的情况下(此时随机的Read IO可以忽略),SQL层就成了最大的瓶颈。为方便不能爬墙的同学,RT一下原文给出的剖分结果:
samples % app name symbol name 259130 4.5199 mysqld MYSQLparse(void*) 196841 3.4334 mysqld my_pthread_fastmutex_lock 106439 1.8566 libc-2.5.so _int_malloc 94583 1.6498 bnx2 /bnx2 84550 1.4748 ha_innodb_plugin.so.0.0.0 ut_delay 67945 1.1851 mysqld _ZL20make_join_statistics P4JOINP10TABLE_LISTP4ItemP16st_dynamic_array 63435 1.1065 mysqld JOIN::optimize() 55825 0.9737 vmlinux wakeup_stack_begin 55054 0.9603 mysqld MYSQLlex(void*, void*) 50833 0.8867 libpthread-2.5.so pthread_mutex_trylock 49602 0.8652 ha_innodb_plugin.so.0.0.0 row_search_for_mysql 47518 0.8288 libc-2.5.so memcpy 46957 0.8190 vmlinux .text.elf_core_dump 46499 0.8111 libc-2.5.so malloc
可以看出,简单的SQL查询方式下,有很大比例的时间消耗在SQL解析、Query Plan、表锁等SQL层上。因此,如果跳过SQL层,直接与存储引擎进行交互,就可以获取很大程度的性能提升。
基于这一思想,他们的团队开发了HandlerSocket。测试结果显示,单机查询性能能够到达75W+(100w条数据,做纯内存主键查询),这个数字意味着已经超过了现在绝大多数KV存储系统、甚至缓存系统的性能。
MySQL的Vadim觉得这玩艺儿挺靠谱(enjoyed),也对它进行了测试,并在MySQL Performance Blog上给出了测试的结果,这个测试关注了数据量大到需要换入换出时handlersocket的性能表现。结论与预期的相符:当数据在内存能装下时,性能稳定在60W+ rps的水平,但当数据大到一定级别时,性能开始下降。此时主要的瓶颈就在于IO,像FusionIO这样强悍的硬件,还可以支撑到40W+,而普通的RAID10就已经惨不忍睹。 也就是说数据量大拼的就是硬件IO性能,此时HandlerSocket在SQL层节省的CPU消耗,在巨大的IO成本前不值一提。
插件结构
再RT一下Yoshinori给出的结构图:

图一:HandlerSocket结构 (来源于slidershare的PPT)

图二: HandlerSocket结构 (来源于原文)
这两张图大同小异,但主旨都是在正常的SQL解析层外,HandlerSocket为我们开了一条后门,直接通过MySQL的HandlerInterface与存储引擎打交道。第二张结构图更详细,可明显看出HandlerSocket要做的事情比正常的SQL少很多。
特性
在原文作者列出了HandlerSocket的一些特性,整理了一下相对重要的,再加上自己的一些粗浅的理解:
- 高效简洁的网络框架
- 在Linux系统上采用epoll的方式驱动,否则采用poll的方式。
- 单客户端连接的成本很低,PHP等可以方便的使用短连接访问,而不用再担心并发连接数问题(ps. fcicq怀疑新浪微博就是这么杯具的)。
- 高性能。除了直接访问引擎接口外,HS还做了一些工作用来提升性能
- 很简洁的网络接口,协议包的冗余数据很少,减少网络带宽占用。
- 自动Group客户端请求
- 用过MySQL的同学应该都知道批量提交/批量查询的技巧,用来提升性能
- HandlerSocket往前走了一步,在Server层面为我们做类似的优化,自动Group尽可能多的请求,一次性提交
- 开放了一个“只能”进行读操作的端口(图二中的9998)
- 两个端口被称之为ReadPort和WritePort,但不要被名字迷惑。虽然Read端口只能读,但Write Port一样可以进行Read操作
- 读请求虽然也是个事务,但纯读可以节省很多成本,比如Transaction Commit,以最大限度提高性能
- 在稍后的测试中,我们将对这两个端口的读性能进行测试
- HandlerSocket支持多种请求
- 支持主键查找、列索引查找、范围查询、LIMIT
- 支持INSERT (注意:无法返回生成的key)
- 支持UPDATE
- 支持DELETE
- 仍然支持SQL查询 ,对于复杂查询,仍然可以走普通的SQL接口进行访问
- 数据由相对成熟的数据库引擎(InnoDB)管理,崩溃安全性良好,也可以快速恢复
- 不需要重新编译MySQL
- 支持Row-Based的主从同步
- 由于它跳过了SQL层,所以只能选择存储引擎的行级数据同步,而不能做Statement级别的同步(行级同步在mysql5.1版本引入的,这就是HandlerSocket要求mysql版本5.1+的原因
) - binlog_format是session相关的变量,HandlerSocket会调用Interface,设置Row-Based同步。
- 由于它跳过了SQL层,所以只能选择存储引擎的行级数据同步,而不能做Statement级别的同步(行级同步在mysql5.1版本引入的,这就是HandlerSocket要求mysql版本5.1+的原因
- 运维方面简单,现有大量的MySQL运维工具和经验可以直接使用
源码分析
作为一个较新的开源项目,HandlerSocket的文档比较薄弱。幸好它的代码还是很简单的,有什么疑问翻一下代码基本都能解决。这里就不展开很细致的代码分析,主要分析一下代码层面重要的几个点。
插件实现
图一告诉我们,HandlerSocket和SQL Layer在同一层,但实际上这个地方有点小trick。它以daemon plugin的形式的,在这个意义上说,它和InnoDB/MyISAM等引擎插件在同一层;但在daemon_handlersocket_init里,就自己listen端口、起worker线程、接收请求、直接与存储引擎交互。
没有插件开发经验的同学,理解这个trick可能会稍有些疑惑:它是如何被调度的?它又是如何直接访问其他存储引擎的?
- 插件引擎都会有一套接口规范,具体实现的插件都必须遵守这个接口规范,以函数指针或者类对象继承方式由插件引擎调用。而接口规范一般都有init接口用于插件初始化。HandlerSocket就利用了这一特性,Init时开了一个后门( 代码文件:handlersocket.cpp )
- MySQL有一层Handler层(图1中的HandlerInterface),它直接与各个存储引擎交互,并负责XA事务的两阶段提交,HandlerSocket调用的就是HandlerInterface的ha_update_row、ha_delete_row、ha_write_row或index_read_map等接口进行CRUD操作。这也是它名字的由来
(代码文件 database.cpp)
工作流程
worker thread的流程清晰明了,总体流程如下:

事务模型
工作流程的图示中可以看出,在一次epoll_wait返回的请求,将一并commit,这也是HandlerSocket的基本事务模型:
- 写线程以一个epoll_wait收到的“所有”读写请求作为一个事务
- 事务隔离级别也没有特殊之处,各存储引擎按照配置进行
- 由HandlerInterface管理XA事务,对事务表和非事务表的提交,与正常SQL处理也相同
- 锁冲突也是由各存储引擎处理,MySQL用行锁,InnoDB用行级锁。这里需要注意:如果在3306端口进行了Lock,HandlerSocket一样会阻塞等待。
- 也不会影响到MVCC
协议
HandlerSocket使用了自定义协议进行交互。具体协议有文档说明,参考源码目录docs-en/protocol.en.txt。协议这里就不详细展开,只提一下基本语法:
- 一个命令一行,采用\n分隔,行内每项数据用\t分隔
- 由于\t\n在协议中有特殊含义,如果数据含有\t、\n,就需要进行转义(转义规则设计的有点奇怪,有哪位同学知晓设计思想,欢迎赐教)
- [0x10-0xff]不转义
- [0x00-0x0f]表示为两字节: [0x01] [0x40+value]
- NULL用\0表示,以区别长度为0的字符串
调研测试
侧重点
- 插入性能
- HandlerSocket与SQL性能对比
- Group提交对时延的影响
- 读取性能
- HandlerSocket与SQL性能对比
- Read Port与Write Port性能对比
- 主从同步
测试环境
- 硬件
两台DELL PowerEdge 2950,4核Intel Xeon 5510 @2.66G, 16G内存
- 软件
Red Hat Enterprise Linux AS release 4 (Nahant Update 3)
mysql 5.1.53 Linux-generic-source
HandlerSocket a485973
- 软件配置
MySQL:
innodb_buffer_pool_size = 8G innodb_flush_log_at_trx_commit = 2 innodb_thread_concurrency = 16 innodb_log_buffer_size = 8M innodb_log_file_size = 256M innodb_max_dirty_pages_pct = 90
HandlerSocket:
loose_handlersocket_port = 9998 loose_handlersocket_port_wr = 9999 loose_handlersocket_threads = 4 loose_handlersocket_threads_wr = 1 open_files_limit = 65535
- 表格式
CREATE TABLE user ( user_id INT UNSIGNED PRIMARY KEY, user_name VARCHAR(50), user_email VARCHAR(255), created DATETIME ) ENGINE=InnoDB;
- 采用tcprstat测量响应时间
测试
写入性能
SQL
seq 1000000 | sed 's/\(.*\)/INSERT INTO user set user_id=\1, user_name=\1, user_email=\1;/' > handlersocket.sql time mysql -D test < handlersocket.sql

HandlerSocket
扩展hstest程序,增加测试用例,插入与SQL相同数据。


读性能
SQL
关闭qcache mysqlslap --query="select user_name from user where user_id=1" --number-of-queries=10000000 --concurrency=30 --host=HOST --port=3306


HandlerSocket Read Port
./hstest test=11 tablesize=1000000 host=10.26.53.34 hsport=9998 num=10000000 num_threads=100 timelimit=10


HandlerSocket WritePort
在这里,我们需要修改loose_handlersocket_threads_wr,将WritePort的工作线程数为4,保持与ReadPort一致,之后再运行hstest。


结论
简单整理分析一下:

- 写性能约为SQL的3.74倍
- 读性能约为SQL的3.83倍,达到20w左右。测试结果离官方宣称的75w+还有一定距离。应该是测试环境的问题:
- 硬件
- mysql版本使用linux-generic版本,未进行configue优化。这个原因可能性较大,因为profile发现锁开销很大,纯读不应该出现这种情况。另外Yoshinori的测试profile结果也显示他们版本的瓶颈在网络层。
- Group提交方式,造成了一定程度的服务时延,平均时延较SQL方式大了1倍左右
总体来看,HandlerSocket有着很不错的性能表现。在以下case应该有不错的应用前景:
- 缓存系统:性能已经接近甚至超过了memcache,还支持固化、崩溃恢复;
- 内存数据库:handlersocket直接用存储引擎做后端,当后端使用InnoDB时,可以理解成一个B树组织、支持Adaptive Hash的内存数据库。虽然几十万级别的数字,对于内存数据库来说,可能还有挖掘潜力,但毕竟这些存储引擎久经考验,数据安全性值得依赖,而且还是免费的。在小数据量高性能存储的场景,HandlerSocket是一个不错的代替方案。
过来看看.
多一个位是吧. 把 + 换成 and 或者 xor 都能用. 多好. 这都是编译器的事情… 跑题…
转义的问题, 如果说把 ([\x00-\x0f]) 替换成 \x01\1 的话, 那替换后的 \x01\x01\x01 之前应该是什么呢? 没有非法串的判定是不行的. special_char_escape_shift 可以调整成大于 special_char_noescape_min (但小于 255 – special_char_noescape_min ) 的数值, 设置成 \x40 也是为了调试方便, \x40 到 \x4f 都属于可见字符, \x30 也挺好的…
读 port 和写 port 的问题, 注意这里的隔离级别相当于 read committed (应该是这样).
不过 innodb 可挖掘的空间还很大, 但 b-tree 再怎么优化也还是逃不脱 IO 复杂度的宿命…
说多了, 睡去了
了解这个转义规则的运作,主要的疑惑在:
1. 为什么是\x0-\xF?\t或者\n在这个范围内?
2. 用telnet这样的工具,一样不能输入\x01,这样的case下,用”\”转义是否更适合?
ps1. 有什么结构能够比B+树更适合于hdd/ssd这样按块存储的设备?如果硬件不变,基于disk的数据存储依然是B树的天下,不知道我的理解对否?
ps2. 事务隔离级别,看代码没有特殊的设定,我理解应该是根据各引擎按照mysql的配置自行处理的
ps3. fcicq大牛过来真是让这小blog蓬荜生辉!
\n 0x0a
\r 0x0d
\t 0×09 —这个应该自己…
echo | awk ‘{ printf( “\n\t\r”);}’ | hexdump
0000000 0a 09 0d
本来就不是让你这么用的. 懒到不会用工具了. 随便一个 hex editor + netcat 不就完了, 还 telnet.
事务隔离级别虽然没有特殊设定, 正是因为这个才得到这个结论. 没有 commit 的这一组数据读不出来, 当然是 read committed. 这里就不像 memcached 的延迟那么小了. 反过来说, 如果用 myisam 的话, 写的时候会把读也阻塞掉, 当然也是 read committed, 虽然 myisam 根本没事务这说.
(bigtable 系) sstable 就没有随机写. 但对整体的要求是挺高的. 谁能把随机写干掉就要谁. 随机读的 io 开销降不下去. nosql 啊, 拼的就是索引技术啊, 事务啥的都靠边了. 先把索引弄好, 再把事务加回来.
@fcicq
呃,我没说清楚。我的本意是:仅仅是因为\r\n在[0x00-0x0F]的范围内,就选了这个作为special_char_noescape_min?为什么不是[0x00-0x1F]或者[0x09-0x0d]? 感觉有点钻牛角尖了….囧
如果在my.cnf中将隔离级别设置成read uncommited呢?在write port中未commit的事务更改一样也可以被读到。所以我理解它就是:mysql的事务隔离级别 + 引擎处理(myisam不支持事务的引擎不关心这个) + 组提交。说是read commited可能不太准确。
SSTable系列在特定场景下是很好的solution,但也有它的局限性,比如range-scan,多个mempatch的merge-dump问题、数据删除问题等。不过总体来看,SSTable系的服务是一个很好的方向,bigtable/cassandra等都基于这个思路实现,我们公司也有类似的产品。memory is disk这个还是大趋势。
行啊. \x00-\x1f 真的也行.
效率下降概不负责.
隔离级别: 这叫自己没事找事降级… 这个降级不是 HS 引起的, 是你干的… 但把 repeatable read 降到 read committed 是 HS 干的. 这里的级别是相对于 read port 来说的. 注意 read port 里面没有事务. 事务读本身也是事务. 直接操作存储引擎这种事情啊… 太乱了. 算了, 这种事情不能说, 一说就要绕进去…
说的是索引技术. 这些局限性… 你全往分布式方向上考虑就没法比了, 你拿 mysql 集群做 range-scan 试试? 偶说单机版 bigtable 就爱怎么事务怎么事务. 这个没错吧? 偶没感觉删除问题有多难, 只是有些人老是想着把删除标记也扔掉.
memory is disk 不假, 但并不是说 tape 就不能用了
memory & SSD 帮你冲 IOPS. 主存用 HDD 完全没问题.
@fcicq
嗯,fcicq大牛的这些话真是举重若轻,吸收ing:) 已经fo了你的twitter,多探讨:)
Hi, 最近我们有测试HandlerSocket的计划,能否加MSN或者QQ探讨一下?
MSN:realzyy@gmail.com
QQ :79251352
想问下用的代码高亮插件是哪个啊?hoho
Easy Google Syntax Highlighter
请问一下,文中,x轴,y轴性能显示,使用的什么软件?
用gnuplot画的
兄弟你这个测试脚本是啥,我用php测的,性能比你测的差很多