电话之于短信、微信的一个很大的不同点在于,前者更加及时,有更快速直接的反馈;而后面两个虽然称之为instant message,但经常时发出去了就得等对方回复,等多久是不确定的。打电话能明确知道对方在不在,我所表达的信息是否已经传达;而短信或者微信,只知道消息发出去了,但对方是否收到,或者是否查看就不清楚了。
在通过网络通信的环境下,也是很难知道一个消息对方是否已经处理,因为要知道对方是否处理,依赖于对方的回复(ack),但即使对方没有回复,也不能说明对方就没有处理,也许仅仅是对方回复的那条消息丢失了
很多时候,一个进程需要判断另外一个进程是否还在工作,如何判断呢?判断是否准确呢,能否保证一致性呢?本文尝试回答这些问题。
本文中,节点通常就是指一个进程,一个提供服务的进程。后文中,只要不加以强调,进程和节点是同一个意思。
本文地址:http://www.cnblogs.com/xybaby/p/8710421.html
进程的状态
一个进程是否crash(在本文中,只要进程是非预期的终止,就成为crash),在单机环境下是很好判断的,只要查看还有没有这个进程就行了。这里有两个问题:
第一:分布式环境下能否准确判断进程crash?
第二:是否需要明确一个进程是否crash?
对于第一个问题,答案是几乎不能的,后面详细分析。
而第二个问题,有的时候是无需明确一个进程是否已经crash,而需要明确的是该进程是否持续对外提供服务,即使没有crash,如果不按程序的预期工作了(比如进程死循环、死锁、断网),那么也可以说这个服务挂了,“有的人活着,他已经死了”。分布式环境中,我们关心的是,节点(进程)是否对外提供服务,真死(crash)假死(活着但不工作)都是死(从系统的角度看)。
我们称一个对外提供服务的进程处于active状态,否则处于none-active状态。
Failure detection
如何判断一个进程是否处于active状态,最简单明了的方式就是:每隔一段时间就和这个进程通通信,如果目标进程在一定的时间阈值内回复,那么我们就说这个进程是active的,否则就是none-active的。这种定时通信的策略我们统称为心跳。
心跳有两种方式:第一种是单向的heartbeat;第二种是ping pong(ping ack)
在后文中,被检测的进程称之为target,而负责检测的进程称之为detector
第一种方式,target进程需要告知detector进程自己的存活性,只需要定时给detector发消息就行了,“hi, duddy, I am Ok!”。detector无需给target回复任何消息,detector的任务是,每隔一定时间去检查一下target是否有来汇报,没有的话,detector就认为target处于none-active状态了
而第二种方式,ping pong或者ping ack,更为常见:
detector给target发消息:hi,man,are you ok?
target回复detector:Yes, I am ok!
然后detector、target定时重复上面两步
detector负责发起检测:如果target连续N次不回复消息,那么detector就可以认为target处于none-active状态了。
这两种方式,区别在于谁来主动发消息,而相同点在于:谁关心active状态,谁来检测。就是说,不管是简单的heartbeat,还是ping ack,都是detector来检测target的状态。在实际中,ping ack的方式会应用得多一些,因为在ack消息中也可以携带一些信息,比如后文会提到的lease。
gunicorn failure detection
gunicorn是一个遵循python wsgi的http server,使用了prefork master-worker模型(在gunicorn中,master被称为Arbiter),能够与各种wsgi web框架协作。在本文,关注的是Arbiter进程对Worker进程状态的检测。
既然Worker进程是Arbiter fork出来的,即Arbiter是Worker进程的父进程,那么当Worker进程退出的时候,Master是可以通过监听signal.SIGCHLD信号来知道子进程的结束。但这还不够,gunicorn还有另外一招,我在《gunicorn syncworker 源码解析》中也有所提及:
(1)gunicorn为每一个Worker进程创建一个临时文件
(2)worker进程在每次轮训的时候修改该临时文件的属性
(3)Arbiter进程检查临时文件最新一次修改时间是否超过阈值,如果超过,那么就给Worker发信号,kill掉该Worker
不难看出,这是上面提到的第一种检测方式(单向的heartbeat),worker进程(target)通过修改文件属性的方式表明自己处于active状态,Arbiter(detector)进程检测文件属性来判断worker进程最近是否是active状态。
这个检测的目的就是防止worker进程处于假死状态,没有crash,但是不工作。
tcp keepalive
在计算机网络中,心跳也使用非常广泛。比如为了检测目标IP是否可达的ping命令、TCP的keepalive。
A keepalive (KA) is a message sent by one device to another to check that the link between the two is operating, or to prevent the link from being broken.
在TCP协议中,是自带keepalive来保活的,有三个很重要的选项(option)
tcp_keepidle :对一个连接进行有效性探测之前运行的最大非活跃时间间隔
tcp_keepintvl :两个探测的时间间隔
tcp_keepcnt :关闭一个非活跃连接之前进行探测的最大次数
三个选项是这么配合的:如果一条连接上tcp_keepidle 这么长时间没有发消息之后,则开始心跳检测,心跳检测的时间间隔是tcp_keepintvl ,如果连续探测tcp_keepcnt 这么多次,还没有收到对应的回应,那么主动关闭连接。
这三个参数的默认值都比较大,就是说需要较长时间才能检测网络不可达,因此一般情况下,程序会将三个参数调小。
可以看到,这是典型的ping ack探测方式。
应用层心跳
如果我们使用TCP最为传输层协议,那么是否就可以依赖TCP的keepalive来检查连接的状态,或者说对端进程的active状态呢?
答案是不能的,因为,TCP只能保证说链接是通的,但并不能表明对端是可用的,比如说对端进程处于死锁状态,但链接仍然是通的,tcp keepalive会持续收到回应,因此传输层的心跳无法发现进程的不可用状态。所以我们需要应用层的心跳,如果收到应用层的心跳回复,那么对端肯定不会是none-active的。
Failure detector与consensus
前面提到,通过心跳,是无法准确判断target是否是crash,但有的情况下我们又需要明确知道对方是crash了?还是说只是网络不通?毕竟这是两个不同的状态。
而且target的crash还分为crash-stop,crash-recovery两种情况。crash-stop是说一个进程如果crash了,那么不会重新启动,也就不会接受任何后续请求;而crash-recovery是说,进程crash之后会重启(是否在同一个物理机上重启没有任何关系)。
completeness vs accuracy
如果目标是检测出target的crash状态,那么检测算法有两个重要的衡量标准:
Completeness: every process failure is eventually detected (no misses)
Accuracy: every detected failure corresponds to a crashed process (no mistakes)
前者(completeness)是说,如果一个进程挂掉,那么一定能被检测到;后者(accuracy)是说,如果detector认为target进程挂掉了,那么就一定挂掉了,不会出现误判。
对于crash-stop模型,只能保证completenss,不能保证accurary。completeness不难理解,而accuracy不能保证是因为网络通信中, 由于延时、丢包、网络分割的存在,导致心跳消息(无论是单向的heartbeat还是ping ack)无法到达,也就没法判断target是否已经crash,也许还活得好好的,只是与detector之间的网络出了问题。
那如果是crash-recovery模型呢,通过简单的心跳,不仅不能保证accuracy,甚至不能保证completeness,因为target进程可能快速重启,当然增加一些进程相关的信息还是能判断crash-recovery的情况,比如target进程维护一个版本号,心跳需要对比版本号。
总之,网络环境下,很难保证故障检测的准确性(accuracy)。
lease
接下来,考虑一个很常见的场景,在系统中有两类进程:一个master和多个slave,master给slave分配任务,master需要通过心跳知道每个slave的状态,如果某个slave处于none-active状态,那么需要将该slave负责的任务转移到其他处于active状态的slave上。master也充当了detector的角色,每个slave都是被检测的target。
在这个场景中,有两点需要注意,第一,使用了心跳作为探测方式,而心跳探测只能保证completeness(完整性),而不能保证accuracy(准确性)。第二,slave负责的任务是否是可重复的?即同一份任务是否可以同时在多个slave上进行。failure detection的不准确性对slave任务的重新分配有至关重要的影响。
如果任务是可重复的,比如幂等的运算,那么当master通过心跳判断slave处于none-active状态的时候,只需要将任务重新分配给其他slave就行,不用管该slave是否还存活着。MapReduce就是这样的例子,master在worker(MapReduce中的slave进程)进程失活(甚至只是运算进度太慢)的时候,将该worker负责的任务(task)调度到其他worker上。因此可能出现多个worker负责同一份任务的情况,但这并不会产生什么错误,因为任务的计算结果需要回报给master,master保证对于一份任务只会采纳一份计算结果,而且同一份任务,不管是哪个worker执行,结果都是一致的。
但如果任务只能由一个节点来执行呢,由于心跳检测的不准确性,那么将任务从本来还在工作的节点重新调度到其他节点时,就会出现严重的问题。比如在primary-secondary副本控制协议中,primary的角色只能由一个节点来扮演,如果同时有两个primary,那么就出现了脑裂(brain-split),从这名字就能听出来问题的严重性。因此,即使在心跳检测出现误判的情况下,也要保证任务的唯一性,或者说,需要detector与target之间达成共识:target不要对外提供服务了。这个时候Lease机制就是很不错的选择,因为Lease机制具有很好的容错性,不会受到网络故障、延迟的影响。
关于lease机制,我在《带着问题学习分布式系统之数据分片》中有简单介绍,这里再简单提一下Lease在GFS中的使用。
GFS采用了primary-seconday副本控制协议,primary决定了数据修改的顺序。而primary的选举、维护是由master节点负责的,master与primary的心跳中,会携带lease信息。这个lease信息是master对primary的承诺:在lease_term这个时间范围内,我不会选举出新的primary。那么对于primary,在这个时间内,执行primary的职责,一旦过了这个时间,就自动失去了primary的权利。如果primary节点本身是ok的,并且与master之间网络正常,那么在每次心跳的时候,会延长这个lease_term,primary节点持续对外服务。一旦超过lease_term约定的时间,master就会选出新的primary节点,而旧的primary节点如果没有crash,在恢复与master的心跳之后,会意识到已经有了新的primary,然后将自己降级为secondary。
关于detector
从上面GFS的例子,可以看到master是一个单点,也就是说由一个节点来负责所以其它节点的failure detection,这就是集中式的故障检测。如下图所示:
由于detector是单点,因此压力会比较大。更为严重的问题,在使用了lease机制的系统中,一旦detector故障,所以节点都无法获取lease,也就无法提供服务,整个系统完全不可用。
因此,detector的高性能、高可用非常重要,以一个集群的形式提供failure detection功能。
references
buffalo cse486 failure_detectors.pdf
Leases: An Efficient Fault-Tolerant Mechanism for Distributed File Cache Consistency