准备工作:
① 配置文件 config.php
② 封装 Memcached 类 hash.class.php,包含普通哈希算法(取模)和一致性哈希算法
③ 初始化 Memcached 节点信息 init.php
④ 减少 Memcached 节点 down.php
⑤ 统计命中率 statistics.php
⑥ 使用 Highcharts(4.1.9) js 图表库来展示减少节点后两种算法命中率的变化
1. 配置文件
config.php
<?php /* Memcached 配置文件 */
//Memcached 节点信息 $mem_servers = array(); $mem_servers['s1'] = array('host'=>'127.0.0.1', 'port'=>'11211'); $mem_servers['s2'] = array('host'=>'127.0.0.1', 'port'=>'11212'); $mem_servers['s3'] = array('host'=>'127.0.0.1', 'port'=>'11213'); $mem_servers['s4'] = array('host'=>'127.0.0.1', 'port'=>'11214'); $mem_servers['s5'] = array('host'=>'127.0.0.1', 'port'=>'11215'); //哈希策略选择 $method = 'mod';//普通哈希 //$method = 'dis';//一致性哈希
说明:模拟 5 台 Memcached 服务器,使用相同的本地主机,不同的端口号。
2. 封装 Memcached 类
在 Memcached 笔记与总结(6)PHP 实现 Memcached 的一致性哈希分布算法 的基础上增加普通哈希类:
//普通哈希 class modHash implements hash, distribute { private $serverList = array();//服务器列表 private $size = 0; //节点的个数 public function _hash($str){ return sprintf('%u', crc32($str));//把字符串转成32为无符号整数 } public function lookup($key){ $key = $this->_hash($key) % $this->size; //取模 return $this->serverList[$key]; } public function addServer($server){ if (in_array($server, $this->serverList)) { return; } $this->serverList[] = $server; $this->size += 1; return true; } public function removeServer($server){ if (!in_array($server, $this->serverList)) { return; } $key = array_search($server, $this->serverList); unset($this->serverList[$key]); $this->serverList = array_merge($this->serverList);//删除节点后重新索引数组 $this->size -= 1; return true; } }
说明:如果仅仅向 array_merge() 函数输入了一个数组,且键名是整数,则该函数将返回带有整数键名的新数组,其键名以 0 开始进行重新索引。
完整 哈希类:
1 <?php 2 //把字符串转换为整数 3 interface hash{ 4 public function _hash($str); 5 } 6 7 interface distribute{ 8 //在当前的服务器列表中找到合适的服务器存放数据 9 public function lookup($key); 10 11 //添加一个服务器到服务器列表中 12 public function addServer($server); 13 14 //从服务器列表中删除一个服务器 15 public function removeServer($server); 16 } 17 18 //一致性哈希 19 class consistentHash implements hash, distribute{ 20 21 private $serverList = array();//以二维数组保存服务器列表和每一个服务器下虚拟节点的哈希值 22 private $position = array();//以键值形式保存所有虚拟节点的哈希值(键)和对应的服务器(值)的一维数组 23 private $isSorted = FALSE; //记录虚拟节点哈希值列表是否已经排列过序 24 25 public function _hash($str){ 26 return sprintf('%u', crc32($str));//把字符串转成32为无符号整数 27 } 28 29 public function lookup($key){ 30 //计算出服务器的Hash值 31 $hash = $this->_hash($key); 32 33 //判断服务器列表是否排过序 34 if (!$this->isSorted) { 35 //倒序排列(把虚拟节点列表转换成逆时针圆环) 36 krsort($this->position, SORT_NUMERIC); 37 $this->isSorted = TRUE; 38 } 39 40 //遍历虚拟节点列表,找到合适的服务器并返回 41 foreach($this->position as $server_hash=> $server){ 42 if ($hash >= $server_hash) return $server; 43 } 44 return end($this->position); 45 } 46 47 public function addServer($server, $nodesNum = 25){ 48 49 if (isset($this->serverList[$server])) { 50 return; 51 } 52 53 //增加虚拟节点,默认每个物理节点变成25个虚拟节点 54 for($i = 0; $i < $nodesNum; $i++){ 55 $hash = $this->_hash($server.'-'.$i);//计算虚拟节点的Hash值 56 $this->position[$hash] = $server; 57 $this->serverList[$server][] = $hash; 58 } 59 60 //此时服务器列表发生了变化,因此标识为FALSE 61 $this->isSorted = FALSE; 62 return TRUE; 63 } 64 65 public function removeServer($server){ 66 67 if (!isset($this->serverList[$server])) { 68 return; 69 } 70 71 //循环position数组,如果要删除的服务器的值等于position数组某个元素的键,则删除该元素 72 foreach($this->position as $k=>$v){ 73 if($server == $v){ 74 unset($this->position[$k]); 75 } 76 } 77 78 unset($this->serverList[$server]); 79 80 $this->isSorted = FALSE; 81 return TRUE; 82 } 83 } 84 85 //普通哈希 86 class modHash implements hash, distribute { 87 private $serverList = array();//服务器列表 88 private $size = 0; //节点的个数 89 90 public function _hash($str){ 91 return sprintf('%u', crc32($str));//把字符串转成32为无符号整数 92 } 93 94 public function lookup($key){ 95 $key = $this->_hash($key) % $this->size; 96 return $this->serverList[$key]; 97 } 98 99 public function addServer($server){ 100 101 if (in_array($server, $this->serverList)) { 102 return; 103 } 104 105 $this->serverList[] = $server; 106 $this->size += 1; 107 108 return true; 109 } 110 111 public function removeServer($server){ 112 113 if (!in_array($server, $this->serverList)) { 114 return; 115 } 116 117 $key = array_search($server, $this->serverList); 118 unset($this->serverList[$key]); 119 $this->serverList = array_merge($this->serverList);//删除节点后重新索引数组 120 $this->size -= 1; 121 122 return true; 123 } 124 }
测试普通哈希类节点是否正确:
<?php require './config.php'; require './hash.class.php'; $hashserver = new modHash(); $hashserver->addServer($mem_servers['s1']); $hashserver->addServer($mem_servers['s2']); $hashserver->addServer($mem_servers['s3']); $hashserver->addServer($mem_servers['s4']); $hashserver->addServer($mem_servers['s5']); function showServer($obj, $key) { $serverInfo = $obj->lookup($key); return $key.' on server:'.$serverInfo['host'].", port:".$serverInfo['port']; } echo showServer($hashserver, 'key1'),'<br />'; echo showServer($hashserver, 'key2'),'<br />';
输出:
key1 on server:127.0.0.1, port:11212
key2 on server:127.0.0.1, port:11213
其中 key1 经过 crc32 转换后得到 744252496,模 5 为 1;key2 经过 crc32 转换后得到 3042260458,模 5 为 3。
3. 初始化 Memcached 节点信息 init.php
循环添加服务器,并且把 10000 条数据(按照普通哈希/一致性哈希)插入到添加的 5 台 Memcached 服务器中,平均每台 2000 条数据
分别开启 5 台 Memcached 服务器:
init.php
<?php header("Content-type:text/html; charset=utf-8"); set_time_limit(0); require './config.php'; require './hash.class.php'; $mem = new memcache(); $hash = new modHash();//普通哈希 //循环添加服务器 foreach($mem_servers as $k=>$v){ $hash->addServer($k); } //向服务器中添加共10000条数据 for($i = 0; $i < 10000; $i++) { $key = 'key'.$i; $value = 'value'.$i; $server = $mem_servers[$hash->lookup($key)]; $mem->pconnect($server['host'], (int)$server['port'], 2);//设置超时时间为2秒 $mem->set($key, $value, 0, 0);//不自动过期 usleep(3000); } echo '初始化数据完毕';
说明:
memcache::pconnect() :打开一个到服务器的持久化连接,它的第 2 个参数要求是长整型 long
执行 init.php
输出:初始化数据完毕
使用 Telnet 客户端连接 Memcached 服务器查看数据:
(127.0.0.1:11211)输入 stats:
其中 total_items 有 2559 个。
4. 减少 Memcached 节点 :down.php
<?php header("Content-type:text/html; charset=utf-8"); set_time_limit(0); require './config.php'; require './hash.class.php'; $mem = new memcache(); $hash = new modHash();//普通哈希 //循环添加服务器 foreach($mem_servers as $k=>$v){ $hash->addServer($k); } //模拟减少一台Memcached服务器 $hash->removeServer('s3'); for($i = 0; $i < 10000; $i++) { $key = 'key'.$i; $value = 'value'.$i; $server = $mem_servers[$hash->lookup($key)]; $mem->pconnect($server['host'], (int)$server['port'], 2);//设置超时时间为2秒 if(!$mem->get($key, $value)){ $mem->set($key, $value, 0, 0);//不自动过期 } usleep(3000); }
5. 统计命中率 statistics.php
统计 Memcached 各节点的平均命中率,用于 Ajax 请求
statistics.php
<?php header("Content-type:text/html; charset=utf-8"); set_time_limit(0); require './config.php'; $mem = new memcache(); $gets = 0;//请求次数 $hits = 0;//命中次数 foreach ($mem_servers as $k => $v) { $mem->pconnect($v['host'], $v['port'], 2);//设置超时时间为2秒 $res = $mem->getstats(); $gets += $res['cmd_get']; $hits += $res['get_hits']; } $rate = 1; if($gets > 0) { $rate = $hits / $gets; } echo $rate;
说明:
memcache::getstats() 输出数据格式如下
Array ( [pid] => 9616 [uptime] => 2742 [time] => 1448207741 [version] => 1.4.24 [libevent] => 2.0.22-stable [pointer_size] => 32 [rusage_user] => 2.168000 [rusage_system] => 8.439000 [curr_connections] => 12 [total_connections] => 13 [connection_structures] => 13 [reserved_fds] => 20 [cmd_get] => 4 [cmd_set] => 7045 [cmd_flush] => 0 [cmd_touch] => 0 [get_hits] => 2 [get_misses] => 2 [delete_misses] => 0 [delete_hits] => 0 [incr_misses] => 0 [incr_hits] => 0 [decr_misses] => 0 [decr_hits] => 0 [cas_misses] => 0 [cas_hits] => 0 [cas_badval] => 0 [touch_hits] => 0 [touch_misses] => 0 [auth_cmds] => 0 [auth_errors] => 0 [bytes_read] => 195866 [bytes_written] => 89803 [limit_maxbytes] => 4194304 [accepting_conns] => 1 [listen_disabled_num] => 0 [threads] => 4 [conn_yields] => 0 [hash_power_level] => 16 [hash_bytes] => 262144 [hash_is_expanding] => 0 [malloc_fails] => 0 [bytes] => 62780 [curr_items] => 1000 [total_items] => 1000 [expired_unfetched] => 0 [evicted_unfetched] => 0 [evictions] => 0 [reclaimed] => 0 [crawler_reclaimed] => 0 [crawler_items_checked] => 0 [lrutail_reflocked] => 0 )
6. 使用 Highcharts(4.1.9) js 图表库来展示减少节点后两种算法命中率的变化
官方地址:http://www.highcharts.com/
下载地址:http://code.highcharts.com/zips/Highcharts-4.1.9.zip
index.html,该文件通过 Ajax 每 2 秒向 statistics.php 发出请求获取 Memcached 的命中率
注:解压 Highcharts 压缩包, 拷贝 Highcharts-4.1.9\examples\dynamic-update\index.html 至项目目录,修改并且重命名为 index.html
<!DOCTYPE HTML> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8"> <title>Highcharts Example</title> <script type="text/javascript" src="jquery-1.8.3.min.js"></script> <style type="text/css"> ${demo.css} </style> <script type="text/javascript"> $(function () { $(document).ready(function () { Highcharts.setOptions({ global: { useUTC: false } }); $('#container').highcharts({ chart: { type: 'spline', animation: Highcharts.svg, // don't animate in old IE marginRight: 10, events: { load: function () { // set up the updating of the chart each second var series = this.series[0]; setInterval(function () { var x = (new Date()).getTime(), // current time y = parseFloat($.ajax({url:'statistics.php', async:false}).responseText); series.addPoint([x, y], true, true); }, 2000); } } }, title: { text: 'Memcached hit rates' }, xAxis: { type: 'datetime', tickPixelInterval: 150 }, yAxis: { title: { text: 'Value' }, plotLines: [{ value: 0, width: 1, color: '#808080' }] }, tooltip: { formatter: function () { return '<b>' + this.series.name + '</b><br/>' + Highcharts.dateFormat('%Y-%m-%d %H:%M:%S', this.x) + '<br/>' + Highcharts.numberFormat(this.y, 2); } }, legend: { enabled: false }, exporting: { enabled: false }, series: [{ name: 'Random data', data: (function () { // generate an array of random data var data = [], time = (new Date()).getTime(), i; for (i = -19; i <= 0; i += 1) { data.push({ x: time + i * 1000, y: 1 }); } return data; }()) }] }); }); }); </script> </head> <body> <script src="./Highcharts-4.1.9/js/highcharts.js"></script> <script src="./Highcharts-4.1.9/js/modules/exporting.js"></script> <div id="container" style="min-width: 310px; height: 400px; margin: 0 auto"></div> </body> </html>
比较过程
当没有减少节点时,访问 index.html 时,命中保持在 100%:
当减少一个服务器节点时,即执行 down.php,命中率的变化(普通哈希)变化如下:
00:46:45 时突然减少一台服务器,命中率急剧下降;
直到 01:00:10 时恢复稳定。耗时约 13 min,稳定后的命中率在 94% - 95% 之间。
一致性哈希
修改 init.php 和 down.php:
$hash = new consistentHash();
首先执行 init.php,然后当减少一个服务器节点时(执行 down.php),一致性哈希命中率的变化变化如下:
01:32:39 模拟宕调一台服务器
到 01:45:55 恢复稳定。耗时约 13 min,稳定后的命中率为 95.03%。