如何在多区域运行Zookeeper?- Ankur


Zookeeper将自己定义为“用于维护配置信息的集中式服务”等。为了对数据建模,它使用具有路径作为标识符并保存值的znode。在 Flipkart,我们使用 zookeeper 作为为多个应用程序提供配置管理的平台的首选后端。代表每个配置对象的 znode 存储一个小的 JSON 有效负载。该平台还将配置更改传播给感兴趣的客户端,并在时间敏感的用例中影响他们的运行时行为。
在 Flipkart,我们经常以主-主模式部署应用程序,为来自多个区域的用户或内部流量提供服务。这确保了我们对一个数据中心或可用性区域的故障保持弹性,以便我们可以继续为我们的客户和卖家提供服务。支持这种持续可用的应用程序通常需要我们的平台提供全局配置对象,即可用于跨区域读取和写入。
在本文中,我们首先对 Zookeeper 的相关功能和事实进行说明性的回顾。我们进一步描述了一组经过编译的最佳实践,这些实践在 Flipkart 部署时对我们很有效,以确保以下方面:

  1. 跨区域强一致性写入:Zookeeper 在多区域仲裁中实现协调。
  2. 处理单区域故障或链接故障:即使一个区域完全宕机或暂时与其他区域断开连接,Zookeeper 仍可用于其他区域的读写。
  3. 在隔离区域中保持读取可用:如果一个区域与其他区域隔离(多个链接故障),Zookeeper 可用于读取。

 
Zookeeper:说明性的回顾

  • 从内存中提供服务的 KV 存储

Zookeeper 只能充当小数据集的 KV 存储,因为无论集群中的实例数量如何,整个数据都必须适合单个实例的内存。另一方面是读取速度很快。
  • 任何写操作都需要多数人的确认

写入必须由至少 1 个从节点跟随者确认(因为领导者无论如何都会处理写入请求)。从用户的角度来看,这可能会导致其他跟随者在一段时间内提供陈旧数据。您可以在此处阅读有关 zookeeper 提供的顺序读取一致性的更多信息。
  • 写入是有序的

每个 znode 都有一个版本,该版本会在成功写入操作时更新。该版本用于检测对 znode 进行的任何并发修改。Zookeeper 拒绝这样的写提议并确保一致性。
  • 支持在znodes增加watch

它支持在 Zookeeper 之上构建变更传播系统,同时不需要客户端不断轮询。客户端使用 znode 路径和已知版本与 Zookeeper 集群中的任何实例连接以设置监视。每当 znode 更新/删除或创建时(如果 znode 不存在),服务器都会通知客户端。
 
多区域 Zookeeper:什么、为什么和如何
我们将多区域 zookeeper 定义为一种部署,其中集群的投票成员分布在多个区域。它允许我们为主动-主动应用程序提供集中配置,并使平台免受数据中心/区域故障和网络分区的影响。
在本节中,我们将介绍在生产环境中跨区域部署 zookeeper 时的一些有用的最佳实践。
  • 跨至少 3 个区域部署实例

Zookeeper 是一个基于共识的数据存储。该集群由参与法定人数的投票成员和可选的非投票成员组成,稍后将介绍这些成员。一旦被大多数投票成员在内部确认,写入就会成功地向客户端确认。这意味着只要 Zookeeper 集群中有 (n/2) + 1 个可用实例,它就仍然可用于写入和读取。
通常,zookeeper 与大小为 5 或 7 的集合一起运行。对于多区域 zookeeper,如果将 5 个实例公平地分布在两个区域中,则意味着在区域 1 (R1) 中部署了 3 个实例,在 R2 中部署了 2 个实例。这种设置有一个主要缺点。如果 R1(拥有集群的大多数实例)出现链路故障或区域故障,区域 R2 中的客户端将无法使用,因为它们无法执行写入或读取操作。
如果 zookeeper 实例部署在三个区域,则即使单个区域故障(任何)或三个区域之间的任何两个区域之间的链接故障,也可以保持对客户端可用。
对于大小为 5 的集群,这意味着在 R1 中部署 2 个实例,在 R2 中部署 2 个实例,在 R3 中部署 1 个实例。
任何区域都不应该拥有多数:经验法则是任何一个区域都不应该拥有多数实例。这确保了可用性,尽管区域或链路故障。对于 size=7 节点的集合,一些可行的放置策略是:
3,3,1
3,2,2

 
避免跨区域客户端连接
客户端可以连接到任何 zookeeper 实例以进行读取和写入。当写入请求被转发到领导者时,读取是由实例从其内存缓存中提供的。
如果客户端最终连接到不在同一可用性区域中的 zookeeper 实例,则每个请求/响应都会产生跨区域延迟。
这可以通过确保可用性区域中的客户端仅发现多区域 zookeeper 集群的本地实例来避免(假设 zookeeper 在该区域中有实例)。一种方法是为 zookeeper 集群保留特定于区域的域。
警告:如果存在局部灾难或网络分区导致本地 zookeeper 实例无法访问,则部署在该区域的客户端将无法使用,除非他们可以发现其他区域的 zookeeper 实例。
 
使用观察者扩展读取
如果不首选跨区域客户端连接,则意味着在跨 3 个区域的 5 个实例集群中,对于任何读/写请求,客户端只能连接到 2 个实例。由于zookeeper 通常用于读取密集型工作负载,因此通常需要将“get”和“watch”请求扩展到超出现有zookeeper 实例所能提供的范围。
观察者通过充当集合的非投票成员来实现这一点,并且可以为客户端的任何读取请求提供最终一致的保证,与投票成员提供的保证相同。主要区别在于观察员不参与提案的法定人数。
 
启用zookeeper读取模式
当一个 zookeeper 实例从集群的其余部分被分区时,它不提供读取或写入服务,并且客户端断开连接。我们使用zookeeper作为后端存储的一些应用程序可以在短时间内处理过时读取(虽然不是乱序),但不会影响读取可用性。
在这种情况下,zookeeper 读取模式可以成为救星。如果在 zookeeper 实例上启用,它允许在从集群分区时为连接的客户端提供陈旧的读取服务。当连接恢复时,这个 zookeeper 实例通过客户端设置的watch发送错过的更新。
当您拥有仅跨 2 个区域的多区域 zookeeper 集群时,这尤其方便。启用读取模式允许区域中的 zk 实例可用于读取,即使该区域与其他区域在短时间内分区也是如此。流行的客户端库“curator”提供了一个显式事件,当它们连接到的 zookeeper 实例进入只读模式时,客户端可以侦听该事件以获取通知。
 
配置滴答时间
Tick 时间是 zk 集群中的时间度量单位,直接管理客户端会话处理和实例间的心跳。例如,如果 Zookeeper 集群中的一个实例没有在足够的滴答声中以心跳响应,则认为它不健康/不可用。
虽然默认值 2 秒通常已经足够了,但是根据区域间网络链接的延迟特性,为多区域 zookeeper 调整 `tickTime`。
 
避免写入繁重的工作负载
考虑到如何将写入委托给集群中的领导者,以及每次写入如何需要来自大多数集合的 ack 来维护全局排序,zookeeper 不是针对为主要处理创建/更新/删除的访问模式。
读取请求应该至少比集群中的写入多一个数量级。事实上,我们努力在我们的生产设置中的读取和写入请求数量之间有 2-3 个数量级的差异。在多区域 zookeeper 中,由于增加了跨区域延迟,大量写入降低集群的可能性进一步放大。
Zookeeper 没有内置速率限制。如果您不控制客户端的行为,那么谨慎的做法是使用 API 层与您的客户端进行交互。通过这种方式,您可以控制对 zookeeper 的底层访问,并保护它免受恶意或意外滥用。
 
进入流量优先级结构
这可能并不适用于所有人。如果您拥有私有数据中心并维护/管理网络基础设施以实现跨这些连接的网络基础设施,您很可能会构建或加入流量优先级,以确保关键网络流量获得一定的带宽。这可确保恶意应用程序不会使跨区域网络链接的可用总带宽饱和。
当您运行多区域 zookeeper 时,如果存在流量优先级结构,它们可以确保以更高的优先级处理 zk 实例之间的 chatter 并保证一定的 QoS。
 
概括
一个zookeeper集群,当部署在至少三个区域时,使得大多数实例不在任何一个区域内,在面对网络分区和区域级灾难时,可以确保读写可用性。像这样部署的全局 zookeeper 集群可以充当双活应用程序的底层配置存储。这种跨区域部署还可以利用:

  • Zookeeper 读取模式可确保更高的读取可用性
  • 用于扩展读取的观察器
  • 降低请求/响应延迟的本地实例发现