天天看点

异曲同工的租约

有这样一个需求:wordpress中有一个叫做wp-cron.php的文件,它负责做一些定时任务,例如定时发送博文,定时清理垃圾回复等。因为wordpress是运行在web php环境下,不借助第三方工具,实现定时任务有一定的困难。它的思路是,每当博客有点击时,就总触发一次cron,为了不阻塞客户的正常访问,用到了fopensocket 发起一个异步cron请求,当cron页面收到这个请求的时候,就开始检查各种执行条件,如果条件满足,则从数据库中获取cron任务并执行。

这里的问题是,如果有两个用户同时点击了页面,同时触发了cron任务,如何保证只起一个cron?如何保证起了一个cron后,它不会退出,常驻后台?如何保证万一cron退出了,会有后备的cron能起来?

对于每一个cron请求,按照下面的顺序执行: 

- select获取一个任务 

- 把这个任务从数据表中删除 

- 检查删除是否成功 

- -如果删除成功,则开始执行select到的任务 

- -否则直接退出(有另外一个cron请求也在执行这个任务)

这种方法简单粗暴,很能解决问题,但是它有这样几个问题:

-顺序问题:select取任务的顺序必须都一致。cron1取a、b;cron2取b、a,则可能两个cron都会先后主动退出,导致后台没有cron了。 

-开销问题:每次都会尝试起cron,意味着每次都会发起一次内部的http连接

顺序问题:这个保证每次select都是按照主键顺序取即可,或者按照某个行值唯一的列顺序执行即可。

开销问题:引入lease(租约)机制,每个cron job一旦启动,就会持有一个lease(3秒),每次执行完一个任务,就续一下自己的lease。任何cron希望启动的时候,必须先看一下lease是否过期,如果lease过期,则立即获取lease,并启动自己。

由于没有加锁,可能两个cron都抢到了lease,没关系,当他们处理任务的时候,会有一个主动放弃(见上面的流程说明)。这种情况比较罕见,不会影响性能。

上面的方案,在特殊情况下还是有一些小缺陷: 

1. cron job中途异常退出后的3秒内,新的任务无法被执行。如果cron job异常退出3秒后,不再有新的请求到来,那么任务队列中堆积的任务将无人处理。

如果cron job异常退出的可能性比较低,则这不是一个很大的问题。如果需要确保任务总能被及时执行,可以考虑使用linux系统自带的crontab,来定时触发php的cron job。

分布式系统中,lease的概念被广泛采用。当我们无法确切了解到彼此的行为时,我们可以依赖一套约定,来规范和预测彼此的行为,以保障系统处于一个一致的状态。所谓“一致的状态”,就是我们觉得正确、可以理解的状态。上文中,两个cron请求无法知道彼此的存在,通过lease的方式,很好地达成了一致,不会出现两个cron job同时运行的窘境。

php脚本的执行时间,是有限制的,即使在脚本执行之初调用了 ignore_user_abort 方法。该方法的语义是设置客户端断开连接时是否中断脚本的执行,并不能改变php脚本最长。控制php最大执行时长的,需要修改php-fpm、nginx等的配置,详细参考 这里和这里 。不过,在脚本中,也是可以改变php的最大执行时间的,相关函数请参考set_time_limit(), ini_set(“max_execution_time”, “45”),这里还有一篇小结。根据php官方文档,希望在脚本中设定最大执行时间的时候,必须保证php.ini配置中safe_mode=off。一般默认改选项都是off,所以你可以在脚本中设置一个无限长的脚本运行时间。不过,安全起见,不建议运行无限长的时间,而是应该在ini_get(“max_execution_time”)的基础上减去若干秒来运行,然后主动释放lease。