目前使用MySQL的网站,多半同时使用Memcache作为键值缓存。虽然这样的架构极其流行,有众多的案例,但过于依赖Memcache,无形中让Memcache成为故障的根源:
注:关于清理过期数据的问题,可以在程序架构上想办法,如果数据操作有统一DAO封装的话,可以利用Observer模式来清理过期数据,非主题内容,资料自查。
面对这些问题,HandlerSocket 项目是个不错的解决方案,它通过插件的方式赋予MySQL完整的NoSQL功能,从原理上讲,它跳过MySQL中最耗时的语法解析,查询计划等步骤,直接读取数据,如果内存够大,能装下索引,MySQL的查询效率能提高若干倍!
性能测试:Using MySQL as a NoSQL – A story for exceeding 750,000 qps
因为HandlerSocket的性能足够好,所以就没有必要使用Memcache了,能节省大量的硬件资源,相当低碳!而且HandlerSocket操作的是MySQL放在内存中的索引,没有额外的缓存,所以自然就不存在数据一致性的问题。
首先要确保已经安装了MySQL5.1以上的版本,我用的是Ubuntu操作系统,事先已经用apt安装了MySQL5.1.49 ,同时还需要相应的mysql_config,如果是Ubuntu的话,可以:
shell> aptitude install libmysqld-dev
注:如果你用的MySQL是从源代码编译的或官方提供的二进制版本,可以略过此步。
接着下载一份和系统MySQL版本一致的MySQL源代码和HandlerSocket源代码:
shell> tar zxf mysql-5.1.49.tar.gz shell> tar zxf ahiguti-HandlerSocket-Plugin-for-MySQL-1.0.6-76-gf5f7443.tar.gz shell> cd ahiguti-HandlerSocket-Plugin-for-MySQL-f5f7443 shell> ./autogen.sh shell> ./configure --with-mysql-source=../mysql-5.1.49 \ --with-mysql-bindir=/usr/bin \ --with-mysql-plugindir=/usr/lib/mysql/plugin
其中的参数含义如下:with-mysql-source表示MySQL源代码目录,with-mysql-bindir表示MySQL二进制可执行文件目录(也就是mysql_config所在目录, 用whereis mysql_config可查得),with-mysql-plugindir表示MySQL插件目录,如果不清楚这个目录在哪,可以按如下方法查询:
mysql> SHOW VARIABLES LIKE 'plugin%'; +---------------+-----------------------+ | Variable_name | Value | +---------------+-----------------------+ | plugin_dir | /usr/lib/mysql/plugin | +---------------+-----------------------+
运行命令后,如果你使用的是MySQL5.1.37版本的话,会遇到如下错误信息:
MySQL source version does not match MySQL binary version
明明我们的MySQL源代码版本和二进制版本都是5.1.37,为什么还会出现这个错误呢?通过查询HandlerSocket的编译脚本,发现原来它会检索MySQL源代码目录中的VERSION文件,可MySQL5.1.37的源代码目录里不知何故竟然没有这个文件,所以就报错了,既然知道了原因,那我们就照猫画虎做一个VERSION文件放到MySQL源代码目录,内容如下:
MYSQL_VERSION_MAJOR=5 MYSQL_VERSION_MINOR=1 MYSQL_VERSION_PATCH=37 MYSQL_VERSION_EXTRA=
再次运行configure脚本,应该就OK了,把剩下的步骤进行完:
shell> make shell> make install
接着需要配置一下HandlerSocket,编辑MySQL配置文件,加入如下内容:
[mysqld] loose_handlersocket_port = 9998 # the port number to bind to (for read requests) loose_handlersocket_port_wr = 9999 # the port number to bind to (for write requests) loose_handlersocket_threads = 16 # the number of worker threads (for read requests) loose_handlersocket_threads_wr = 1 # the number of worker threads (for write requests) open_files_limit = 65535 # to allow handlersocket accept many concurrent # connections, make open_files_limit as large as # possible.
此外,InnoDB的innodb_buffer_pool_size,或MyISAM的key_buffy_size等关系到缓存索引的选项尽可能设置大一些,这样才能发挥HandlerSocket的潜力。
注:apt包管理下的配置文件一般是/etc/mysql/my.cnf,否则一般是/etc/my.cnf
最后登陆MySQL并激活HandlerSocket插件:
mysql> INSTALL PLUGIN handlersocket soname 'handlersocket.so';
重启一下MySQL服务,如果没有问题,就能在MySQL里看到HandlerSocket的线程了:
mysql> SHOW PROCESSLIST;
也可以通过查询刚配置的端口是否已经被MySQL占用来确认是否安装成功:
shell> lsof -i :9998 shell> lsof -i :9999
完活儿!现在你的MySQL已经具备NoSQL的能力了!
首先创建一个测试用的表:
CREATE TABLE IF NOT EXISTS `test`.`t` ( `id` int(10) unsigned NOT NULL AUTO_INCREMENT, `a` varchar(10) NOT NULL, `b` varchar(10) NOT NULL, PRIMARY KEY (`id`), KEY `a_b` (`a`,`b`) ) ENGINE=InnoDB;
注:理论上HandlerSocket支持MyISAM,InnoDB等各种引擎,不过推荐使用InnoDB。
HandlerSocket的协议 非常简单,指令通过TAB分割,一行就是一个请求。本文用到了:
API:
谷歌code(http://code.google.com/p/php-handlersocket/ )中提供了PHP扩展作者的API,这里我将每个方法的参数具体说明一下(也可以去https://github.com/ahiguti/HandlerSocket-Plugin-for-MySQL/blob/master/docs-en/perl-client.en.txt 参考一下perl扩展的API说明,其实实现都是一样的,只不过是不同语言):
实例化:
/* * String $host:MySQL ip; * String $port:handlersocket插件的监听端口,它有两个端口可选:一个用于读、一个用于写 */ $hs = new HandlerSocket($host, $port);
打开一个数据表:
/* * Int $index:这个数字相当于文件操作里的句柄,HandlerSocket的所有其他方法都会依据这个数字来操作由这个 openIndex打开的表, * String $dbname:库名 * String $table:表名 * String $key:表的“主键”(HandlerSocket::PRIMARY)或“索引名”作为搜索关键字段,这就是说表必须有主键或索引 * 个人理解:要被当做where条件的key字段,这样可以认为handlersocket只有一个where条件 * String $column:'column1,column2' 所打开表的字段(以逗号隔开),就是说$table表的其他字段不会被操作 */ $hs->openIndex($index, $dbname, $table, $key, $column);
查询:
/* * Int $index: openIndex()所用的$index * String $operation:openIndex方法中指定的$key字段所用的操作符,目前支持'=', '>=', '<=', '>',and '<';可以理解为where条件 * Array $value * Int $number(默认是1):获取结果的最大条数;相当于SQL中limit的第二个参数 * Int $skip(默认是0):跳过去几条;相当于SQL中limit的第一个参数 */ $retval = $hs->executeSingle($index, $operation, $value, $number, $skip);
插入(注意:此处的openIndex要用$port_wr,即读写端口) :
/* * Int $index: openIndex()所用的$index * Array $arr:数字元素数与openIndex的$column相同 */ $retval = $hs->executeInsert($index, $arr);
删除(注意:此处的openIndex要用$port_wr,即读写端口):
/* * Int $index: openIndex()所用的$index * String $operation:openIndex方法中指定的$key字段所用的操作符,目前支持'=', '>=', '<=', '>',and '<';可以理解为where条件 * Array $value * Int $number(默认是1):获取结果的最大条数;相当于SQL中limit的第二个参数 * Int $skip(默认是0):跳过去几条;相当于SQL中limit的第一个参数 */ $retval = $hs->executeDelete($index, $operation, $value, $number, $skip);
更新(注意:此处的openIndex要用$port_wr,即读写端口):
/* * Int $index: openIndex()所用的$index * String $operation:openIndex方法中指定的$key字段所用的操作符,目前支持'=', '>=', '<=', '>',and '<';可以理解为where条件 * Array $value * Int $number(默认是1):获取结果的最大条数;相当于SQL中limit的第二个参数 * Int $skip(默认是0):跳过去几条;相当于SQL中limit的第一个参数 */ $retval = $hs->executeUpdate($index, $operation, $value, $number, $skip);
测试:
新建一个1000w条数据的用户表,id为主键,包括uname、email、add_time字段,使用两台不同的机器做ab压力测试:
读测试:
并发50,5000次压力测试:
MySQL: min: 0.504740953445 max:13.1727859974 average: 1.05 CPU:0.7%us, 0.3%sy use:111s
HandlerSocket:min: 0.302443981171 max:9.37712621689 average:0.736 CPU:0.4%us, 0.3%sy use:77s
并发70,5000次压力测试:
MySQL: min: 0.504750013351 max:10.4482009411 average: 1.094 CPU:0.9%us, 0.4%sy use:85s
HandlerSocket:min: 0.302488803864 max:10.3345310688 average: 0.788 CPU:0.5%us, 0.4%sy use:62s
并发110,5000次压力测试:
MySQL: min:0.505280017853 max:21.3242678642 average:1.095 CPU:1.5%us, 0.7%sy use:55s
HandlerSocket:min: 0.30281996727 max:10.6022770405 average:0.786 CPU:1.1%us, 0.7%sy use:39s
并发150,5000次压力测试:
MySQL: min: 0.505041122437 max:28.8087069988 average:1.073 CPU:1.8%us, 0.9%sy use:61s
HandlerSocket:min: 0.302739143372 max:12.878344059 average:0.774 CPU:1.0%us, 0.9%sy use:30s
总结:
共同点:并发越高,性能越好
hs系统占用和执行时间都少于MySQL 性能约好30%~40%
写测试:
并发50,5000次压力测试:
MySQL: min: 0.507106781006 max: 4.95259904861 average: 0.594 CPU:0.76%us, 0.49%sy use:62s
HandlerSocket:min: 0.303457021713 max: 7.0854101181 average: 0.383 CPU:0.4%us, 0.2%sy use:43s
并发70,5000次压力测试:
MySQL: min: 0.508066892624 max: 12.8451189995 average: 0.659 CPU:1.0%us, 0.6%sy use:51s
HandlerSocket:min: 0.30427312851 max: 12.4244120121 average: 0.417 CPU:0.53%us, 0.29%sy use:32s
并发90,5000次压力测试:
MySQL: min: 0.507676839828 max: 12.8466610909 average: 0.689 CPU:1.3%us, 0.72%sy use:45s
HandlerSocket:min: 0.304312229156 max: 12.4680581093 average: 0.465 CPU:0.66%us, 0.38%sy use:29s
并发110,5000次压力测试:
MySQL: min: 0.507092952728 max: 11.7785778046 average: 0.775 CPU:1.34%us, 0.82%sy use:45s (13条未写入)
HandlerSocket:min: 0.219769954681 max: 12.6269509792 average: 0.556 CPU:0.63%us, 0.37%sy use:32s (15条未写入)
并发150,5000次压力测试:
MySQL: min: 0.507570981979 max: 13.4538660049 average: 0.75 CPU:1.9%us, 1.1%sy use:29s (写多1条)
HandlerSocket:min: 0.304651975632 max: 16.3402500153 average: 0.555 CPU:0.7%us, 0.43%sy use:26s (8条未写入)
总结:
共同点:并发越高,性能越好
hs系统占用和执行时间都少于MySQL 性能约好50%~60%
测试结果确实比较明显,HandlerSocket可以在高并发、简单表操作的环境下替代MySQL。
原文链接:http://www.cnblogs.com/yangligogogo/articles/1969823.html
http://database.51cto.com/art/201105/261741.htm
最后给大家推荐两篇文章:
1、google的php说明文档:http://code.google.com/p/php-handlersocket/wiki/Classes
2、一篇很不错的学习笔记:http://www.livingelsewhere.net/2011/06/02/php-extension-for-interfacing-with-mysql-handler-socket
最后发一段粗糙的类,眼前用而已:
<?php /** * 本类是对HandlerSocket的一个简单封装。 * 有三个常量:DB_HOST: 服务器ip; DB_HS_PORT:只读端口; DB_HS_WRPORT:读写端口 * * $db = new HSMysql('topic'); //设置表明 * * #插入操作 * $row = array('f_uid' => 12, 'f_uname' => 'too'); * $db->insert($row); * * #读取操作 * #取出的列、索引名、条件关系符、条件对应的数据(与索引顺序一致 )、limit $skip, $number * $db->query($columns, $index, '=', $fields, $number=1, $skip=0); * * #更新操作 * $data = array('f_uid' => 13, 'f_uname' => 'tooo'); * #更新数据、索引、条件关系符、条件对应的数据(与索引顺序一致 )、limit $skip, $number * $db->update($data, $index, $operation, $fields, $number=1, $skip=0); * * #删除操作 * #索引、条件关系符、条件对应的数据(与索引顺序一致 )、limit $skip, $number * $db->delete($index, $operation, $fields, $number=1, $skip=0) * * @author tuyl * */ class HSMysql{ protected static $_db, $_rdb; protected $_table; public function __construct($t_name){ $this->setTable($t_name); } public function setTable($t_name){ $this->_table = DB_PREFIX.$t_name; } public function getInstance($readonly = 1){ if($readonly){ if(!self::$_rdb){ self::$_rdb = new HandlerSocket(DB_HOST, DB_HS_PORT); } return self::$_rdb; }else{ if(!self::$_db){ self::$_db = new HandlerSocket(DB_HOST, DB_HS_WRPORT); } return self::$_db; } } public function openIndex($id, $index = '', $columns = array(), $readonly = 0){ $cls = implode(',', $columns); $db = $this->getInstance($readonly); if (!$db->openIndex($id, DB_NAME, $this->_table, $index, $cls)){ $this->_error('open index :'.$hs->getError()); } return $db; } public function query($columns, $index, $operation, $fields, $number=1, $skip=0){ $hs = $this->openIndex(1, $index, $columns, 1); $res = $hs->executeSingle(1, $operation, $fields, $number, $skip); if($res === false){ $this->_error('query error:'.$hs->getError()); } return $res; } public function insert($rows){ $hs = $this->openIndex(3, HandlerSocket::PRIMARY, array_keys($rows)); $f = $hs->executeInsert(3, array_values($rows)); if($f === false){ $this->_error('executeInsert :'.$hs->getError()); } return $f; } public function update($data, $index, $operation, $fields, $number=1, $skip=0){ $hs = $this->openIndex(2, $index, array_keys($data)); return $hs->executeUpdate(2, $operation, $fields, array_values($data), $number, $skip); } public function delete($index, $operation, $fields, $number=1, $skip=0){ $hs = $this->openIndex(4, $index, ''); return $hs->executeDelete(4, $operation, $fields, $number, $skip); } public function asc($data, $index){ return $this->_sort($data, $index, SORT_ASC); } public function desc($data, $index){ return $this->_sort($data, $index, SORT_DESC); } private function _sort($data, $index, $sort){ if(!$data || !is_array($data)) return $data; $args = array(); foreach ($data as $k => $v) { $args[] = $v[$index]; } array_multisort($args, $sort, $data); return $data; } private function _error($msg){ echo $msg; exit(); } }