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

前言

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

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

服务注册中心 | 记一次 Consul 故障分析与优化_第1张图片 Consul 传送门****

01.

注册中心背景及 Consul 的使用

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

服务注册中心 | 记一次 Consul 故障分析与优化_第2张图片

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

02.

与 QAE 容器应用平台集成

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

服务注册中心 | 记一次 Consul 故障分析与优化_第3张图片

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

服务注册中心 | 记一次 Consul 故障分析与优化_第4张图片

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

03.

与 API 网关集成

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

服务注册中心 | 记一次 Consul 故障分析与优化_第5张图片

这里我们使用了 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 的执行耗时异常增大,如下图所示。

服务注册中心 | 记一次 Consul 故障分析与优化_第6张图片

此时 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 执行耗时也线性增长,如下图所示;

服务注册中心 | 记一次 Consul 故障分析与优化_第7张图片

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 更新,如下图所示。

服务注册中心 | 记一次 Consul 故障分析与优化_第8张图片

因此怀疑 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 的发布历史中找到了以下两条记录:

服务注册中心 | 记一次 Consul 故障分析与优化_第9张图片

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

服务注册中心 | 记一次 Consul 故障分析与优化_第10张图片

回顾语言的更新历史,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 也注册一份,如下图所示。

服务注册中心 | 记一次 Consul 故障分析与优化_第11张图片

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

04.

保障 API 网关

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

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

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

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

服务注册中心 | 记一次 Consul 故障分析与优化_第12张图片

图中红色的是原有的 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)