简介

在计算机诞⽣之后很长的⼀段时间⾥,⼀个应⽤服务是在⼀个独⽴的单处理器计算机上运⾏⼀段程序。时⾄今⽇,应⽤服务已经发⽣了很⼤的变化。

在⼤数据和云计算盛⾏的今天,应⽤服务由很多个独⽴的程序组成,这些独⽴的程序则运⾏在形形⾊⾊、千变万化的⼀组计算机上。

开发这样的应⽤,很容易让很多开发⼈员陷⼊如何使多个程序协同⼯作的逻辑中,最后导致没有时间更好地思考和实现他们⾃⼰的应⽤程序逻辑;又或者开发⼈员对协同逻辑关注不够,只是⽤很少的时间开发了⼀个简单脆弱的主协调器,导致不可靠的单⼀失效点。

ZooKeeper的设计保证了其健壮性,这就使得应⽤开发⼈员可以更多关注应⽤本⾝的逻辑,⽽不是协同⼯作上。ZooKeeper从⽂件系统API得到启发,提供⼀组简单的API,使得开发⼈员可以实现通⽤的协作任务,包括选举主节点、管理组内成员关系、管理元数据等。ZooKeeper包括⼀个应⽤开发库(主要提供Java和C两种语⾔的API)和⼀个⽤Java实现的服务组件。ZooKeeper的服务组件运⾏在⼀组专⽤服务器之上,保证了⾼容错性和可扩展性。

ZooKeeper的使命

试着说明ZooKeeper能为我们做什么,就像解释螺丝能为我们做什么⼀样。我们可以简单地表述,螺丝⼑可以让我们拧动螺丝。但是这种⽅式并不能完全表达螺丝⼑的能⼒。实际上,螺丝⼑还可以让我们组装各种家具和电⼦设备,甚⾄在某些情况下你还可以⽤它把画挂在墙上。就像螺丝⼑的例⼦⼀样,我们将介绍ZooKeeper能做什么,虽然未必详尽。

协同并不总是采取像群⾸选举或者加锁等同步原语的形式。配置元数据也是⼀个进程通知其他进程需要做什么的⼀种常⽤⽅式。⽐如,在⼀个主-从系统中,从节点需要知道任务已经分配到它们。即使在主节点发⽣崩溃的情况下,这些信息也需要有效。

使用实例

让我们看⼀些ZooKeeper的使⽤实例,以便更直观地理解其⽤处:

  • Apache HBase

HBase是⼀个通常与Hadoop⼀起使⽤的数据存储仓库。在HBase中,ZooKeeper⽤于选举⼀个集群内的主节点,以便跟踪可⽤的服务器,并保存集群的元数据。

  • Apache Kafka

Kafka是⼀个基于发布-订阅(pub-sub)模型的消息系统。其中ZooKeeper⽤于检测崩溃,实现主题(topic)的发现,并保持主题的⽣产和消费状态。

  • Apache Solr

Solr是⼀个企业级的搜索平台。Solr的分布式版本命名为SolrCloud,它使⽤ZooKeeper来存储集群的元数据,并协作更新这些元数据。

  • Yahoo!Fetching Service

Yahoo!Fetching Service是爬⾍实现的⼀部分,通过缓存内容的⽅式⾼效地获取⽹页信息,同时确保满⾜⽹页服务器的管理规则(⽐如robots.txt⽂件)。该服务采⽤ZooKeeper实现主节点选举、崩溃检测和元数据存储。

  • Facebook Messages

Facebook推出的这个应⽤(http://on.fb.me/1a7uViK)集成了email、短信、Facebook聊天和Facebook收件箱等通信通道。该应⽤将ZooKeeper作为控制器,⽤来实现数据分⽚、故障恢复和服务发现等功能。

除了以上介绍的这些应⽤外,还有很多使⽤ZooKeeper的例⼦。

根据这些代表应⽤,我们可以从更抽象的层次上讨论ZooKeeper。当开发⼈员使⽤ZooKeeper进⾏开发时,开发⼈员设计的那些应⽤往往可以看成⼀组连接到ZooKeeper服务器端的客户端,它们通过ZooKeeper的客户端API连接到ZooKeeper服务器端进⾏相应的操作。

Zookeep的客户端API功能强⼤,其中包括:

  • 保障强⼀致性、有序性和持久性。

  • 实现通用的同步原语的能⼒

  • 在实际分布式系统中,并发往往导致不正确的⾏为。ZooKeeper提供了⼀种简单的并发处理机制。

当然,ZooKeeper也并不是万能的,我们还不能让它解决所有问题。对我们来说,更重要的是要了解ZooKeeper为我们提供了什么,并知道如何处理其中的⼀些棘⼿问题。这本书的⽬标之⼀就是讨论如何处理这些问题。

ZooKeeper改变了什么

使⽤ZooKeeper是否意味着需要以全新的⽅式进⾏应⽤程序开发?事实并⾮如此,ZooKeeper实际上简化了开发流程,提供了更加敏捷健壮的⽅案。

ZooKeeper不适⽤的场景

整个ZooKeeper的服务器集群管理着应⽤协作的关键数据。ZooKeeper不适合⽤作海量数据存储。对于需要存储海量的应⽤数据的情况,我们有很多备选⽅案,⽐如说数据库和分布式⽂件系统等。因为不同的应⽤有不同的需求,如对⼀致性和持久性的不同需求,所以在设计应⽤时,最佳实践还是应该将应⽤数据和协同数据独⽴开。

通过ZooKeeper构建分布式系统

对分布式系统的定义有很多,但对于本书的⽬的,我们对分布式系统的定义为:

分布式系统是同时跨越多个物理主机,独⽴运⾏的多个软件组件所组成的系统。

我们采⽤分布式去设计系统有很多原因,分布式系统能够利⽤多处理器的运算能⼒来运⾏组件,⽐如并⾏复制任务。⼀个系统也许由于战略原因,需要分布在不同地点,⽐如⼀个应⽤由多个不同地点的服务器提供服务。

消息延迟

消息传输可能会发⽣任意延迟,⽐如,因为⽹络拥堵。这种任意延迟可能会导致不可预期的后果。⽐如,根据基准时钟,进程P先发送了⼀个消息,之后另⼀个进程Q发送了消息,但是进程Q的消息也许会先完成传送。

处理器性能

操作系统的调度和超载也可能导致消息处理的任意延迟。当⼀个进程向另⼀个进程发送消息时,整个消息的延时时间约等于发送端消耗的时间、传输时间、接收端的处理时间的总和。如果发送或接收过程需要调度时间进⾏处理,消息延时会更⾼。

时钟偏移

使⽤时间概念的系统并不少见,⽐如,确定某⼀时间系统中发⽣了哪些事件。处理器时钟并不可靠,它们之间也会发⽣任意的偏移。因此,依赖处理器时钟也许会导致错误的决策。

⽰例:主-从应⽤

我们从理论上介绍了分布式系统,现在,是时候让它更具体⼀点了。

⼀般在这种架构中,主节点进程负责跟踪从节点状态和任务的有效性,并分配任务到从节点。对ZooKeeper来说,这个架构风格具有代表性,阐述了⼤多数流⾏的任务,如选举主节点,跟踪有效的从节点,维护应⽤元数据。

关键问题

要实现主-从模式的系统,我们必须解决以下三个关键问题:

  • 主节点崩溃

如果主节点发送错误并失效,系统将⽆法分配新的任务或重新分配已失败的任务。

  • 从节点崩溃

如果从节点崩溃,已分配的任务将⽆法完成。

  • 通信故障

如果主节点和从节点之间⽆法进⾏信息交换,从节点将⽆法得知新任务分配给它。

为了处理这些问题,之前的主节点出现问题时,系统需要可靠地选举⼀个新的主节点,判断哪些从节点有效,并判定⼀个从节点的状态相对于系统其他部分是否失效。

我们将会在下⽂中介绍这些任务。

主节点失效

主节点失效时,我们需要有⼀个备份主节点(backup master)。

当主要主节点(primary master)崩溃时,备份主节点接管主要主节点的⾓⾊,进⾏故障转移,然⽽,这并不是简单开始处理进⼊主节点的请求。新的主要主节点需要能够恢复到旧的主要主节点崩溃时的状态。对于主节点状态的可恢复性,我们不能依靠从已经崩溃的主节点来获取这些信息,⽽需要从其他地⽅获取,也就是通过ZooKeeper来获取。

针对这个场景中导致的问题,我们⼀般称之为脑裂(split-brain):系统中两个或者多个部分开始独⽴⼯作,导致整体⾏为不⼀致性。我们需要找出⼀种⽅法来处理主节点失效的情况,关键是我们需要避免发⽣脑裂的情况。

从节点失效

客户端向主节点提交任务,之后主节点将任务派发到有效的从节点中。从节点接收到派发的任务,执⾏完这些任务后会向主节点报告执⾏状态。主节点下⼀步会将执⾏结果通知给客户端。

通信故障

如果⼀个从节点与主节点的⽹络连接断开,⽐如⽹络分区(networkpartition)导致,重新分配⼀个任务可能会导致两个从节点执⾏相同的任务。如果⼀个任务允许多次执⾏,我们在进⾏任务再分配时可以不⽤验证第⼀个从节点是否完成了该任务。如果⼀个任务不允许,那么我们的应⽤需要适应多个从节点执⾏相同任务的可能性。

通信故障导致的另⼀个重要问题是对锁等同步原语的影响。因为节点可能崩溃,⽽系统也可能⽹络分区(network partition),锁机制也会阻⽌任务的继续执⾏。因此ZooKeeper也需要实现处理这些情况的机制。⾸先,客户端可以告诉ZooKeeper某些数据的状态是临时状态(ephemeral);其次,同时ZooKeeper需要客户端定时发送是否存活的通知,如果⼀个客户端未能及时发送通知,那么所有从属于这个客户端的临时状态的数据将全部被删除。通过这两个机制,在崩溃或通信故障发⽣时,我们就可以预防客户端独⽴运⾏⽽发⽣的应⽤宕机。

回想⼀下之前讨论的内容,如果我们不能控制系统中的消息延迟,就不能确定⼀个客户端是崩溃还是运⾏缓慢,因此,当我们猜测⼀个客户端已经崩溃,⽽实际上我们也需要假设客户端仅仅是执⾏缓慢,其在后续还可能执⾏⼀些其他操作。

任务总结

根据之前描述的这些,我们可以得到以下主-从架构的需求:

  • 主节点选举

这是关键的⼀步,使得主节点可以给从节点分配任务。

  • 崩溃检测

主节点必须具有检测从节点崩溃或失去连接的能⼒。

  • 组成员关系管理

主节点必须具有知道哪⼀个从节点可以执⾏任务的能⼒。

  • 元数据管理

主节点和从节点必须具有通过某种可靠的⽅式来保存分配状态和执⾏状态的能⼒。

理想的⽅式是,以上每⼀个任务都需要通过原语的⽅式暴露给应⽤,对开发者完全隐藏实现细节。ZooKeeper提供了实现这些原语的关键机制,因此,开发者可以通过这些实现⼀个最适合他们需求、更加关注应⽤逻辑的分布式应⽤。贯穿本书,我们经常会涉及像主节点选举、崩溃检测这些原语任务的实现,因为这些是建⽴分布式应⽤的具体任务。

分布式协作的难点

当开发分布式应⽤时,其复杂性会⽴即突显出来。

例如,当我们的应⽤启动后,所有不同的进程通过某种⽅法,需要知道应⽤的配置信息,⼀段时间之后,配置信息也许发⽣了变化,我们可以停⽌所有进程,重新分发配置信息的⽂件,然后重新启动,但是重新配置就会延长应⽤的停机时间。

注意:拜占庭将军问题

拜占庭将军问题(Byzantine Faults)是指可能导致⼀个组件发⽣任意⾏为(常常是意料之外的)的故障。这个故障的组件可能会破坏应用的状态,甚⾄是恶意⾏为。系统是建立在假设会发⽣这些故障,需要更⾼程度的复制并使用安全原语的基础上。

尽管我们从学术⽂献中知道,针对拜占庭将军问题技术发展已经取得了巨⼤进步,我们还是觉得没有必要在ZooKeeper中采用这些技术,因此,我们也避免代码库中引⼊额外的复杂性。

在独⽴主机上运⾏的应⽤与分布式应⽤发⽣的故障存在显著的区别:在分布式应⽤中,可能会发⽣局部故障,当独⽴主机崩溃,这个主机上运⾏的所有进程都会失败,如果是独⽴主机上运⾏多个进程,⼀个进程执⾏的失败,其他进程可以通过操作系统获得这个故障,操作系统提供了健壮的多进程消息通信的保障。在分布式环境中这⼀切发⽣了改变:如果⼀个主机或进程发⽣故障,其他主机继续运⾏,并会接管发⽣故障的进程,为了能够处理故障进程,这些仍在运⾏的进程必须能够检测到这个故障,⽆论是消息丢失或发⽣了时间偏移。

理想的情况下,我们基于异步通信的假设来设计系统,即我们使⽤的主机有可能发⽣时间偏移或通信故障。我们做出这个假设是因为这⼀切的确会发⽣,时间偏移时常会发⽣,我们偶尔就会遇到⽹络问题,甚⾄更不幸的,发⽣故障。我们可以做什么样的限制呢?

这个例⼦原本是⼀个在分布式计算领域⾮常著名的定律,被称为FLP(由其作者命名:Fischer,Lynch,Patterson),这个结论证明了在异步通信的分布式系统中,进程崩溃,所有进程可能⽆法在这个⽐特位的配置上达成⼀致[1]。类似的定律称为CAP,表⽰⼀致性(Consistency)、可⽤性(Availability)和分区容错性(Partition-tolerance),该定律指出,当设计⼀个分布式系统时,我们希望这三种属性全部满⾜,但没有系统可以同时满⾜这三种属性[2]。因此ZooKeeper的设计尽可能满⾜⼀致性和可⽤性,当然,在发⽣⽹络分区时ZooKeeper也提供了只读能⼒。

因此,我们⽆法拥有⼀个理想的故障容错的、分布式的、真实环境存在的系统来处理可能发⽣的所有问题。但我们还是可以争取⼀个稍微不那么宏伟的⽬标。⾸先,我们只好对我们的假设或⽬标适当放松,例如,我们可以假设时钟在某种范围内是同步的,我们也可以牺牲⼀些⽹络分区容错的能⼒并认为其⼀直是⼀致的,当⼀个进程运⾏中,也许多次因⽆法确定系统中的状态⽽被认为已经发⽣故障。虽然这些是⼀些折中⽅案,⽽这些折中⽅案允许我们建⽴⼀些印象⾮常深刻的分布式系统。

ZooKeeper的成功和注意事项

不得不指出,完美的解决⽅案是不存在的,我们重申ZooKeeper⽆法解决分布式应⽤开发者⾯对的所有问题,⽽是为开发者提供了⼀个优雅的框架来处理这些问题。多年以来,ZooKeeper在分布式计算领域进⾏了⼤量的⼯作。Paxos算法[1]和虚拟同步技术(virtual synchrony)[2]给ZooKeeper的设计带来了很⼤影响,通过这些技术可以⽆缝地处理所发⽣的某些变化或情况,并提供给开发者⼀个框架,来应对⽆法⾃动处理的某些情况。

参考资料

《Zookeeper分布式过程协同技术详解》