双十一备战的那些天,我们那个支撑 200 万日活的核心交易链路,注册中心在压测到 8000 QPS 时出现了严重的服务列表推送延迟。当时用的是 Nacos 2.2.3,问题表现为订单服务(Order-Service)实例上下线后,网关(Gateway)需要 15-20 秒才能感知到变化。这意味着在大促流量洪峰到来时,如果我们要扩容 50 个实例,至少有 5 分钟的时间流量是分配不均的,部分新节点空载,老节点被打爆。
我排查了 Nacos 的 Distro 协议日志,发现是 AP 模式下的数据同步在跨机房场景下出现了冲突解决机制的延迟。我们当时是“北京 + 广州”双活架构,Nacos 的多数据中心方案本质上还是通过 Synchronizer 做数据双向同步,这种逻辑层的同步在 100ms+ 的专线延迟下,一致性很难保证。
为了彻底解决这个问题,我带队做了技术选型复盘。我们对比了 Consul 1.20(2024年5月刚发布的稳定版)和 Nacos 2.3。
为什么最终选了 Consul 1.20?
核心原因有三点,都是基于我们那个电商场景的痛点:
一次真实的线上故障排查
有一次线上接口突然变慢,从平均 120ms 飙升到 800ms。我通过 Consul 的 UI 查看 payment-service 的健康检查状态,发现虽然服务实例是“Passing”,但 Consul 日志里显示 TCP 检查偶尔超时。
我反查了配置,发现当时为了省事,健康检查间隔设的是 10s,超时 5s。在 K8s 集群网络抖动时,这种长超时会导致请求积压。
我立刻调整了策略,改用了 Consul 1.20 支持的 gRPC 健康检查,并将间隔缩短为 5s,超时设为 2s。
这是当时修改的注册配置(基于 Spring Cloud Consul):
改完这个配置,重启服务后,支付链路的 P99 耗时直接从 800ms 降回了 150ms。因为 gRPC 检查是基于长连接的,消耗比 HTTP 轮询小得多,且 2 秒超时能迅速发现故障节点并剔除,不再把流量转发给那些“假死”的 Pod。
我们现在的部署模式已经全面切向云原生。不再像以前那样在虚拟机上裸跑 Consul Agent,而是直接利用 Consul 1.20 对 Kubernetes CRD 的深度集成。
我在设计这套架构时,主要考虑了两点:一是去掉 Sidecar 模式的资源浪费,二是利用 CRD 实现声明式配置。
架构细节
我们在“华为云(ARM64)+ 自建 IDC(x86)”的混合云环境下部署。Consul Server 集群部署在 IDC 的 3 台物理机上(为了保证 Raft 日志落盘性能),而 K8s 集群里只部署 Consul Client 或者说利用 consul-k8s 的“Agentless”模式(这是 2024 年的趋势)。
具体的部署,我直接使用了 HashiCorp 官方的 Helm Chart,但做了针对 ARM64 的定制化。
这是我当时用的 values.yaml 片段,重点在于镜像架构和同步机制的配置:
为什么这么做?
我不建议在 K8s 里用 DaemonSet 跑 Consul Client 去注册 Pod IP。因为 Pod 生命周期太短,DaemonSet 模式下的 Node 宕机可能会导致该 Node 上所有的健康检查失效。
我采用了 Consul Catalog 同步 的方式。K8s 里的服务通过 CRD 注册,Consul 通过 Watch K8s API 来感知服务变化。这样,Consul 里的服务实例 IP 实际上是 K8s Service 的 ClusterIP 或者 NodePort,而不是具体的 Pod IP,大大减少了注册表的变更频率。
遇到的问题与解决
有一次,我们在华为鲲鹏 ARM 服务器上部署时,Consul Client 启动后疯狂报错 segmentation fault。排查下来发现是当时用的某个基础镜像里的 glibc 版本太旧,不兼容 Consul 1.20 的 Go 运行时。
解决方案:我直接打了一个新的 Dockerfile,基于 alpine:3.19,并强制指定了 Consul 1.20.0 的 ARM64 镜像。
部署后,单节点内存占用稳定在 45MB 左右(Nacos 当时要吃掉 1.2GB),这对于我们那些 2C4G 的 ARM 小规格节点来说,简直是福音。
在代码层面,我们要解决的核心问题是:如何让北京(DC1)的订单服务调用广州(DC2)的物流服务,且不需要改任何业务代码。
Consul 1.20 的 ConsulCatalog 提供了跨数据中心查询的能力。我在 Spring Cloud 项目里,通过自定义 ConsulDiscoveryClient 的逻辑,实现了优先本地机房,失败后 fallback 到远程机房的策略。
环境配置
首先,在 bootstrap.yml 里,我通过环境变量注入了数据中心标识。
自定义负载均衡策略
这是最关键的一步。默认的 ConsulDiscoveryClient 只会查本地 DC。我写了一个 CustomConsulDiscoveryClient 继承它,重写了获取实例的逻辑。
实际效果
在我们的订单系统里,调用物流服务 logistics-service。当北京机房的所有 logistics-service 实例因为发布重启而暂时不可用时,上面的代码会自动去查 dc2(广州)。
我实测过,在开启跨 DC 调用后,虽然因为专线延迟,单次调用增加了 30ms 的 RTT,但保证了服务在单机房故障时的 100% 可用性。如果不做这个跨 DC 兜底,当时北京机房发布期间,会有大约 2 分钟的订单无法发货,那是绝对不能接受的。
这种基于代码层面的控制,比单纯依赖 Consul 的 wanfed 配置更灵活,我可以随时在代码里控制流量权重,比如“广州机房只承载 10% 的流量”,而不需要去改 Consul 集群的配置。
去年我们团队接手了一个混合云迁移项目,目标是把原来部署在三个传统IDC机房里的8000多个服务实例,逐步迁移到K8s集群和公有云上。当时面临一个很现实的问题:Consul 1.20作为服务发现的核心组件,能不能扛住万级节点规模?我们之前在测试环境里只跑过几百个节点,心里完全没底。
我带着两个后端同事搭了一套1:1的压测环境。Server端用了5台16核32G的虚拟机,部署Consul 1.20.0稳定版(2024年5月刚发布的那版),Client端模拟了12000个节点,每个节点注册2-3个服务实例。压测的核心目标是验证Raft和Gossip两个协议在大规模下的表现,因为这两个协议直接决定了Consul的稳定性和响应速度。
先说说Raft协议。它是Server节点之间保证数据一致性的核心,所有服务注册、健康状态变更都会通过Raft日志同步。我们一开始用默认配置,压测时发现当服务实例每秒新增500个时,Raft日志提交延迟从平均30ms飙升到800ms,甚至出现日志复制失败的情况。排查下来发现默认的raft_log_buffer_size只有16MB,万级节点下日志产生速度远超过缓冲区处理能力。后来我们把它调到64MB,同时把raft_multiplier从默认的5改成3(这个值控制Raft选举超时时间的倍数,调小能加快选举但增加误判风险,我们测试后发现在万级节点下3是平衡点),调整后Raft日志提交延迟稳定在80ms以内,QPS从1200提升到了3500。
再看Gossip协议。它负责Client节点之间的成员管理和故障探测,默认是每秒通过UDP广播一次状态。压测到8000节点时,网络监控显示每台Client的UDP流量达到了15Mbps,部分交换机出现丢包,导致健康检查结果误判。我们尝试把gossip_interval从1s改成2s,同时把gossip_nodes从默认的3改成5(每次广播给更多节点,减少传播延迟),调整后单节点UDP流量降到了6Mbps,故障探测的准确率从92%提升到了99.5%。
这里有个关键数据对比:默认配置下,12000节点时服务发现的P99延迟是1200ms,Raft日志同步失败率3.2%;调优后P99延迟降到180ms,失败率0.1%。内存方面,Server节点在默认配置下内存占用峰值是28GB,调优后稳定在22GB左右,因为减少了无效的日志缓冲和Gossip重传。
我们当时还遇到一个坑:一开始为了省事,把Server和Client都部署在同一网段,没有做网络隔离。压测时Gossip的广播风暴直接影响了业务服务的网络,后来给Consul单独划了VLAN,问题才解决。这也让我明白,万级节点下网络规划比参数调优更重要。
下面是压测时用的Server端核心配置,都是我们实际验证过的:
今年3月,我们电商系统的订单服务在大促预热期间突然出现了大面积超时,从监控看订单接口的P99响应时间从200ms飙升到3秒,半小时后直接不可用。我当时正在家里陪孩子过生日,接到电话后立刻打开电脑排查,那次事故让我对Consul的健康检查机制有了刻骨铭心的认识。
先说背景:订单服务有12个实例,部署在K8s集群里,通过Consul 1.20做服务发现。健康检查配置的是HTTP方式,每10秒调用一次/health接口,超时时间设为5秒。当时我们觉得这个配置很合理,因为订单服务的/health接口只是检查数据库和Redis连接,应该很快返回。
排查过程持续了40分钟,我一步步还原了事故链。首先看Consul的Server日志,发现大量"Health check failed for service 'order-service'"的记录,但订单服务的Pod在K8s里显示都是Running状态。然后登录其中一个订单实例,发现/health接口的响应时间确实变慢了,平均要4.8秒,接近5秒的超时阈值。为什么会慢?再查数据库监控,发现当时有一个慢查询把数据库连接池占满了,/health接口里的数据库检查语句也被阻塞了。
问题就出在这里:健康检查本身成了压垮服务的最后一根稻草。因为/health接口每10秒被调用一次,12个实例就是每秒1.2次请求,这些请求都要等数据库连接。当连接池满了之后,健康检查请求堆积,反过来又占用更多连接,形成恶性循环。更糟糕的是,Consul发现健康检查失败后,会把实例从服务列表中剔除,然后我们的API网关就会把流量转发到其他健康的实例,导致那些实例也很快被压垮,最终整个服务雪崩。
我们当时的临时解决方案是:立刻把健康检查的超时时间改成10秒,同时把检查间隔改成30秒,给数据库留出缓冲时间。然后紧急扩容数据库连接池,优化那个慢查询。恢复后我做了复盘,发现三个致命问题:第一,健康检查接口不应该依赖核心业务资源(比如直接查数据库),第二,超时时间设置太接近实际响应时间,第三,没有做健康检查的熔断机制。
后来我们重构了健康检查逻辑,把/health接口改成只检查服务进程是否存活和内存使用率,核心依赖的检查放到单独的/health/deep接口,并且只在Consul的自定义检查脚本里调用,间隔设为60秒。同时给健康检查加了熔断:如果连续3次检查失败,才标记为不健康,避免偶发延迟导致的误判。
下面是修改后的健康检查配置,现在还在生产环境跑着:
对应的深度检查脚本deep_health_check.sh:
这次事故后,我们给所有服务的健康检查都加了"轻量检查+深度检查"的双层机制,再也没出现过类似问题。现在回想起来,健康检查的初衷是好的,但如果配置不当,反而会成为系统稳定性的杀手。
去年我们公司过了等保三级,其中有个硬性要求:所有微服务之间的通信必须加密,并且要基于身份进行授权。当时我们评估了几个方案,最后选了Consul 1.20的Connect功能,因为它能自动给服务颁发证书,实现mTLS(双向TLS)通信,不用我们手动管理证书,运维成本很低。
先讲为什么需要这个。我们之前的服务通信都是明文的,订单服务调用支付服务时,数据包在网络里是裸奔的。有一次安全团队做渗透测试,在交换机上抓包直接拿到了支付接口的请求参数,包括用户ID和金额,这要是被黑客利用,后果不堪设想。所以零信任架构的核心就是:不管服务在哪个网络,都要验证身份,加密通信。
Consul Connect的实现逻辑很清晰:每个服务实例启动时,会向Consul申请一个SPIFFE格式的证书,证书里包含服务名称、数据中心等信息。当服务A调用服务B时,Connect会通过Sidecar代理(我们用的Envoy)拦截流量,先验证对方证书的合法性,再建立加密连接。整个过程对业务代码透明,不用改一行调用逻辑。
我们当时在测试环境先跑了两周,用的是Consul 1.20.0的新特性:支持ARM64架构的Envoy镜像(我们部分服务器是国产ARM芯片,之前一直担心兼容性,实测下来性能比x86还稳定)。配置过程比想象中简单,首先要在Consul Server开启Connect功能:
然后给服务配置Connect,比如支付服务,我们要允许订单服务调用它,其他服务一律拒绝。下面是支付服务的注册配置:
订单服务调用支付服务时,不用改代码,只要把调用地址改成Sidecar的地址就行。比如原来调用http://payment-service:8080/pay,现在改成http://localhost:20001/pay(20001是订单服务Sidecar配置的上游支付服务的端口)。Envoy会自动处理证书验证和加密,业务完全无感知。
我们上线后做了一次压测,对比了明文通信和mTLS通信的性能:在1000 QPS下,明文通信的平均延迟是80ms,mTLS是110ms,增加了30ms的加密开销,但在可接受范围内。内存方面,每个Envoy Sidecar占用约50MB内存,12个实例就是600MB,对我们32GB的服务器来说影响不大。
有个实际场景:有一次支付服务的证书过期了,我们还没来得及手动更新,Consul已经自动给它颁发了新证书,业务完全没中断。这就是Connect自动轮换证书的好处,之前用手动证书时,我们至少因为证书过期出过两次事故。
现在我们的所有核心服务都接入了Connect,安全团队扫描后说,服务间的通信加密覆盖率从0提升到了100%,等保三级的这项要求也顺利通过了。如果你也在做零信任架构,Consul Connect确实是个低成本的切入点,尤其是已经用了Consul做服务发现的话,几乎不用额外学习成本。
去年双11前,我负责把公司核心交易链路从Eureka迁移到Consul。当时业务是跨境电商,海外节点多,Eureka在跨洋网络抖动时经常误摘流量,我们决定赌一把Consul。
迁移到第三周,灰度环境突然告警:下单服务实例大面积被标记为不健康,但实际上CPU和内存都正常。我盯着日志看了半小时,发现Consul Client Agent在ARM64节点上,因为时钟偏差导致健康检查的时间窗口计算错误。当时我第一反应是调大check_timeout,结果雪崩更严重了——因为超时变长,实例虽然没被摘掉,但请求全卡在半路。
最后我翻了三天源码,发现是consul-client在ARM架构下对System.nanoTime()的处理有偏差。临时方案是强制所有K8s节点同步chrony,并把健康检查从http改成grpc + tls_skip_verify,同时把interval从10s调到30s。那个晚上我守到凌晨四点,看着监控里错误率掉下来,才敢关电脑。
* 适合上的场景:如果你有多数据中心、混合云(尤其是ARM和x86混布),或者需要做零信任内网通信,Consul的Connect比自己搞mTLS省心太多。
* 没必要上的场景:如果你的服务就跑在一个K8s集群里,且规模不到500实例,真的没必要折腾。K8s原生的Service发现已经够用,引入Consul反而多一层运维负担。
* 选型坑:别迷信Consul的"全能",它的KV存储性能一般,千万别拿它当配置中心用,我们当初想省事这么干,后来差点把Raft状态机撑爆。
别只看官方文档里的"万级节点支持",那是在调优后的实验室环境。真上生产前,一定要在脏环境下压测,特别是网络丢包和时钟漂移的情况。技术选型没有银弹,能解决你当下最痛那个问题的,才是好技术。