代码方式实现PHP服务定时拉取
定时拉取是一种很常见的操作,比如需要定时从配置中心拉取一下配置,或者同步一下状态等。这对于编译型语言来说十分简单,只需要开一个独立的线程,拉一下、sleep一会就好了。但是对于无状态的PHP服务,比如wordpress,或者一些API层的服务,由于他们没有常驻进程,想跑定时任务就比较麻烦了(swoole的除外,我们只讨论fpm的)。
其实搞定时任务的方式有挺多,比如起一个cron脚本,定时拉(配置或者什么)写到一个文件里,php每次读这个文件结果。这么搞是最简单的,但是当你维护的是一个几百台docker的集群的时候,这么搞运维可能会骂死你;如果是用git的webhook部署的话,如果改了脚本逻辑得手动上去改一下……我就是遇到了一个类似的复杂情况,不得已用了这种方法。留着以后当个面试题也是极好的。
这种方式需要几个小部分来配合:
- xxx.lock 文件锁,里面存有一个timestamp,表示更新时间。
- xxx.conf 拉取到的配置(或其他什么东西,存在里面)。
- lock.sh 脚本,用户检查锁
- sync.sh 脚本,用于执行拉取操作
- xxx.php php文件
他们之间相互的关系大概是这样:

xxx.php及钩子
xxx.php主要任务有两个:
- 从lockfile读一下时间,看需不需要启动任务
- 如果需要,启动lock脚本
class Sync
{
private $lockFile;
private $confFile;
private static $instance = null;
public static function getInstance()
{
if (self::$instance == null) {
self::$instance = new self();
}
return self::$instance;
}
private function __construct()
{
$this->lockFile = dirname(__FILE__) . '/' . 'xxx.lock';
$this->confFile = dirname(__FILE__) . '/' . 'xxx.conf';
}
public function updateConfHook()
{
try {
$fpTime = fopen($this->lockFile, 'r');
if ($fpTime !== false) {
if (flock($fpTime, LOCK_EX | LOCK_NB)) {
$size = filesize($this->lockFile);
if ($size > 0) {
$content = fread($fpTime, $size);
if (time() - (int)($content) > 60) {
@flock($fpTime, LOCK_UN);
@exec('nohup sh lock.sh &');
} else {
@flock($fpTime, LOCK_UN);
}
} else {
@flock($fpTime, LOCK_UN);
@exec('nohup sh lock.sh &');
}
}
@fclose($fpTime);
}
} catch (\Exception $e) {
}
}
}
注意updateConfHook这个函数,他首先会尝试从lockfile把更新时间读出来,读之前会使用flock对文件加锁,如果成功,并且当前时间距离上次更新时间超过1分钟,那么我们解锁lockfile并启动lock.sh,后面的事就不用管了。
当然了,还需要哪里触发一下这个函数,不妨把钩子写成一个middleware,对代码侵入性较小。flock这里使用的目的是保证只有一个脚本存在,且尽可能不影响php性能。
lock.sh
#!/bin/sh
flock -n xxx.lock sh sync.sh > /dev/null
if [ 0 -ne $? ]; then
exit
fi
lock.sh尝试对文件上锁,如果成功则进行下一步,失败则说明有其他脚本在跑,自己退出就好。
sync.sh
#!/bin/sh
curl --connect-timeout 1 -s $yourConfigCenterUrl > xxx.conf.tmp
if [ 0 -ne $? ]; then
exit
fi
# do something here
# do check here
mv -f xxx.conf.tmp xxx.conf
date +%s > xxx.lock
sleep 60
首先拉取配置,或者做你想做的事,然后保存、检查。如果合法则替换,并把更新日期写入lock文件。因为sync.sh是被lock.sh调用的,sync.sh不退出,文件锁就不会释放,所以这里sleep 60秒是为了避免其他脚本被启动,毕竟1分钟内不需要更新。
PS:虽然xxx.lock是在锁死状态,但是并不妨碍它被修改,大家可以试下。
流程图
