如果一个worker和master失去连接,比如网络分区的原因,重新分配任务可能导致两个worker执行同一个任务。如果一个任务被执行多次是可以接受的,我们在重新分配任务时就可以不用校验第一个worker是否执行过任务。如果不能接受,那么应用必须能适应多个worker最终都可能执行该任务的可能。


仅有一次(Exactly-Once)和最多一次(At-Most-Once)的语义

对任务(比如领导者选举)使用锁并不能完全避免任务被执行多次,因为可能会存在以下几个连续的事件:

  1. Master M1指定任务T1给Worker W1。
  2. W1获取T1的锁,然后执行T1,接着释放锁。
  3. Master M1怀疑W1奔溃了,然后重新分配任务T1给Worker W2。
  4. W2获取了T1的锁,执行T1,接着释放锁。

这种情况下,T1的锁没有阻止任务被执行两次,因为两个worker之间在执行任务时没有重叠的步骤。为了处理那些需要仅有一次或者最多一次语义的场景,应用需要依赖特定的机制。比如说,应用数据包含时间戳,而一个任务需要修改这个应用数据,那么任务的成功执行取决于那个数据创建的时间戳的值。该应用在状态不是自动修改的情况下需要有能回滚部分变更的能力;否则,它可能最终导致一个不一致的状态。

我们通过这些讨论来阐述了为应用实现这些语义的难点。讨论这些语言的具体实现不在本书的范围内。

另外一个通信失败的重要问题是它们对同步原语(比如锁)产生的影响。因为节点可能奔溃,系统可能会有网络分区,锁会产生问题:如果一个节点奔溃或者被分区,锁可以阻止其他的节点继续运行。Zookeeper随后需要实现处理这种场景的机制。首先,它使得客户端指定在Zookeeper中的某些数据状态是临时的。其次,Zookeeper集群需要客户端周期性通知它们是存活的。如果一个客户端没有及时地通知集群,那么所有属于这个客户端的临时状态都会被删除。通过这两个机制,在奔溃和通信失败的情况下,我们能防止单独的客户端导致应用停止执行。

回想前面我们讨论过,在系统中我们无法控制消息的延迟,所以无法分辨一个客户端是否奔溃还是只是运行很慢。因此,当我们怀疑一个客户端已经奔溃时,我们需要假设它仅仅是运行地很慢,这样在未来它还有可能执行某些其他的动作。