<audio id="audio" title="16 | ZooKeeper是如何保证数据一致性的？" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/7e/ef/7e2d9bd920765fc14fd39499e8ac1aef.mp3"></audio>

你可能还记得，我们在讲HDFS和HBase架构分析时都提到了ZooKeeper。在分布式系统里的多台服务器要对数据状态达成一致，其实是一件很有难度和挑战的事情，因为服务器集群环境的软硬件故障随时会发生，多台服务器对一个数据的记录保持一致，需要一些技巧和设计。

这也就是我们今天要讨论的分布式系统一致性与ZooKeeper的架构。

在讲分布式系统一致性前，我们先回顾一下HDFS。HDFS为了保证整个集群的高可用，需要部署两台NameNode服务器，一台作为主服务器，一台作为从服务器。当主服务器不可用的时候，就切换到从服务器上访问。但是如果不同的应用程序（Client）或者DataNode做出的关于主服务器是否可用的判断不同，那么就会导致HDFS集群混乱。

比如两个应用程序都需要对一个文件路径进行写操作，但是如果两个应用程序对于哪台服务器是主服务器的判断不同，就会分别连接到两个不同的NameNode上，并都得到了对同一个文件路径的写操作权限，这样就会引起文件数据冲突，同一个文件指向了两份不同的数据。

这种不同主服务器做出不同的响应，在分布式系统中被称作“脑裂”。光看这个词你也可以看出问题的严重性，这时候集群处于混乱状态，根本无法使用。那我们引入一个专门进行判断的服务器当“裁判”，让“裁判”决定哪个服务器是主服务器不就完事了吗？

但是这个做出判断决策的服务器也有可能会出现故障不可访问，同样整个服务器集群也不能正常运行。所以这个做出判断决策的服务器必须由多台服务器组成，来保证高可用，任意一台服务器宕机都不会影响系统的可用性。

那么问题又来了，这几台做出判断决策的服务器又如何防止“脑裂”，自己不会出现混乱状态呢？有时候真的很无奈，分布式系统设计就像是一个追着自己尾巴咬的喵喵，兜兜转转回到开头。

但是问题必须还要解决，我们比较常用的多台服务器状态一致性的解决方案就是ZooKeeper。

## 分布式一致性原理

讲分布式一致性时，相信你肯定多少听过著名的CAP原理。CAP原理认为，一个提供数据服务的分布式系统无法同时满足数据**一致性**（Consistency）、**可用性**（Availibility）、**分区耐受性**（Patition Tolerance）这三个条件，如下图所示。

<img src="https://static001.geekbang.org/resource/image/71/c0/711e087cf895cc58e02d19dcaafb44c0.jpg" alt="">

一致性是说，每次读取的数据都应该是最近写入的数据或者返回一个错误（Every read receives the most recent write or an error），而不是过期数据，也就是说，数据是一致的。

可用性是说，每次请求都应该得到一个响应，而不是返回一个错误或者失去响应，不过这个响应不需要保证数据是最近写入的（Every request receives a (non-error) response, without the guarantee that it contains the most recent write），也就是说系统需要一直都是可以正常使用的，不会引起调用者的异常，但是并不保证响应的数据是最新的。

分区耐受性是说，即使因为网络原因，部分服务器节点之间消息丢失或者延迟了，系统依然应该是可以操作的（The system continues to operate despite an arbitrary number of messages being dropped (or delayed) by the network between nodes）。

当网络分区失效发生的时候，我们要么取消操作，这样数据就是一致的，但是系统却不可用；要么我们继续写入数据，但是数据的一致性就得不到保证。

对于一个分布式系统而言，网络失效一定会发生，也就是说，分区耐受性是必须要保证的，那么在可用性和一致性上就必须二选一。当网络分区失效，也就是网络不可用的时候，如果选择了一致性，系统就可能返回一个错误码或者干脆超时，即系统不可用。如果选择了可用性，那么系统总是可以返回一个数据，但是并不能保证这个数据是最新的。

所以，关于CAP原理，更准确的说法是，在分布式系统必须要满足分区耐受性的前提下，可用性和一致性无法同时满足。

## Paxos算法与ZooKeeper架构

ZooKeeper主要提供数据的一致性服务，其实现分布式系统的状态一致性依赖一个叫Paxos的算法。Paxos算法在多台服务器通过内部的投票表决机制决定一个数据的更新与写入。Paxos的基本思路请看下面的图。

<img src="https://static001.geekbang.org/resource/image/96/2f/96c6615c359f79922b9b087f6be4172f.png" alt="">

应用程序连接到任意一台服务器后提起状态修改请求（也可以是获得某个状态锁的请求），从图上看也就是服务器1，会将这个请求发送给集群中其他服务器进行表决。如果某个服务器同时收到了另一个应用程序同样的修改请求，它可能会拒绝服务器1的表决，并且自己也发起一个同样的表决请求，那么其他服务器就会根据时间戳和服务器排序规则进行表决。

表决结果会发送给其他所有服务器，最终发起表决的服务器也就是服务器1，会根据收到的表决结果决定该修改请求是否可以执行，事实上，只有在收到多数表决同意的情况下才会决定执行。当有多个请求同时修改某个数据的情况下，服务器的表决机制保证只有一个请求会通过执行，从而保证了数据的一致性。

ZooKeeper作为一个数据一致性解决方案产品，事实上是牺牲了部分可用性，换来的数据一致性。在Paxos算法中，如果某个应用程序连接到一台服务器，但是这台服务器和其他服务器的网络连接出现问题，那么这台服务器将返回一个错误，要求应用程序重新请求。

ZooKeeper通过一种树状结构记录数据，如下图所示。

<img src="https://static001.geekbang.org/resource/image/76/5f/76526be77b0026a0c3b2d661d362665f.png" alt="">

应用程序可以通过路径的方式访问ZooKeeper中的数据，比如/services/YaView/services/stupidname这样的路径方式修改、读取数据。ZooKeeper还支持监听模式，当数据发生改变的时候，通知应用程序。

因为大数据系统通常都是主从架构，主服务器管理集群的状态和元信息（meta-info），为了保证集群状态一致防止“脑裂”，所以运行期只能有一个主服务器工作（active master），但是为了保证高可用，必须有另一个主服务器保持热备（standby master）。那么应用程序和集群其他服务器如何才能知道当前哪个服务器是实际工作的主服务器呢？

所以很多大数据系统都依赖ZooKeeper提供的一致性数据服务，用于选举集群当前工作的主服务器。一台主服务器启动后向ZooKeeper注册自己为当前工作的主服务器，因此另一台服务器就只能注册为热备主服务器，应用程序运行期都和当前工作的主服务器通信。

如果当前工作的主服务器宕机（在ZooKeeper上记录的心跳数据不再更新），热备主服务器通过ZooKeeper的监控机制发现当前工作的主服务器宕机，就向ZooKeeper注册自己成为当前工作的主服务器。应用程序和集群其他服务器跟新的主服务器通信，保证系统正常运行。

因为ZooKeeper系统的多台服务器存储相同数据，并且每次数据更新都要所有服务器投票表决，所以和一般的分布式系统相反，ZooKeeper集群的性能会随着服务器数量的增加而下降。

<img src="https://static001.geekbang.org/resource/image/0f/53/0f9b13ac86458209f2755bda76e0c653.png" alt="">

ZooKeeper通过Paxos选举算法实现数据强一致性，并为各种大数据系统提供主服务器选举服务。虽然ZooKeeper并没有什么特别强大的功能，但是在各类分布式系统和大数据系统中，ZooKeeper的出镜率非常高，因此也是很多系统的基础设施。

## 小结

如果我们单独看大数据和分布式系统的很多解决方案，如果不把它们放在大规模数据和大规模服务器集群的场景下思考，可能会觉得很多问题和方案都很莫名其妙。比如要保证分布式系统中数据的一致性，才诞生了Paxos这样专门的算法和ZooKeeper这样的产品。

Paxos算法只考虑所有服务器都是可信任的情况。但在分布式系统中还有一类场景，需要考虑当集群中的服务器存在恶意服务器的情况。当这些恶意服务器企图篡改伪造数据，或者传递虚假信息的时候，如何保证系统继续有效运行呢？比如目前非常火的区块链，就需要考虑这种场景。

区块链采取的解决方案是工作量证明。一台服务器要想在分布式集群中记录数据（即所谓分布式记账），必须进行一个规模庞大的计算，比如计算一个256 Bit的hash值，这个值的前若干位必须为0。比特币区块链就是采用类似这样的工作量证明算法，为了进行这样的hash计算，目前比特币区块链消耗的电量相当于一个中等规模国家的用电量。

通过这种工作量证明方式，保证了恶意服务器要想伪造篡改数据，必须拥有强大的计算能力（占整个集群服务器计算能力的51%以上），而只要我们认为大多数服务器是善意的，那么这样的区块链分布式集群就是可靠的。

## 思考题

除了工作量证明，还有什么方法可以保证分布式系统中不可信任的机器无法篡改或伪造数据？

欢迎你写下自己的思考或疑问，与我和其他同学一起讨论。
