Featured image of post 注册中心镜像实战:Nacos / Consul / etcd / ZooKeeper 部署、集群、配置与坑点

注册中心镜像实战:Nacos / Consul / etcd / ZooKeeper 部署、集群、配置与坑点

把 Nacos 2.x、Consul、etcd、ZooKeeper 四款注册中心/服务发现组件的 docker 部署、集群方案、客户端接入、坑点对比整理成一份实战清单——每款都给完整 docker-compose 模板 + 排错指南。

微服务架构里,“服务 A 怎么知道服务 B 的地址"这个问题被反复解决。最早大家用硬编码 IP127.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 接入、常见排错

一、四款注册中心对比

维度NacosConsuletcdZooKeeper
语言JavaGoGoJava
服务发现✅(弱)✅(弱)
配置中心✅(核心✅(KV)✅(KV)❌(需自己实现)
健康检查
一致性协议自研(AP/CP 可切)RaftRaftZAB
多数据中心
镜像大小250 MB130 MB50 MB300 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.32.0 LTS老版本,没有 gRPC 端口(只用 8848)
nacos/nacos-server:v2.2.22.2 LTS推荐——修复了 2.2.0/2.2.1 的鉴权 bug
nacos/nacos-server:latest2.x latest跟随最新 dev 版,生产慎用

2.2 关键变化:Nacos 2.x 端口

Nacos 客户端升级到 2.x 后,新增了 gRPC 通信方式——这意味着除了主端口 8848,还要开两个 gRPC 端口:

端口偏移量用途
8848HTTP(API + 控制台)
98481000客户端 gRPC 请求服务端
98491001服务间 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

坑 5dev 模式是开发用的,不能用于生产。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.101192.168.3.102192.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.0localhost——必须是具体 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:98489848 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 加密

参考资料

使用 Hugo 构建
主题 StackJimmy 设计