ZooKeeper是一个高可用且可靠的协调系统。分布式应用程序使用 ZooKeeper 来存储和调解更新的关键配置信息。ZooKeeper 可用于领导选举、组成员资格和配置维护。另外可以使用ZooKeeper用于事件通知、锁定和作为优先队列机制。它是分布式系统的一种中枢神经系统,其中大脑的作用由协调服务扮演,轴突是网络,过程是被监控和控制的身体部位,事件是用于传递信息的激素和神经递质。每个复杂的分布式应用程序都需要某种协调和编排系统,因此雅虎的 ZooKeeper 人员决定构建一个好的应用程序并将其开源供所有人使用。
ZooKeeper 的目标市场是在数据中心运行的多主机、多进程 C 和 Java 系统。ZooKeeper 使用分布式进程通过一个共享的分层命名空间来相互协调,该命名空间以文件系统为模型。数据保存在内存中并备份到日志中以确保可靠性。通过使用内存,ZooKeeper 速度非常快,并且可以处理通常在大量进程之间的繁琐协调协议中看到的高负载。使用基于内存的系统还意味着您受到内存中可以容纳的数据量的限制,因此它不能用作通用数据存储。它旨在存储少量配置信息而不是大块。复制用于可扩展性和可靠性,这意味着它更喜欢基于大量读取的应用程序。在典型的分层系统中,您可以在树的任何点添加节点、获取树中的条目列表、获取与条目关联的值,并在条目更改或消失时获得通知。使用这些原语和一点点肘部油脂,您可以构建上面提到的更高级别的服务。
为什么需要分布式协调系统?听起来有点奇怪。这更多是我将在这篇文章中解决的问题,而不是它是如何工作的,因为幻灯片和视频在高层次上很好地解释了 ZooKeeper 可以做什么。然而,低级细节可以使用另一篇论文。据报道,它使用了著名的Paxos 算法的一个版本,以在面对最令人生畏的故障时保持副本的一致性。真正缺少的是展示如何在自己的系统中使用协调服务的动机,这就是我希望提供的......
Kevin Burton 希望使用 ZooKeeper为他的Spinn3r博客索引 Web 服务配置 Munin 和 Ganglia 等外部监控系统。他建议每个服务在树“/services/www”下的 ZooKeeper 中注册它在集群中的存在。Munin 配置程序将在该节点上添加ZooKeeper Watch,以便在 /services/www 下的服务列表发生更改时收到通知。当 Munin 配置程序收到更改通知时,它会读取服务列表并自动为该服务重新生成munin.conf文件。为什么不简单地使用数据库?由于 ZooKeeper 对其服务的保证:
- 手表是根据其他事件、其他手表和异步回复进行排序的。ZooKeeper 客户端库确保按顺序分派所有内容。
- 在看到对应于该 znode 的新数据之前,客户端将看到它正在监视的 znode 的监视事件。
- 来自 ZooKeeper 的观察事件的顺序与 ZooKeeper 服务看到的更新顺序相对应。
您无法从数据库顶部的事件系统中获得这些保证,而这些是您在连接断开、节点、失败、重新传输和混乱统治的复杂分布式系统中所需的那种保证。晚上的规则太可怕了,无法考虑。例如,重要的是在服务停止后看到服务启动事件,否则您可能会由于事件无序问题而不必要地放弃产生收入的工作。并不是说我对此一无所知:-)
ZooKeeper 的一个弱点是发生的更改会被删除:因为 watch 是一次性触发器,并且在获取事件和发送新请求以获取 watch 之间存在延迟,所以您无法可靠地看到 ZooKeeper 中节点发生的每个更改。准备好处理 znode 在获取事件和再次设置 watch 之间多次更改的情况。(你可能不在乎,但至少意识到它可能会发生。)
这意味着 ZooKeeper 是一个基于状态的系统,而不是一个事件系统。监视被设置为获取数据的副作用,因此您将始终拥有有效的初始状态,并且在任何后续更改事件中,您将刷新以获取新值。例如,如果您想使用事件来记录更改的时间和方式,那么您就不能这样做。您必须在数据本身中包含更改历史记录。
让我们看一下 ZooKeeper 可能有用的另一个例子。想象一个复杂的后端系统,比方说,一个数据中心有 100 个节点(可能少很多,也可能多很多)。例如,假设该系统是一个用于向网站提供广告的广告系统。广告系统是复杂的野兽,需要相当多的协调。想象一下需要在这 100 个节点上运行的所有子系统:数据库、监控、欺诈检测器、信标服务器、Web 服务器事件日志处理器、故障转移服务器、客户仪表板、目标引擎、活动规划器、活动场景测试器、升级、安装、媒体管理器, 等等。有很多事情要发生。
现在想象一下数据中心的电源翻转并且所有机器都开启。跨所有主机的所有进程如何知道要做什么?现在想象一下,一切正常,几台机器停机。在这种情况下,所有进程如何知道该怎么做?这就是协调服务的用武之地。协调服务充当底板,所有这些子系统在底板上确定它们相对于产品中的所有其他子系统应该做什么。
例如,广告服务器如何知道要使用哪个数据库?显然,解决这个问题有很多选择。使用标准的 DNS 命名约定就是其中之一。配置文件是另一个。硬编码仍然是最受欢迎的。使用引导服务定位器服务是另一种(假设您可以引导引导服务)。理想情况下,任何解决方案都必须在单元测试、系统测试和部署期间同样有效。在这种情况下,ZooKeeper 充当服务定位器。每个进程都会转到 ZooKeeper 并找出哪个是主数据库。如果选择了一个新的主数据库,比如说因为主机故障,那么 ZooKeeper 会发送一个事件,允许依赖于该数据库的每个人通过获取新的主数据库来做出反应。在应用程序代码中使用复杂的重试逻辑来故障转移到另一个数据库服务器简直是一场灾难,因为每个程序员都会以自己的方式把它搞砸。使用协调服务可以很好地解决所有场景中服务定位其他服务的问题。当然,使用像这样的代理MySQL 代理将在处理故障转移和请求路由时消除更多的应用程序级复杂性。
数据库服务器最初是如何决定它们将扮演哪个角色的?所有数据库服务器都启动并说“我是一个勇敢而强大的数据库服务器,我在生活中的角色是什么?我是主服务器还是辅助服务器?或者如果我是分片,我服务于哪个键范围?” 如果 10 个服务器是数据库服务器,则协商角色可能是一个非常复杂且容易出错的过程。通过在配置文件中指定故障转移环的配置文件的声明性方法是一种很好的方法,但在本地开发和测试环境中工作是一种痛苦,因为机器总是在变化。更容易让数据库服务器在初始角色选举和失败情况下启动并自我组织。该系统的优势在于,它可以轻松地在数据中心的一台机器或十几台机器上本地运行。ZooKeeper 支持这种类型的协调行为。
现在假设我想更改当前在 40 个不同主机上的 40 个进程中运行的广告定位状态机的配置。我怎么做?第一种方法是没有方法。大多数系统都这样做,因此必须发布新代码,这非常慢。另一种方法是配置文件。配置放在一个分发包中并推送到所有节点。每个进程然后定期检查以查看配置文件是否已更改以及是否已读取新配置。这就是基础知识。可以跟随无限变化。您可以对不同的子系统进行配置。这很复杂,因为您必须知道哪些程序包在哪些节点上运行。您必须处理回滚,以防所有包都没有正确推送。你必须改变配置,制作一个包,测试它,然后将其推送给数据中心运营团队,他们可能需要一段时间来执行升级。这是一个缓慢的过程。当您进行影响多个子系统的更改时,它会变得更加复杂。
我采用的另一种方法是在每个进程中嵌入一个 Web 服务器,以便您可以查看指标并动态更改每个进程的配置。虽然对于单个流程来说功能强大,但使用这种方法在数据中心内操作多组流程更加困难。
使用 ZooKeeper,我可以将我的状态机定义存储为一个节点,该节点是从从产品中每个分发包收集的静态配置中加载的。依赖于该节点的每个进程在最初读取状态机时都可以注册为观察者。当状态机更新时,所有依赖实体都会收到一个事件,导致它们将状态机重新加载到进程中。简单明了。所有进程最终都会得到更改,任何重新启动的进程都会在初始化时获取新的状态机。一种可靠且集中控制大型分布式应用程序的非常酷的方法。
一个警告是我在 ZooKeeper 论坛上没有看到太多活动。确实被问到的问题基本上没有答案。在考虑这样一个关键的基础设施时,这不是一个好兆头。
另一个初读时可能不太明显的警告是,您使用 ZooKeeper 的应用程序状态机必须与 ZooKeeper 的状态机密切相关。例如,当 ZooKeeper 服务器死机时,您的应用程序必须处理该事件并在新服务器上重新建立所有监视。当观察事件到来时,您的应用程序必须处理该事件并设置新的观察。执行锁和队列等更高级别操作的算法由必须由应用程序正确管理的多步状态机驱动。由于 ZooKeeper 处理可能存储在您的应用程序中的状态,因此担心线程安全很重要。ZooKeeper 线程的回调可以访问共享数据结构。
一些快速的事实
- 数据如何跨多台机器分区?在内存中完成复制。(是的,这是限制性的)
- 更新如何发生(跨机器交互)?所有更新都流经主节点,并在
仲裁确认更新时被视为完成。 - 读取是如何发生的(是否可能获得过时的副本)?读取到集群的任何成员。是的,可以
退回陈旧的副本。然而,通常情况下,这些都非常新鲜。 - 一个领导的责任是什么?为所有更新分配序列号并确认法定人数已收到更新。
- 这种架构有几个突出的限制:
- 完全复制限制了可以
使用 Zookeeper 管理的数据的总大小。这在某些应用中是可以接受的,而在其他应用中是不可接受的。在 Zookeeper 的原始领域(配置和状态的管理)中,这不是问题,但 Zookeeper 足以鼓励创造性滥用,这可能会成为问题。
- 通过单个领导序列化所有更新可能是性能
瓶颈。另一方面,可以
通过领导者和相关的法定人数每秒推送 50K 更新,因此这个限制
非常高。
- 数据存储模型是完全非关系的。