本文字数:5380 字
精读时间:12 分钟
也可在 6 分钟内完成速读
作者郑伟,小米信息技术部架构组
01
前言
gRPC Name Resolver(名称解析)是 gRPC 核心功能之一,目前大部分 gRPC Name Resolver 都采用 ETCD 来实现,通过引入 ETCD Client sdk,和 ETCD Server 之间通过 gRPC 双向流的方式进行数据交互。服务端定时上报服务名称、实例数据至 ETCD 实现服务注册,客户端进行监听指定服务名称对应实例变化来实现服务发现。
基于 ETCD 实现的 Name Resolver 已有很多相关的文章,同时 github 上也有很多相关类库,本文不做赘述。
有些公司内部已有成熟的 Name Server,比如我们小米内部 SOA 平台,已稳定运行多年。所以我们没有采用直连 ETCD 的方案,而是基于该 Name Server 来做适配。下文会给大家介绍下实现原理。
02
实现自定义 Name Resolver
gRPC 支持将 DNS 作为默认的 Name System,同时也提供了一些 API 方便开发者构建和使用自定义的 Resolver。本文所有代码均基于 [email protected] 实现。自定义 gRPC Name Resolver 源码结构大概如下所示:
整个 resolver 代码比较简单,包含三个 go 文件:resolver.go
、resolver_build.go
、dail.go
。
ns # 自定义 resolver 包名
├── dial.go # 封装了 gRPC 包的 grpc.DialContext() 方法,严格来说 dail.go 不应该放在 ns 包下,本例中这么做只是为简化包布局,方便读者理解
├── resolver.go # 实现了 gRPC resolver 包 Resolver 接口的 nsResolver
└── resolver_builder.go # 实现了 gRPC resolver 包 ResolverBuilder 接口的 nsResolverBuilder
03
定义 nsResolver
主要逻辑在 resolver.go
:
package ns
import (
"context"
"encoding/json"
"fmt"
"strings"
"time"
"mypkg/internal/logz" // 私有日志包,基于 uber 开源的 zap 实现
sdk "mypkg/internal/soa-sdk" // 私有 ns sdk 包,封装了内部 soa 平台进行服务发现的 sdk
_ "google.golang.org/grpc"
"google.golang.org/grpc/resolver"
"google.golang.org/grpc/serviceconfig"
)
const (
// syncNSInterval 定义了从 NS 服务同步实例列表的周期
syncNSInterval = 1 * time.Second
)
// nsResolver 实现了 resolver.Resolver 接口
type nsResolver struct {
target resolver.Target
cc resolver.ClientConn
ctx context.Context
cancel context.CancelFunc
...
}
// watcher 轮询并更新指定 CalleeService 服务的实例变化
func (r *nsResolver) watcher() {
r.updateCC()
ticker := time.NewTicker(syncNSInterval)
for {
select {
// 当* nsResolver Close 时退出监听
case <-r.ctx.Done():
ticker.Stop()
return
case <-ticker.C:
// 调用* nsResolver.updagteCC() 方法,更新实例地址
r.updateCC()
}
}
}
// updateCC 更新 resolver.Resolver.ClientConn 配置
func (r *nsResolver) updateCC() {
// 从 NS 服务获取指定 target 的实例列表
instances, err := r.getInstances(r.target)
// 如果获取实例列表失败,或者实例列表为空,则不更新 resolver 中实例列表
if err != nil || len(instances.CalleeIns) == 0 {
logz.Warn("[mis] error retrieving instances from Mis", logz.Any("target", r.target), logz.Error(err))
return
}
...
// 组装实例列表 []resolver.Address
// resolver.Address 结构体表示 grpc server 端实例地址
var newAddrs []resolver.Address
for k := range instances.CalleeIns {
newAddrs = append(newAddrs, instances.CalleeIns)
}
...
// 更新实例列表
// grpc 底层 LB 组件对每个服务端实例创建一个 subConnection。并根据设定的 LB 策略,选择合适的 subConnection 处理某次 RPC 请求。
// 此处代码比较复杂,后续在 LB 相关原理文章中再做概述
r.cc.UpdateState(resolver.State{Add