时间是河流
如果时间是一条河流,那么历史就是无数的浪花,伫立在河岸回眸,常常有意识的想要改变某一朵浪花。
2015年中,一群少年为爱发电来到B站,创建了一个项目,并写下了第一行Go代码。这个项目后来成为了B站微服务、中间件及各类平台的孵化器,服务注册与发现模块也应运而生。
时间回溯到2010年11月,zookeeper正式成为Apache的顶级项目,标志着它是工业级的成熟稳定产品。再到2015年,etcd刚刚发布了2.0版本,consul才初出茅庐,我们自然而然的选择了zookeeper作为我们的服务注册与发现组件。
魔改net/rpc,我们实现了性能优化
链路追踪trace
鉴权
超时传递
数据采集监控
等功能,再加上zookeeper,我们还实现了net/rpc server服务注册
net/rpc client服务发现
负载均衡
等功能。
这套框架支撑我们度过了业务快速迭代和频繁改造的阶段,但时间这条河流奔腾不息,转眼进入了2018年。
当我们伫立河畔,回首日新月异的新潮技术变更,凝视一路跌跌撞撞溅起的浪花时,在服务注册与发现领域,我们已经落后了。
落后就要挨打
伴随B站业务的快速发展,微服务的节点数量几乎指数级增长。zookeeper逐渐在下面的场景不堪重负:
大量长连接及session探活,已经支撑不了辣么高TPS
CP系统,当机房间脑裂后不可用
没有专家,出问题全靠运维三宝:重启、重装、换机器
于是,我们调研了已经成熟的etcd、consul、eureka等流行的服务注册与发现系统,经过一番横向对比之后,遵循 注册中心不能因为自身的任何原因破坏服务之间本身的可连通性
,我们选择了AP系统eureka。
但我们团队整体都是Go技术栈,eureka在部署及维护上让我们觉得有些不够得心应手,并且以下几点在eureka1.0版本中也是已知存在的问题:
靠轮询拉取节点,无法及时通知
客户端拉取全量节点,无法按需获取
eureka服务间数据同步随着业务节点数增加而成倍增加
没有完善的日志支撑节点变更过程查询
没有管理面板管理节点
针对以上问题,eureka官方也推出了2.0版本计划,但不幸的是停止开发了(幸亏我们选择了自研,不然就被坑了
所以,我们决定基于eureka的机制,打造属于B站的服务注册与发现系统:Discovery
Discovery
时间进入2018,我们也顺应技术的大潮,打造了基于k8s的PAAS平台,大量的业务在准备和正在迁入。我们制定准入规范,将业务标识appid
、容器启动行为entrypoint
、服务的healthcheck
等等进行了统一。
最关键的,我们需要统一服务注册
!
Discovery在这个大背景下应运而生,设计之初,我们与运维童鞋讨论了很多细节,最终拍定以下设计目标:
实现AP服务注册与发现系统,保证数据最终一致性
与PAAS平台结合,多种发布方式的自动服务注册
网络闪断时服务可开启自我保护,保证健康的服务可用
实现各个语言sdk,基于HTTP协议保证交互简易
基本抽象
在Discovery中我们以appid
作为服务的标识,以hostname
定位实例instance
。定义了三种角色server
provider
consumer
,分别代表:
角色 | 功能 |
---|---|
server | Discovery服务节点,提供存储实例信息、检查和剔除无效节点、自我保护等功能 |
provider | 服务提供者,提供包括注册register 、30s周期心跳renew 、取消注册cancel 等功能 |
consumer | 服务消费者,基于appid 获取所需服务的节点信息,并可选30s周期的长轮询监听服务及时变更状态通知 |
instance | 存储在discovery内的实例信息抽象对象,包含appid hostname addrs metadata 等信息 |
架构图
基本逻辑
provider
启动后会请求Discovery的register
接口进行实例信息注册,注册成功后要进行30s周期一次的renew
心跳,用于维护Discovery内在线状态。
consumer
启动后请求Discovery的fetch
接口,根据appid
获取所有的实例信息。如果有实时接收appid
变更的需要,可以请求poll
接口进行长轮询,首次请求会拿到server
节点下发的latestTimestamp
(表示appid
的最后变更时间,该时间为server
自身时间且不server
间同步)。当再有变更发生时Discovery更新自身latestTimestamp
,与consumer
请求时携带的latestTimestamp
对比,如超过则下发最新实例信息,否则维持长轮询连接直到30s超时或有变更发生。
server
开始会收到appid
的某一个实例的注册请求,在内存中存储为instance
,通过Peer to Peer将数据同步给其他server
节点,之后实例会进行每30s一次的renew
心跳请求,并经过LB后打给任意一个server
节点,节点间再通过P2P进行数据同步,每次renew
都会更新server
内instance
的renewTimestamp
时间戳。当该实例发送cancel
取消注册请求后,server
节点将从内存中将该instance
信息删除。
server
运行期间则会进行每90s一个周期的心跳请求检测,当90s周期内某一instance
最近一次的renewTimestamp
比当前时间小于90s,则判断该instance
失效并删除。为了避免网络故障而导致90s内大量instance
全无心跳被全部删除的情况,server
内还会进行每60s周期一次的自我保护判断,当 (60s内收集的所有心跳数) 小于
(所有instance
的总数*2*0.85) 时进入自我保护模式,此时每90s的删除检测会无效,否则取消自我保护,恢复正常模式。而为了避免确实有大量节点突然挂掉(或其他异常情况)而触发进入自我保护模式但无法恢复为正常模式的情况,设置了最大自我保护时间2h,当超过2h还处于自我保护模式,则自动恢复为正常模式。
重要逻辑
- 复制(Peer to Peer),数据一致性的保障:
-
appid
注册时根据当前时间生成dirtyTimestamp
,Discovery的serverA
向serverB
同步(register
)时,serverB
可能有以下两种情况:- 返回
-404
则serverA
携带dirtyTimestamp
向serverB
发起注册请求,把最新信息同步:-
serverB
中不存在实例 -
serverB
中dirtyTimestamp
较小
-
- 返回
-409
serverB
不同意采纳serverA
信息,且返回自身信息,serverA
使用该信息更新自身
- 返回
-
appid
注册成功后,provider
每30s发起一次renew
请求,处理流程同上
-
-
instance
管理- 正常检测模式,随机分批踢掉无心跳
instance
节点,尽量避免单应用节点被一次全踢 - 网络闪断和分区时自我保护模式
- 60s内丢失大量(小于
instance
总数*2*0.85)心跳数,“好”“坏”instance
信息都保留 - 所有
server
都会持续提供服务,单个server
的注册和发现功能不受影响 - 最大保护时间,防止分区恢复后大量原先
instance
真的已经不存在时,一直处于保护模式
- 60s内丢失大量(小于
- 正常检测模式,随机分批踢掉无心跳
-
consumer
客户端- 长轮询+
server
推送,服务发现准实时 - 订阅式,只需要关注想要关注的
appid
的instance
列表变化 - 缓存实例
instance
列表信息,保证与server
网络不通等无法访问到server
情况时原先的instance
可用
- 长轮询+
特别注意
server
间同步复制是需要时间的,那如何保证consumer
请求serverB
时,因为携带的latestTimestamp
是来自serverA,但serverB
晚于该次请求才收到同步事件,而导致获取的节点信息不一致?
我们通过consumer
启动后,从nodes
接口获取到Discovery的所有server
节点后,随机选取一个serverA
进行fetch
poll
等请求,保证在consumer
生命周期内,实例信息和时间信息始终来自同一个serverA
。除非遇到网络等错误才切换节点到serverB
并清空latestTimestamp
,再当做首次请求重新拉取appid
的全部实例信息和时间信息。
多注册中心
Discovery的同步复制机制天生好支持多注册中心。
我们用zone
来表示机房,假设zoneA
和zoneB
的Discovery集群之间要相互同步,那我们只需要将zoneA
当做zoneB
的特殊server
节点,同理将zoneB
当做zoneA
的特殊server
节点。
当zoneA
的serverA
收到appid1
的注册请求,并同步给内部的其他server
后,再同步给server-zoneB
,zoneB
即可复制到appid1
的实例信息。
但zoneB
内部server
间同步后不再需要同步回zoneA
,所以特殊server
就是指在发送同步请求时,判断该请求是否来自相同的zone
,是的话就像zoneA
同步给zoneB
,否的话就像zoneB
内部同步后不再向其他zone
同步。
注:zoneA
与zoneB
间,建议使用SLB进行负载均衡
与PAAS在一起
我们的PAAS平台已经集成了Discovery的服务注册,也就是provider
能力。业务只需要正常发布就可以直接注册到Discovery,并依赖pod的生命周期进行renew
心跳请求管理。
如果服务需要提供RPC、集群、权重等自定义信息,则只需要暴露 /register
接口并返回map[string]string
格式的json
数据,PAAS在启动实例后和注册信息前,通过回调该接口获取信息,将信息作为metedata
同时注册到Discovery。
基于此,依赖服务(consumer
)就可以获取到实例信息,并对服务进行访问。
管理节点
我们还实现了简单的管理能力,可以基于appid
和环境信息
获取到所有实例信息。并基于此扩展了查询依赖服务
生成CPU和内存profile图 火焰图
等功能。
奔腾不息的河流
当我们再一次伫立在河岸回眸,发现时光的浪花翻腾,但总有那么几朵浪花丑陋,让人想要在今后扔石子时,扔的漂亮~
结语
Discovery已经服务于B站几万+的实例规模,通过借此总结我们在服务注册与发现领域的实践经验,希望对业界阅读此文的童鞋能够有所帮助和启发。同时,我们也希望收到大家的反馈意见,详情请看Discovery开源项目【点我到Github】。
本文作者:冠冠爱看书