Consul 服务注册与发现:告别手动 Nginx 配置
一句话结论(30s)
Consul 解决的不是”怎么找到服务”(那是 DNS 查询),而是”在动态扩缩容环境中始终知道谁活着、谁挂了、谁新来”——因为实例 IP 会随容器重启变化、故障节点会让 Nginx 继续把请求打到已死的实例,所以用「服务注册 + 健康检查摘除 + consul-template 自动渲染 Nginx」三件套,把运维从半夜改 nginx.conf 变成全自动无感扩缩容。
核心原理(2min)
- 服务注册:实例启动后 HTTP PUT 向 Consul 注册,Consul Server(Raft 强一致)存储服务列表。
- 健康检查:每个节点上的 Consul Agent 每 15 秒探活
/actuator/health,非 200 或超时 → 标记 critical → 自动摘除。 - consul-template:监听 Consul 变更,重新渲染 Nginx upstream 模板并
nginx -s reload,全程自动化、延迟 < 5 秒。 - 对比:Consul 是 CP、Agent 主动探测、原生多数据中心;Eureka 是 AP、客户端心跳;Nacos 可切换 CP/AP。
底层深入(5-10min)
传统运维的噩梦
每次新上两个服务实例,运维需要做的:
# 1. SSH 到 Nginx 服务器
# 2. 编辑 /etc/nginx/conf.d/upstream.conf
upstream order-service {
server 10.0.1.10:8080 weight=1;
server 10.0.1.11:8080 weight=1;
# server 10.0.1.12:8080 weight=1; ← 新增这一行
}
# 3. nginx -t # 检查语法
# 4. nginx -s reload # 重载配置
看起来不难?考虑现实场景:
- 你有 20 个微服务,每个 3-8 个实例 → 总共 100+ 个 upstream 条目
- 每次发版,实例 IP 会变(容器重启 → 新 IP)
- 凌晨 2 点自动扩容了 5 个实例 → 人工改配置?不现实。
- 一个实例挂掉了,Nginx 不知道 → 1/3 的请求返回 502
手动维护 Nginx upstream 在微服务时代是不可持续的。 服务注册与发现(Service Registry & Discovery)正是为解决这个问题而生。
想一想:为什么手动维护在微服务时代必然失效? 因为实例 IP 随容器重启持续变化、扩缩容又动态发生,配置的”更新速度”永远追不上实例的”变化速度”。手工改配置本质是”人肉轮询”,而故障和扩容是随机事件——当系统规模到几十个服务、上百个实例,人工维护的延迟和遗漏会让 Nginx 长期拿着过期清单,把请求打到已死的实例上。
Consul 的位置与工作原理
服务实例启动 → 向 Consul Agent 注册(HTTP PUT)→ Consul Server 存储
↓
Nginx ← consul-template 监听变更 ← Consul Watch(长轮询)
三大组件
| 组件 | 作用 | 部署方式 |
|---|---|---|
| Consul Server | 存储服务注册信息,强一致性(Raft) | 3-5 节点集群 |
| Consul Agent | 运行在每个服务节点上,负责健康检查 | 每个节点一个 |
| consul-template | 监听 Consul 变更,动态渲染 Nginx 配置 | 与 Nginx 同机部署 |
第一步:服务注册
Spring Boot 服务启动时,自动向 Consul 注册:
# application.yml
spring:
cloud:
consul:
host: consul-server-1
port: 8500
discovery:
service-name: order-service
instance-id: ${spring.application.name}:${random.value}
health-check-path: /actuator/health
health-check-interval: 15s
服务一启动,Consul 里就会出现:
order-service:
- 10.0.1.10:8080 [healthy]
- 10.0.1.11:8080 [healthy]
- 10.0.1.12:8080 [healthy] ← 新增实例,自动出现
第二步:健康检查——自动摘除故障节点
Consul Agent 每 15 秒请求一次 /actuator/health:
- 返回 200 → 标记 healthy → 留在服务列表
- 超时 / 返回非 200 → 标记 critical → 自动从服务列表移除
order-service:
- 10.0.1.10:8080 [healthy]
- 10.0.1.11:8080 [critical] ← 挂了,请求不再打到这个节点
- 10.0.1.12:8080 [healthy]
不需要人工发现故障、不需要手动改配置、不需要 reload Nginx——故障节点的流量自动被其他健康节点接替。故障恢复后,健康检查重新通过,节点自动回到服务列表。
想一想:为什么健康检查是”主动探测”,而不是等实例自己上报? 因为实例挂掉时往往已经没机会”上报自己死了”——进程被 kill、OOM、网络中断,都不可能主动发一条”我挂了”的消息。只有靠 Agent 从外部主动探测、超时判死,才能可靠发现故障。这也是 Consul(主动探测)比 Eureka(客户端心跳)在故障发现上更及时的原因:心跳只能靠”收不到”来推断,还容易误判。
第三步:consul-template 动态更新 Nginx
consul-template 的核心能力:监听 Consul 的服务列表变更,渲染模板,reload Nginx。
# nginx-upstream.ctmpl(consul-template 模板文件)
upstream order-service {
{{ range service "order-service" }}
server {{ .Address }}:{{ .Port }} weight=1;
{{ end }}
}
consul-template 运行时:
consul-template \
-template "nginx-upstream.ctmpl:/etc/nginx/conf.d/upstream.conf:nginx -s reload"
当 Consul 中 order-service 的服务列表发生变更(节点上线/下线/故障),consul-template:
- 重新渲染模板 → 生成新的
upstream.conf - 执行
nginx -s reload(平滑重载,不中断现有连接)
整个过程 完全自动化,延迟 < 5 秒。
想一想:为什么 consul-template 要 reload Nginx,而不是让 Nginx 直接查 Consul? 因为 Nginx 本身不理解服务发现,它只会读自己的静态 upstream 配置。consul-template 扮演的是”翻译官”——把 Consul 里动态的服务列表渲染成 Nginx 认识的配置格式,再触发平滑 reload。而
nginx -s reload是平滑重载、不中断现有连接,所以对在线请求零影响。
完整架构对比
传统方式
运维手动操作 → 改 Nginx upstream → nginx reload
↑
人工发现新增/故障实例
人工修改配置
人工确认生效
Consul 方式
新实例启动 → 自动注册到 Consul ← → 健康检查自动摘除
↓
consul-template 自动渲染
↓
nginx 配置自动更新 + reload
无感扩缩容:用户感知不到实例增减
扩容
- K8s HPA 检测到 CPU > 80% → 启动 3 个新 Pod
- 新 Pod 启动 → 自动向 Consul 注册
- consul-template 检测变更 → 更新 Nginx upstream → reload
- 新的流量自动分发到新实例
用户体验:响应时间从 500ms 降到 80ms。他们不知道有加实例——只知道”变快了”。
缩容
- 流量下降 → K8s 缩减 Pod
- Consul Agent 检测到
SIGTERM→ 服务反注册(deregister) - consul-template 更新 upstream → 移除下线节点
- Nginx 把新流量发给剩余节点,已建立连接不受影响
关键:服务优雅下线前,先从 Consul 反注册 → 等待 Nginx reload(3-5 秒)→ 再真正关闭 → 确保没有请求被中断。
// Spring Boot 优雅下线
@PreDestroy
public void shutdown() {
// 1. 从 Consul 反注册(停止接受新请求)
consulDiscoveryClient.deregister();
// 2. 等待 Nginx 感知变更(consul-template 会更新配置)
Thread.sleep(5000);
// 3. 处理完所有进行中的请求
// Spring Boot 的 graceful shutdown 自动处理
}
想一想:优雅下线为什么要”先反注册、等几秒、再关进程”? 因为从反注册到 consul-template 渲染、再到 Nginx reload 生效,中间有几秒的传播延迟。如果反注册后立刻 kill 进程,这几秒内 Nginx 还在往这个即将关闭的实例转发请求,就会产生 502。先反注册、再等 Nginx 感知变更,才能保证在真正关闭前,不再有新的请求被分给它。
Consul vs Eureka vs Nacos
| Consul | Eureka | Nacos | |
|---|---|---|---|
| CAP | CP(强一致性) | AP(可用性) | CP + AP 可切换 |
| 健康检查 | Agent 主动探测 | 客户端心跳上报 | 客户端心跳 + 服务端探测 |
| 多数据中心 | 原生支持 | 不支持 | 支持 |
| 生态 | consul-template、Envoy | Spring Cloud 深度集成 | Spring Cloud Alibaba |
| 学习成本 | 低(单一二进制文件) | 低 | 中 |
Consul 的最大优势:独立于 Java 生态——consul-template 让 Nginx、HAProxy、Envoy 这些非 JVM 基础设施也能无缝接入服务发现。
总结
Consul 解决的不是”怎么找人”的问题(那是一个简单的 DNS 查询),而是”怎么在动态变化的环境中始终知道谁能干活、谁挂了、谁新来的”。
三件套:
- 服务注册:实例启动自动注册
- 健康检查:故障节点自动摘除
- consul-template:Nginx 配置自动更新
从”运维半夜起来改 nginx.conf”到”自动扩缩容无感知”——这就是服务发现从”手动模式”到”自动驾驶”的跨越。
章末提问
Q1:Consul 和 Eureka 的本质区别是什么? 结论先行:本质区别在 CAP 取舍和健康检查机制——Consul 是 CP、Agent 主动探测,Eureka 是 AP、客户端心跳上报。 因为 Consul 用 Raft 保证服务列表强一致,Agent 从外部主动探测每个实例,宁可短暂不可用也要列表准确;Eureka 靠客户端定时心跳,集群分区时宁可返回可能过期的列表也保持可用。所以故障发现及时性上 Consul 更强,可用性上 Eureka 更宽松。
Q2:健康检查为什么能自动摘除故障节点?摘除的代价是什么? 结论先行:因为 Agent 每 15 秒主动探活,非 200 或超时就标记 critical 并从服务列表移除,流量自然不再打过去;代价是存在”探活间隔内的误判窗口”。 因为探测是周期性的,一个实例在两次探活之间挂掉,这十几秒内它仍在服务列表里、仍可能收到请求;反过来探测太频繁又会给实例和 Consul 带来额外开销。所以 15 秒是”发现及时性”和”探测成本”之间的折中。
Q3:consul-template 触发 Nginx reload 会不会中断正在处理的请求?
结论先行:不会,nginx -s reload 是平滑重载,旧 worker 处理完存量连接后才退出。
因为 Nginx 的 reload 不是”停掉再启动”,而是拉起新 worker 加载新配置,旧 worker 继续把已在处理的请求走完,新连接交给新 worker,整个过程对在线请求无感。这正是 consul-template 选 nginx -s reload 而不是 restart 的原因。