《从Paxos到zookeeper》第6章 Zookeeper的典型应用场景(下)
6.2 Zookeeper在大型分布式系统中的应用
6.2.1 Hadoop
YARN介绍
YARN是Hadoop为了提高计算节点的扩展性,同时为了支持多计算模型和提供资源的细粒度调度而引入的全新一代分布式协调框架。
核心为ResourceManager,资源管理中心,负责集群中所有资源的统一管理和分配。
(可理解为YARN的大脑)
如何解决ResourceManager单点问题,实现高可用?
Active/StandBy模式
运行期间,会有多个ResouceManager,其中仅有一个初入Active状态,其余(一个或多个)为Standby状态。
当Active节点无法正常工作(宕机或重启),其余机器会选举产生新的Active节点。
(以上图片取自 ResourceManager High Availability https://hadoop.apache.org/docs/current/hadoop-yarn/hadoop-yarn-site/ResourceManagerHA.html)
选举过程
所有ResourceManager启动的时候,都会竞争创建一个Lock临时节点/yarn-leader-election/yarn-rm-cluster/ActiveStandbyElectorLock。
创建成功的为Active状态;没有成功的为Standby状态,并且向Lock节点注册一个节点变更监听。
由于临时节点的特性,Standby机器能够快速感知Active机器运行情况。
主备切换
当Active状态的ResourceManager无法正常工作(宕机或重启),Lock节点会被删除。其他Standby状态的ResourceManager收到通知,重新进行选举。
“假死”现象
在分布式环境中,经常会出现机器“假死“的情况。
”假死“是指机器由于网络闪断或负载过高(如GC时间过长、CPU负载过高)导致无法正常对外及时响应。
“假死”带来的”脑裂“问题
两台机器,其中ResourceManager1为Active状态,ResourceManager2为Standby状态。
某一时刻,ResourceManager1”假死“,Zookeeper认为ResourceManager1挂了,发生主备切换,ResourceManager2变为Active状态。
随后,ResourceManager1恢复正常,认为自己仍为Active状态。
此时,发生了”脑裂“,存在多个Active状态的ResourceManager在同时工作!
(PS:“脑裂”如何理解?“脑”,人体最重要的器官,脑子发出指令,人做出相应行动。“脑裂”,有多个脑,人格分裂?精神病?那不就乱了。对于分布式系统,亦然)
如何解决“假死”带来的”脑裂“问题?
隔离机制,利用Zookeeper的ACL权限控制实现不同ResourceManager之间的隔离。
ResourceManager1出现”假死“,Lock节点被Zookeeper删除,ResourceManager2创建Lock节点,变为Active状态。
ResourceManager1恢复之后,试图更新Lock节点相关数据,但是发现没有权限(因为Lock节点不是自己创建的),自动切换为Standby状态。
6.2.3 Kafka
Kafka高性能的分布式消息队列。
术语介绍
-
消息生产者:Producer,消息产生源头,负责生成消息并发送到Kafka服务器Broker上。
-
消息消费者:Consumer,消息使用方,负责消费Kafka服务器上的消息。
-
Broker:Kafka的服务器,用于存储消息
-
主题:Topic,用于建立消费者和生产者之间的订阅关系,生产者发送消息到Topic下,消费者从这个Topic下消费消息
-
消费者分组:Consumer Group,用于归类同类消费者。分组内的多个消费者可以消费一个Topic下的消息,每个消费者消费其中的部分消息。
-
消息分区:Partition,一个Topic有多个Partition,可分布在多个Broker上。
-
Offset:偏移量,消息存储在Broker上,消费者消费消息需要知道消息在文件中的偏移量
问题
1)Consumer、Broker实现动态添加和删除,并且能够被其他机器感知到?
1.1)生产者如何感知到Broker的添加和删除
1.2)消费者如何感知到Broker、其他消费者的添加和删除
2)生产者的负载均衡
3)消费者的负载均衡
4)消费者的宕机或重启后,如何能够从上次消费的地方继续消费?
Kafka与Zookeeper
1)管理broker与consumer的动态加入与离开。
(Producer不需要管理,随便一台计算机都可以作为Producer向Kakfa Broker发消息)
2)触发负载均衡
当broker或consumer加入或离开时会触发负载均衡算法,使得一个consumer group内的多个consumer的消费负载平衡。
(说明:一个partition只能被一个consumer消费,一个comsumer消费一个或多个partition)
3)维护consumer和partition之间消费关系及每个partition的消费进度。
Broker注册管理
”Broker节点“:在zookeeper上有一个专门用来进行Borker服务器列表记录的节点,节点路径为/brokers/ids。
每个Broker都有一个全局唯一的Broker ID,每个Broker启动后,会到“Broker节点”下创建自己的节点(临时节点),节点路径为/brokers/ids/[0......N]
创建完后,每个Broker会将自己的IP地址和端口等信息写入该节点中。
因为为临时节点,当Broker宕机或下线后,对应的节点会被删除,因此,可通过zookeeper上Broker节点变化动态表征Broker服务器的可用性。
举例:
比如/brokers/ids/1和/brokers/ids/2分别为两个id为1和id为2的Broker创建的节点。
Topic注册管理
在Kafka中,同一个Topic的消息会分成多个Partition并将其分不到多个Broker上。
那么如何维护Partition和Broker之前的关系?
"Topic节点":在zookeeper上有一个专门维护Broker和Topic关系的节点,节点路径为/brokers/topics
每一个Topic,会到Topic节点下创建自己的节点,节点路径/borkers/topics/[topic]
举例:
login主题对应的节点为/brokers/topics/login,search主题对应的节点为/borkers/topics/search
Broker启动后,会在对应topic节点下注册自己的BrokerID节点(临时节点),并写入针对该Topic的分区总数。
举例:
/brokers/topics/login/3->2 表示BrokerID为3的Broker服务器,对于login主题的消息,提供了2个分区进行消息存储。
生产者负载均衡
如何进行生产者的负载均衡:生产者如何将消息合理地发送到Broker上?
生产者会对“Broker的新增与减少”,“Topic的新增与减少”,“Broker与Topic关联关系的变化”等注册监听,
通过这些信息,生产者就能够感知到Broker服务器列表的变化,从而实现动态负载均衡。
消费者负载均衡
如何进行消费者的负载均衡:多个消费者如何合理地从对应Broker上接收消息?
消费组之间互不干扰,消费者的负载均衡可看做是同一个消费组内部的负载均衡。
消费分区与消费者关系
每个消费组有一个全局唯一的GroupID,分组内所有消费者共享。
每个消费者有一个ConsumerID,通常为“Hostname:UUID”。
一个partition有且只能由一个consumer进行消费。
如何维护consumer与partition之间的关系?
在zookeeper上有一个专门维护消费者和消息分区关系的节点,节点路径为/consumers/[group_id]/owners/[topic]/[broker_id-partition_id]
其中,[broker_id-partition_id]为一个消息分区标识,当每个消费者确定了对一个消息分区的消费权利,会将ConsumerID写入对应消息分区临时节点上。
举例:
/consumers/[group_id]/owners/[topic]/[broker_id-partition_id]。其中,[broker_id-partition_id]为一个消息分区标识,节点内容就是消费此分区的消费者ConsumerID。
消息消费进度Offset记录
在消费者对指定消息分区的消费过程中,消费者重启了怎么办?
因此,需要将消费进度记录下来,以便其他消费者重新接管该分区的时候能够从之前的进度的地方继续消费。
/consumers/[group_id]/offsets/[topic]/[broker_id-parttion_id]
[broker_id-partition_id]为一个消息分区标识,节点内容就是Offset值。
消费者注册
下面来看看消费者在初始化启动时加入消费者分组的过程。
1)注册到消费组
每个消费者启动时,会创建一个属于自己的临时节点。节点路径为/consumers/[group_id]/ids/[consumer_id]。
并会将自己订阅的Topic信息写入该节点。此节点为临时节点,消费者宕机或下线,对应节点将删除。
2)注册监听
-
对分组内的消费者变化进行注册监听
对/consumers/[group_id]/ids节点注册子节点变化的监听。发现消费者新增或减少,会触发消费者的负载均衡
-
对Broker的变化注册监听
对/borker/ids/[0....N]注册监听。发现Broker列表发生变化,决定是否需要进行消费者的负载均衡
4)消费者负载均衡
消费者负载均衡:
为了让同一个Topic下不同Partition的消息尽量均衡地被多个Consumer消费而进行的一个消费者与消息分区分配的过程。
通常,当分组内的消费者列表发生变更或Broker列表发生变更,会触发消费者的负载均衡。
负载均衡
在 Kafka 内部存在两种默认的分区分配策略:Range 和 RoundRobin。
1)Range策略
RangeAssignor策略的原理是按照消费者总数和分区总数进行整除运算来获得一个跨度,然后将分区按照跨度进行平均分配,以保证分区尽可能均匀地分配给所有的消费者。
对于每一个topic,RangeAssignor策略会将消费组内所有订阅这个topic的消费者按照名称的字典序排序,然后为每个消费者划分固定的分区范围,如果不够平均分配,那么字典序靠前的消费者会被多分配一个分区。
假设n=分区数/消费者数量,m=分区数%消费者数量,那么前m个消费者每个分配n+1个分区,后面的(消费者数量-m)个消费者每个分配n个分区。
举例:
假设消费组内有2个消费者C0和C1,都订阅了主题t0和t1,并且每个主题都有4个分区,那么所订阅的所有分区可以标识为:t0p0、t0p1、t0p2、t0p3、t1p0、t1p1、t1p2、t1p3。
最终的分配结果为:
消费者C0:t0p0、t0p1、t1p0、t1p1
消费者C1:t0p2、t0p3、t1p2、t1p3
假设上面例子中2个主题都只有3个分区,那么所订阅的所有分区可以标识为:t0p0、t0p1、t0p2、t1p0、t1p1、t1p2。最终的分配结果为:
消费者C0:t0p0、t0p1、t1p0、t1p1
消费者C1:t0p2、t1p2
可以明显的看到这样的分配并不均匀,如果将类似的情形扩大,有可能会出现部分消费者过载的情况。
这就是Range strategy的一个很明显的弊端。
2)RoundRobin策略
RoundRobinAssignor策略的原理是将消费组内所有消费者以及消费者所订阅的所有topic的partition按照字典序排序,然后通过轮询方式逐个将分区以此分配给每个消费者。
使用RoundRobin策略有两个前提条件必须满足:
-
同一个Consumer Group里面的所有消费者的num.streams必须相等;
-
每个消费者订阅的主题必须相同。
如果同一个消费组内所有的消费者的订阅信息都是相同的,那么RoundRobin策略的分区分配会是均匀的。
举例,假设消费组中有2个消费者C0和C1,都订阅了主题t0和t1,并且每个主题都有3个分区,那么所订阅的所有分区可以标识为:t0p0、t0p1、t0p2、t1p0、t1p1、t1p2。
最终的分配结果为:
消费者C0:t0p0、t0p2、t1p1
消费者C1:t0p1、t1p0、t1p2
资料
https://gitbook.cn/books/5ae1e77197c22f130e67ec4e/index.html
https://blog.csdn.net/eric_sunah/article/details/46891901
https://blog.csdn.net/caiyefly/article/details/77938777
https://www.jianshu.com/p/8a61bb2a9219
https://blog.csdn.net/u013256816/article/details/81123600
https://blog.csdn.net/u013256816/article/details/81123625
http://www.thinkyixia.com/2017/10/25/kafka-2/
6.3 Zookeeper在阿里巴巴的实践与应用
6.3.2 案例二 RPC服务框架:Dubbo
主要讲了Dubbo中基于Zookeeper实现的服务注册中心
(参考资料:Dubbo官方文档——zookeeper注册中心 http://dubbo.apache.org/zh-cn/docs/user/references/registry/zookeeper.html)
Dubbo zookeeper注册中心节点设计如下。
-
/dubbo:Dubbo在zookeeper上创建的根节点
-
/dubbo/com.foo.BarService:服务节点,代表Dubbo的一个服务
-
/dubbo/com.foo.BarService/providers:服务提供者根节点,每个子节点代表一个服务提供者
-
/dubbo/com.foo.BarService/consumers:服务消费者根节点,每个子节点代表一个服务消费者
服务提供者
服务提供者启动时,会在/dubbo/com.foo.BarService/providers节点下创建一个临时子节点,并写入自己的地址。代表了”com.foo.BarService“服务的一个提供者。
(因为为临时节点,当提供者出现故障而无法提供服务时,节点会自动被删除,消费者和监控中心都能感知到服务提供者变化)
服务消费者
服务消费者启动时,读取并订阅/dubbo/com.foo.BarService/providers节点下的所有子节点,作为服务地址列表,发起调用。
同时,还会在/dubbo/com.foo.BarService/consumers下创建一个临时子节点,并写入自己的地址。代表了”com.foo.BarService“服务的一个消费者。
监控中心
监控中心需要知道一个服务所用提供者和消费者,及其变化情况。
监控中心启动时,获取/dubbo/com.foo.BarService/节点下所有提供者和消费者的地址,并注册watcher监听节点变化。
6.3.3 案例三 基于MySQL Binlog的增量订阅和消费组件:Canal
Canal基本工作原理
模拟MySQL slave的交互协议,Canal Server将自己伪装成一个MySQL的slave机器,不断向Master机器发送Dump请求。
Master收集Dump请求后,推送binlog给Canal Server。
Canal Server收到binlog后,后面Canal Client会进行解析消费。
Canal Server主备切换设计
基于容灾考虑,一个MySQL数据库实例会由两个或多个Canal Server负责进行数据增量复制。
但只有一个处于Running状态,其他都处于StandBy状态。
主备切换机制如下:
1)尝试启动
每个Canal Server启动Canal instance时,向zookeeper创建一个临时节点/otter/canal/destinations/example/running,哪个Canal Server创建成功了,哪个就启动。
2)启动instance
创建节点成功后,会将自己的机器信息写入该节点中去,并启动instance。
其他Canal Server为standby状态,对/otter/canal/destinations/example/running节点注册监听,监听节点变化
3)主备切换
Canal Server发生异常,需要进行主备切换。因为为临时节点,/otter/canal/destinations/example/running会被删除。
其他Canal Server接收到通知后,重复步骤1)
如何解决主备切换过程中的”假死“问题?
何谓”假死“?由于网络闪断,导致zookeeper认为running状态的Canal Server会话失效,将runing节点删除——但此时,Canal Server JVM未退出,其工作状态是正常的。
为避免假死带来的无谓的资源释放和重新分配。
解决方法:延迟机制
状态为standby的,收到running节点删除通知后,延迟一段时间(默认5秒)再抢占running节点。
原来状态为running的,无需等待延迟,直接取得running节点。
Canal Client的HA设计
Canal Client进行数据消费前,需要找到当前正在提供服务的Canal Server,即Master。
1)Client启动后,从/otter/canal/destinations/example/running节点上读取出running状态的Server。
将自己的信息注册到/otter/canal/destinations/example/1001/running节点,表明自己正在消费,其中,1001,为clientId唯一标识。
2)对/otter/canal/destinations/example/running节点进行监听,以便发生主备切换时,可以感知到。
3)连接server,进行消费。
数据消费位点记录
Canal Client可能会重启,为避免重复消费和顺序错乱,必须对消费位点进行实时记录。
消费成功时,会记录最后一次消费成功的binlog位点,client重启时,从最后的位点继续消费即可。
在/otter/canal/destinations/example/1001/cursor节点下记录client消费的详细位点信息。
6.3.4 案例四 分布式数据库同步系统:Otter
分布式SEDA模型
SEDA(Staged Event-Driven Architecture):阶段事件驱动架构。
Otter将整个数据同步过程抽象为4个Stage(阶段)
1)Select:数据接入
2)Extract:数据提取
3)Transform:数据转换
4)Load:数据载入
(其实就是讲了怎么样用zookeeper实现多阶段任务处理)
数据模型
每个阶段在zookeeper上对应一个节点。比如:/seda/stage/s1,表示阶段1;/seda/stage/s2,表示阶段2;/seda/stage/s3,表示阶段3;/seda/stage/s4,表示阶段4
每个任务事件对应该节点下的一个子节点,比如:/seda/stage/s1/RequestA,表示阶段1的RequestA这个任务。
任务处理流程(多阶段任务协调处理)
角色
1)任务调度器:负责任务分配、调度
2)任务处理机器:负责任务执行
任务处理机器上的监听线程
任务处理机器上的工作线程
流程
1)接收到RequestA工作请求,任务调度器会在s1对应节点下创建个RequestA节点。
2)每个阶段的任务处理机器上都会有一个监听线程,监听对应阶段的任务节点变化。
比如:关注s1就是关注/seda/stage/s1子节点变化情况。
各个机器上有一组工作线程,负载各个阶段的任务处理。
a.通过监听节点变化,接收任务
b.处理完成后,反馈信息到任务调度器
3)任务调度器接收到任务完成反馈后,会删除s1节点下的RequestA任务,表示任务完成。
任务调度器会在s2对应节点下创建个RequestA节点,告知阶段2有一个新的请求要处理。
多台机器共同完成一个阶段任务的处理,多个节点之间如何协调?
比如s1有多台机器协同处理,每台机器都有一个监听线程,在s1子节点变化时都会收到通知。
抢占式的模式,尝试在RequestA下创建一个lock节点(临时节点),谁创建成功就代表谁抢到了任务。
没抢到的机器则关注lock节点的变化(因为一旦lock节点消失,代表当前抢占任务节点可能异常退出了,没有完成任务)继续抢占模型。