在微服务架构中,各个服务实例的地址和端口是动态变化的,服务发现机制能够让服务消费者自动获取可用的服务提供者实例信息,而服务注册中心就是存储这些实例信息的核心组件。常见的服务注册中心有etcd、Consul、Nacos等,本文以etcd为例讲解Golang中的实现方式。

服务注册中心的核心作用
服务注册中心主要承担三个核心职责:
- 服务注册:服务提供者启动后,将自己的地址、端口、服务名等信息上报到注册中心
- 健康检查:注册中心定期检查服务实例的存活状态,移除不可用的实例
- 服务查询:服务消费者向注册中心查询指定服务名对应的可用实例列表
基于etcd实现服务注册
etcd是一个高可用的分布式键值存储系统,天然支持租约机制,非常适合作为服务注册中心。首先需要引入etcd的Golang客户端依赖:
// 引入etcd客户端包
import (
"context"
"fmt"
"time"
"go.etcd.io/etcd/client/v3"
)
接下来实现服务注册逻辑,核心是利用etcd的租约机制,让服务实例注册的信息带有过期时间,通过定期续租维持注册信息的有效性:
// 服务注册结构体
type ServiceRegister struct {
cli *clientv3.Client // etcd客户端
leaseID clientv3.LeaseID // 租约ID
serviceName string // 服务名
serviceAddr string // 服务地址
leaseTTL int64 // 租约有效期,单位秒
}
// 创建服务注册实例
func NewServiceRegister(endpoints []string, serviceName, serviceAddr string, leaseTTL int64) (*ServiceRegister, error) {
// 创建etcd客户端
cli, err := clientv3.New(clientv3.Config{
Endpoints: endpoints,
DialTimeout: 5 * time.Second,
})
if err != nil {
return nil, fmt.Errorf("创建etcd客户端失败: %v", err)
}
return &ServiceRegister{
cli: cli,
serviceName: serviceName,
serviceAddr: serviceAddr,
leaseTTL: leaseTTL,
}, nil
}
// 执行服务注册
func (r *ServiceRegister) Register() error {
// 创建租约
leaseResp, err := r.cli.Grant(context.Background(), r.leaseTTL)
if err != nil {
return fmt.Errorf("创建租约失败: %v", err)
}
r.leaseID = leaseResp.ID
// 注册服务信息,key格式为 /服务名/实例地址,value为实例地址
key := fmt.Sprintf("/%s/%s", r.serviceName, r.serviceAddr)
_, err = r.cli.Put(context.Background(), key, r.serviceAddr, clientv3.WithLease(r.leaseID))
if err != nil {
return fmt.Errorf("注册服务失败: %v", err)
}
// 启动自动续租
keepAliveChan, err := r.cli.KeepAlive(context.Background(), r.leaseID)
if err != nil {
return fmt.Errorf("启动自动续租失败: %v", err)
}
// 监听续租响应
go func() {
for {
select {
case resp := <-keepAliveChan:
if resp == nil {
fmt.Println("租约已过期,服务注销")
return
}
}
}
}()
fmt.Printf("服务 %s 注册成功,地址: %sn", r.serviceName, r.serviceAddr)
return nil
}
// 关闭注册,注销服务
func (r *ServiceRegister) Close() error {
// 撤销租约,自动删除对应的注册信息
_, err := r.cli.Revoke(context.Background(), r.leaseID)
if err != nil {
return fmt.Errorf("撤销租约失败: %v", err)
}
r.cli.Close()
return nil
}
基于etcd实现服务发现
服务消费者需要从etcd中查询指定服务的可用实例列表,同时需要监听实例变化,实时更新本地缓存的实例信息:
// 服务发现结构体
type ServiceDiscovery struct {
cli *clientv3.Client
serviceName string
instances []string // 可用实例列表
}
// 创建服务发现实例
func NewServiceDiscovery(endpoints []string, serviceName string) (*ServiceDiscovery, error) {
cli, err := clientv3.New(clientv3.Config{
Endpoints: endpoints,
DialTimeout: 5 * time.Second,
})
if err != nil {
return nil, fmt.Errorf("创建etcd客户端失败: %v", err)
}
return &ServiceDiscovery{
cli: cli,
serviceName: serviceName,
}, nil
}
// 查询可用服务实例
func (d *ServiceDiscovery) Discover() ([]string, error) {
// 查询前缀为 /服务名/ 的所有key
keyPrefix := fmt.Sprintf("/%s/", d.serviceName)
resp, err := d.cli.Get(context.Background(), keyPrefix, clientv3.WithPrefix())
if err != nil {
return nil, fmt.Errorf("查询服务实例失败: %v", err)
}
instances := make([]string, 0)
for _, kv := range resp.Kvs {
instances = append(instances, string(kv.Value))
}
d.instances = instances
return instances, nil
}
// 监听服务实例变化
func (d *ServiceDiscovery) Watch() {
keyPrefix := fmt.Sprintf("/%s/", d.serviceName)
watchChan := d.cli.Watch(context.Background(), keyPrefix, clientv3.WithPrefix())
go func() {
for watchResp := range watchChan {
for _, event := range watchResp.Events {
switch event.Type {
case clientv3.EventTypePut:
// 新增实例
instanceAddr := string(event.Kv.Value)
d.instances = append(d.instances, instanceAddr)
fmt.Printf("新增服务实例: %sn", instanceAddr)
case clientv3.EventTypeDelete:
// 移除实例
instanceAddr := string(event.Kv.Value)
newInstances := make([]string, 0)
for _, addr := range d.instances {
if addr != instanceAddr {
newInstances = append(newInstances, addr)
}
}
d.instances = newInstances
fmt.Printf("移除服务实例: %sn", instanceAddr)
}
}
}
}()
}
// 获取当前可用实例列表
func (d *ServiceDiscovery) GetInstances() []string {
return d.instances
}
// 关闭发现客户端
func (d *ServiceDiscovery) Close() error {
return d.cli.Close()
}
使用示例
下面是服务提供者和服务消费者的简单使用示例:
func main() {
// 服务提供者逻辑
register, err := NewServiceRegister([]string{"127.0.0.1:2379"}, "user_service", "127.0.0.1:8080", 10)
if err != nil {
panic(err)
}
defer register.Close()
register.Register()
// 服务消费者逻辑
discovery, err := NewServiceDiscovery([]string{"127.0.0.1:2379"}, "user_service")
if err != nil {
panic(err)
}
defer discovery.Close()
// 首次查询实例
instances, err := discovery.Discover()
if err != nil {
panic(err)
}
fmt.Printf("初始可用实例: %vn", instances)
// 启动监听实例变化
discovery.Watch()
// 阻塞主线程,模拟服务运行
select {}
}
注意事项
- etcd集群需要保证高可用,避免单点故障导致服务发现不可用
- 租约TTL需要根据服务的健康检查间隔合理设置,避免实例正常但租约过期被误删
- 服务消费者需要做好本地实例缓存,避免每次请求都查询注册中心,减少注册中心压力
- 如果服务注册中心不可用,需要有降级策略,比如使用本地缓存的实例列表继续提供服务