关于租约机制Lease

最近在工作中遇到了这类的问题,就是在多个后端的环境下需要在单一的后端锁定某个任务独立执行。这个需求其实是在需要强制取消某个正在进行的任务时才需要的,例如我们需要在不同的后端A、B、C等等机器上强制终止某些正在执行的脚本,但是调用强制终止的接口还是需要通过API调用,那么由于HA的机制,还是会随机的落在A、B、C上,假设处理这个接口的机器落在A上,那执行任务的脚本可能不在A上,那么此时想终止其他机器上正在执行的任务,可能就需要SSH连接到其他的机器去ps -ef|grep 找出pid,然后再kill掉进程。这种思路其实是不合理的,它主要会产生两个问题,第一:靠找pid可能存在安全隐患,万一找错了呢?杀掉了核心进程例如apache的就悲剧了;第二:这样会增加系统对其他环境的依赖,例如需要ssh,配置后端机器之间的ssh key等。而且这个非常不利于系统的调整,例如新加一台后端机器,还需要添加很多东西。那么在此时使用租约机制就会有一种柳暗花明的感觉。

我们可以利用最土的mysql数据库作为锁,将作业任务以一条条的记录推到表中,其中设置两个字段,第一个可以称之为status字段,这个字段用于标识当前任务有没有被认领。那么有人可能会说,这样就可以了呀,认领后状态从waiting变成doing不就能标识这个任务的状态了。但其实不行,因为在这种情况下会发生一种叫“死锁”的状态。一旦出现死锁,其实是很麻烦的事情,首先它会影响你的业务逻辑,让作业任务变得混乱不堪。其次死锁问题手工解决很麻烦,你得慢慢的顺着你的程序逻辑去一步一步的追踪导致应该杀死哪个机器的哪个进程或者修改什么字段才能解决这个死锁问题。那么在这种情况下,我们可以在表中再加上一个timeline字段,这个字段可以简单的用time()【php】得到的时间戳填进去,我们可以假定它是有时效性的。例如100s,那么作业任务在执行的时候,我们强制规定脚本在执行操作的时候每一百秒之内必须要更新这个时间戳,你没有更新我们就认为你已经阵亡。就算你没有阵亡在你下一次判断的时候也必须要推出脚本。

以上这个实现的原理其实在我们的实际生活中也是常常应用的,例如北京的地铁司机,虽然地铁和飞机等等完全可以做到无人驾驶了,而且可以做到很好,但是每列地铁还是有几个司机在那盯着。而且规定30s之内必须要踩一次特定的装置,这样调度中心才会确认,司机没有睡着,如果没有触发不论地铁是否运行正常,都会强制停车。那么以上的租约锁也同样是这样,客户端需要时刻提醒调度中心,就是我们存放作业任务的mysql,告诉它你还有心跳。

下面引一段比较经典的关于租约的介绍。

1989年斯坦福大学的Cary G. Gray和David R. Cheriton提出了利用租约来维护缓存一致性的方法。所谓租约,其实就是一个合同,即服务器给予客户端在一定期限内可以控制修改操作的权力。如果服务器要修改数据,首先要征求拥有这块数据的租约的客户端的同意,之后才可以修改。客户端从服务器读取数据时往往就同时获取租约,在租约期限内,如果没有收到服务器的修改请求,就可以保证当前缓存中的内容就是最新的。如果在租约期限内收到了修改数据的请求并且同意了,就需要清空缓存。在租约过 期以后,客户端如果还要从缓存读取数据,就必须重新获取租约,我们称这个操作为“续约”。

在租约期限内,客户端可以保证其缓存中的数据是最新的。同时,租约可以容忍各种非拜占庭式失效(机器崩溃、网络分割等)。如果客户端崩溃或者网络中断,服务器只需要等待其租约过期就可以进行修改操作。如果服务器出错丢失了所有客户端的信息,它只需要知道租约的最长期限,就可以在这个期限之后安全的修改数据。与回调方式相比,服务器只需记住还拥有租约的客户端即可。

租约与带期限的锁非常相似,但更加灵活,因为租约还提供了“寻求同意”的机制(我觉得可以称为“带期限可妥协的锁”)。服务器还可以实现多种租约,比如“写租约”和“读租约”,并保证一个时间段内只有一个写租约或者多个读租约,这就相当于是单写者多读者的锁协议。

因为租约是基于时间的,因此其有效性需要系统时间来保证。如果服务器的时钟快而客户端时钟慢,那么有可能服务器认为一个租约已经过期而客户端仍然认为其有 效,就可能导致错误。对这种情况就必须通过时钟同步协议来解决了,不过这种情况很少见。一般情况下,我们可以认为一个分布式系统的时间是同步在一个很小的时间差e之内,只需把这个e考虑到租约期限内即可。

租约的属性和管理有多种选择,首先要考虑的就是租约期限的长短。

一般情况下,应当选择较短的租约期限。与长租约相比,短租约有三个优点。首先,在失效情况下修改操作往往需要等待租约过期,因此短租约就意味着更短的失效延迟。其次,就算一个客户端已经不再需要读取数据,但在其租约过期前,任何的修改操作仍然需要征求它的同意,这种情况叫做“假共享”,显然租约期限越长,这个问题就越严重。最后,短租约也使得服务器要维护的客户端信息更少。然而短租约也意味着更大的续约开销, 因此对于要反复读取却很少修改的数据,长租约会更有效。因此,对租约期的选择要权衡失效延迟、假共享开销和续约开销等多个因素,服务器可以根据数据访问特 性和客户端的性质灵活设置期限。事实上,如果我们把租约期限设为零,就相当与轮询,此时修改操作随时可以进行,而读取数据总是要联系服务器。如果把租约期 限设为无限长,就相当于回调。

除了期限的选择,还有很多管理选项。对客户端来说,可以选择 是否续约、何时续约以及是否同意修改等。比如为了减少读取延迟,客户端可以在租约过期前就续约,不过这样可能加重服务器的负担。对服务器来说,可以选择是 否发放租约、租约覆盖粒度以及对如何进行修改操作。比如在收到修改请求后,服务器可以不征求客户端同意,而是简单的等待所有租约过期(等待时不再发放新租 约以避免无限期的延迟)。对于“安装文件”,也就是修改极少的文件(比如头文件、库文件),服务器可以用一个租约来覆盖一批文件,同时定期广播续约通知来节省开销,如果需要修改数据,就停止广播并等待租约过期即可。