【web3基础】go-zero使用etcd实现服务注册与发现(四)

go-zero使用etcd实现服务注册与发现

关键词:服务注册与发现、Etcd、Go-Zero、负载均衡、微服务等关键概念

前言:我们要解决什么问题?

假设你开了一家连锁餐厅,有两家分店:

  • 分店A:地址在"人民路1号"
  • 分店B:地址在"解放路2号"

顾客想吃你家菜,怎么知道去哪家店?

传统做法:顾客拿个小本本,记下两家店的地址,自己决定去哪家。

问题来了

  1. 如果你开了第三家分店,顾客不知道
  2. 如果分店A倒闭了,顾客还往那里跑
  3. 顾客不知道哪家店人少、哪家店人多

聪明的做法:搞个"订餐热线",顾客打电话,系统自动告诉他:

  • 现在有2家店营业
  • 分店A现在空闲,建议去分店A
  • 如果分店B突然关门了,系统自动更新

这就是服务注册与发现


第一部分:概念解释(目标)

1. 什么是服务注册?

服务注册 = 服务启动时,告诉注册中心:"我开业了,地址是XXX"

就像分店开业时,向总部登记:

  • "我是分店A,地址人民路1号,开始营业"

2. 什么是服务发现?

服务发现 = 客户想调用服务时,问注册中心:"哪家店开门?地址多少?"

就像顾客打电话问总部:

  • "我想吃饭,哪家店开门?"
  • 总部回复:"现在有分店A(人民路1号)和分店B(解放路2号),分店A人少,建议去分店A"

3. 什么是负载均衡?

负载均衡 = 系统自动帮你选一个不忙的服务

就像总部不仅告诉你地址,还帮你参谋:

  • "分店A现在只有1桌客人,去那边吧"
  • "分店B排队都排到门口了,别去了"

第二部分:实际操作(跟着我一步步来)

准备工作

你需要准备的东西:

  • 一台电脑(Linux/Mac/Windows都行)
  • 安装好Go语言(1.20以上版本)
  • 一个能用的终端

步骤1:安装Etcd(注册中心)

Etcd就是我们的"订餐热线",所有服务都往这里登记。

bash 复制代码
# 下载 Etcd
wget https://github.com/etcd-io/etcd/releases/download/v3.5.12/etcd-v3.5.12-linux-amd64.tar.gz

# 解压
tar -zxvf etcd-v3.5.12-linux-amd64.tar.gz
cd etcd-v3.5.12-linux-amd64

# 移动到系统目录(方便随时调用)
sudo mv etcd etcdctl /usr/local/bin/

# 验证安装成功
etcd --version
# 输出类似:etcd Version: 3.5.12 ...

# 启动 Etcd(保持这个终端开着,不要关)
etcd
# 看到类似输出就成功了
# {"level":"info","msg":"started","time":"2024-01-01T10:00:00Z"}

此时发生了什么?

  • Etcd启动成功,监听2379端口
  • 等待服务来注册

步骤2:创建项目

bash 复制代码
# 创建一个叫"user"的RPC项目
goctl rpc new user

# 进入项目目录
cd user

# 初始化Go模块依赖
go mod tidy

项目结构长这样:

复制代码
user/
├── etc/
│   └── user.yaml        # 配置文件(关键)
├── internal/
│   ├── config/         # 配置定义
│   ├── logic/          # 业务逻辑
│   ├── server/         # RPC服务实现
│   └── svc/            # 服务上下文
├── user/               # 生成的proto代码
├── user.proto          # 接口定义
├── user.go             # 服务入口
└── client_discovery.go # 客户端(我们待会儿写)

步骤3:配置服务端(8080端口)

打开 etc/user.yaml,内容如下:

yaml 复制代码
Name: user.rpc           # 服务名称
ListenOn: 0.0.0.0:8080   # 监听8080端口

Etcd:                    # 服务注册配置
  Hosts:
  - 127.0.0.1:2379       # Etcd地址
  Key: user.rpc          # 注册的Key(重要!)

这个配置的意思是:

  • 我的服务叫"user.rpc"
  • 我监听在8080端口
  • 我要向Etcd(127.0.0.1:2379)注册自己
  • 注册的Key是"user.rpc"

步骤4:创建第二个服务端(8081端口)

为了演示负载均衡,我们需要启动两个服务实例。

bash 复制代码
# 复制配置文件
cp etc/user.yaml etc/user-8081.yaml

# 编辑新配置文件
# 把 ListenOn 改成 8081
# 其他保持不变(特别是 Etcd.Key 要一样)

编辑 etc/user-8081.yaml

yaml 复制代码
Name: user.rpc
ListenOn: 0.0.0.0:8081   # 改成8081端口

Etcd:
  Hosts:
  - 127.0.0.1:2379
  Key: user.rpc          # Key必须和上面一样!

为什么Key要一样?

  • 就像两家分店都属于同一个品牌
  • 客户通过品牌名找到所有分店
  • Key就是这个"品牌名"

步骤5:启动两个服务端

打开三个终端窗口:

  • 终端1:Etcd(已经启动)
  • 终端2:服务端8080
  • 终端3:服务端8081

终端2(启动8080服务):

bash 复制代码
cd ~/go-zero-learn/grpc-demo/user
go run user.go -f etc/user.yaml

# 看到这个就成功了:
# Starting rpc server at 0.0.0.0:8080...

终端3(启动8081服务):

bash 复制代码
cd ~/go-zero-learn/grpc-demo/user
go run user.go -f etc/user-8081.yaml

# 看到这个就成功了:
# Starting rpc server at 0.0.0.0:8081...

此时发生了什么?

两个服务都向Etcd注册了自己:

复制代码
Etcd里的数据:
┌─────────────────────────────────────────────┐
│ Key: user.rpc                               │
│                                             │
│ 服务列表:                                   │
│ - 172.29.206.73:8080  (分店A)             │
│ - 172.29.206.73:8081  (分店B)             │
└─────────────────────────────────────────────┘

你可以用etcdctl验证:

bash 复制代码
etcdctl get user.rpc --prefix

# 输出类似:
# user.rpc/172.29.206.73:8080
# {"Host":"172.29.206.73","Port":8080}
# user.rpc/172.29.206.73:8081
# {"Host":"172.29.206.73","Port":8081}

步骤6:创建客户端

创建 client_discovery.go 文件:

go 复制代码
package main

import (
	"context"
	"log"
	"user/userclient"

	"github.com/zeromicro/go-zero/core/discov"
	"github.com/zeromicro/go-zero/zrpc"
)

func main() {
	// 配置服务发现
	clientConf := zrpc.RpcClientConf{
		Etcd: discov.EtcdConf{
			Hosts: []string{"127.0.0.1:2379"}, // Etcd地址
			Key:   "user.rpc",                  // 服务Key
		},
		Timeout: 3000,
	}

	// 创建客户端(自动从Etcd发现服务)
	conn := zrpc.MustNewClient(clientConf)
	client := userclient.NewUser(conn)

	// 调用服务
	req := &userclient.Request{
		Ping: "go-zero",
	}
	resp, err := client.Ping(context.Background(), req)
	if err != nil {
		log.Println(err)
		return
	}

	log.Println(resp)
}

这段代码做了什么?

  1. 告诉客户端:"去Etcd查询user.rpc这个服务"
  2. Etcd返回:"有两个地址:8080和8081"
  3. 客户端:"我选一个连接"
  4. 调用Ping方法
  5. 打印结果

步骤7:运行客户端,看效果!

bash 复制代码
# 在第四个终端运行
cd ~/go-zero-learn/grpc-demo/user
go run client_discovery.go

第一次运行:

json 复制代码
{"@timestamp":"2026-07-19T16:23:24.265+08:00","caller":"p2c/p2c.go:195","content":"p2c - conn: 172.29.206.73:8080, load: 1, reqs: 0; conn: 172.29.206.73:8081, load: 1046, reqs: 1","level":"stat"}
2026/07/19 16:23:24 pong:"go-zero"

解读:

  • 系统发现两个服务:8080(负载=1)和8081(负载=1046)
  • 这次选中了8081(虽然它更忙,但P2C算法就是这样)
  • 返回"pong:go-zero"

第二次运行:

json 复制代码
{"@timestamp":"2026-07-19T16:23:25.959+08:00","caller":"p2c/p2c.go:195","content":"p2c - conn: 172.29.206.73:8081, load: 969, reqs: 1","level":"stat"}
2026/07/19 16:23:25 pong:"go-zero"

这次又选中了8081。

第三次运行:

json 复制代码
{"@timestamp":"2026-07-19T16:23:27.641+08:00","caller":"p2c/p2c.go:195","content":"p2c - conn: 172.29.206.73:8080, load: 1, reqs: 0; conn: 172.29.206.73:8081, load: 985, reqs: 1","level":"stat"}
2026/07/19 16:23:27 pong:"go-zero"

这次选中了8080!

看到规律了吗?

  • 有时访问8080,有时访问8081
  • 系统自动帮你做负载均衡
  • 你不需要知道具体地址,只要知道Key="user.rpc"

第三部分:原理剖析(这背后到底发生了什么?)

整体流程图

复制代码
┌────────────────────────────────────────────────────────────────┐
│                    完整流程时序图                               │
│                                                                 │
│  第一步:服务注册                                                │
│  ┌──────────────┐                    ┌──────────────┐          │
│  │ 服务端 8080  │                    │ 服务端 8081  │          │
│  │              │                    │              │          │
│  │ 启动时...    │                    │ 启动时...    │          │
│  └──────────────┘                    └──────────────┘          │
│         │                                   │                   │
│         │ 向Etcd注册                         │ 向Etcd注册        │
│         ▼                                   ▼                   │
│  ┌──────────────────────────────────────────────────────┐      │
│  │                     Etcd                              │      │
│  │                                                      │      │
│  │  存储的服务列表:                                      │      │
│  │  user.rpc → [172.29.206.73:8080, 172.29.206.73:8081]│      │
│  └──────────────────────────────────────────────────────┘      │
│                                                                 │
│  第二步:服务发现                                                │
│  ┌──────────────┐                                             │
│  │   客户端      │                                             │
│  │              │                                             │
│  │ 启动时...    │                                             │
│  └──────────────┘                                             │
│         │                                                       │
│         │ 1. 查询Etcd:"user.rpc有哪些服务?"                   │
│         ▼                                                       │
│  ┌──────────────────────────────────────────────────────┐      │
│  │                     Etcd                              │      │
│  │                                                      │      │
│  │  返回:[172.29.206.73:8080, 172.29.206.73:8081]      │      │
│  └──────────────────────────────────────────────────────┘      │
│         │                                                       │
│         │ 2. 收到服务列表                                        │
│         ▼                                                       │
│  ┌──────────────┐                                             │
│  │   客户端      │                                             │
│  │              │                                             │
│  │ 负载均衡...  │                                             │
│  │ 选择8080     │                                             │
│  │ 或8081       │                                             │
│  └──────────────┘                                             │
│         │                                                       │
│         │ 3. 连接选中的服务                                      │
│         ▼                                                       │
│  ┌──────────────┐        或        ┌──────────────┐          │
│  │ 服务端 8080  │                  │ 服务端 8081  │          │
│  │              │                  │              │          │
│  │ 处理请求     │                  │ 处理请求     │          │
│  └──────────────┘                  └──────────────┘          │
│                                                                 │
└────────────────────────────────────────────────────────────────┘

关键组件说明

1. Etcd(注册中心)

作用: 存储"服务名 → 地址列表"的映射

数据结构:

复制代码
Key: user.rpc
Value: [
  {"Host":"172.29.206.73","Port":8080},
  {"Host":"172.29.206.73","Port":8081}
]

关键特性:

  • 高可用:可以搭建集群
  • 实时性:服务启动立即注册
  • 健康检查:服务挂了自动剔除
2. 服务端(Server)

做了什么:

go 复制代码
// 1. 加载配置
var c config.Config
conf.MustLoad(*configFile, &c)

// 2. 创建服务
s := zrpc.MustNewServer(c.RpcServerConf, ...)

// 内部自动执行:
// - 连接Etcd
// - 注册服务(Key=user.rpc, Value=IP:Port)
// - 启动心跳(每5秒续约一次)
3. 客户端(Client)

做了什么:

go 复制代码
// 1. 配置服务发现
clientConf := zrpc.RpcClientConf{
    Etcd: discov.EtcdConf{
        Hosts: []string{"127.0.0.1:2379"},
        Key:   "user.rpc",
    },
}

// 2. 创建连接(自动服务发现)
conn := zrpc.MustNewClient(clientConf)

// 内部自动执行:
// - 连接Etcd
// - 查询Key=user.rpc的服务列表
// - 启动监听(服务变化自动更新)
// - 负载均衡选择一个实例
4. P2C负载均衡算法

P2C = Pick of 2 Choices(二选一)

原理:

复制代码
┌─────────────────────────────────────────────────────────────┐
│                    P2C负载均衡算法                            │
│                                                             │
│  服务列表:[8080, 8081]                                      │
│                                                             │
│  第一步:随机选两个                                           │
│  ┌─────────────┐     ┌─────────────┐                       │
│  │   8080      │     │   8081      │                       │
│  │  load: 100  │     │  load: 500  │                       │
│  └─────────────┘     └─────────────┘                       │
│                                                             │
│  第二步:比较负载                                             │
│  100 < 500,选负载低的                                        │
│                                                             │
│  第三步:选中 8080                                            │
│                                                             │
│  优点:                                                      │
│  - 简单高效                                                   │
│  - 无需知道全局负载                                           │
│  - 自动适应后端变化                                           │
│                                                             │
└─────────────────────────────────────────────────────────────┘

日志解读

json 复制代码
{
  "@timestamp": "2026-07-19T16:23:24.265+08:00",
  "caller": "p2c/p2c.go:195",
  "content": "p2c - conn: 172.29.206.73:8080, load: 1, reqs: 0; conn: 172.29.206.73:8081, load: 1046, reqs: 1",
  "level": "stat"
}

逐字段解读:

  • timestamp:时间戳
  • caller:日志来源(p2c.go第195行)
  • content:负载情况
    • 8080: load=1(负载很低),reqs=0(当前请求数)
    • 8081: load=1046(负载较高),reqs=1
  • level:日志级别(stat=统计信息)

这行日志的意思是:

"我发现两个服务:8080很闲,8081有点忙。我选择了XXX。"


第四部分:本质思考(这到底解决了什么问题?)

问题一:为什么需要服务注册与发现?

传统方式(硬编码):

go 复制代码
// 客户端写死地址
conn, _ := grpc.Dial("192.168.1.100:8080", ...)

问题:

  1. 地址变了,要改代码重新编译
  2. 无法动态扩容
  3. 服务挂了,客户端还往那里发请求
  4. 多个实例,无法自动负载均衡

服务发现方式:

go 复制代码
// 客户端只关心服务名
clientConf := zrpc.RpcClientConf{
    Etcd: discov.EtcdConf{
        Key: "user.rpc",  // 只需要服务名
    },
}

优势:

  1. 地址变了,Etcd自动更新,客户端无感知
  2. 新增服务实例,自动注册,客户端自动发现
  3. 服务挂了,Etcd自动剔除,客户端自动切换
  4. 多实例自动负载均衡

问题二:负载均衡的意义是什么?

没有负载均衡:

复制代码
请求1 → 8080
请求2 → 8080
请求3 → 8080
...
请求1000 → 8080  (8080累死)
8081一直空闲      (8081闲死)

有负载均衡(P2C):

复制代码
请求1 → 8081(8080空闲,但随机选中8081)
请求2 → 8081
请求3 → 8080(发现8081忙,选8080)
请求4 → 8080
...
最终:8080处理500个,8081处理500个(均衡)

问题三:这和微服务有什么关系?

微服务的核心:服务拆分

复制代码
单体应用:
┌──────────────────────┐
│    一个大应用        │
│  ┌──────────────┐   │
│  │ 用户模块      │   │
│  │ 订单模块      │   │
│  │ 支付模块      │   │
│  └──────────────┘   │
└──────────────────────┘

微服务:
┌──────────┐   ┌──────────┐   ┌──────────┐
│ 用户服务 │   │ 订单服务 │   │ 支付服务 │
│  8080    │   │  8081    │   │  8082    │
└──────────┘   └──────────┘   └──────────┘
     ↑              ↑              ↑
     │              │              │
     └──────────────┴──────────────┘
                    │
              ┌─────────┐
              │  Etcd   │
              └─────────┘

微服务需要解决的问题:

  1. 服务之间怎么找到对方?→ 服务发现
  2. 多个实例选哪个?→ 负载均衡
  3. 服务挂了怎么办?→ 健康检查

Go-Zero的RPC就是解决这些问题的工具!


第五部分:总结

核心概念总结

概念 作用 比喻
Etcd 注册中心 订餐热线
服务注册 服务启动时登记 分店开业登记
服务发现 查找可用服务 顾客打电话查询
负载均衡 自动选择实例 推荐人少的分店
P2C算法 负载均衡策略 比较两家店,选人少的

关键配置总结

服务端配置:

yaml 复制代码
Etcd:
  Hosts: [127.0.0.1:2379]  # 注册中心地址
  Key: user.rpc            # 服务标识(重要!)

客户端配置:

go 复制代码
Etcd: discov.EtcdConf{
    Hosts: []string{"127.0.0.1:2379"},
    Key:   "user.rpc",  // 必须和服务端一致!
}

关键点:Key必须一致!

  • 就像品牌名要一致
  • 客户端通过Key找服务端
  • Key错了,找不到服务

操作步骤总结

复制代码
第一步:启动注册中心
├── etcd
│
第二步:启动服务端(可多个)
├── go run user.go -f etc/user.yaml      (8080)
├── go run user.go -f etc/user-8081.yaml (8081)
│
第三步:运行客户端
└── go run client_discovery.go
    ├── 自动从Etcd发现服务
    ├── 自动负载均衡
    └── 返回结果

本质总结

服务注册与发现的本质:

把"地址硬编码"变成"名字查找",把"手动管理"变成"自动管理"。

负载均衡的本质:

把"请求均匀分布到多个实例",避免"忙的忙死,闲的闲死"。

微服务通信的本质:

通过"服务名"而不是"地址"来通信,实现解耦。


第六部分:常见问题

Q1:如果我启动10个服务实例会怎样?

答: 都会注册到Etcd,客户端自动发现10个实例,负载均衡在10个实例之间进行。

Q2:如果某个服务挂了会怎样?

答:

  1. Etcd检测到心跳失败
  2. 自动剔除该实例
  3. 客户端自动感知更新
  4. 后续请求不会发到挂掉的服务

Q3:Etcd挂了会怎样?

答:

  1. 服务无法注册,新服务启动失败
  2. 已注册的服务继续工作
  3. 客户端可以使用缓存的地址列表
  4. 生产环境建议Etcd集群(3-5个节点)

Q4:为什么有时选中负载高的实例?

答: P2C算法是"二选一",随机选2个,比较后选负载低的。如果恰好随机选了两个负载都高的,就会选中负载高的。但长期来看,负载会趋于均衡。

Q5:可以在不同机器上部署服务吗?

答: 可以!

  • 服务A在机器192.168.1.100:8080
  • 服务B在机器192.168.1.101:8080
  • 客户端自动发现,自动负载均衡
  • 这就是微服务架构的基础!

附录:关键核心代码

服务端配置(etc/user.yaml)

yaml 复制代码
Name: user.rpc
ListenOn: 0.0.0.0:8080

Etcd:
  Hosts:
  - 127.0.0.1:2379
  Key: user.rpc

客户端代码(client_discovery.go)

go 复制代码
package main

import (
	"context"
	"log"
	"user/userclient"

	"github.com/zeromicro/go-zero/core/discov"
	"github.com/zeromicro/go-zero/zrpc"
)

func main() {
	clientConf := zrpc.RpcClientConf{
		Etcd: discov.EtcdConf{
			Hosts: []string{"127.0.0.1:2379"},
			Key:   "user.rpc",
		},
		Timeout: 3000,
	}

	conn := zrpc.MustNewClient(clientConf)
	client := userclient.NewUser(conn)

	req := &userclient.Request{
		Ping: "go-zero",
	}
	resp, err := client.Ping(context.Background(), req)
	if err != nil {
		log.Println(err)
		return
	}

	log.Println(resp)
}

复盘:

本文是一篇使用 Go-Zero 框架结合 Etcd 实现服务注册与发现的实战教程。

文章通过餐厅分店的生动比喻,系统性地讲解了微服务架构中的核心概念和实现方法。

主要内容包括:

  • 1)服务注册、服务发现、负载均衡等核心概念解释;
  • 2)从安装 Etcd、创建项目、配置服务端到运行客户端的完整操作步骤;
  • 3)深入剖析 Etcd 注册中心、服务端、客户端及 P2C 负载均衡算法的工作原理;
  • -4)探讨服务注册与发现解决的传统硬编码问题、负载均衡的意义及其与微服务架构的关系;
  • 5)总结核心概念、关键配置和操作步骤,并解答常见问题。通过本文,读者可以掌握使用 Go-Zero 和 Etcd 构建可扩展、高可用的微服务系统的基础技能。
相关推荐
Wang's Blog1 天前
Go-Zero项目开发4: 用户服务搜索、详情与统一错误处理
开发语言·golang
灯澜忆梦1 天前
GO_文件处理---字符串操作
开发语言·golang
Wzx1980122 天前
RAG优化:17中RAG优化方案,那种才是最佳选择?
golang
李燚2 天前
Graph 执行引擎源码:Eino 的节点调度是怎么工作的(第56篇-E42)
golang·ai编程·graph·agent框架·eino
wujf902 天前
Go 内存泄漏排查完整教程 (pprof)
开发语言·golang·xcode
Wang's Blog2 天前
Go-Zero项目开发5: 数据库与缓存一致性问题及 go-zero 缓存机制解析
数据库·缓存·golang
布朗克1682 天前
Go 入门到精通-18-文件操作
java·数据库·golang
怒放de生命20102 天前
【web3基础】go-zero环境搭建(一)
开发语言·后端·golang·web3·区块链
脱胎换骨-军哥2 天前
C++ 与 Go 生成质量比拼,谁是分布式主力语言
c++·分布式·golang