02 注册中心:如何搭建一个高可用、健壮的注册中心?
注册中心如何做到高可用以及 Service Mesh 中的注册中心与微服务架构有什么差别。通过上一讲的学习你已经掌握了注册中心组件的基础知识,下面我们进一步学习一下搭建一个高可用、健壮的注册中心会遇到哪些问题。
# 搭建一个高可用、健壮的注册中心
当你完全理解了注册中心后,可以很容易自己实现一个简单的注册中心。但是作为微服务最核心的组件,想要做到工业级产品,生产高可用,并不是那么容易的一件事。我们先来思考下,在引入注册中心后,可能会遇到哪些问题。
- 注册中心完全故障了,服务是否还能正常访问?
- 注册中心因为高负载,推送了异常的数据,服务是否还能正常访问?
- 新加入的机器,出现了网络连通性问题(注册中心和机器网络正常,但服务机器之间网络异常),应该怎样应对?
- 服务是否应该完全信任注册中心推送的数据?
- 服务发布后,节点频繁变更造成 N×M 次事件通知,形成广播风暴,该如何解决?
看到上面的问题,你还认为注册中心是一件简单的事情吗? 不要慌,我们一起来看看,思考下如何解决。
实际上前面四个问题,无论是注册中心完全故障,还是推送了异常数据,都可以总结为是否应该完全信任注册中心的数据。
在传统的观念中,我们肯定会选择信任引入的第三方基础设施,比如 MySQL 、Redis ,这种数据层的中间件,我们肯定是要完全信任其中的数据的。但对于注册中心,信任推送数据的风险非常大。
下面我们具体聊一聊这五个问题,你就能理解我这么说的原因了。
# 1. 注册中心完全故障了,服务是否还能正常访问?
这是一个相对简单的问题,实际上注册中心完全故障的情况,大家或多或少会考虑到。另外在程序进程中缓存访问服务的节点,几乎是一件必然的事情,总不能每个请求都去一次注册中心拿相应服务的注册信息吧。
所以只要在进程中缓存服务的节点,影响面就可控。但你要注意,当注册中心完全故障的时候,服务注册功能是失效的,此时的扩容操作无法进行。如果在容器中,因为 Pod 滚动升级的原因造成会先启动新的 Pod,一定要在程序启动注册失败时抛出异常,使程序无法启动,否则容器 IP 的变化也会导致服务的访问异常。
# 2. 注册中心因为高负载,推送了异常的数据,服务是否还能正常访问?
如果服务节点不是特别多,很难遇到这个问题,但随着微服务规模的增大,注册中心很有可能遇到瓶颈。一旦出现高负载,会使服务和注册中心之间的健康检查或保活出现问题,注册中心使节点异常下线,只推送部分节点数据到订阅的服务。
这个问题看似不严重,但一旦推送了过少的节点到服务,会导致主调服务打挂被调服务,长时间不能恢复,甚至会导致整个微服务集群雪崩。
解决此类问题,我们可以在客户端的服务发现 SDK 中加入自我保护机制:一旦服务的节点数量下降超过一定阈值,就进入自我保护状态,放弃使用新推送过来的服务注册信息。
# 3. 新加入的机器,出现了网络连通性问题(注册中心和机器网络正常,但服务机器之间网络异常),应该怎样应对?
实际上网络连通性问题是比较容易发生的,往往出于安全考虑,各个部门之间可能会处在不同的 VPC ,但现实中又有互相访问的情况,一旦网络规则维护不好,很容易出现新添加的机器注册中心的网段可以访问,但是服务之间却无法访问的情况。在注册中心的使用场景中,网络故障是我们最优先考虑的问题,如果发生了分区故障,问题 2 描述的情况也会发生。
如何解决此类问题呢,这个就要发挥负载均衡器模块的作用了:在负载均衡中我们可以加入被动健康检查(节点熔断)和主动健康检查来在客户端主动剔除失效的节点。
# 4. 服务是否应该完全信任注册中心推送的数据?
看完了上面三个问题,我相信这个问题你已经有了自己的答案。我比较倾向 Service Mesh 数据面之一 Envoy 的做法:相比注册中心的数据,更信任本地数据,所以 Envoy 设计了 2×2 矩阵来决定节点是否应该路由。

如上表所示,只有在健康检查失败和注册中心未发现的情况才会删除节点,只要健康检查成功,无论是否发现此节点,都会路由。
实际上采用了这种方式,我前面说的三个问题,都可以迎刃而解了。当然实现一个健壮的负载均衡器可没这么简单,还有很多边缘情况你需要考虑,具体内容我将会在负载均衡器的章节详细展开。
我们说一下最后一个问题。如果我们有了前面问题的解决方案,实际上第 5 个问题出现了我们也不必慌张,因为它对线上服务的影响非常小,但这里我还需要说一下如何避免问题 5 的发生。
# 5. 服务发布后,节点频繁变更造成 N×M 次事件通知,形成广播风暴,该如何解决?
实际上此问题也可能导致问题 2 的发生。大量广播事件的发生,挤占网络带宽,甚至会导致网络带宽占满,此时注册中心和服务间的健康检查或保活,都会因为带宽不足造成信息丢失,使注册中心推送错误的数据。
如何解决呢?其实很简单,我们可以将事件消息合并推送。在 Istio 的 Pilot 的模块中,实现了一种合并机制,100ms 内有新的事件消息时,便会继续等待下一条,最多等待 1s,当然时间的参数是可以配置的,这里我们说的是默认参数。
虽然这个解决方案会影响事件通知的时效性,但相对于收益来说,它是一个非常好的解决方案,可想而知,如果进一步增加时效性,那么付出的研发成本和机器资源成本都将呈指数级增加,显然是得不偿失的。
# Service Mesh 中的注册中心
实际上在 Service Mesh 方案中,服务节点发现的问题用传统的注册中心方案也是可以解决的,但如果涉及 Kubernetes 和 ECS 跨集群访问,最好还是支持 Envoy 定义的 xDS 协议中的 EDS 协议。EDS是 endpoint discovery service 的缩写,无论是 Istio,还是最新版本的 gRPC,都已经默认支持了 EDS 协议,可以说EDS 实际上已经是服务发现的规范了。
在 Service Mesh 方案中,因为大多是和 Kubernetes 集群结合的方案,所以你要特别注意发版或者自动扩缩容引起的节点 IP 变化的问题。节点的频繁变化,对注册中心的健壮性提出了更高的要求,这些问题我在本讲前半部分已经详细说过了,这里就不再赘述了。
除了用传统的注册中心组件外,Kubernetes 内部的发现机制在 Service Mesh 中也得到了广泛应用,例如 Istio通过监听 Kubernetes Pod 的变化,实现服务发现的功能,这样就不需要服务自身来做服务注册了。
那么 Service Mesh 中实现的注册发现功能,相比传统微服务有哪些优势呢?
# 1. 无须服务自身注册,由 sidecar 代理注册
sidecar 通过接受控制面下发的配置信息,进行服务注册。相对于服务自身注册,这样可以减少服务自身开发的工作量,同时也很容易做到注册的配置信息一致化。比如如果服务自己注册,其实很难控制服务注册的 metadata 信息,在 SDK 中很难约束和升级,比如运行环境、地域、健康检查方式等。
sidecar 代理还带来了可以随时更新 meta 信息的好处。在传统的 SDK 模式中,你想要动态调整服务的权重、metadata 等信息的时候,需要重新发布版本,或者依靠配置中心的能力,但这些控制信息往往散落在各个服务中,不方便管理,在 Service Mesh 中你只需要依靠控制面的能力,就可以轻松做到了。
# 2. 通过控制面聚合多种、多个注册中心数据
像 Istio 的 pilot 模块,在 1.1 版本就支持了单控制面多集群的功能,通过 pilot 将多个注册中心的数据聚合,可以有效降低单一注册中心的读写压力,使注册中心更容易水平扩展。
比如在实践中,我就将多个 Consul 数据中心的数据通过 pilot 模块聚合,然后提供 xDS 协议,供服务发现使用,实现了虚拟机到 Kubernetes 环境的无缝迁移。
# 3. 通过 sidecar 提供服务正确性 check 功能
上一讲我们提到过,在注册中心中,有一种健康检查方式是注册中心主动 ping 服务的模式。实际上如果服务 IP 发生变化,又用了同样的 ping 接口时,健康检查会出现错误。而通过 sidecar 模式,当发现服务 ping 接口过来的流量时,进行服务名称的检测,通过 header 中增加服务名称与本地服务名称做校验的方式进行检测,可以有效避免这样的错误。