本篇文章基于这篇: https://blog.csdn.net/wanmei002/article/details/117659330
代码地址
https://github.com/wanmei002/grpc-learn/tree/master/ch10
原理
- 首先为服务起一个名字,启动的时候,把信息存储到
etcd
中
1.1 比如: key是服务名 值是ip。 多个相同的服务让key的前缀一样,这样 读取etcd的时候就可以通过前缀来读取
1.2 生成租约,并设置比较短的过期时间,把 etcd 中存储的值跟租约绑定,这样租约如果没有在有效的时间内续期(很可能已经不能提供服务了),则 etcd 保存的值就会过期
1.3 定期更新租约的过期时间 - 客户端注册相关服务的解析器,在解析器中拼接 etcd的key的前缀,获取提供服务的ip列表,前端通过特定的算法实现负载均衡
2.1 客户端拼接 etcd 中key的前缀,读取提供服务的ip列表,更新拨号列表中的ip列表
2.2 同时,启动一个协程,监听 etcd 中服务信息是否有变化(增加了服务ip或减少了服务ip)。如果有变化则更新拨号列表中的ip
服务端代码
// 需要注册服务到 etcd 并设置租约,定期续租,如果服务停止了则租约过期,
// 就会从 etcd 服务中消失
type RegisterEtcdServer struct {
etcdCli *clientv3.Client
leaseId clientv3.LeaseID
ctx context.Context
}
// 主要逻辑
// 1. 创建 etcd 客户端
// 2. 创建租约
// 3. k v 跟租约绑定
// 4. 定期续期
// 服务端调用这个方法 实现服务注册
func RegisterServer(k, v string, expire int64) (*RegisterEtcdServer, error) {
etcdClient, err := clientv3.New(
clientv3.Config{
Endpoints: []string{":2379"},
DialTimeout: 5 * time.Second,
},
)
if err != nil {
log.Println("new etcd client failed; err:", err)
return nil, err
}
svr := &RegisterEtcdServer{
etcdCli: etcdClient,
ctx: context.Background(),
}
// 创建租约
err = svr.createLease(expire)
if err != nil {
log.Println("创建租约失败 err:", err)
return nil, err
}
// 值绑定租约
err = svr.BindLease(k, v)
if err != nil {
log.Println("etcd put failed; err:", err)
return nil, err
}
// 定时给租约续期,如果服务停止,则租约过期
err = svr.keepAlive()
if err != nil {
log.Println("定时续期失败l err:", err)
return nil, err
}
return svr, nil
}
func (s *RegisterEtcdServer) createLease(expire int64) error {
res, err := s.etcdCli.Grant(s.ctx, expire)
if err != nil {
log.Println("create grant failed; err : ", err)
return err
}
s.leaseId = res.ID
return nil
}
func (s *RegisterEtcdServer) BindLease(k, v string) error {
res, err := s.etcdCli.Put(s.ctx, k, v, clientv3.WithLease(s.leaseId))
if err != nil {
log.Println("etcd put failed; err:", err)
return err
}
log.Printf("etcd put succ : %+v\n", res)
return nil
}
func (s *RegisterEtcdServer) keepAlive() error {
leaseResCh, err := s.etcdCli.KeepAlive(s.ctx, s.leaseId)
if err != nil {
log.Println("etcd keep live failed; err :", err)
return err
}
go s.watch(leaseResCh)
return nil
}
func (s *RegisterEtcdServer) watch(leaseCh <-chan *clientv3.LeaseKeepAliveResponse) {
for k := range leaseCh {
log.Printf("续约成功; val:%+v\n", k)
}
log.Println("租约续期关闭")
}
func (s *RegisterEtcdServer) Close() error {
// 撤销租约
res, err := s.etcdCli.Revoke(s.ctx, s.leaseId)
if err != nil {
log.Println("撤销租约失败")
return err
}
log.Printf("撤销租约返回的结果: %+v\n", res)
return s.etcdCli.Close()
}
客户端主要代码实现
type serverDiscovery struct {
cli *clientv3.Client // 用来连接etcd
conn resolver.ClientConn // 用来 调用 UpdateState 这个方法,更新本地服务ip列表
serviceIpList sync.Map // 用来存储获得的ip列表,因为监听服务是新的协程在运行,可能会存在对map的同时读写,引起资源冲突
}
var etcdPort = ":2379"
func NewServerDiscovery() resolver.Builder {
etcdCli, err := clientv3.New(
clientv3.Config{
Endpoints: []string{etcdPort},
DialTimeout: 5 * time.Second,
},
)
if err != nil {
log.Println("client get etcd cli failed; err :", err)
panic(err)
}
return &serverDiscovery{cli: etcdCli}
}
// 服务解析器的入口,在这里初次
func (s *serverDiscovery) Build(target resolver.Target, cc resolver.ClientConn,
opts resolver.BuildOptions) (resolver.Resolver, error) {
defer func() {
if err := recover(); err != nil {
log.Println("build err:", err)
}
}()
s.conn = cc
// 获取在 etcd 保存的前缀
prefix := fmt.Sprintf("/%s.%s/", target.Scheme, target.Endpoint)
// 获取 etcd 中服务保存的ip列表
res, err := s.cli.Get(context.Background(), prefix, clientv3.WithPrefix())
if err != nil {
log.Println("Bulid etcd get addr failed; err:", err)
return nil, err
}
log.Printf("etcd res:%+v\n", res)
for _, kv := range res.Kvs {
s.store(kv.Key, kv.Value)
}
// 更新 拨号里的ip列表
s.updateState()
// 启动 etcd 观察者模式,监听etcd 中保存的服务信息是否有变化
go s.watch(prefix)
return s, nil
}
func (s *serverDiscovery) watch(prefix string) {
res := s.cli.Watch(context.Background(), prefix, clientv3.WithPrefix())
// res 是一个只读的管道
for val := range res {
for _, event := range val.Events {
switch event.Type {
case 0:// 0 是有数据增加
s.store(event.Kv.Key, event.Kv.Value)
log.Println("put:", string(event.Kv.Key))
s.updateState()
case 1:// 1是有数据减少
log.Println("del:", string(event.Kv.Key))
s.del(event.Kv.Key)
s.updateState()
}
}
}
}
func (s *serverDiscovery) Scheme() string {
return scheme
}
func (s *serverDiscovery) ResolveNow(resolver.ResolveNowOption) {
}
func (s *serverDiscovery) Close() {
log.Print("i am close\n")
}
func (s *serverDiscovery) store(k, v []byte) {
s.serviceIpList.Store(string(k), string(v))
}
func (s *serverDiscovery) del(k []byte) {
s.serviceIpList.Delete(string(k))
}
// 更新拨号中的ip列表
func (s *serverDiscovery) updateState() {
var addrList resolver.State
s.serviceIpList.Range(func(k, v interface{}) bool {
tA, ok := v.(string)
if !ok {
return false
}
log.Printf("conn.UpdateState key[%v];val[%v]\n", k, v)
addrList.Addresses = append(addrList.Addresses, resolver.Address{Addr: tA})
return true
})
s.conn.UpdateState(addrList)
}
// 注册服务解析器到解析列表中
func init() {
resolver.Register(NewServerDiscovery())
}