微服务架构里,“服务 A 怎么知道服务 B 的地址"这个问题被反复解决。最早大家用硬编码 IP(127.0.0.1:8080),后来用负载均衡(Nginx upstream),再后来才有了注册中心——服务启动时把自己注册上去,调用方从注册中心拉。
这篇文章把 Nacos / Consul / etcd / ZooKeeper 四款主流注册中心的 docker 部署、集群、客户端接入、坑点整理成一份实战清单。和之前 0.4 批次的 CI 工具对比 / 0.4 批次的 Java 全家桶不同,本文只聚焦"镜像实战”——拉哪个镜像、怎么配集群、客户端怎么连。
阅读对象:需要从零搭一套注册中心,或在生产环境做注册中心选型的开发者、运维
覆盖范围:四款组件的核心特性对比、Nacos 2.2.2 单机/集群部署、Consul 集群部署、etcd 3.4 单机/多机集群部署、ZooKeeper 3.8 部署 + WebUI、客户端 Spring Cloud 接入、常见排错
一、四款注册中心对比
| 维度 | Nacos | Consul | etcd | ZooKeeper |
|---|
| 语言 | Java | Go | Go | Java |
| 服务发现 | ✅ | ✅ | ✅(弱) | ✅(弱) |
| 配置中心 | ✅(核心) | ✅(KV) | ✅(KV) | ❌(需自己实现) |
| 健康检查 | ✅ | ✅ | ❌ | ❌ |
| 一致性协议 | 自研(AP/CP 可切) | Raft | Raft | ZAB |
| 多数据中心 | ❌ | ✅ | ❌ | ❌ |
| 镜像大小 | 250 MB | 130 MB | 50 MB | 300 MB |
| 内存占用 | 1 GB~ | 200 MB~ | 100 MB~ | 500 MB~ |
| 启动时间 | 30 秒 | 5 秒 | 3 秒 | 30 秒 |
| 客户端语言支持 | Java、Go、Node、Python | 多语言 | 多语言 | 多语言 |
| 适用场景 | Spring Cloud Alibaba 一站式 | 多语言 + 多数据中心 | K8s 内部(kube-apiserver) | 大数据生态(Hadoop、Kafka) |
When to use:
- Spring Cloud Alibaba 全家桶 → Nacos(一站式服务发现 + 配置中心)
- 多语言微服务 + 多数据中心 → Consul
- K8s 内部 / 强一致性 KV → etcd
- 大数据组件依赖(Hadoop、Kafka、HBase) → ZooKeeper
二、Nacos 部署
2.1 镜像选择
| 镜像 | 版本 | 备注 |
|---|
nacos/nacos-server:v2.0.3 | 2.0 LTS | 老版本,没有 gRPC 端口(只用 8848) |
nacos/nacos-server:v2.2.2 | 2.2 LTS | 推荐——修复了 2.2.0/2.2.1 的鉴权 bug |
nacos/nacos-server:latest | 2.x latest | 跟随最新 dev 版,生产慎用 |
2.2 关键变化:Nacos 2.x 端口
Nacos 客户端升级到 2.x 后,新增了 gRPC 通信方式——这意味着除了主端口 8848,还要开两个 gRPC 端口:
| 端口 | 偏移量 | 用途 |
|---|
| 8848 | — | HTTP(API + 控制台) |
| 9848 | 1000 | 客户端 gRPC 请求服务端 |
| 9849 | 1001 | 服务间 gRPC 同步 |
坑 1:Nacos 2.x 部署时必须同时开 8848/9848/9849 三个端口——少一个客户端就连不上。只开 8848 会报 “9848 port is unavailable”。
2.3 单机部署(MySQL 后端)
前置:先准备一个 MySQL 库 nacos-config:
1
2
3
| # nacos-db.sql 是 Nacos 官方提供的初始化脚本
# 包含 config_info / config_info_gray / config_tags_relation / his_config_info / config_info_beta 等表
mysql -u root -p < nacos-db.sql
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| docker run -d \
--name nacos \
--restart=always \
-p 8848:8848 \
-p 9848:9848 \
-p 9849:9849 \
-e NACOS_AUTH_ENABLE=true \
-e NACOS_AUTH_IDENTITY_KEY=nacos \
-e NACOS_AUTH_IDENTITY_VALUE=nacos \
-e PREFER_HOST_MODE=ip \
-e MODE=standalone \
-e SPRING_DATASOURCE_PLATFORM=mysql \
-e MYSQL_SERVICE_HOST=<INTERNAL_HOST> \
-e MYSQL_SERVICE_PORT=3306 \
-e MYSQL_SERVICE_USER=root \
-e MYSQL_SERVICE_PASSWORD=<REDACTED> \
-e MYSQL_SERVICE_DB_NAME=nacos-config \
-e MYSQL_SERVICE_DB_PARAM="characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true&useUnicode=true&useSSL=false&serverTimezone=Asia/Shanghai" \
nacos/nacos-server:v2.2.2
|
访问 http://<HOST>:8848/nacos,默认账号 nacos/nacos。
2.4 Nacos 2.2.0/2.2.1 鉴权坑
Nacos 2.2.0.1 和 2.2.1 移除了 3 个鉴权默认值(token.secret.key / nacos.core.auth.server.identity.key / nacos.core.auth.server.identity.value),避免用户部署时因未修改而引入撞库风险。但这导致:
- 不开启鉴权时,控制台 UI 登录要求被强制依赖
token.secret.key → 启动失败 - 开启鉴权时,用户必须自定义设置这三个值 → 否则节点无法启动
Nacos 2.2.2 的修复:
- 不开启鉴权 → 取消控制台 UI 登录要求
- 开启鉴权 → 用户自定义设置(启动时用环境变量传):
1
2
3
| -e NACOS_AUTH_ENABLE=true \
-e NACOS_AUTH_IDENTITY_KEY=nacos \
-e NACOS_AUTH_IDENTITY_VALUE=nacos
|
坑 2:2.2.2 之前(2.2.0/2.2.1)的"无鉴权启动失败"问题是2.2.2 解决的——别在 2.2.0/2.2.1 上踩坑,直接用 2.2.2。
2.5 关键 application.properties
容器内 /home/nacos/conf/application.properties:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
| # spring
server.servlet.contextPath=/nacos
server.contextPath=/nacos
server.port=8848
# 数据库
spring.datasource.platform=mysql
db.num=1
db.url.0=jdbc:mysql://<INTERNAL_HOST>:3306/nacos-config?serverTimezone=GMT%2B8&characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true
db.user=root
db.password=<REDACTED>
# 鉴权
nacos.core.auth.enabled=true
nacos.core.auth.server.identity.key=nacos
nacos.core.auth.server.identity.value=nacos
nacos.core.auth.system.type=nacos
nacos.core.auth.default.token.expire.seconds=18000
nacos.core.auth.default.token.secret.key=SecretKey012345678901234567890123456789012345678901234567890123456789
# 关闭鉴权缓存(开发环境)
nacos.core.auth.caching.enabled=false
# 性能调优(naming 模块)
nacos.naming.distro.taskDispatchThreadCount=10
nacos.naming.distro.taskDispatchPeriod=200
nacos.naming.distro.batchSyncKeyCount=1000
nacos.naming.distro.initDataRatio=0.9
nacos.naming.distro.syncRetryDelay=5000
nacos.naming.data.warmup=true
|
2.6 命名空间
真实场景:开发 / 测试 / 生产三套环境用同一个 Nacos 集群,靠 namespace 隔离。
Web 端:命名空间 → 新建命名空间 → 拿到 namespaceId
客户端(Spring Cloud):
1
2
3
4
5
6
7
8
9
10
| spring:
cloud:
nacos:
discovery:
server-addr: <INTERNAL_HOST>:8848
namespace: <NAMESPACE_ID> # 命名空间 ID
config:
server-addr: <INTERNAL_HOST>:8848
namespace: <NAMESPACE_ID>
file-extension: yaml
|
坑 3:namespace ID 一定要用生成的 UUID,不要用命名空间名(如 dev)——客户端按 UUID 匹配,写错就报 “namespace not found”。
2.7 集群部署
Nacos 集群需要 3 节点 + MySQL 后端(至少 3 节点保证 Raft 一致性)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
| # docker-compose-nacos-cluster.yml
version: '3'
services:
nacos1:
image: nacos/nacos-server:v2.2.2
container_name: nacos1
restart: always
ports:
- "8848:8848"
- "9848:9848"
- "9849:9849"
environment:
- NACOS_SERVERS=nacos1:8848 nacos2:8848 nacos3:8848
- MYSQL_SERVICE_HOST=<INTERNAL_HOST>
- MYSQL_SERVICE_DB_NAME=nacos-config
- MYSQL_SERVICE_USER=root
- MYSQL_SERVICE_PASSWORD=<REDACTED>
- SPRING_DATASOURCE_PLATFORM=mysql
- JVM_XMS=512m
- JVM_XMX=512m
nacos2:
image: nacos/nacos-server:v2.2.2
container_name: nacos2
restart: always
ports:
- "8849:8848"
- "9850:9848"
- "9851:9849"
environment:
- NACOS_SERVERS=nacos1:8848 nacos2:8848 nacos3:8848
- MYSQL_SERVICE_HOST=<INTERNAL_HOST>
- MYSQL_SERVICE_DB_NAME=nacos-config
- MYSQL_SERVICE_USER=root
- MYSQL_SERVICE_PASSWORD=<REDACTED>
- SPRING_DATASOURCE_PLATFORM=mysql
- JVM_XMS=512m
- JVM_XMX=512m
nacos3:
image: nacos/nacos-server:v2.2.2
container_name: nacos3
restart: always
ports:
- "8850:8848"
- "9852:9848"
- "9853:9849"
environment:
- NACOS_SERVERS=nacos1:8848 nacos2:8848 nacos3:8848
- MYSQL_SERVICE_HOST=<INTERNAL_HOST>
- MYSQL_SERVICE_DB_NAME=nacos-config
- MYSQL_SERVICE_USER=root
- MYSQL_SERVICE_PASSWORD=<REDACTED>
- SPRING_DATASOURCE_PLATFORM=mysql
- JVM_XMS=512m
- JVM_XMX=512m
|
坑 4:Nacos 集群节点之间用容器名互通——nacos1:8848 是 Docker 网络里的 hostname。如果跨主机部署(多台物理机),要把 nacos1:8848 换成 10.8.x.x:8848 这种宿主机 IP。
三、Consul 部署
3.1 镜像
1
| docker pull consul:1.9.5
|
3.2 单机模式
1
2
3
| docker run -d \
-e CONSUL_BIND_INTERFACE=eth0 \
consul agent -dev -join=172.17.0.2
|
坑 5:dev 模式是开发用的,不能用于生产。Consul dev 模式没有持久化、所有数据存内存、重启丢。
3.3 集群部署
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
| # docker-compose-consul-cluster.yml
version: '3.7'
services:
consul-server-1:
image: consul:1.9.5
container_name: consul-server-1
restart: always
command:
- "agent"
- "-server"
- "-bootstrap-expect=3"
- "-ui"
- "-data-dir=/consul/data"
- "-bind=0.0.0.0"
- "-client=0.0.0.0"
- "-advertise=<NODE1_IP>"
- "-retry-join=<NODE2_IP>"
- "-retry-join=<NODE3_IP>"
ports:
- "8500:8500" # WebUI
- "8600:8600/udp" # DNS
- "8300:8300" # Server RPC
- "8301:8301" # Serf LAN
- "8302:8302" # Serf WAN
volumes:
- "/data/consul/server1:/consul/data"
consul-server-2:
image: consul:1.9.5
container_name: consul-server-2
# 同上, advertise=<NODE2_IP>
consul-server-3:
image: consul:1.9.5
container_name: consul-server-3
# 同上, advertise=<NODE3_IP>
|
3 节点各启动一次:
1
| docker-compose -f docker-compose-consul-cluster.yml up -d
|
WebUI:http://<NODE_IP>:8500/
3.4 客户端注册
Spring Cloud:
1
2
3
4
5
6
7
8
9
| spring:
cloud:
consul:
host: <INTERNAL_HOST>
port: 8500
discovery:
prefer-ip-address: true
health-check-interval: 10s
instance-id: ${spring.application.name}:${server.port}
|
Go:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| import "github.com/hashicorp/consul/api"
config := api.DefaultConfig()
config.Address = "<INTERNAL_HOST>:8500"
client, _ := api.NewClient(config)
// 注册服务
reg := &api.AgentServiceRegistration{
ID: "my-service-1",
Name: "my-service",
Port: 8080,
Check: &api.AgentServiceCheck{
HTTP: "http://<HOST>:8080/health",
Interval: "10s",
},
}
client.Agent().ServiceRegister(reg)
|
坑 6:Consul 默认会做"健康检查 HTTP"——如果服务还没起来就报健康检查失败。解决:先启动服务再注册,或者把 Check.HTTP 改成 Check.TCP(更轻量)。
四、etcd 部署
4.1 镜像
1
2
3
| docker pull bitnami/etcd:3
# 或
docker pull quay.io/coreos/etcd:v3.4.16
|
4.2 单机部署
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| # docker-compose.yml
version: '3.7'
services:
etcd:
image: bitnami/etcd:3
container_name: etcd-single
restart: always
environment:
- ALLOW_NONE_AUTHENTICATION=yes
- ETCD_ADVERTISE_CLIENT_URLS=http://<INTERNAL_HOST>:2379
- ETCD_LISTEN_CLIENT_URLS=http://0.0.0.0:2379
ports:
- "2379:2379"
volumes:
- "/data/etcd:/bitnami/etcd/data"
|
启动后验证:
1
2
3
4
5
6
7
8
9
10
11
12
13
| # 复制 etcdctl 客户端
docker cp etcd-single:/usr/local/bin/etcdctl /home/docker/etcd
sudo mv /home/docker/etcd/etcdctl /usr/local/bin/
# 让 etcdctl 使用 v3 API
echo 'export ETCDCTL_API=3' >> ~/.zshrc
source ~/.zshrc
# 查看版本
etcdctl version
# 查看成员
etcdctl member list
|
4.3 多机集群部署
前置:3 个节点(192.168.3.101、192.168.3.102、192.168.3.103),IP 可达。
每个节点的 docker-compose.yml 结构一样,只有 3 个参数不同(--name、--initial-advertise-peer-urls、--advertise-client-urls):
192.168.3.101 节点:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
| version: '3.7'
services:
etcd:
image: quay.io/coreos/etcd
container_name: my-etcd
restart: always
environment:
- ETCDCTL_API=3
command:
- etcd
- --name=etcd1
- --data-dir=/etcd-data
- --listen-peer-urls=http://0.0.0.0:2380
- --listen-client-urls=http://0.0.0.0:2379
- --initial-advertise-peer-urls=http://192.168.3.101:2380
- --advertise-client-urls=http://192.168.3.101:2379
- --initial-cluster-token=cluster-token
- --initial-cluster=etcd1=http://192.168.3.101:2380,etcd2=http://192.168.3.102:2380,etcd3=http://192.168.3.103:2380
- --initial-cluster-state=new
volumes:
- $PWD/etcd-data:/etcd-data
ports:
- 2379:2379
- 2380:2380
network_mode: "host"
|
192.168.3.102 / 192.168.3.103 节点:把 etcd1 改为 etcd2 / etcd3,URL 改对应 IP 即可。
1
2
3
4
5
6
7
8
9
| # 3 个节点分别启动
docker-compose up -d
# 查看集群成员
export ENDPOINTS=192.168.3.101:2379,192.168.3.102:2379,192.168.3.103:2379
etcdctl --endpoints=$ENDPOINTS member list
# 查看成员状态
etcdctl --write-out=table --endpoints=$ENDPOINTS endpoint status
|
4.4 常见排错
症状:retrying of unary invoker failed
原因:--advertise-client-urls 和 --initial-advertise-peer-urls 设成了 0.0.0.0 或 localhost——必须是具体 IP 或 hostname。
解决:把两个 URL 改成节点实际 IP。
4.5 客户端使用
1
2
3
4
5
6
7
8
9
10
11
12
13
| # 写
etcdctl put /config/db/url "jdbc:mysql://<HOST>:3306/test"
etcdctl put /config/db/user "root"
# 读
etcdctl get /config/db/url
etcdctl get /config --prefix
# 删
etcdctl del /config/db/url
# 监听变更
etcdctl watch /config/db/url
|
五、ZooKeeper 部署
5.1 镜像
1
| docker pull zookeeper:3.8.0
|
5.2 单机部署
1
| docker run -d -p 2181:2181 --name zookeeper --restart always zookeeper:3.8.0
|
5.3 客户端使用
1
2
3
4
5
6
7
8
9
10
11
| # 进入容器
docker exec -it zookeeper /bin/bash
cd bin
./zkCli.sh -server 127.0.0.1:2181
# zkCli 命令
ls /
create /test "hello"
get /test
set /test "world"
delete /test
|
5.4 WebUI 部署
1
2
3
4
5
6
7
8
9
10
| docker pull tobilg/zookeeper-webui:latest
docker run -d \
-p 8098:8080 \
-e ZK_DEFAULT_NODE=<INTERNAL_HOST>:2181/ \
-e USER=admin \
-e PASSWORD=<REDACTED> \
--name zk-web-ui \
--restart always \
-t tobilg/zookeeper-webui
|
访问 http://<HOST>:8098/。
5.5 集群部署
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
| # docker-compose.yml
version: '3'
services:
zk1:
image: zookeeper:3.8.0
container_name: zk1
restart: always
environment:
- ZOO_MY_ID=1
- ZOO_SERVERS=server.1=zk1:2888:3888;2181 server.2=zk2:2888:3888;2181 server.3=zk3:2888:3888;2181
ports:
- "2181:2181"
- "8081:8080"
networks:
- zk-net
zk2:
image: zookeeper:3.8.0
container_name: zk2
restart: always
environment:
- ZOO_MY_ID=2
- ZOO_SERVERS=server.1=zk1:2888:3888;2181 server.2=zk2:2888:3888;2181 server.3=zk3:2888:3888;2181
ports:
- "2182:2181"
- "8082:8080"
networks:
- zk-net
zk3:
image: zookeeper:3.8.0
container_name: zk3
restart: always
environment:
- ZOO_MY_ID=3
- ZOO_SERVERS=server.1=zk1:2888:3888;2181 server.2=zk2:2888:3888;2181 server.3=zk3:2888:3888;2181
ports:
- "2183:2181"
- "8083:8080"
networks:
- zk-net
networks:
zk-net:
driver: bridge
|
坑 7:ZooKeeper 集群节点数 必须为奇数(3、5、7)——ZAB 协议要求多数派,奇数能容忍 n/2 节点故障。4 节点集群和 3 节点集群的容错能力是一样的,但 4 节点更耗资源。
六、四款组件的 Spring Cloud 接入
6.1 Nacos
1
2
3
4
5
6
7
8
| <dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
|
1
2
3
4
5
6
7
8
9
10
11
12
| spring:
application:
name: order-service
cloud:
nacos:
discovery:
server-addr: <INTERNAL_HOST>:8848
namespace: <NAMESPACE_ID>
config:
server-addr: <INTERNAL_HOST>:8848
namespace: <NAMESPACE_ID>
file-extension: yaml
|
6.2 Consul
1
2
3
4
5
6
7
8
| <dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-consul-discovery</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-consul-config</artifactId>
</dependency>
|
1
2
3
4
5
6
7
8
9
10
11
12
| spring:
cloud:
consul:
host: <INTERNAL_HOST>
port: 8500
discovery:
prefer-ip-address: true
health-check-interval: 10s
config:
format: yaml
prefix: config
default-context: order-service
|
6.3 etcd
1
2
3
4
| <dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-etcd-discovery</artifactId>
</dependency>
|
坑 8:Spring Cloud Etcd 已经被官方弃用——推荐用 Kubernetes Service 做服务发现(K8s 内部 etcd 自动做)。如果是非 K8s 环境用 etcd 做服务发现,建议直接用 Nacos。
6.4 ZooKeeper
1
2
3
4
| <dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-zookeeper-discovery</artifactId>
</dependency>
|
1
2
3
4
| spring:
cloud:
zookeeper:
connect-string: <INTERNAL_HOST>:2181,<INTERNAL_HOST>:2182,<INTERNAL_HOST>:2183
|
七、常见排错
7.1 Nacos 客户端报 “9848 port is unavailable”
症状:Spring Boot 启动时报 failed to req API:127.0.0.1:9848 或 9848 port is unavailable
解决:
- 服务端要开放 9848 端口(不只是 8848)
- 客户端 → 服务端的网络是通的
7.2 Consul 健康检查失败导致服务被踢
症状:服务注册到 Consul 后,30 秒后被自动剔除。
解决:
- 检查
Check.HTTP 路径是否真的存在(如 /actuator/health) - Spring Boot 加
management.endpoints.web.exposure.include: health - 调长
deregister-critical-service-after 时间
7.3 etcd 集群脑裂
症状:etcdctl member list 显示 3 个节点都是 leader。
解决:
- 强制重置集群:停所有节点 → 删
--data-dir 目录 → 重新 --initial-cluster-state=new - 加
--election-timeout 避免偶发切换
7.4 ZooKeeper 启动失败 “Cannot open channel to X at election address”
症状:ZooKeeper 容器日志反复报 Cannot open channel to 2 at election address /xx:3888
解决:
- 3888 端口是否开放
- 容器名(
zk1 / zk2 / zk3)和 ZOO_SERVERS 配置是否一致 ZOO_MY_ID 是否和 server.X 中的 X 一致
八、写在最后
注册中心选型没有银弹——四款组件定位不同:
- Nacos:Spring Cloud Alibaba 一站式,国内中小团队首选
- Consul:多语言、多数据中心场景的"多面手"
- etcd:K8s 内部组件、非 K8s 场景慎用
- ZooKeeper:大数据生态依赖、传统微服务可用
下一步建议:
- Nacos 2.x 集群 + MySQL 后端高可用(MySQL 主从 + Keepalived VIP)
- Nacos + Sentinel 做流量控制和熔断
- Consul Connect 做服务间 mTLS 加密
参考资料