漫话ZooKeeper

ZooKeeper现在越来越火,各种分布式系统中都有它的身影,比如消息系统Kafka和近实时搜索SolrCloud。那ZK到底是做什么用的呢?按照ZK官网的话,它是分布式应用程序的分布式协调服务。听上去很玄,为什么分布式应用还需要一个协调服务呢?

协调

我们还是先来看看,分布式应用本身有什么问题需要解决。比如说我们的应用有N个节点,某个节点突然出现了故障,但其他节点却并不知情,只有跟它通信时才发现异常,这时候可能就有些晚了。于是问题来了,怎么才能提前让每个节点都知道其他所有节点的状态呢。难道让每个节点去轮询所有其他节点不成?

......

或者,让所有节点把状态放到一个公共的地方怎么样?没错!在单机时代我们不就是用共享内存来跨进程通信的吗。只不过在分布式时代,我们需要的不是一块内存,而是另一个节点,一个可以被所有节点访问的,更加可靠的节点。是的,ZK就是可以解决这样问题的节点。

当然,ZooKeeper能做的不仅于此,它很复杂很强大。但如果让我用一句话来概括ZooKeeper,我觉得它就是分布式时代的那块共享内存,一个可监听的,像文件系统的,分布式的共享内存。

观察者模式

熟悉设计模式的朋友对此肯定不陌生,它非常基础和高效,多见于各种框架设计中。而它在ZK中被发挥得淋漓尽致。

还是刚才那个例子,ZK提供一个共享列表,每个节点都在其中写下自己的名字,如果有节点出现故障,ZK通过心跳机制来移除故障节点。好了,ZK的优势在于,每个节点并不需要去轮询(这样未免太低效了),而可以监听这个共享列表,列表的任何变化,ZK都会自动通知给每个节点。于是,本来复杂繁琐的N个节点通信问题,被观察者模式优雅的化解了。

这样的应用可以有很多。比方说,N个节点需要保持一致的配置,可以让每个节点监听ZK的一个共享配置,如有变化就更新自身配置即可。那么对于这N个节点集群的配置维护,就变成了对ZK上一个共享配置的维护了。

文件系统

我们已经提到了不同的应用场景,那么ZK中当然也不只有一块共享区域。事实上,ZK有命名空间的概念,而且它更进一步地实现了一个内存文件系统。内存,当然是因为速度快,而文件系统,则是让ZK更通用和强大一个利器。

ZK中的命名空间就是通过文件系统来实现的。这个文件系统的节点比较特殊,它可以用路径名来区分命名空间,还可以在节点上存放少量的数据,比如状态信息、配置信息等等,相当于既是目录也是文件。上一节提到的观察者模式,其实就是对这个文件系统中一个节点的监听。

ZK的文件系统十分强大。它不仅为每个节点提供了版本号、时间戳和访问控制,还提供一种所谓瞬时节点,它只有在会话保持时存在,会话中断时消失。我们之前提到ZK通过心跳机制来移除故障节点,就是利用了这种节点。

分布式

既然ZK是用来协调分布式系统的,大家都和它通信,那么其自身的可用性就变得十分重要。自然而然的,ZK也用分布式来提高自身的可用性。

其实这分布式真是ZK的难点所在,值得大书特书。因为上面的这些功能到了分布式环境中,就会碰到各种各样的问题。比如分布式文件系统如何保持一致性就是个复杂的问题。

ZK的办法是实现了一个叫做ZAB的选举协议。它先在所有内部节点中推举产生一个Leader节点,所有其他节点成为它的Follower。之后所有的写操作都转交给这个Leader节点处理,这样能够保证写请求按到达顺序生效。而Leader节点并不直接完成写入,而是通过所谓二步提交,先将这个写入提议广播给ZK内部其他节点,在得到半数以上节点响应后,再自己把写入生效,并广播给其他节点,最后客户端得到写入成功的通知。

这样复杂的过程虽然牺牲了一定的性能,但保证了几个关键的特性:

ZK内部的每个节点上内存里都有一个文件系统的副本,ZAB协议便是保持了这些副本的一致性。由于客户端可以从任意一个副本读取,因此ZK可以提供极高的读取性能。而为了保证故障后的恢复,ZK在写入时,其实不是直接写入内存,而是先写入磁盘,这也意味着写入速度会更慢,因此ZK在读远大于写的情况下会表现得更好。

关于ZooKeeper的更多内容,请参见官方文档