可以。用 etcd 做注册中心,核心其实就三件事:
- 服务启动 → 注册实例
- 服务存活 → 续租(KeepAlive)
- 服务下线/租约过期 → etcd 自动删除实例,消费者通过 Watch 感知
下面给你一个尽量接近真实微服务场景、但保持最小化的 Go Demo。
1. 安装依赖
go get go.etcd.io/etcd/client/v3
假设本地 etcd:
127.0.0.1:2379
2. 服务注册
package main
import (
"context"
"fmt"
"log"
"time"
clientv3 "go.etcd.io/etcd/client/v3"
)
func main() {
cli, err := clientv3.New(clientv3.Config{
Endpoints: []string{"127.0.0.1:2379"},
DialTimeout: 5 * time.Second,
})
if err != nil {
log.Fatal(err)
}
defer cli.Close()
// 创建 10 秒租约
leaseResp, err := cli.Grant(context.Background(), 10)
if err != nil {
log.Fatal(err)
}
// 注册服务
key := "/services/order/192.168.1.10:8080"
_, err = cli.Put(
context.Background(),
key,
`{"host":"192.168.1.10","port":8080}`,
clientv3.WithLease(leaseResp.ID),
)
if err != nil {
log.Fatal(err)
}
fmt.Println("服务注册成功:", key)
// 自动续租
keepAliveCh, err := cli.KeepAlive(
context.Background(),
leaseResp.ID,
)
if err != nil {
log.Fatal(err)
}
// 持续消费续租响应
for resp := range keepAliveCh {
fmt.Println("续租成功,TTL:", resp.TTL)
}
}
这里最关键的是:
clientv3.WithLease(leaseResp.ID)
意味着:
/service/order/192.168.1.10:8080
│
└── 绑定 lease
│
└── TTL = 10s
只要服务不断:
KeepAlive
↓
Lease TTL重新变成10s
↓
KeepAlive
↓
Lease TTL重新变成10s
如果服务直接崩溃,无法继续 KeepAlive:
10s
↓
9s
↓
...
↓
0
↓
Lease 过期
↓
etcd 自动删除 key
所以不需要服务自己执行注销操作。
3. 消费者发现服务
例如 order 服务可能有三个实例:
/services/order/10.0.0.1:8080
/services/order/10.0.0.2:8080
/services/order/10.0.0.3:8080
消费者可以:
package main
import (
"context"
"fmt"
"log"
"time"
clientv3 "go.etcd.io/etcd/client/v3"
)
func main() {
cli, err := clientv3.New(clientv3.Config{
Endpoints: []string{"127.0.0.1:2379"},
DialTimeout: 5 * time.Second,
})
if err != nil {
log.Fatal(err)
}
defer cli.Close()
prefix := "/services/order/"
// 先获取当前所有实例
resp, err := cli.Get(
context.Background(),
prefix,
clientv3.WithPrefix(),
)
if err != nil {
log.Fatal(err)
}
for _, kv := range resp.Kvs {
fmt.Printf(
"发现服务: %s -> %s\n",
string(kv.Key),
string(kv.Value),
)
}
// 监听后续变化
watchCh := cli.Watch(
context.Background(),
prefix,
clientv3.WithPrefix(),
)
for watchResp := range watchCh {
for _, event := range watchResp.Events {
switch event.Type {
case clientv3.EventTypePut:
fmt.Println(
"服务上线/更新:",
string(event.Kv.Key),
string(event.Kv.Value),
)
case clientv3.EventTypeDelete:
fmt.Println(
"服务下线:",
string(event.Kv.Key),
)
}
}
}
}
于是整个过程就是:
etcd
│
┌──────────┼──────────┐
│ │ │
order-1 order-2 order-3
│ │ │
lease lease lease
│ │ │
KeepAlive KeepAlive KeepAlive
consumer
│
├── Get /services/order/
│
└── Watch /services/order/
│
├── PUT → 新实例上线
│
└── DELETE → 实例下线
4. 真实项目一般会再抽象一层
你可以把它封装成:
type Registry struct {
client *clientv3.Client
}
func (r *Registry) Register(
serviceName string,
instanceID string,
addr string,
) error
func (r *Registry) Unregister(
serviceName string,
instanceID string,
) error
func (r *Registry) Discover(
serviceName string,
) ([]string, error)
func (r *Registry) Watch(
serviceName string,
callback func(event Event),
)
这样业务代码基本就是:
registry.Register(
"order",
"192.168.1.10:8080",
"192.168.1.10:8080",
)
消费者:
instances, _ := registry.Discover("order")
registry.Watch("order", func(event Event) {
// 更新本地服务列表
})
这里有一个非常重要的设计点:消费者通常不会每次请求都去 etcd 查询,而是:
etcd
│
Watch事件流
↓
┌────────────────┐
│ Consumer本地缓存 │
│ │
│ 10.0.0.1:8080 │
│ 10.0.0.2:8080 │
│ 10.0.0.3:8080 │
└───────┬────────┘
│
请求 order
↓
本地负载均衡
这才是注册中心真正有价值的地方:etcd负责服务状态的一致性,本地缓存负责请求路径上的高性能服务发现。