PHP的问题
表面上看,无论是使用哪个配置中心实现方案甚至是mysql去保存配置差异都不会太大;但如果我们把php这一应用程序实现语言考虑进去的话,问题俨然会变得异常棘手起来。
与c#、Java、Go、Python、Node.JS等等应用不同,php的程序不支持常驻内存,php每处理完一个请求后,资源都会被释放掉;这也就意味着如果我们在请求进来时,都需要去访问配置中心获得各种配置信息。
因为配置中心是外部程序,每次访问都是跨进程通讯;假设获取一个配置需要耗时1ms,那么一百项配置就是0.1s;这是巨大的潜在性能影响。
无论使用什么配置中心实现,我们都很可能需要对php做特殊处理。
曾有php电商使用memcached保存一些配置信息,我压测的时候发现这些配置信息读取是耗时最大的操作一。
如果我们要引入swoole这样的php运行方案以“缓存”配置信息的话
又很可能需要对现有的php项目代码大幅度的检查、修改。
缓存方案
因为php的动态性,我们可以使用动态生成php文件的形式来做缓存,比方说参考Graphiq的方案,我们可以尝试使用写这样的函数:
$cache_path = '/tmp/confcache';
function cache_set($key, $val) {
global $cache_path;
echo "set key: $key";
$val = var_export($val, true);
// HHVM fails at __set_state, so just use object cast for now
$val = str_replace('stdClass::__set_state', '(object)', $val);
// Write to temp file first to ensure atomicity
$tmp = $cache_path.$key."." . uniqid('', true) . '.tmp';
file_put_contents($tmp, '<?php $val = ' . $val . ';', LOCK_EX);
rename($tmp, "$cache_pathe$key");
}
function get_key($key) {
global $cache_path;
if(!substr($key, 0, 1) === "/") {
throw new Exception('key must start with /');
}
@include "$cache_path$key";
if(isset($val)) {
return $val;
}
$client = new ConfigClient();
$val = $client->get($key);
cache_set($key, $val);
return $val;
}
get_key函数会首先尝试include跟key名相关的php缓存文件,如果文件存在,便可以直接返回配置值。
如果不存在,则调用etcd去获得配置值,然后写入缓存文件。
因为缓存的php文件会进一步被opcache编译、缓存起来,这样子后续的读性能,便跟直接读程序常量无异;完全不需要担心性能问题。
缓存实时更新
另一方面,我们也可以使用类似confd这样的工具做去监控watch配置中心中各个值。
在值发生变更时,便触发脚本去删除掉缓存文件,那么下次程序再调用get_key函数时,便会自动去配置中心获得新值,并重新缓存。
若opcache开启了file_cache等配置,脚本还可能需要显式的去调用opcache_invalidate去过期脚本缓存。
这方案的核心是利用了php内置的opcache来实现缓存,可靠性会非常很高。
其他参考
etcd
etcd官方不支持php,而其第三方库都非常不活跃:
即便我们不考虑常驻内存的问题,etcd的php客户端可能都需要仔细研究一下才可以使用。
consul
consul官方同样不支持php,但其第三方库稍微好一丢丢:
不过,consul的template功能导可能被用来解决php这特有问题。
consul template可以接收任意文本文件,然后调用consul服务器,将文本文件中placeholder替换为具体的配置值,最后输出为新的文本文件。
也就是说,我们可以将现有的php config文件内容修改为类似:
// application components
'components' => array(
'user' => array(
'key' => '{{key "web/user-key"}}',
),
'email' => array(
'Username' => '{{key "email/username"}}',
'Password' => '{{key "email/password"}}',
// ..
)
)
这样的形式,然后在程序部署前,先使用consul template进行预处理。