服务注册中心 | 记一次 Consul 故障分析与优化

标签: 服务 中心 consul | 发表时间:2020-11-22 03:52 | 作者:爱奇艺技术
出处:https://juejin.im/backend

前言

在微服务体系中,服务注册中心是最基础的组件,它的稳定性会直接影响整个服务体系的稳定性。本文主要介绍了爱奇艺微服务平台基于 Consul 的服务注册中心建设方式,与内部容器平台、API 网关的集成情况,并重点记录了 Consul 遇到的一次故障,分析解决的过程,以及针对这次故障从架构上的优化调整措施。

Consul 是近几年比较流行的服务发现工具,用于实现分布式系统的服务发现与配置。与其它分布式服务注册与发现的方案相比 Consul 的方案更 “一站式”,使用起来也较 为简单。他的主要应用场景为:服务发现、服务隔离、服务配置。

Consul 传送门****

01.

注册中心背景及 Consul 的使用

从微服务平台的角度出发希望提供统一的服务注册中心,让任何的业务和团队只要使用这套基础设施,相互发现只需要协商好服务名即可;还需要支持业务做多 DC 部署和故障切换。由于在扩展性和多 DC 支持上的良好设计,我们选择了 Consul,并采用了 Consul 推荐的架构,单个 DC 内有 Consul Server 和 Consul Agent,DC 之间是 WAN 模式并且相互对等,结构如下图所示。

注:图中只画了四个 DC,实际生产环境根据公司机房建设以及第三方云的接入情况,共有十几个 DC。

02.

与 QAE 容器应用平台集成

爱奇艺内部的容器应用平台 QAE 与 Consul 进行了集成。由于早期是基于 Mesos/Marathon 体系开发,没有 POD 容器组概念,无法友好的注入 sidecar 的容器,因此我们选择了微服务模式中的第三方注册模式,即由 QAE 系统实时向 Consul 同步注册信息,如下图所示;并且使用了 Consul 的 external service 模式,这样可以避免两个系统状态不一致时引起故障,例如 Consul 已经将节点或服务实例判定为不健康,但是 QAE 没有感知到,也就不会重启或重新调度,导致没有健康实例可用。

其中 QAE 应用与服务的关系表示例如下:

每个 QAE 应用代表一组容器,应用与服务的映射关系是松耦合的,根据应用实际所在的 DC 将其关联到对应 Consul DC 即可,后续应用容器的更新、扩缩容、失败重启等状态变化都会实时体现在 Consul 的注册数据中。

03.

与 API 网关集成

微服务平台 API 网关是服务注册中心最重要的使用方之一。网关会根据地区、运营商等因素部署多个集群,每个网关集群会根据内网位置对应到一个 Consul 集群,并且从 Consul 查询最近的服务实例,如下图所示。

这里我们使用了 Consul 的 PreparedQuery 功能,对所有服务优先返回本 DC 服务实例,如果本 DC 没有则根据 DC 间 RTT 由近到远查询其它 DC 数据。

故障与分析优化

01.

Consul 故障

Consul 从 2016 年底上线开始,已经稳定运行超过三年时间,但是最近我们却遇到了故障,收到了某个 DC 多台 Consul Server 不响应请求、大量 Consul Agent 连不上 Server 的告警,并且没有自动恢复。Server 端观察到的现象主要有:

1. raft 协议不停选举失败,无法获得 leader;

2. HTTP&DNS 查询接口大量超时,观察到有些超过几十秒才返回(正常应当是毫秒级别返回);

3. goroutine 快速线性上升,内存同步上升,最终触发系统 OOM;在日志中没能找到明确的问题,从监控 metrics 则观察到 PreparedQuery 的执行耗时异常增大,如下图所示。

此时 API 网关查询服务信息也超时失败,我们将对应的网关集群切到了其它 DC,之后重启 Consul 进程,恢复正常。

02.

故障分析

经过日志排查,发现故障前发生过 DC 间的网络抖动(RTT 增加,伴随丢包),持续时间大约 1 分钟,我们初步分析是 DC 间网络抖动导致正常收到的 PreparedQuery 请求积压在 Server 中无法快速返回,随着时间积累越来越多,占用的 goroutine 和内存也越来越多,最终导致 Server 异常。

跟随这个想法,尝试在测试环境复现,共有 4 个 DC,单台 Server 的 PreparedQuery QPS 为 1.5K,每个 PreparedQuery 查询都会触发 3 次跨 DC 查询,然后使用 tc-netem 工具模拟 DC 间的 RTT 增加的情况,得到了以下结果:

1. 当 DC 间 RTT 由正常的 2ms 变为 800ms 之后,Consul Server 的 goroutine、内存确实会线性增长,PreparedQuery 执行耗时也线性增长,如下图所示;

2. 虽然 goroutine、内存在增长,但是在 OOM 之前,Consul Server 的其它功能未受影响,raft 协议工作正常,本 DC 的数据查询请求也能正常响应;

3. 在 DC 间 RTT 恢复到 2ms 的一瞬间,Consul Server 丢失 leader,接着 raft 不停选举失败,无法恢复;

以上操作能够稳定的复现故障,使分析工作有了方向。首先基本证实了 goroutine 和内存的增长是由于 PreparedQuery 请求积压导致的,而积压的原因在初期是网络请求阻塞,在网络恢复后仍然积压原因暂时未知,这时整个进程应当是处于异常状态;那么,为什么网络恢复之后 Consul 反而故障了呢?raft 只有 DC 内网络通信,为什么也异常了呢?是最让我们困惑的问题。

最开始的时候将重点放在了 raft 问题上,通过跟踪社区 issue,找到了 hashicorp/raft#6852,其中描述到我们的版本在高负载、网络抖动情况下可能出现 raft 死锁,现象与我们十分相似。但是按照 issue 更新 raft 库以及 Consul 相关代码之后,测试环境复现时故障依然存在。

之后尝试给 raft 库添加日志,以便看清楚 raft 工作的细节,这次我们发现 raft 成员从进入 Candidate 状态,到请求 peer 节点为自己投票,日志间隔了 10s,而代码中仅仅是执行了一行 metrics 更新,如下图所示。

因此怀疑 metrics 调用出现了阻塞,导致整个系统运行异常,之后我们在发布历史中找到了相关优化,低版本的 armon/go-metrics 在 prometheus 实现中采用了全局锁 sync.Mutex,所有 metrics 更新都需要先获取这个锁,而 v0.3.3 版本改用了 sync.Map,每个 metric 作为字典的一个键,只在键初始化的时候需要获取全局锁,之后不同 metric 更新值的时候就不存在锁竞争,相同 metric 更新时使用 sync.Atomic 保证原子操作,整体上效率更高。更新对应的依赖库之后,复现网络抖动之后,Consul Server 可以自行恢复正常。

这样看来的确是由于 metrics 代码阻塞,导致了系统整体异常。但我们依然有疑问,复现环境下单台 Server 的 PreparedQuery QPS 为 1.5K,而稳定的网络环境下单台 Server 压测 QPS 到 2.8K 时依然工作正常。也就是说正常情况下原有代码是满足性能需求的,只有在故障时出现了性能问题。

接下来的排查陷入了困境,经过反复试验,我们发现了一个有趣的现象:使用 go1.9 编译的版本(也是生产环境使用的版本)能复现出故障;同样的代码使用 go1.14 编译就无法复现出故障。经过仔细查看,我们在 go 的发布历史中找到了以下两条记录:

根据代码我们找到了用户反馈在 go1.9~1.13 版本,在大量 goroutine 同时竞争一个 sync.Mutex 时,会出现性能急剧下降的情况,这能很好的解释我们的问题。由于 Consul 代码依赖了 go1.9 新增的内置库,我们无法用更低的版本编译,因此我们将 go1.14 中 sync.Mutex 相关的优化去掉,如下图所示,然后用这个版本的 go 编译 Consul,果然又可以复现我们的故障了。

回顾语言的更新历史,go1.9 版本添加了公平锁特性,在原有 normal 模式上添加了 starvation 模式,来避免锁等待的长尾效应。但是 normal 模式下新的 goroutine 在运行时有较高的几率竞争锁成功,从而免去 goroutine 的切换,整体效率是较高的;而在 starvation 模式下,新的 goroutine 不会直接竞争锁,而是会把自己排到等待队列末端,然后休眠等待唤醒,锁按照等待队列 FIFO 分配,获取到锁的 goroutine 被调度执行,这样会增加 goroutine 调度、切换的成本。在 go1.14 中针对性能问题进行了改善,在 starvation 模式下,当 goroutine 执行解锁操作时,会直接将 CPU 时间让给下一个等待锁的 goroutine 执行,整体上会使得被锁保护部分的代码得到加速执行。

到此故障的原因就清楚了,首先网络抖动,导致大量 PreparedQuery 请求积压在 Server 中,同时也造成了大量的 goroutine 和内存使用;在网络恢复之后,积压的 PreparedQuery 继续执行,在我们的复现场景下,积压的 goroutine 量会超过 150K,这些 goroutine 在执行时都会更新 metrics 从而去获取全局的 sync.Mutex,此时切换到 starvation 模式并且性能下降,大量时间都在等待 sync.Mutex,请求阻塞超时;除了积压的 goroutine,新的 PreparedQuery 还在不停接收,获取锁时同样被阻塞,结果是 sync.Mutex 保持在 starvation 模式无法自动恢复;另一方面 raft 代码运行会依赖定时器、超时、节点间消息的及时传递与处理,并且这些超时通常是秒、毫秒级别的,但 metrics 代码阻塞过久,直接导致时序相关的逻辑无法正常运行。

接着生产环境中我们将发现的问题都进行了更新,升级到 go1.14,armon/go-metrics v0.3.3,以及 hashicorp/raft v1.1.2 版本,使 Consul 达到一个稳定状态。此外还整理完善了监控指标,核心监控包括以下维度:

1. 进程:CPU、内存、goroutine、连接数

2. raft:成员状态变动、提交速率、提交耗时、同步心跳、同步延时

3. RPC:连接数、跨 DC 请求数

4. 写负载:注册 & 解注册速率

5. 读负载:Catalog/Health/PreparedQuery 请求量,执行耗时

03.

冗余注册

根据 Consul 的故障期间的故障现象,我们对服务注册中心的架构进行了重新审视。

在 Consul 的架构中,某个 DC Consul Server 全部故障了就代表这个 DC 故障,要靠其它 DC 来做灾备。但是实际情况中,很多不在关键路径上的服务、SLA 要求不是特别高的服务并没有多 DC 部署,这时如果所在 DC 的 Consul 故障,那么整个服务就会故障。

针对本身并没有做多 DC 部署的服务,如果可以在冗余 DC 注册,那么单个 DC Consul 故障时,其它 DC 还可以正常发现。因此我们修改了 QAE 注册关系表,对于本身只有单 DC 部署的服务,系统自动在其它 DC 也注册一份,如下图所示。

QAE 这种冗余注册相当于在上层做了数据多写操作。Consul 本身不会在各 DC 间同步服务注册数据,因此直接通过 Consul Agent 方式注册的服务还没有较好的冗余注册方法,还是依赖服务本身做好多 DC 部署。

04.

保障 API 网关

目前 API 网关的正常工作依赖于 Consul PreparedQuery 查询结果在本地的缓存,目前的交互方式有两方面问题:

1. 网关缓存是 lazy 的,网关第一次用到时才会从 Consul 查询加载,Consul 故障时查询失败会导致请求转发失败;

2. PreparedQuery 内部可能会涉及多次跨 DC 查询,耗时较多,属于复杂查询,由于每个网关节点需要单独构建缓存,并且缓存有 TTL,会导致相同的 PreparedQuery 查询执行很多次,查询 QPS 会随着网关集群规模线性增长。

为了提高网关查询 Consul 的稳定性和效率,我们选择为每个网关集群部署一个单独的 Consul 集群,如下图所示。

图中红色的是原有的 Consul 集群,绿色的是为网关单独部署的 Consul 集群,它只在单 DC 内部工作。我们开发了 Gateway-Consul-Sync 组件,它会周期性的从公共 Consul 集群读取服务的 PreparedQuery 查询结果,然后写入到绿色的 Consul 集群,网关则直接访问绿色的 Consul 进行数据查询。这样改造之后有以下几方面好处:

1. 从支持网关的角度看,公共集群的负载原来是随网关节点数线性增长,改造后变成随服务个数线性增长,并且单个服务在同步周期内只会执行一次 PreparedQuery 查询,整体负载会降低;

2. 图中绿色 Consul 只供网关使用,其 PreparedQuery 执行时所有数据都在本地,不涉及跨 DC 查询,因此复杂度降低,不受跨 DC 网络影响,并且集群整体的读写负载更可控,稳定性更好;

3. 当公共集群故障时,Gateway-Consul-Sync 无法正常工作,但绿色的 Consul 仍然可以返回之前同步好的数据,网关还可以继续工作;

4. 由于网关在改造前后查询 Consul 的接口和数据格式是完全一致的,当图中绿色 Consul 集群故障时,可以切回到公共 Consul 集群,作为一个备用方案。

总结与展望

作为统一的服务注册中心,稳定性、可靠性始终是我们的首要目标。一方面在保证服务注册中心本身的稳定性,另一方面也会在架构上通过部署、数据、组件等多维度的冗余来提高整个技术体系的稳定性。

目前我们有了一系列监控指标,可以帮助我们评估系统整体的容量、饱和度。随着接入服务越来越多,还要继续完善服务维度的监控指标,当系统负载发生预期外的变化时,能够快速定位到具体的服务、节点。

引用

1. 爱奇艺微服务 API 网关

mp.weixin.qq.com/s/joaYcdmee…

2.Consul raft deadlock github.com/hashicorp/c…

3.Consul prometheus update github.com/hashicorp/c…

4.Go sync.Mutex performance issue github.com/golang/go/i…

5.Go sync.Mutex update 

go-review.googlesource.com/c/go/+/2061…

也许你还想看

基于微服务成熟度模型的高可用优化实践

爱奇艺号基于 Prometheus 的微服务应用监控实践

相关 [服务 中心 consul] 推荐:

服务注册中心 | 记一次 Consul 故障分析与优化

- - 掘金后端
在微服务体系中,服务注册中心是最基础的组件,它的稳定性会直接影响整个服务体系的稳定性. 本文主要介绍了爱奇艺微服务平台基于 Consul 的服务注册中心建设方式,与内部容器平台、API 网关的集成情况,并重点记录了 Consul 遇到的一次故障,分析解决的过程,以及针对这次故障从架构上的优化调整措施.

服务发现:Zookeeper vs etcd vs Consul

- - 企业架构 - ITeye博客
服务发现:Zookeeper vs etcd vs Consul. 【编者的话】本文对比了Zookeeper、etcd和Consul三种服务发现工具,探讨了最佳的服务发现解决方案,仅供参考. 如果使用预定义的端口,服务越多,发生冲突的可能性越大,毕竟,不可能有两个服务监听同一个端口. 管理一个拥挤的比方说被几百个服务所使用的所有端口的列表,本身就是一个挑战,添加到该列表后,这些服务需要的数据库和数量会日益增多.

基于 Consul 的 Docker Swarm 服务发现

- - IT瘾-dev
基于 Consul 的 Docker Swarm 服务发现. 2017 年 1 月 10 日发布. Docker 是一种新型的虚拟化技术,它的目标在于实现轻量级操作系统的虚拟化. 相比传统的虚拟化方案,Docker. 虚拟化技术有一些很明显的优势:启动容器的速度明显快于传统虚拟化技术,同时创建一台虚拟机占用的资源也要远远小于传统的虚拟技术.

使用Consul做服务发现的若干姿势

- - 程序猿DD
从2016年起就开始接触Consul,使用的主要目的就是做服务发现,后来逐步应用于生产环境,并总结了少许使用经验. 最开始使用Consul的人不多,为了方便交流创建了一个QQ群(群号在最后),这两年微服务越来越火,使用Consul的人也越来越多,目前群里已有400多人,经常有人问一些问题,比如:. 服务注册到节点后,其他节点为什么没有同步.

使用Consul实现服务发现:instance-id自定义

- - 周立的博客 - 关注Spring Cloud、Docker
本文基于Spring Cloud Hoxton,理论支持Spring Cloud所有版本. 本文探讨如何自定义微服务注册到Consul的InstanceId. Consul把InstanceId作为唯一标识,而Spring Cloud Consul默认的InstanceId是 ${spring.application.name}-${server.port}.

注册中心 Consul 使用详解 - 纯洁的微笑博客

- -
在上个月我们知道 Eureka 2.X 遇到困难停止开发了,但其实对国内的用户影响甚小,一方面国内大都使用的是 Eureka 1.X 系列,另一方面 Spring Cloud 支持很多服务发现的软件,Eureka 只是其中之一,下面是 Spring Cloud 支持的服务发现软件以及特性对比:. (弱)长连接,keepalive.

基于Nginx和Consul构建高可用及自动发现的Docker服务架构 - DockOne.io

- -
如果你在大量接触或使用微服务的话,你可能会碰到一个问题:当你创建的服务数量越来越多时,这些服务之间的通信便越难管理,而且维护代价会越来越高. 针对这个问题,Consul给出了一份完美的答卷. Consul是一套开源的分布式服务发现和配置管理系统,支持多数据中心分布式高可用. Consul是HashiCorp(Vagrant的创建者)开发的一个服务发现与配置项目,用Go语言开发,基于 Mozilla Public License 2.0 的协议开源.

安装Consul集群

- - 周立的博客 - 关注Spring Cloud、Docker
本文基于Consul 1.5.3,理论适用于Consul 1.6及更低版本. 运行一个consul agent. 将agent加入到consul集群. 列出consul cluster集群中的members. 这里只列出几个常用的命令,consul有将近20个命令,本文不作展开,详见: https://www.consul.io/docs/commands/index.html.

[转]consul VS zookeeper、etcd、doozerd

- - Xiao_Qiang_的专栏
  zookeeper、doozerd、etcd都有着相似的架构,这三者的服务节点都需要一个仲裁节点来操作,它们是强一致的,并提供各种操作原语. 应用程序可以通过客户端lib库来构建分布式的系统. 在一个单datacenter中,consul的server节点工作在一种简单的方式下,consul server需要一个仲裁操作,并提供强一致性.

Consul注销实例时候的问题

- - 程序猿DD
当我们在Spring Cloud应用中使用Consul来实现服务治理时,由于Consul不会自动将不可用的服务实例注销掉(deregister),这使得在实际使用过程中,可能因为一些操作失误、环境变更等原因让Consul中存在一些无效实例信息,而这些实例在Consul中会长期存在,并处于断开状态. 它们虽然不会影响到正常的服务消费过程,但是它们会干扰我们的监控,所以我们可以实现一个清理接口,在确认故障实例可以清理的时候进行调用来将这些无效信息清理掉.