HandlerSocket的原理 |
|
HandlerSocket的优势和缺陷阐述 |
|
HandlerSocket的性能测试 |
|
HandlerSocket的原理 |
|
HandlerSocket的应用场景: |
|
MySQL自身的局限性,很多站点都采用了MySQL+Memcached的经典架构,甚至一些网站放弃MySQL而采用NoSQL产品,比如Redis/MongoDB等。不可否认,在做一些简单查询(尤其是PK查询)的时候,很多NoSQL产品比MySQL要快很多,而且前台网站上的80%以上查询都是简洁的查询业务。 |
|
MySQL通过HandlerSocket插件提供了API访问接口,在我们的基准测试中,普通的R510服务器单实例Percona/XtraDB达到了72W+QPS(纯读),如果采用更强劲的CPU增加更多的网卡,理论上可以获得更高的性能。而同等条件下Memcached仅有40W+QPS(纯读),并且在R510上Memcached单实例已经无法提升性能,因为Memcached对内存的一把大锁限制了它的并发能力。 |
|
HandlerSocket原理: |
|
MySQL的架构是“数据库管理”和“数据管理”分离,即MySQL Server+Storage Engine的模式。MySQL Server是直接与Client交互的一层,它负责管理连接线程,解析SQL生成执行计划,管理和实现视图、触发器、存储过程等这些与具体数据操作管理无关的事情,通过调用Handler API让存储引擎去操作具体的数据。Storage Engine通过继承实现Handler API的函数,负责直接与数据交互,数据存取实现(必须实现),事务实现(可选),索引实现(可选),数据缓存实现(可选)。 |
|
(图1-1 MySQL架构) |
|
HandlerSocket是在MySQL的内部组件,以MySQL Daemon Plugin的形式提供类似NoSQL的网络服务,它并不直接处理数据,只是侦听配置好的某个端口方式,接收采用NoSQL/API的通讯协议,然后通过MySQL内部的Handler API来调用存储引擎(例如InnoDB)处理数据。理论上,HanderSocket可以处理各种MySQL存储引擎,但是用MyISAM时,会出现插入的数据查不出来,这个实际上是构造行时第一字节没有初始化为0xff,初始化以后就没有问题,MyISAM也一样可以支持,但是为了更好地利用内存,用HandlerSocket都会搭配InnoDB存储引擎一起使用。 |
|
图1-2描述HandlerSocket具体做了哪些事情: |
|
(图1-2 HandlerSocket原理) |
|
因为HandlerSocket是以MySQL Daemon Plugin形式存在,所以在应用中,可把MySQL当NoSQL使用。它最大的功能是实现了与存储引擎交互,比如InnoDB,而这不需要任何SQL方面的初始化开销。访问MySQL的TABLE时,当然也是需要open/close table的,但是它并不是每次都去open/close table,因为它会将以前访问过的table cache保存下来以重复使用,而opening/closing tables是最耗资源的,而且很容易引起互斥量的争夺,这样一来,对于提高性能非常有效。在流量变小时,HandlerSocket会close tables,所以它一般不会阻塞DDL。 |
|
HandlerSocket与MySQL+Memcached的区别在哪呢?对比图1-2和图1-3,可从中看出其不同点,图1-3展示了典型的MySQL+Memecached的应用架构。因为Memcached的get操作比MySQL的内存中或磁盘上的主键查询要快很多,所以Memcached用于缓存数据库记录。若是HandlerSocket的查询速度和相应时间能与Memcached媲美,我们就可以考虑替换Memcached缓存记录的架构层。 |
|
(图1-3 典型MySQL+Memcached架构) |
|
HandlerSocket的优势和缺陷阐述 |
|
HandlerSocket的优势和特点: |
|
1) 支持多种查询模式 |
|
HandlerSocket目前支持索引查询(主键索引和非主键的普通索引均可),索引范围扫描,LIMIT子句,也即支持增加、删除、修改、查询完整功能,但还不支持无法使用任何索引的操作。另外支持execute_multi() 一次网络传输多个Query请求,节省网络传输时间。 |
|
2) 处理大量并发连接 |
|
HandlerSocket的连接是轻量级的,因为HandlerSocket采用epoll() 和worker-thread/thread-pooling架构,而MySQL内部线程的数量是有限的(可以由my.cnf中的handlersocket_threads/handlersocket_threads_wr参数控制),所以即使建立上千万的网络连接到HandlerSocket,也不会消耗很多内存,它的稳定性不会受到任何影响(消耗太多的内存,会造成巨大的互斥竞争等其他问题,如bug#26590,bug#33948,bug#49169)。 |
|
3) 优秀的性能 |
|
HandlerSocket的性能见文章HandlerSocket的性能测试报告描述,相对于其它NoSQL产品,性能表现一点也不逊色,它不仅没有调用与SQL相关的函数,还优化了网络/并发相关的问题: |
|
(1). 更小的网络数据包:和传统 MySQL 协议相比,HandlerSocket 协议更简短,因此整个网络的流量更小。 |
|
(2). 运行有限的MySQL内部线程数:参考上面的内容。 |
|
(3). 将客户端请求分组:当大量的并发请求到达HandlerSocket时,每个工作线程尽可能多地聚集请求,然后同时执行聚集起来的请求和返回结果。这样,通过牺牲一点响应时间,而大大地提高性能。例如,可以减少fsync()调用的次数,减少复制延迟。 |
|
4) 无重复缓存 |
|
当使用Memcached缓存MySQL/InnoDB记录时,在Memcached和InnoDB Buffer Pool中均缓存了这些记录,因此效率非常低(实际上有两份数据,Memcached本身可能还需要做HA支持),而采用 HandlerSocket插件, 它直接访问 InnoDB 存储引擎,记录缓存在InnoDB Buffer Pool,于是其它SQL语句还可以重复使用缓存的数据。 |
|
5) 无数据不一致的现象 |
|
由于数据只存储在一个地方(InnoDB存储引擎缓存区内),不像使用Memcached时,需要在Memcached和MySQL之间维护数据一致性。 |
|
6) 崩溃安全 |
|
后端存储是InnoDB引擎,支持事务的ACID特性,能确保事务的安全性,即使设置innodb_flush_log_at_trx_commit=2,若数据库服务器崩溃时,也只会丢掉<= 1s的数据。 |
|
7) SQL/NOSQL并存 |
|
在许多情况下,我们仍然希望使用SQL(例如复杂的报表查询),而大多数NoSQL产品都不支持SQL接口,HandlerSocket仅仅是一个 MySQL 插件,我们依然可以通过MySQL客户端发送SQL语句,但当需要高吞吐量和快速响应时,则使用 HandlerSocket。 |
|
8) 继承MySQL的功能 |
|
因为HandlerSocket运行于MySQL,因此所有MySQL的功能依然被支持,例如:SQL、在线备份、复制、HA、监控等等。 |
|
9) 不需要修改/重建MySQL |
|
因为HandlerSocket是一个插件并且开源,所以它支持从任何MySQL源码、甚至是第三方版本(例如Percona)构建,而无需对MySQL做出任何修改。 |
|
10) 独立于存储引擎 |
|
虽然我们只测试了MySQL-EnterpriseInnoDB和Percona XtraDB插件,但HandlerSocket理论上可以和任何存储引擎交互。MyISAM通过简单的修改也是可以被支持的,但是从数据缓存而利用内存的角度看这个意义不大。 |
|
HandlerSocket的缺陷和注意事项 |
|
1) 协议不兼容 |
|
HandlerSocket API与Memcached API并不兼容,尽管它很容易使用,但仍然需要一点学习来学会如何与HandlerSocket交互。不过我们可以通过重载Memecached函数来翻译到HandlerSocket API。 |
|
2) 没有安全功能 |
|
与其它NoSQL数据库类似,HandlerSocket不支持安全功能,HandlerSocket的工作线程以系统用户权限运行,因此应用程序可以通过HandlerSocket协议访问所有的表对象,但是可以通过简单的修改协议,在my.cnf中增加一个配置项为密码,连接时通过这个配置的密码验证,当然也可以通过网络防火墙来过滤数据包。 |
|
3) 对于磁盘IO密集的场景没有优势 |
|
对于IO密集的应用场景,数据库每秒无法执行数千次查询,通常只有1-10%的CPU利用率,在这种情况下,SQL解析不会成为性能瓶颈,因此使用HandlerSocket没有什么优势,应当只在数据完全装载到内存的服务器上使用 HandlerSocket。但是对于PCI-E SSD(例如Fusion-IO)设备,每秒可以提供4w+ IOPS,并且IO设备本身消耗CPU比较大,使用HandlerSocket依然具有优势。 |
|
HandlerSocket的性能测试 |
|
HandlerSocket Oprofile测试报告 |
|
(MySQL通过SQL执行K/V查询的Oprofile信息) |
|
MySQL执行SQL语句,首先要经过SQL解析阶段,调用MYSQLparse() 和MYSQLlex() 进行语法和词法解析;然后进入查询优化阶段,调用make_join_statistics() 和JOIN::optimize() 获得统计信息和生成执行计划,可以清洗第发现,主要耗资源的是SQL解析和优化层,而不是InnoDB存储层,row_search_for_mysql只消耗了很少的时间。 |
|
因此我们对比Memcached/NoSQL,知道MySQL除了数据操作,还要很多额外的步骤需要完成: |
|
1 Parsing SQL statements【解析SQL】 |
|
2 Opening, locking tables【打开并锁定表】 |
|
3 Making SQL execution plans SQL【解析SQL并生成执行计划】 |
|
4 Unlocking, closing tables【解锁并关闭表】 |
|
另外,MySQL 还必须要做大量的并发控制,比如在发送/接收网络数据包的时候,fcntl() 就要被调用很多次;Global mutexes比如LOCK_open,LOCK_thread_count也被频繁地取得/释放。所以在Oprofile的输出中,排在第二位的是my_pthread_fastmutex_lock()。并且Mutex的竞争带来的上下文切换,导致%system占用CPU使用比例相当高(>20%)。 |
|
其实, MySQL 开发团队和外围的开发团体早已意识到大量并发控制对性能的影响,MySQL 5.5中已经解决了一些问题,Percona也对Mutex做了一些拆分处理,未来的MySQL版本中,也应该会越来越好。 |
|
在完全内存操作的情况时,CPU的效率非常重要。如果只有一小部分数据进入内存,那么SQL语句带来的消耗可以忽略不计。很简单,因为机械磁盘IO操作的时间消耗远比CPU解析SQL语句的时间消耗多,这种情况下,就不需要过分考虑SQL语句所带来的消耗。但是对于SSD盘,尤其是PCI-E SSD盘,响应时间在微秒级(Fusion I/O为30us左右),就必须考虑SQL带来的消耗了。 |
|
在大多数的MySQL 服务器中,大部分的热点数据都缓存在内存中,因而访问变得只受CPU的限制。Profiling 的结果就类似上所述的情况:SQL 层消耗了大量的资源。假设需要做大量的PK查询(例如:SELECT x FROM t WHERE id=?)或者是做LIMIT的范围查询,即使有70-80%都是在同一张表中做PK查询(仅仅只是查询条件中给定的值不同,即value不同而已), MySQL 还是每次需要去做 parse/open/lock/unlock/close, 这对我们来说是非常影响效率的事情。 |
|
(MySQL通过HandlerSocket执行K/V查询的Oprofile信息) |
|
HandlerSocket性能测试报告: |
|
【测试主机】 |
|
机型:R510 |
|
CPU:Intel(R) Xeon(R) CPU E5520 @ 2.27GHz |
|
内存:4G*6 |
|
磁盘:146G2(OS) + 300G12 RAID10(data) |
|
1) 完全随机测试 |
|
测试场景描述: |
|
单实例MySQL 5.1.48 InnoDB Plugin |
|
测试SQL:INSERT INTO table (key, value) VALUES(#key#, #value#) / SELECT value FROM table WHERE key=#key# |
|
HS API:execute_single |
|
2) 重复获取同一条数据 |
|
测试场景描述: |
|
1 单实例Percona 5.1.57-12.8 XtraDB |
|
2 测试SQL:SELECT value FROM table WHERE key=#key# |
|
3 HS API:execute_single |
|
4 MC API:get |
|
转自:http://www.uml.org.cn/sjjm/201211093.asp
|
|
php使用handlersocket详解 |
有关于 HandlerSocket 的介绍、性能及其安装,可参考Using SQL as NoSQL。而 PHP extension for interfacing with MySQL Handler Socket,实际上这里php-handlersocket有整体的介绍,包括其安装、使用方法。现在纯粹是因为自己测试时犯了一很基础的错误,所以,罚自己多敲点字。 |
安装 |
|
[root@localhost php-handlersocket]# /usr/local/php/bin/phpize |
[root@localhost php-handlersocket]# ./configure --with-php-config=/usr/local/php/bin/php-config |
[root@localhost php-handlersocket]# make |
[root@localhost php-handlersocket]# make install |
说明: |
1 编译时需要 libhsclient 库(libhsclient – HandlerSocket client library)。 |
2 安装成功时,在 PHP 的 extension dir 生成一名为 handlersocket.so,将extension=handlersocket.so加入 php.ini, 重启 PHP 服务。 |
HandlerSocket Class methods |
HandlerSocket::construct |
创建一 HandlerSocket Object。 |
|
HandlerSocket::__construct ( string $host, string $port [, array $options ] ) |
参数: |
|
$host MySQL 服务器 host name。 |
$port HandlerSocket 的端口地址。 |
|
返回值: |
返回 HandlerSocket Object。 |
HandlerSocket::openIndex |
在对数据库表做任何的增删改查操作前,必须先选择一索引。 |
|
public bool HandlerSocket::openIndex ( int $id, string $db, string $table, string $index, string $fields ) |
参数: |
|
$id HandlerSocket ID; 1 SELECT, 2 UPDATE, 3 INSERT, 4 DELETE。 |
$db 数据库名 |
$table 表名 |
$index 索引名, 可以是手动创建的索引名。这个参数可为空,一般指定时是用于 SELECT,eg: 指定为主键:HandlerSocket::PRIMARY |
$fields 字段名(多个字段名,用逗号分隔),可为空。 |
|
返回值: |
成功时返回 TRUE, 反之亦然。 |
HandlerSocket::executeSingle |
在表上做增删改查操作。 |
|
public mixed HandlerSocket::executeSingle ( int $id, string $op, array $fields [, int $limit, int $skip, string $modop, array $values, array $filters, int $invalues_key, array $invalues ] ) |
参数: |
|
$id HandlerSocket ID; 1 SELECT, 2 UPDATE, 3 INSERT, 4 DELETE。 |
$op 操作符,有如下可选项, ‘=’, ‘>=’, ‘<=’, ‘>’, ‘<’, ‘+’。 |
$fields 查询中所用到的字段,数组,其长度必须等于或小于指定的列数。 |
$limit 最多影响的行数(最开始根据这个函数名称有在怀疑这个参数,测试时发现,如果存在满足条件的多条记录时,会根据这个参数指定的值返回记录数)。 |
$skip 在检索记录前忽略掉的行数。 |
$modop 指定修改操作,可选值:’U‘, ‘D’。 |
$values 数组,用于做 UPDATE 操作时指定修改的值。 |
$filters 过滤的选项。 |
$invalues_key ? (enabled : 0 / disabled : -1). |
$invalues IN options |
|
返回值: |
返回做对应操作时的执行结果。 |
HandlerSocket::executeMulti |
在一次调用中执行多个操作,即多个 HandlerSocket::executeSingle 的合并。 |
|
public mixed HandlerSocket::executeMulti ( array $requests ) |
参数: |
|
$requrest 多组 executeSingle 参数,用数组的形式体现。 |
|
注意: |
等同于:HandlerSocket::executeSingle($requests00, $requests01, ...), HandlerSocket::executeSingle($requests10, ...) ...。 |
返回结果: |
返回做对应操作时的执行结果。 |
HandlerSocket::executeUpdate |
To update a record from a table using an index. |
|
public mixed HandlerSocket::executeUpdate ( int $id, string$op, array $fields, array $values [, int $limit, int $skip, array $filters, int $invalues_key, array $invalues ] ) |
参数: |
|
$id HandlerSocket ID; 2 UPDATE 。 |
$op 操作符,有如下可选项, ‘=’, ‘>=’, ‘<=’, ‘>’, ‘<’, ‘+’。 |
$fields 查询中所用到的字段,数组,其长度必须等于或小于指定的列数。 |
$values UPDAET 时指定修改的值。 |
$limit 最多影响的行数。 |
$skip 在检索记录前忽略掉的行数。 |
$filters 过滤的选项。 |
$invalues_key ? (enabled : 0 / disabled : -1). |
$invalues IN options |
|
注意: |
等同于:HandlerSocket::executeSingle($id, $op, $fields, $limit, $skip, ‘U‘, $values, $filters, $invalues_key, $invalues)。 |
返回值: |
返回做对应操作时的执行结果。 |
HandlerSocket::executeDelete |
To delete a record from a table using an index. |
|
public mixed HandlerSocket::executeDelete ( int $id, string $op, array $fields [, int $limit, int $skip, array $filters, int $invalues_key, array $invalues ] ) |
参数: |
|
$id HandlerSocket ID; 4 DELETE 。 |
$op 操作符,有如下可选项, ‘=’, ‘>=’, ‘<=’, ‘>’, ‘<’, ‘+’。 |
$fields 查询中所用到的字段,数组,其长度必须等于或小于指定的列数。 |
$limit 最多影响的行数。 |
$skip 在检索记录前忽略掉的行数。 |
$filters 过滤的选项。 |
$invalues_key ? (enabled : 0 / disabled : -1). |
$invalues IN options |
|
注意: |
等同于:HandlerSocket::executeSingle($id, $op, $fields, $limit, $skip, ‘D‘, NULL, $filters, $invalues_key, $invalues)。 |
返回值: |
返回做对应操作时的执行结果。 |
HandlerSocket::executeInsert |
To insert a record from a table using an index. |
|
public mixed HandlerSocket::executeInsert ( int $id, array $values ) |
参数: |
|
$id HandlerSocket ID; 3 INSERT 。 |
$values HandlerSocket::openIndex 指定的字段参数所对应的值,但是以数组的形式体现。 |
|
注意: |
等同于:HandlerSocket::executeSingle($id, ‘+‘, $values, 0, 0, NULL, NULL, NULL) ,第三个参数中指定的值必须和在此之前调用 HandlerSocket::openIndex 时第五个参数指定的字段对应。 |
返回值: |
返回做对应操作时的执行结果。 |
HandlerSocket::getError |
取得最近一次的错误信息。 |
|
public string HandlerSocket::getError ( void ) |
返回值: |
返回最近的错误信息(时间上)。 |
|
Example |
|
测试表 schema: |
|
|
CREATE TABLE hstesttbl ( id int(11) NOT NULL AUTO_INCREMENT, |
k char(6) DEFAULT NULL, |
v char(6) DEFAULT NULL, |
PRIMARY KEY (id ), |
KEY idx_hstesttbl_k (k ) |
) ENGINE=InnoDB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8; |
|
|
|
|
$host = ‘localhost‘; |
$port = 9998; |
$port_wr = 9999; |
$dbname = ‘hstestdb‘; |
$table = ‘hstesttbl‘; |
|
//GET |
$hs = new HandlerSocket($host, $port); |
if (!($hs->openIndex(1, $dbname, $table, HandlerSocket::PRIMARY, ‘k,v‘))) { |
echo $hs->getError(), PHP_EOL; |
die(); |
} |
|
$retval = $hs->executeSingle(1, ‘=‘, array(‘k1‘), 1, 0); |
var_dump($retval); |
|
$retval = $hs->executeMulti( |
array( |
array(1, ‘=‘, array(‘k1‘), 1, 0), |
array(1, ‘=‘, array(‘k2‘), 1, 0) |
) |
); |
var_dump($retval); |
unset($hs); |
|
//UPDATE |
$hs = new HandlerSocket($host, $port_wr); |
if (!($hs->openIndex(2, $dbname, $table, ‘‘, ‘v‘))) { |
echo $hs->getError(), PHP_EOL; |
die(); |
} |
|
if ($hs->executeUpdate(2, ‘=‘, array(‘k1‘), array(‘V1‘), 1, 0) === false) { |
echo $hs->getError(), PHP_EOL; |
die(); |
} |
|
unset($hs); |
|
//INSERT |
$hs = new HandlerSocket($host, $port_wr); |
if (!($hs->openIndex(3, $dbname, $table, ‘‘, ‘k,v‘))) { |
echo $hs->getError(), PHP_EOL; |
die(); |
} |
|
if ($hs->executeInsert(3, array(‘k2‘, ‘v2‘)) === false) { |
echo $hs->getError(), PHP_EOL; |
} |
if ($hs->executeInsert(3, array(‘k3‘, ‘v3‘)) === false) { |
echo ‘A‘, $hs->getError(), PHP_EOL; |
} |
if ($hs->executeInsert(3, array(‘k4‘, ‘v4‘)) === false) { |
echo ‘B‘, $hs->getError(), PHP_EOL; |
} |
|
unset($hs); |
|
//DELETE |
$hs = new HandlerSocket($host, $port_wr); |
if (!($hs->openIndex(4, $dbname, $table, ‘‘, ‘‘))) { |
echo $hs->getError(), PHP_EOL; |
die(); |
} |
|
if ($hs->executeDelete(4, ‘=‘, array(‘k2‘)) === false) { |
echo $hs->getError(), PHP_EOL; |
die(); |
} |
|
PS: 因为建立测试表时忘记指定存储引擎为 InnoDB, 测试 INSERT 操作时,怎样都是失败。后面为了验证问题的出处,用 perl 的 API 做同样的测试操作,结果也是失败。查看表结构后,修改储存引擎为 InnoDB,才成功。只是这个问题的错误信息太难理解,就几个数字,在没找到答案之前,害我还去查看了下 HandlerSocket 的源代码,当然,没有从中得到任何的提示。 |