电话之于短信, 微信的一个很大的不同点在于, 前者更加及时, 有更快速直接的反馈; 而后面两个虽然称之为 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 http://www.cse.buffalo.edu/~stevko/courses/cse486/spring13/lectures/07-failure_detectors.pdf
- http://gunicorn.org/
- Failure_detector
- Leases: An Efficient Fault-Tolerant Mechanism for Distributed File Cache Consistency http://web.eecs.umich.edu/~mosharaf/Readings/Leases.pdf
来源: https://www.cnblogs.com/xybaby/p/8710421.html