Consul 服务注册与健康检查实战:三节点集群、服务自动上下线、DNS 接口与 Nginx 动态上游

服务越来越多之后,麻烦从哪来

一个个人站刚起步时,所有东西都在一台机器上:Nginx、PHP、MySQL、Redis 挤在一起,谁在哪、端口是多少,全记在脑子里。但随着站点变大,你会开始拆分:把 MySQL 挪到一台专门的数据库机,Redis 独立出来,图片处理单独一台,甚至几个不同的后端服务分别跑在不同机器上。问题立刻出现了:Nginx 的 upstream 里写死了 IP,换一台机器就得改配置重载;某个后端挂了,Nginx 还在傻傻地往它转发;新加一台机器,得手动登到每台服务器上改配置。

这些问题的本质是:服务的位置信息是硬编码的,没有集中管理,也没有健康感知。Consul 就是解决这类问题的工具。它是一个分布式服务注册与发现系统,核心能力有四块:服务注册、健康检查、KV 配置存储、DNS/HTTP 查询接口。这篇就来讲清楚 Consul 的用法,以及个人站长能怎样把它用在真实场景里。

Consul 的四个核心概念

Agent 是 Consul 运行在每台机器上的进程,分两种模式:Server 和 Client。Server 参与集群一致性(用 Raft 协议),保存整个集群的状态;Client 只是转发请求给 Server,几乎不存什么数据。生产环境至少要有 3 个 Server 组成高可用集群,其余机器跑 Client。

Service(服务注册)是 Consul 最常用的功能。你的后端服务启动时,通过配置文件或 HTTP API 告诉本机的 Consul Agent:"我叫 api,监听 8080 端口,健康检查是这个地址",Agent 就会把这条记录同步到整个集群。查询方通过 Consul 就能知道当前有哪些健康的 api 实例、它们的地址是什么。

Health Check(健康检查)让注册信息不再是死的。Consul 会周期性地按照你定义的方式探测服务,探测失败的实例会被标记为不健康,查询时默认不会返回它。这样就实现了"服务挂了自动从列表里摘掉"。

KV Store(键值存储)是一个分布式的配置中心,可以存一些需要多机共享的配置项,配合 Consul Template 或 consul kv get 使用,实现配置的集中管理和动态下发。

用 Docker Compose 起一个三节点集群

下面用三台机器(或三个 Docker 容器)搭一个最小的高可用 Consul 集群。先给出每台机器上 Agent 的配置文件。假设三台的 IP 分别是 10.0.0.11、10.0.0.12、10.0.0.13:

# /etc/consul.d/server.hcl  (每台机器都要写,只是 node_name 和 IP 不同)
datacenter = "dc1"
data_dir   = "/opt/consul"
bind_addr  = "0.0.0.0"

server           = true
bootstrap_expect = 3

client_addr = "0.0.0.0"
retry_join  = ["10.0.0.11", "10.0.0.12", "10.0.0.13"]

ui_config {
  enabled = true
}

其中 bootstrap_expect = 3 让三个节点自动选出 Leader,不需要手动 bootstrap。client_addr = "0.0.0.0" 很重要——默认 Client 只监听 127.0.0.1,这样其他机器上的服务就没法注册到本机 Agent。retry_join 让节点自动发现彼此,省去手动 join。

用 Docker 的话,启动命令大致如下:

docker run -d --name consul \
  --restart=always \
  --network host \
  -v /etc/consul.d:/consul/config \
  -v /opt/consul:/consul/data \
  hashicorp/consul:1.18 agent -config-dir=/consul/config

用 --network host 是为了让 Consul 直接使用宿主机的网络,避免容器网络的端口麻烦。三台都起来后,在其中一台上执行验收:

# 看集群成员,应该有三个 server,一个是 leader
consul members

# 看 raft 状态,确认有 leader
consul operator raft list-peers

# 最直观的:Server 数量应为 3
curl -s http://127.0.0.1:8500/v1/status/peers | jq

如果三台都显示在列表里、且有明确的 leader,集群就健康了。8500 是 HTTP API 端口,8501 是 gRPC,8600 是 DNS,8300-8302 是集群内部通信。记住一条安全底线:8300/8301/8302 和 8500 绝对不能暴露到公网,Consul 默认没有认证,暴露出去等于把整个服务拓扑送人。

注册一个服务并加上健康检查

Consul 最舒服的地方是服务定义可以写成 JSON 文件放在 /etc/consul.d/,Agent 会自动加载。假设我们有一个后端 API 跑在 8080 端口,服务定义如下:

{
  "service": {
    "name": "webapi",
    "id": "webapi-1",
    "port": 8080,
    "tags": ["v1", "php"],
    "check": {
      "http": "http://127.0.0.1:8080/healthz",
      "interval": "10s",
      "timeout": "2s",
      "deregister_critical_service_after": "90s"
    }
  }
}

几个关键点:id 在同一节点内必须唯一,如果一台机器上跑多个同样的服务实例,用不同的 id 区分。check.http 是主动探测健康检查接口,你的应用要提供一个返回 200 的 /healthz。deregister_critical_service_after 表示持续不健康超过 90 秒后,Consul 会自动把这个服务注销掉,避免残留死实例。

也可以不用文件,直接调 API 注册:

curl -X PUT http://127.0.0.1:8500/v1/agent/service/register \
  -d '{
    "Name": "webapi",
    "ID": "webapi-1",
    "Port": 8080,
    "Check": {
      "HTTP": "http://127.0.0.1:8080/healthz",
      "Interval": "10s"
    }
  }'

注册之后,查询当前健康的实例:

# 只看健康的实例(passing 过滤)
curl -s "http://127.0.0.1:8500/v1/health/service/webapi?passing=true" | jq

# 用 DNS 接口查询,Consul 在 8600 端口提供 DNS
dig @127.0.0.1 -p 8600 webapi.service.consul

DNS 接口是 Consul 一个特别好用的特性:任何支持 DNS 的程序(传统的 Nginx、系统解析器)都能直接通过域名找到服务。地址格式是 <服务名>.service.consul,加上 .dc1 可以指定数据中心,webapi.service.consul 会返回所有健康实例的 IP(多个 A 记录)。

把 Consul 接进 Nginx 做动态上游

Consul 最有价值的地方在于,它能让 Nginx 的上游列表动态更新。有两种主流方式。第一种是用 DNS:

resolver 127.0.0.1 valid=10s;
upstream backend {
    zone backend 64k;
    server webapi.service.consul:8080 resolve;
}

关键点是 resolve 参数和 resolver 指令。普通 upstream 里的域名只在 Nginx 启动时解析一次,加上 resolve 后 Nginx 会定期重新解析,Consul 返回的 IP 列表变了,Nginx 就自动跟着变。注意 resolver 必须指向 Consul 的 DNS(127.0.0.1:8600,或配置系统 resolver 转发),valid=10s 控制缓存时间。这个功能需要 Nginx 1.27.3 以上版本(开源版),旧版本需要商业版或第三方模块。

第二种方式是 Consul Template,它是一个独立的进程,监听 Consul 的变化,自动渲染出 Nginx 配置文件并触发 reload:

{{range service "webapi"}}
server {{.Address}}:{{.Port}} max_fails=3;
{{end}}

把这段模板存成 backend.ctmpl,运行时用 consul-template -template "backend.ctmpl:/etc/nginx/conf.d/backend.conf:nginx -s reload",就实现了"服务上下线 → Nginx 配置自动更新并平滑重载"的完整闭环。这是生产环境里最成熟、兼容性最好的方案。

KV 存储做配置中心

除了服务发现,Consul 的 KV 还能当轻量配置中心用。比如你想让多台机器共享一个"是否开启维护模式"的开关,或者存一些密钥、接口地址:

# 写入一个键值
consul kv put config/maintenance_mode "false"

# 读取
consul kv get config/maintenance_mode

# 列出某个前缀下的所有键
consul kv get -recurse config/

# 删除
consul kv delete config/maintenance_mode

配合 Consul Template,你可以把 KV 里的值渲染进应用的配置文件。比如把数据库连接串放在 KV,应用启动时通过模板生成配置,改配置只需 consul kv put 一次,所有机器同步生效,不用逐台登录修改。

四个必须避开的坑

坑一:用两个或四个 Server。和大多数基于 Raft 的系统一样,Consul 的 Server 数量必须是奇数,推荐 3 或 5。两个 Server 无法形成多数派容错,四个反而比三个更容易出现脑裂。写到 3 就够了,多出来的机器跑 Client 即可。

坑二:健康检查频率设得太高。把 interval 设成 1s、2s 看着"灵敏",但在几十个服务实例下,Consul 集群会被海量的检查请求压垮,还会产生大量无意义的日志。10s 到 30s 是合理区间。另外检查接口本身一定要轻量——别在 /healthz 里去查数据库全表,健康检查应该只反映"进程是否活着、能否接受请求",而不是"所有下游是否都正常"。

坑三:把 Consul 的 HTTP API 暴露到公网。Consul 的 8500 端口默认没有任何认证,任何人访问 /v1/catalog/services 就能拿到你全部的服务拓扑和地址。正确做法是只监听内网或本机,需要远程访问时通过 SSH 隧道,或者至少加上 ACL Token 并把 binds 限制在私网。个人站的 Consul 应该永远藏在防火墙后面。

坑四:服务注册了但没人清理。如果你的应用是容器化的,容器重启后新注册一个实例,旧实例没被注销,Consul 里就会积累一堆死地址。要么配置 deregister_critical_service_after,要么在容器停止时调用 curl -X PUT http://127.0.0.1:8500/v1/agent/service/deregister/<id> 主动注销。健康检查虽然能让死实例不出现在 passing 列表里,但记录堆积本身会拖慢 Consul。

给个人站的实际取舍

最后要说清楚:Consul 不是万能的,也不一定适合每个个人站。如果你只有一两台后端机器,Nginx 里写死 IP 加上简单的 upstream 健康检查就已经够用了,引入 Consul 反而增加了运维负担——多了几个要监控的进程、多了网络通信、多了安全配置。真正值得上 Consul 的场景是:后端服务在 5 个以上并频繁扩缩容、需要跨多台机器动态感知服务上下线、或者需要集中管理配置。对个人站长来说,如果站点还处于"一台机器跑全部"的阶段,请先把 Nginx 的 upstream 和健康检查配置写扎实;等到真的遇到"改一次 upstream 要登好几台机器"的痛苦时,再上 Consul,那时它带来的价值才配得上它带来的复杂度。

Last modification:October 6th, 2026 at 08:25 pm

Leave a Comment