基于http 的 web 程序,由于其无状态的属性,很多时候并不是很适合循环定时执行的任务。Drupal 本身对于这种需求有多种解决方案,比如核心的 hook_cron, 第三方的现有模块 Utilimate Cron, Ellisa Cron, Scheduler Job等。然而核心 API 和现有模块的问题在于构建在在 drupal 的 cron 之上,本身核心的 cron 已经负担足够沉重,比较轻量的或者时间跨度比较长的任务比较适合通过这些途径来实现。当任务是长时间短间隔高频率循环执行的时候,通过这种方式很可能让核心的 cron 负担过于称重,一方面影响系统的稳定性,同时也会影响本身需求目标的实现。
以上解决办法主要是针对 shared host 用户,由于这一类用户没有完整的 Unix/Linux 主机权限,因此无法利用 Unix/Linux 核心自带的 crontab 带来的稳定性和遍历性。话说9102年都已经快结束了,这年头还没上云的用户或者还在用 sharedhost 跑 drupal 的用户不多了吧。
我要跑的任务是高频率定时循环执行的,所以以上的解决方案并不能很好的满足我的需求。
废话不多说,上解决办法。
解决方案1 - Crontab
思路如下
- 根据任务的需求定义路径
- 使用 crontab + curl 的方式请求路径,调用后台的 callback 执行相关的任务
- 这里需要注意的是需要做请求验证,验证其是否来自 127.0.0.1, token 是否正确,否则返回 403 错误
针对不同时段需要执行的任务,可以在 web 界面输出格式化的 crontab 列表,然后直接进入 shell 界面将这些任务加入 Linux 的 crontab 列表中即可
这里需要区分两种任务类型,一种是定时执行如每天12点钟运行,一种是循环执行如每隔10分钟执行一次。
比如说,每天的早上8点和晚上8点各运行一次更新任务,更新最新的产品信息和行业信息。
如何设计逻辑?(这里只针对目前手上已经有处理好的产品信息和行业信息的情况)
~~这里就需要用到 Drupal 核心的 Queue API + Utimate Cron API ~~
之前使用的是 Utimate Cron,这个模块的问题在于官方的文档实在是太烂了,使用他的最简设置是没问题的,比如我要使用数据库 logger 而非默认的缓存 logger, 该如何配置文档中没有说明。说是可以直接对接 drupal 的 queue, 我没有找到具体的设置方法。
考虑到快速上手解决问题,最终选取了 Queue API + Elysia Cron, 这里选用 Queue 的主要原因是当任务的负载很重且执行的时间很长,可能会超过PHP的 max_execution_time 时,使用队列的处理方式,每次执行一个任务或一小批任务,不至于进程卡死。
具体使用方式如下:
# 1. 使用 hook_cronapi() 定义定时任务,注意这里我的模块名是 jobs_every_minute
/**
* Implements hook_cronapi().
*/
function jobs_every_minute_cronapi($op, $job = NULL){
$items = array();
$items['ec_job_test'] = array(
'description' => 'Send nid to Queue',
'rule' => '* * * * *',
'arguments' => array(20),
'callback' => 'jobs_every_minute_cron_callback',
);
return $items;
}
# 2. 定义定时任务的队列
/**
* Implements hook_cron_queue_info().
*/
function jobs_every_minute_cron_queue_info() {
$queues = array();
$queues['1st_test_queue'] = array(
'worker callback' => 'jobs_every_minute_insert_sql_record',
'time' => 2,
'skip on cron' => TRUE,
);
return $queues;
}
# 3. 定义定时任务执行时的回调
function jobs_every_minute_cron_callback($arg){
// 这里一定要用 get 的方法,而不是直接 new 一个 Queue 的对象
// 因为 hook_cron_queue_info() 已经在模块载入的初始,声明了 Queue 的实例
// 如果直接 new 的话,会直接覆盖我们声明好的 Queue,那样的话一堆函数回调就会断掉
$queue = DrupalQueue::get('1st_test_queue');
$queue->createQueue();
for ($i=1; $i <= $arg; $i++){
$queue->createItem($i);
$item = $queue->claimItem(30);
// 这里无需使用 deleteItem 方法来删除 item,因为 Queue 默认再 claimItem 之后会自动删除 item
//$queue->deleteItem($item);
}
}
# 4. 定义队列中每一个 item 需要执行的操作,这里我是向数据库中插入一条记录并用 watchdog()打印一条日志来测试操作的情况
function jobs_every_minute_insert_sql_record($item){
db_insert('sql_insert_test')
->fields(array('text' => date("Y-m-d H:i:s",time())))
->execute();
watchdog('Job scheduler' ,
'Message: Data inserted to table at @t',
array('@t'=> date("Y-m-d H:i:s",time())),
WATCHDOG_NOTICE);
}
这样一个完整 Queue + Cron API 的完整流程就走完了。
其实这里也是有问题的,这里我们定义的定时任务 ec_job_test 每运行一次都会在系统的 queue 表中插入新的 item,而要运行每一个 item 对应的处理任务,最终还是要运行系统的 cron。 即hook_cronapi+queue定义的定时任务,不是即时执行的,需要drupal核心的 cron 运行时,检查 queue 表中的 item, 如果有任务,就会执行。弄了半天根本没有解决我的问题。我的需求是定时添加新的处理任务,并实时监听任务状态,如果有则立即执行。由于核心的 cron 负载很重,我不可能每分钟运行一下,因此这个解决办法肯定是不行的。
回到最开始的思路,既然我能使用 linux 的 crontab,使用这些第三方的 cron 模块真的是多此一举。
我只需要定义一个路径并使用 curl + crontab 定时请求来扫描任务状态,如果有任务,每次返回一条,并使用回调处理,如果没有则闲置, 简单方便。
解决方案2 - PHP 添加进程控制扩展
MAMP 安装php第三方扩展库 - 进程控制扩展 Ev
本来这个问题在linux上极好解决,直接 pear 命令安装即可。然而在 OSX 上面却很痛苦,是的,因为我们用的都是 MAMP 的集成环境。
那么怎么办?
比如我现在用的 PHP 版本为 php7.0.15
那么这个版本 PHP 对应的 pear 的包管理工路径为
/Applications/MAMP/bin/php/php7.0.15/bin/pear
PHP 插件的预编译工具的路径为
/Applications/MAMP/bin/php/php7.0.15/bin/phpize
我没有使用 pear 命令的方式安装,因为感觉可能会报错安装失败,所以最终还是选择手动编译安装。
# 1. 下载 Ev 扩展的源码包
# 下载地址为 https://pecl.php.net/get/ev-1.0.6.tgz wget 或是 curl 就看大家的喜好了
# 2. 解压 tgz 压缩包
tar -xzvf ev-1.0.6.tgz
# 3. 进入源码目录,注意源码目录直接就能看到 php5 和 php7 文件夹,不是含有 xml 文件的那个目录
cd ev-1.0.6/ev-1.0.6
# 4. 运行预编译工具,获取系统变量
/Applications/MAMP/bin/php/php7.0.15/bin/phpize
# 5. 编译前配置
./configure --with-ev
# 6. 解决依赖错误问题
# error dyld: Library not loaded: /usr/local/opt/readline/lib/libreadline.7.dylib
# 出现这个问题的原因是插件依赖的readline库文件太老了, OSX 系统已经由 libreadline.8.0.dylib 取代了老版本
# 创建一个软连接,解决依赖的问题
cd /usr/local/opt/readline/lib
ln -s libreadline.8.0.dylib libreadline.7.dylib
# 切换回原来的安装目录
cd -
# 7. 重新配置
./configure --with-ev --with-php-config=/Applications/MAMP/bin/php/php7.0.15/bin/php-config
# 配置运行结果出现一下表示通过
> creating libtool
> appending configuration tag "CXX" to libtool
> configure: creating ./config.status
> config.status: creating config.h
# 8. 编译并且安装
make && make install
# 提示编译成功别切扩展安装至相关目录
> Build complete.
> Don't forget to run 'make test'.
>
> Installing shared extensions: /Applications/MAMP/bin/php/php7.0.15/lib/php/extensions/no-debug-non-zts-20151012/
# 如果你出现了 ev-1.0.6/ev-1.0.6/php7/common.h:25:10: fatal error: 'php.h' file not found 这样的错误,是因为编译器没有找到相关的 php环境 配置器的地址,需要重新配置并带上参数 --with-php-config=php-config (对应 php 版本的配置器地址)
# 查看编译后的插件
ls /Applications/MAMP/bin/php/php7.0.15/lib/php/extensions/no-debug-non-zts-20151012/
# 发现 ev.so 躺在那里,编译的工作算是完成了。
# 9. 启用扩展
# 打开 MAMP,菜单 -> File -> Edit Template -> PHP(php.ini) -> 7.0.15
# 找到第 538 行,追加如下设置项
extension=ev.so
# 保存后重启 MAMP 即可
解决方案3 - PHP DAEMON 方案 - 守护进程
此方案需要用到的模块为 drushd + PHP-DAEMON 扩展库