电话之于短信、微信的一个很大的不一样点在于,前者更加及时,有更快速直接的反馈;然后面两个虽然称之为instant message,但常常时发出去了就得等对方回复,等多久是不肯定的。打电话能明确知道对方在不在,我所表达的信息是否已经传达;而短信或者微信,只知道消息发出去了,但对方是否收到,或者是否查看就不清楚了。html
在经过网络通讯的环境下,也是很难知道一个消息对方是否已经处理,由于要知道对方是否处理,依赖于对方的回复(ack),但即便对方没有回复,也不能说明对方就没有处理,也许仅仅是对方回复的那条消息丢失了python
不少时候,一个进程须要判断另一个进程是否还在工做,如何判断呢?判断是否准确呢,可否保证一致性呢?本文尝试回答这些问题。web
本文中,节点一般就是指一个进程,一个提供服务的进程。后文中,只要不加以强调,进程和节点是同一个意思。算法
本文地址:http://www.cnblogs.com/xybaby/p/8710421.htmlspring
一个进程是否crash(在本文中,只要进程是非预期的终止,就成为crash),在单机环境下是很好判断的,只要查看还有没有这个进程就好了。这里有两个问题:微信
第一:分布式环境下可否准确判断进程crash?网络
第二:是否须要明确一个进程是否crash?框架
对于第一个问题,答案是几乎不能的,后面详细分析。tcp
而第二个问题,有的时候是无需明确一个进程是否已经crash,而须要明确的是该进程是否持续对外提供服务,即便没有crash,若是不按程序的预期工做了(好比进程死循环、死锁、断网),那么也能够说这个服务挂了,“有的人活着,他已经死了”。分布式环境中,咱们关心的是,节点(进程)是否对外提供服务,真死(crash)假死(活着但不工做)都是死(从系统的角度看)。分布式
咱们称一个对外提供服务的进程处于active状态,不然处于none-active状态。
如何判断一个进程是否处于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是一个遵循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,可是不工做。
在计算机网络中,心跳也使用很是普遍。好比为了检测目标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的。
前面提到,经过心跳,是没法准确判断target是不是crash,但有的状况下咱们又须要明确知道对方是crash了?仍是说只是网络不通?毕竟这是两个不一样的状态。
并且target的crash还分为crash-stop,crash-recovery两种状况。crash-stop是说一个进程若是crash了,那么不会从新启动,也就不会接受任何后续请求;而crash-recovery是说,进程crash以后会重启(是否在同一个物理机上重启没有任何关系)。
若是目标是检测出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)。
接下来,考虑一个很常见的场景,在系统中有两类进程:一个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。
从上面GFS的例子,能够看到master是一个单点,也就是说由一个节点来负责因此其它节点的failure detection,这就是集中式的故障检测。以下图所示:
因为detector是单点,所以压力会比较大。更为严重的问题,在使用了lease机制的系统中,一旦detector故障,因此节点都没法获取lease,也就没法提供服务,整个系统彻底不可用。
所以,detector的高性能、高可用很是重要,以一个集群的形式提供failure detection功能。
buffalo cse486 failure_detectors.pdf
Leases: An Efficient Fault-Tolerant Mechanism for Distributed File Cache Consistency