gRPC微服务—服务发现的原理和使用示例

科技   2024-10-08 08:55   北京  

对于一个客户端创建请求的过程

conn, err := grpc.Dial("example:8009", grpc.WithInsecure())
if err != nil {
  panic(err)
}
  1. gRPC客户端通过服务发现解析请求,将名称解析为一个或多个IP地址,以及服务配置
  2. 客户端使用上一步的服务配置、ip列表、实例化负载均衡策略
  3. 负载均衡策略为每个服务器地址创建一个子通道(channel),并监测每一个子通道状态
  4. 当有rpc请求时,负载均衡策略决定那个子通道即gRPC服务器将接收请求,当可用服务器为空时客户端的请求将被阻塞

gRPC官方提供了基本的服务发现和负载均衡逻辑,并提供了接口供扩展用于开发自定义的服务发现与负载均衡

服务发现

用通俗易懂的方式来解释下什么是服务发现。通常情况下客户端需要知道服务端的IP+端口号才能建立连接,但服务端的IP和端口号并不是那么容易记忆。还有更重要的,在云部署的环境中,服务端的IP和端口可能随时会发生变化。

所以我们可以给某一个服务起一个名字,客户端通过名字创建与服务端的连接,客户端底层使用服务发现系统,解析这个名字来获取真正的IP和端口,并在服务端的IP和端口发生变化时,重新建立连接。这样的系统通常也会被叫做name-system(名字服务)

gRPC 中的默认name-system是 DNS,同时在客户端以插件形式提供了自定义name-system的机制。

名字格式

gRPC采用的名字格式遵循的RFC 3986中定义的URI语法

scheme:[//[user[:password]@]host[:port]][/path][?query][#fragment]

例如

URI示例

gRPC关注其中两部分

  • URI scheme:第一个:前面的标识。对于gRPC客户端来说,它指示要使用的服务发现解析器,如果未指定前缀或方案未知,则默认使用DNS方案

  • URI path:指示要解析的名字

大部分gRPC实现默认支持以下的URI schemes

  • dns:[//authority/]host[:port] -- DNS (default)

  • unix:path, unix://absolute_path -- Unix domain sockets (Unix systems only)

  • unix-abstract:abstract_path -- Unix domain socket in abstract namespace (Unix systems only)

服务的服务注册

如果gRPC服务端的地址是静态的,可以在客户端服务发现时直接解析为静态的地址

如果gRPC服务端的地址是动态的,可以有两种选择

  • 自注册:当gRPC的服务启动后,向一个集中的注册中心进行注册
  • 平台的服务发现:使用k8s平台时,平台会感知gPRC实例的变化

关于服务注册这里不在做更多介绍了

客户端的服务发现

自定义服务发现需要在客户端启动前,注册一个服务解析器(Resolve

Golang中使用google.golang.org/grpc/resolver.Register(resolver.Builder)注册,这个函数不是直接接收一个解析器,而是使用工厂模式接收一个解析器的构造器

type Builder interface {
 // Build creates a new resolver for the given target.
 //
 // gRPC dial calls Build synchronously, and fails if the returned error is
 // not nil.
 Build(target Target, cc ClientConn, opts BuildOptions) (Resolver, error)
 // Scheme returns the scheme supported by this resolver.
 // Scheme is defined at https://github.com/grpc/grpc/blob/master/doc/naming.md.
 Scheme() string
}

Scheme()需要返回的就是名字格式中提到的URI scheme

Build(...)需要返回一个服务发现解析器google.golang.org/grpc/resolver.Resolver

✨✨cc ClientConn代表客户端与服务端的连接,其拥有的cc.UpdateState(State) error可以让我们更新链接的状态

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 尝试再一次对域名进行解析,在个人实践中,服务端进程挂掉会触发该调用
 ResolveNow(ResolveNowOptions)
  // Close closes the resolver.
  // 资源释放。
 Close()
}

解析器需要有能力从注册中心获取解析结果,并更新客户端中连接(cc ClientConn)的信息。还可以持续watch一个名字的解析结果,实时的更新客户端中连接的信息

  • 解析出来的地址列表(包含ip和port)
  • 针对连接的服务配置,如负载均衡等,来想覆盖全局的负责均衡配置
  • 每一个地址可以包含一系列的属性(kv),他们可以用来支持后续的负载均衡策略

示例代码

gRPC resolver 原理

🌲 在 init() 阶段时

  • 创建并注册Builder 实例,将其注册到 grpc 内部的 resolveBuilder 表中(其实是一个全局 map,key 为协议名;value 为构造的 resolveBuilder)

🌲 客户端启动时通过自定义Dail()方法构造grpc.ClientConn单例

  • grpc.DialContext() 方法内部解析 URI,分析协议类型,并从 resolveBuilder 表中查找协议对应的 resolverBuilder

  • 将地址作为 Target 、conn单例作为resolver.ClientConn参数调用 resolver.Build 方法实例化出 Resolver

  • 用户 Resolver实现中调用 cc.UpdateState 传入 State.Addresses 地址,gRPC使用这个地址建立连接,传入State.ServiceConfig,gRPC使用这份服务配置覆盖默认配置

name-reslover原理

代码

# builder.go
package resolver

import "google.golang.org/grpc/resolver"

var _ resolver.Builder = Builder{}

type Builder struct {
 addrsStore map[string][]string
}

func NewResolverBuilder(addrsStore map[string][]string) *Builder {
 return &Builder{addrsStore: addrsStore}
}

func (b Builder) Build(target resolver.Target, cc resolver.ClientConn, opts resolver.BuildOptions) (resolver.Resolver, error) {
 r := &Resolver{
  target:     target,
  cc:         cc,
  addrsStore: b.addrsStore,
 }
 r.Start()
 return r, nil
}

func (b Builder) Scheme() string {
 return "example"
}
# resolver.go
package resolver

import (
 "google.golang.org/grpc/resolver"
)

var _ resolver.Resolver = &Resolver{}

// impl google.golang.org/grpc/resolver.Resolver
type Resolver struct {
 target resolver.Target
 cc     resolver.ClientConn

 addrsStore map[string][]string
}

func (r *Resolver) Start() {
 // 在静态路由表中查询此 Endpoint 对应 addrs
 var addrs []resolver.Address
 for _, addr := range r.addrsStore[r.target.URL.Opaque] {
  addrs = append(addrs, resolver.Address{Addr: addr})
 }

 r.cc.UpdateState(resolver.State{
  Addresses: addrs,
    // 设置负载均衡策略为round_robin
  ServiceConfig: r.cc.ParseServiceConfig(
    `{"loadBalancingPolicy":"round_robin"}`),
 })
}

func (r *Resolver) ResolveNow(resolver.ResolveNowOptions) {

}

func (r *Resolver) Close() {

}
# main.go
package main

import (
 "context"
 "log"

 rs "github.com/liangwt/note/grpc/name_resolver_lb_example/client/resolver"
 pb "github.com/liangwt/note/grpc/name_resolver_lb_example/ecommerce"
 "google.golang.org/grpc"
 "google.golang.org/grpc/resolver"
)

func main() {
 resolver.Register(rs.NewResolverBuilder(map[string][]string{
  "cluster@callee": {
   "127.0.0.1:8009",
  },
 }))

 conn, err := grpc.Dial("example:cluster@callee", grpc.WithInsecure())
 if err != nil {
  panic(err)
 }
  
 // ...
}

最后推荐一下我的Go项目实战专栏本课程是教大家用Go语言从零开始搭建项目和做需求开发的实战课程,使用的技术栈均为实际开发所常用的组件和框架如:Gin、Viper、Zap、GORM、go-redis 、lo 等等。


课程分为五大部分:


  • 第一部分介绍让框架变得好用的诸多实战技巧,比如通过自定义日志门面让项目日志更简单易用、支持自动记录请求的追踪信息和程序位置信息、通过自定义Error在实现Go error接口的同时支持给给错误添加错误链,方便追溯错误源头。
  • 第二部分:讲解项目分层架构的设计和划分业务模块的方法和标准,让你以后无论遇到什么项目都能按这套标准自己划分出模块和逻辑分层。后面几个部分均是该部分所讲内容的实践。
  • 第三部分:设计实现一个套支持多平台登录,Token泄露检测、同平台多设备登录互踢功能的用户认证体系,这套用户认证体系既可以在你未来开发产品时直接应用
  • 第四部分:商城app C端接口功能的实现,强化分层架构实现的讲解,这里还会讲解用责任链、策略和模版等设计模式去解决订单结算促销、支付方式支付场景等多种多样的实际问题。
  • 第五部分:单元测试、项目Docker镜像、K8s部署和服务保障相关的一些基础内容和注意事项

具体的章节想去课扫码下方的海报二维码或者访问 

https://xiaobot.net/p/golang

点击下方阅读原文即可跳转。

网管叨bi叨
分享软件开发和系统架构设计基础、Go 语言和Kubernetes。
 最新文章