最近项目中用到PHP连接Memcache的场景。这个场景对于PHP开发者很常见。但就是这么常见的一个场景,带来了意想不到的一个问题。
我这边的php memcached扩展版本是:
memcached
MEMCACHED SUPPORT ENABLED Version 2.1.0 libmemcached version 1.0.8 Session support yes igbinary support no json support no
案例如下:
在本地服务器启动两个memcache实例,端口分别为1122,1123。PHP我使用的是MemcacheD扩展(注意,是D)。测试代码如下:
$memcache = new Memcached; $memcache->setOption(Memcached::OPT_DISTRIBUTION, Memcached::DISTRIBUTION_CONSISTENT); $memcache->setOption(Memcached::OPT_BINARY_PROTOCOL, true); $memcache->setOption(Memcached::OPT_LIBKETAMA_COMPATIBLE, true); $memcache->setOption(Memcached::OPT_TCP_NODELAY, true); $memcache->setOption(Memcached::OPT_NO_BLOCK, true); $memcache->setOption(Memcached::OPT_CONNECT_TIMEOUT, 50); $memcache->setOption(Memcached::OPT_SEND_TIMEOUT, 50); $memcache->setOption(Memcached::OPT_RECV_TIMEOUT, 50); $memcache->setOption(Memcached::OPT_POLL_TIMEOUT, 50); $memcache->setOption(Memcached::OPT_HASH, Memcached::HASH_MD5); $memcache->addServers(array(array("HOST" => "127.0.0.1", "PORT" => 1122),array("HOST" => "127.0.0.1", "PORT" => 1123)); $mKey = "test_bocheng11"; $status = 2; $r = $memcache->set($mKey, $status,60*60*24); $status1 =$memcache->get($mKey); var_dump($status1); $memcache->delete($mKey);
然后运行这个脚本。正常情况下可以返回
int(2)
这时候关掉实例1123,再次运行上面的代码,会返回
bool(false)
大家看到这里应该明白出了什么问题,我们上面这段代码无法自动的将fail的server踢出去。这将是一个非常大的隐患。
更麻烦的是,下面这段代码依然无法自动将fail的server去除(使用memcached扩展的默认配置)
$memcache = new Memcached; $memcache->addServers(array(array("HOST" => "127.0.0.1", "PORT" => 1122),array("HOST" => "127.0.0.1", "PORT" => 1123)); $mKey = "test_bocheng11"; $status = 2; $r = $memcache->set($mKey, $status,60*60*24); $status1 =$memcache->get($mKey); var_dump($status1); $memcache->delete($mKey);
这段代码中我的key="test_bocheng11"是可以被hash到1122的实例上的,因此我们打开1123实例,关闭1122实例后,会发现返回的是
bool(false)
这个问题最终是看了看libmemcached的源码找到了一个解决方案(我当时的libmemcached的版本是1.0.8,也许高版本的libmemcached已经解决了这个问题)
如下的代码可以解决自动failover的问题?
$memcache = new Memcached; $memcache->setOption(Memcached::OPT_DISTRIBUTION, Memcached::DISTRIBUTION_CONSISTENT); $memcache->setOption(Memcached::OPT_BINARY_PROTOCOL, true); $memcache->setOption(Memcached::OPT_LIBKETAMA_COMPATIBLE, true); //自动failover配置 /*$memcache->setOption(Memcached::OPT_SERVER_FAILURE_LIMIT, 1); $memcache->setOption(Memcached::OPT_RETRY_TIMEOUT, 30); $memcache->setOption(Memcached::OPT_AUTO_EJECT_HOSTS, true); */ //最新的libmemcached的文档中可以看到更建议使用如下的选项 $memcache->setOption(Memcached::OPT_REMOVE_FAILED_SERVERS,true); $memcache->addServers(array(array("HOST" => "127.0.0.1", "PORT" => 1122),array("HOST" => "127.0.0.1", "PORT" => 1123)); $mKey = "test_bocheng11"; $status = 2; $r = $memcache->set($mKey, $status,60*60*24); $status1 =$memcache->get($mKey); var_dump($status1); $memcache->delete($mKey);
这时只要1122,1123有一个实例正常运行,就可以保证memcache的数据能够存取。
重点是第7--9行的参数设置,但要注意的是,这套参数配置必须配合DISTRIBUTION_CONSISTENT生效,对于memcached扩展默认的DISTRIBUTION_MODULA依然是无法实现failover的。从libmemcached源码中依稀也可以看到一些踪影。
memcached_return_t run_distribution(memcached_st *ptr) { if (ptr->flags.use_sort_hosts) { sort_hosts(ptr); } switch (ptr->distribution) { case MEMCACHED_DISTRIBUTION_CONSISTENT: case MEMCACHED_DISTRIBUTION_CONSISTENT_KETAMA: case MEMCACHED_DISTRIBUTION_CONSISTENT_KETAMA_SPY: case MEMCACHED_DISTRIBUTION_CONSISTENT_WEIGHTED: return update_continuum(ptr); case MEMCACHED_DISTRIBUTION_VIRTUAL_BUCKET: case MEMCACHED_DISTRIBUTION_MODULA: break; case MEMCACHED_DISTRIBUTION_RANDOM: srandom((uint32_t) time(NULL)); break; case MEMCACHED_DISTRIBUTION_CONSISTENT_MAX: default: assert_msg(0, "Invalid distribution type passed to run_distribution()"); } return MEMCACHED_SUCCESS; }
这段代码中可以看到 默认的 MEMCACHED_DISTRIBUTION_MODULA 选项,不会触发 update_continuum 操作,而这个操作会影响到下面这个方法中是否重新选择host的判断
static inline void _regen_for_auto_eject(memcached_st *ptr) { if (_is_auto_eject_host(ptr) && ptr->ketama.next_distribution_rebuild) //这个next_distribution_rebuild值会在update_continuum方法调用时被改变。 { struct timeval now; if (gettimeofday(&now, NULL) == 0 and now.tv_sec -> ptr->ketama.next_distribution_rebuild) { run_distribution(ptr); } } } void memcached_autoeject(memcached_st *ptr) { _regen_for_auto_eject(ptr); } uint32_t memcached_generate_hash_with_redistribution(memcached_st *ptr, const char *key, size_t key_length) { uint32_t hash= _generate_hash_wrapper(ptr, key, key_length); _regen_for_auto_eject(ptr); return dispatch_host(ptr, hash); }
update_continuum 部分代码摘要如下,可以看到判断server是否fail以及修改next_distribution_rebuild的部分。
if (is_auto_ejecting) { live_servers= 0; ptr->next_distribution_rebuild= 0; for (host_index= 0; host_index < memcached_server_count(ptr); ++host_index) { if (list[host_index].next_retry <= now.tv_sec) live_servers++; else { if (ptr->next_distribution_rebuild == 0 || list[host_index].next_retry < ptr->next_distribution_rebuild) ptr->next_distribution_rebuild= list[host_index].next_retry; } } } else { live_servers= memcached_server_count(ptr); }
技术交流
原文链接:注意!PHP memcached扩展默认配置下无法自动failover,转载请注明来源!