ZooKeeper是一个分布式协作服务,用于实现锁和并发事务排队等协作原语。ZooKeeper 的一个优势是可伸缩性(Scalability)。五台或七台机器集群可以满足很多大型应用的需求。

最近我们给ZooKeeper增加了一个新的特性,进一步增强了它可伸缩性——一种新的称为 Observers 的服务器。在这篇文章中,我想要介绍一下添加这个特征的动机,并解释这个服务器如何帮助我们的系统更具有可伸缩性。可伸缩性对不同的人意味着不同的事情,而在这里是说,如果我们的工作负载可以通过给系统分配更多的资源来分担,那么这个系统就是可伸缩的;一个不可伸缩的系统却无法通过增加资源来提升性能,甚至会在工作负载增加时,性能急剧下降。

要了解 Observers 为何能影响 ZooKeeper 的可伸缩性,我们需要首先了解一下这个服务时如何工作的。宽泛地说,ZooKeeper 集群上的任何操作不是读操作就是写操作。ZooKeeper 确保所有读和写操作在所有客户端看来,都具有完全相同的顺序,这样他们就不会为操作的顺序而疑惑了。

在提供强一致性保障的同时,ZooKeeper同时给出高可用性承诺,这可以被简单地解释为它可以在多台服务器失效的情况下仍然为客户端提供服务。ZooKeeper使用一个传统的手段来达到可用性——通过将数据读写分布到几台机器上来实现,这样如果一台失效了,其它的可以接管它的服务,而无需让客户端更聪明。

然而,一致性和可用性这两个属性是很难同时达到的,目前,ZooKeeper 必须确保集群中的每个副本都对读写操作是顺序性的。它通过一个一致性协议来达到这一目标。简单地说,这个协议由一个选定的领导者将新操作高速其他服务器,所有节点投票支持并反馈给领导。一旦领导节点收集到过半数的投票,它就认为投票已经获得了通过,并将进一步消息传送给服务器们,以使他们可以继续工作,将操作提交到内存中。

这个从始至终的数据流如下图所示。客户进程将一个值提交给它连接的服务器。服务器将消息转送给领导节点,它发起这个一致性协议,一旦最初的服务器从领导节点得到结果,它就可以返回给用户了。


图1:简化的写请求工作流

Observers 的需求源于 ZooKeeper 服务器在上述协议中实际扮演了两个角色。它们从客户端接受连接与操作请求,之后对操作结果进行投票。这两个职能在 ZooKeeper集群扩展的时候彼此制约。如果我们希望增加 ZooKeeper 集群服务的客户数量(我们经常考虑到有上万个客户端的情况),那么我们必须增加服务器的数量,来支持这么多的客户端。然而,从一致性协议的描述可以看到,增加服务器的数量增加了对协议的投票部分的压力。领导节点必须等待集群中过半数的服务器响应投票。于是,节点的增加使得部分计算机运行较慢,从而拖慢整个投票过程的可能性也随之提高,投票操作的会随之下降。这正是我们在实际操作中看到的问题——随着 ZooKeeper 集群变大,投票操作的吞吐量会下降。

所以,这让我们不得不在增加客户节点数量的期望和我们希望保持较好吞吐性能的期望间进行权衡。要打破这一耦合关系,我们引入了不参与投票的服务器,称为 Observers。 Observers 可以接受客户端的连接,将写请求转发给领导节点。但是,领导节点不会要求 Observers 参加投票。相反,Observers 不参与投票过程,仅仅在上述第3歩那样,和其他服务节点一起得到投票结果。

这个简单的扩展给 ZooKeeper 的可伸缩性带来了全新的镜像。我们现在可以加入很多 Observers 节点,而无须担心严重影响写吞吐量。规模伸缩并非无懈可击——协议中的一歩(通知阶段)仍然与服务器的数量呈线性关系。但是,这里的穿行开销非常低。我们可以认为在通知服务器阶段的开销无法成为主要瓶颈。


图2: Observers 写吞吐量 Benchmark

图2 显示了一个简单评测的结果。纵轴是我能够从一个单一的客户端发出的每秒钟同步写操作的数量(一个调优的 ZooKeeper 可以得到更好的每秒钟操作数——这里我们更感兴趣的是相对值)。横轴是 ZooKeeper 集群的尺寸。蓝色的是每个服务器都是 voting 服务器的情况,而绿色的则只有三个是 voting 服务器,其它都是 Observers。图中显示,我们扩充 Observers,写性能几乎可以保持不便,但如果同时扩展 voting 节点的数量的话,性能会明显下降。显然 Observers 是有效的。

Observers 同样提升读性能的可伸缩性

增加客户端的数量是 Observers 的一个重要用例,但是实际上它们还给集群带来很多其它的好处。

作为一个优化,ZooKeeper 服务器可以直接读取它们的本地数据存储,而无需经过投票过程,这面临一定的“时光旅行”风险,可能在读到新值之后又读到老值,但这只在服务器故障时才会发生。事实上,在这种情况下客户端可以请求一个 ‘sync’ 操作来保证下一个值是最新的。

因此,在大量读操作的工作负载下,Observers 是个巨大的性能提升。写操作直接进入标准的投票路径,这样,与客户端可扩展性类似,提高投票服务器数量来承担读操作会影响写性能。Observers 允许我们将读性能和写性能分开。这适用于 ZooKeeper 的很多应用场景,大部分客户端很少写,但经常读。

Observers 提供了广域网能力

Observers 还能做更多。Observers 对于跨广域网连接的客户端来说是很好的候选方案。这有三个原因。为了获得很好的读性能,有必要让客户端离服务器尽量近,这样往返时延不会太高。然而,将 ZooKeeper 集群分散到两个集群是非常不可取的设计,因为良好配置的 ZooKeeper 应该让投票服务器间用低时延连接互联——否则我们将会遇到上面提到的低反映速度的问题。

而 Observers 可以被部署在需要访问 ZooKeeper 的任意数据中心中。这样,投票协议不会受到数据中心间链路的高时延的影响,性能得到提升。投票过程中 Observers 和领导节点间的消息远少于投票服务器和领导节点间的消息。这有助于在远程数据中心高写负载的情况下降低带宽需求。

最后,由于Observers即使失效也不会影响到投票集群,这样如果数据中心间链路发生故障,不会影响到服务本身的可用性。这种故障的发生概率要远高于一个数据中心中机架间的连接的故障概率,所以不依赖于这种链路是个优点。

如何开始使用 Observers

Observers 还没有成为某个 ZooKeeper release 的一部分,所以要使用它,你需要从 Subversion trunk 获取源代码。

下面的内容提取自 Observers 用户手册,可以在源代码的 docs/zooKeeperObservers.html 文件中看到。

如何使用 Observers

注意,在ZOOKEEPER-578 解决之前,你必须在每个服务器的配置文件中设置 electionAlg=0 。否则当你启动服务的时候会抛出一个异常。

原因:因为 Observers 并不参与领导节点的选举,它们依赖于投票 Followers 来获知领导的变动。目前,只有基本选举算法启动一个线程来响应 Observers 确定当前领导的请求。其他 JIRA 上的工作将会让其他所有的领导选举协议都支持这一功能。

设置 ZooKeeper 使用 Observers 非常简单,只需要在配置文件中有两处改动。首先是每个 Observer 的配置文件中都要有这么一行:

peerType=observer

这行让服务器作为一个 Observer 来工作。之后,在每个服务器配置文件中,你必须在服务器定义行给每个 Observer 加入 :observer  。比如:

server.1:localhost:2181:3181:observer

这让每个其他服务器知道 server.1 是一个 Observer,就不会期望它进行投票了。这就是要加入一个 Observer 的时候,所有你需要做的配置。现在可以将它作为一个正常的 Follower 来看待了。可以这么试试:

bin/zkCli.sh -server localhost:2181

这里 localhost:2181 是 Observer 在每个配置文件中指定的主机名和端口号。你应该看到命令行提示了,这时就可以查询 Zookeeper 服务了。

下一步工作

Observers 特性还有很多工作要做。短期内,我们将致力于让 Observers 与 ZooKeeper 中的所有领导选举算法完全兼容 —— 我们希望这个可以在未来几天内完成。长期看,我们希望能研究一下进行性能优化,比如基于 Observers 的集群的批量和离线读取,来更好的利用 Observers 不像一般 ZooKeeper 服务器一样严格要求时延的好处。


Logo

权威|前沿|技术|干货|国内首个API全生命周期开发者社区

更多推荐