【grpc】 5.命名解析
假设一个非常庞大的业务系统,采用微服务的架构,为了提高系统的稳定性,每一个微服务至少部署2个节点以上,那么客户端每次进行RPC调用时,如何感知到后端的两个(甚至多个)节点,然后选择其中一个进行RPC调用。撇开RPC实现的技术,如Dubbo框架、Spring Cloud、gRPC,这里面会涉及两个非常核心的问题, grpc的命名解析就是解决了问题1, 问题2由balance解决,见后续章节
如何感知后端node数量
如何选择其中的节点进行RPC
命名解析是指将服务名称转换为对应的网络地址的过程。在分布式系统中,服务通常被部署在多个节点上,每个节点有一个唯一的网络地址(如IP地址或域名)。客户端需要知道服务在哪些节点上运行,才能够与之进行通信。而命名解析的作用就是将服务名称映射到对应的网络地址,从而实现客户端和服务之间的通信。
gRPC 的命名解析(Name Resolution)是指将逻辑服务名称(例如
myservice
)映射到实际的网络地址(例如localhost:50051
)的过程。与balancer结合使用来实现进程内负载均衡与服务发现。
# 官方实现
https://pkg.go.dev/google.golang.org/grpc/naming
- 包:google.golang.org/grpc/naming(v1.29以上没有这个包了)
- 新包:google.golang.org/grpc/resolver
# 命名解析方式
- 目前gRPC内置了四种比较常用的命名解析功能
dns
:格式为"hostname:port",例如"example.com:50051"。ipv4
(IPv4 address):格式为"ipv4:address:port",例如"ipv4:192.168.1.1:50051"。ipv6
(IPv6 address):格式为"ipv6:[address]:port",例如"ipv6:[2001:db8::1]:50051"。unix
(path to unix domain socket – unix systems only):格式为"unix:path",例如"unix:/var/run/my.sock"。
# 自定义命名解析
gRPC客户端工具包预留命名解析服务接口,供开发人员自定义命名解析功能。
核心接口
//该接口实时监听指定目标的状态,并及时更新配置 type Resolver interface { // ResolveNow will be called by gRPC to try to resolve the target name // again. It's just a hint, resolver can ignore this if it's not necessary. // // It could be called multiple times concurrently. ResolveNow(ResolveNowOptions) // Close closes the resolver. Close() } ... // 建立scheme 与 service.name之间的关系;并绑定到客户端连接上 type Builder interface { Build(target Target, cc ClientConn, opts BuildOptions) (Resolver, error) // 返回命名解析所支持的 scheme信息 Scheme() string } /* Builder 是接口类型,用于创建命名解析器,可监视命名空间是否发生变化,其方法有: 1) Scheme() string // 返回解析器支持的方案 2) Build(target Target, cc ClientConn, opts BuildOptions) (Resolver, error) // 创建解析器 Resolver 是接口类型,用于监控目标变化,当目标发生变化时,会相应地更新地址、服务配置,其方法有: 1) Close() // 关闭解析器 2) ResolveNow(ResolveNowOptions) // 备用接口,GRPC可以再次调用用于目标的解析 *、
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28常量定义
const ( exampleScheme = "example" exampleServiceName = "resolver.example.grpc.io" backendAddr = "localhost:50051" )
1
2
3
4
5
6
7自定义resolver
type exampleResolver struct { target resolver.Target cc resolver.ClientConn addrsStore map[string][]string } func (r *exampleResolver) start() { addrStrs := r.addrsStore[r.target.Endpoint] addrs := make([]resolver.Address, len(addrStrs)) for i, s := range addrStrs { addrs[i] = resolver.Address{Addr: s} } r.cc.UpdateState(resolver.State{Addresses: addrs}) } func (*exampleResolver) ResolveNow(o resolver.ResolveNowOptions) {} func (*exampleResolver) Close()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17自定义Builder
type exampleResolverBuilder struct{} func (*exampleResolverBuilder) Build(target resolver.Target, cc resolver.ClientConn, opts resolver.BuildOptions) (resolver.Resolver, error) { r := &exampleResolver{ target: target, cc: cc, addrsStore: map[string][]string{ exampleServiceName: {backendAddr}, }, } r.start() return r, nil } func (*exampleResolverBuilder) Scheme() string { return exampleScheme }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15加载命名服务
func init() { //这一步非常关键,否则就会出现解析不了的情况,错误信息如下 //Unavailable desc = connection error: desc = "transport: Error while dialing dial tcp: lookup tcpresolver.example.grpc.io: nodename nor servname provided, or not known" resolver.Register(&exampleResolverBuilder{}) }
1
2
3
4
5
6完整客户端
package main import ( "context" "fmt" "log" "time" "google.golang.org/grpc" "google.golang.org/grpc/credentials/insecure" ecpb "google.golang.org/grpc/examples/features/proto/echo" "google.golang.org/grpc/resolver" ) const ( exampleScheme = "example" exampleServiceName = "resolver.example.grpc.io" backendAddr = "localhost:50051" ) func callUnaryEcho(c ecpb.EchoClient, message string) { ctx, cancel := context.WithTimeout(context.Background(), time.Second) defer cancel() r, err := c.UnaryEcho(ctx, &ecpb.EchoRequest{Message: message}) if err != nil { log.Fatalf("could not greet: %v", err) } fmt.Println(r.Message) } func makeRPCs(cc *grpc.ClientConn, n int) { hwc := ecpb.NewEchoClient(cc) for i := 0; i < n; i++ { callUnaryEcho(hwc, "this is examples/name_resolving") } } func main() { exampleConn, err := grpc.Dial( fmt.Sprintf("%s:///%s", exampleScheme, exampleServiceName), // Dial to "example:///resolver.example.grpc.io" grpc.WithTransportCredentials(insecure.NewCredentials()), ) if err != nil { log.Fatalf("did not connect: %v", err) } defer exampleConn.Close() fmt.Printf("--- calling helloworld.Greeter/SayHello to \"%s:///%s\"\n", exampleScheme, exampleServiceName) makeRPCs(exampleConn, 5) } type exampleResolverBuilder struct{} func (*exampleResolverBuilder) Build(target resolver.Target, cc resolver.ClientConn, opts resolver.BuildOptions) (resolver.Resolver, error) { r := &exampleResolver{ target: target, cc: cc, addrsStore: map[string][]string{ exampleServiceName: {backendAddr}, }, } r.start() return r, nil } func (*exampleResolverBuilder) Scheme() string { return exampleScheme } // exampleResolver is a // Resolver(https://godoc.org/google.golang.org/grpc/resolver#Resolver). type exampleResolver struct { target resolver.Target cc resolver.ClientConn addrsStore map[string][]string } func (r *exampleResolver) start() { addrStrs := r.addrsStore[r.target.Endpoint] addrs := make([]resolver.Address, len(addrStrs)) for i, s := range addrStrs { addrs[i] = resolver.Address{Addr: s} } r.cc.UpdateState(resolver.State{Addresses: addrs}) } func (*exampleResolver) ResolveNow(o resolver.ResolveNowOptions) {} func (*exampleResolver) Close() {} func init() { resolver.Register(&exampleResolverBuilder{}) }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
上次更新: 2023/05/04, 15:30:48