gRPC 的截止时间与命名解析


截止时间


简介

在分布式计算中,截止时间(deadline)和超时时间(timeout)是两个常用的模式。超时时间可以指定客户端应用程序等待 RPC 完成的时间(之后会以错误结束),它通常会以持续时长的方式来指定,并且在每个客户端本地进行应用。

例如,一个请求可能会由多个下游 RPC 组成,它们会将多个服务链接在一起。因此,可以在每个服务调用上,针对每个 RPC 都指定超时时间。这意味着超时时间不能直接应用于请求的整个生命周期,这时需要使用截止时间。

截止时间以请求开始的绝对时间来表示(即使 API 将它们表示为持续时间偏移),并且应用于多个服务调用。发起请求的应用程序设置截止时间,整个请求链需要在截止时间之前进行响应。gRPC API 支持为 RPC使用截止时间,出于多种原因,在 gRPC 应用程序中使用截止时间始终是一种最佳实践。

由于 gRPC 通信是在网络上发生的,因此在 RPC 和响应之间会有延迟。另外,在一些特定的场景中,gRPC 服务本身可能要花费更多的时间来响应,这取决于服务的业务逻辑。如果客户端应用程序在开发时没有指定截止时间,那么它们会无限期地等待自己所发起的 RPC 请求的响应,而资源都会被正在处理的请求所占用。这会让服务和客户端都面临资源耗尽的风险,增加服务的延迟,甚至可能导致整个 gRPC 服务崩溃。

例如,在下图中 gRPC 客户端应用程序调用商品管理服务,而商品管理服务又调用库存服务。

  • 客户端应用程序的截止时间设置为 50 毫秒(截止时间 = 当前时间 + 偏移量)。

  • 客户端和 ProductMgt 服务之间的网络延迟为 0 毫秒,ProductMgt 服务的处理延迟为 20 毫秒。

  • 商品管理服务(ProductMgt 服务)必须将截止时间的偏移量设置为 30 毫秒。因为库存服务(Inventory 服务)需要 30 毫秒来响应,所以截止时间的事件会在两个客户端上发生(ProductMgt 调用 Inventory 服务和客户端应用程序)。

  • ProductMgt 服务的业务逻辑将延迟时间增加了 20 毫秒。随后,ProductMgt 服务的调用逻辑触发了超出截止时间的场景,并且传播回客户端应用程序。因此,在使用截止时间时,要明确它们适用于所有服务场景。

在 Go 语言中,设置 gRPC 应用程序的截止时间是通过调用 context 包的 context.WithDeadline() 函数设置。context 包通常用来向下传递通用的数据,使其能够在整个下游操作中使用,当 gRPC 客户端应用程序发起调用时,客户端的 gRPC 库就会创建所需的 gRPC 头信息,用来表述客户端应用程序和服务器端应用程序之间的截止时间。当 RPC 发送之后,客户端应用程序会在截止时间所声明的时间范围内等待,如果在该时间内 RPC 没有返回,那么该 RPC 会以 DEADLINE_EXCEEDED 错误的形式终止。


程序示例

(1)在任意目录下,分别创建 serverclient 目录存放服务端和客户端文件,proto 目录用于编写 IDL 的 deadline.proto 文件,cert 目录存放证书文件,具体的目录结构如下所示:

Deadline
├── client
│   ├── cert
│   └── proto
│       └── deadline.proto
└── server
    ├── cert
    └── proto
        └── deadline.proto

deadline.proto 文件的具体内容如下所示:

syntax = "proto3"; // 版本声明,使用 Protocol Buffers v3 版本

option go_package = "../proto";  // 指定生成的 Go 代码在项目中的导入路径

package deadline; // 包名


// 定义服务
service Greeter {
    	// SayHello 方法
    	rpc SayHello (HelloRequest) returns (HelloResponse) {}
}

// 请求消息
message HelloRequest {
   	 	string name = 1;
}

// 响应消息
message HelloResponse {
    	string reply = 1;
}

(2)移动以下相应的证书文件到 cert 文件夹下,进入 proto 目录生成 gRPC 源代码程序,在 proto 目录下执行以下的命令:

protoc --go_out=. --go_opt=paths=source_relative --go-grpc_out=. --go-grpc_opt=paths=source_relative *.proto

正确生成后的目录结构如下所示:

Deadline
├── client
│   ├── cert
│   │   ├── ca.crt
│   │   ├── server.key
│   │   └── server.pem
│   └── proto
│       ├── deadline_grpc.pb.go
│       ├── deadline.pb.go
│       └── deadline.proto
└── server
    ├── cert
    │   ├── ca.crt
    │   ├── server.key
    │   └── server.pem
    └── proto
        ├── deadline_grpc.pb.go
        ├── deadline.pb.go
        └── deadline.proto

(3)在 server 目录下初始化项目( go mod init server ),编写 Server 端程序重写定义的方法,该程序的具体代码如下:

package main

import (
        "fmt"
        "net"
        pb "server/proto"
        "golang.org/x/net/context"
        "google.golang.org/grpc"
        "google.golang.org/grpc/codes"
        "google.golang.org/grpc/credentials" // 引入grpc认证包
        "log"
        "google.golang.org/grpc/metadata" // 引入grpc meta包
)

const (
        // Address gRPC 服务地址
        Address = "127.0.0.1:50052"
)

// 定义 helloService    并实现约定的接口
type helloService struct{
    
    
        pb.UnimplementedGreeterServer
}

// SayHello 实现Hello服务接口
func (h *helloService) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloReply, error) {
    
    
        // 解析 metadata 中的信息并验证
        md, ok := metadata.FromIncomingContext(ctx)
        if !ok {
    
    
                return nil, grpc.Errorf(codes.Unauthenticated, "无 Token 认证信息")
        }
        var (
                appid  string
                appkey string
        )
        if val, ok := md["appid"]; ok {
    
    
                appid = val[0]
        }
        if val, ok := md["appkey"]; ok {
    
    
                appkey = val[0]
        }
        if appid != "101010" || appkey != "i am key" {
    
    
                return nil, grpc.Errorf(codes.Unauthenticated, "Token 认证信息无效: appid=%s, appkey=%s", appid, appkey)
        }
        resp := new(pb.HelloReply)
        resp.Message = fmt.Sprintf("Hello %s.\nToken info: appid=%s,appkey=%s", in.Name, appid, appkey)
        return resp, nil
}

func main() {
    
    
        listen, err := net.Listen("tcp", Address)
        if err != nil {
    
    
                log.Fatalf("failed to listen: %v", err)
        }
    // TLS 认证
        creds, err := credentials.NewServerTLSFromFile("cert/server.pem", "cert/server.key")
        if err != nil {
    
    
                log.Fatalf("Failed to generate credentials %v", err)
        }
        // 实例化 grpc Server , 并开启 TLS 认证
        s := grpc.NewServer(grpc.Creds(creds))
        // 注册 HelloService
        pb.RegisterGreeterServer(s, &helloService{
    
    })
        log.Println("Listen on " + Address + " with TLS + Token")
        s.Serve(listen)
}

(4)在 client 目录下,编写 Client 端程序调用服务,该程序的具体代码如下:

package main

import (
        pb "client/proto" // 引入 proto 包
        "golang.org/x/net/context"
        "google.golang.org/grpc"
        "google.golang.org/grpc/credentials" // 引入 grpc 认证包
        "log"
        "time"
        "google.golang.org/grpc/status"
)

const (
        // Address gRPC 服务地址
        Address = "127.0.0.1:50052"
        // OpenTLS 是否开启 TLS 认证
        OpenTLS = true
)

// customCredential 自定义认证
type customCredential struct{
    
    }

// GetRequestMetadata 实现自定义认证接口
func (c customCredential) GetRequestMetadata(ctx context.Context, uri ...string) (map[string]string, error) {
    
    
        return map[string]string{
    
    
                "appid":  "101010",
                "appkey": "i am key",
        }, nil
}

// RequireTransportSecurity 自定义认证是否开启 TLS
func (c customCredential) RequireTransportSecurity() bool {
    
    
        return OpenTLS
}

func main() {
    
    
        var err error
        var opts []grpc.DialOption
        if OpenTLS {
    
    
                // TLS 连接
                creds, err := credentials.NewClientTLSFromFile("cert/server.pem", "*.cqupthao.com")
                if err != nil {
    
    
                        log.Fatalf("Failed to create TLS credentials %v", err)
                }
                opts = append(opts, grpc.WithTransportCredentials(creds))
        } else {
    
    
                opts = append(opts, grpc.WithInsecure())
        }
        // 使用自定义认证
        opts = append(opts, grpc.WithPerRPCCredentials(new(customCredential)))
        conn, err := grpc.Dial(Address, opts...)
        if err != nil {
    
    
                log.Fatalln(err)
        }
        defer conn.Close()
        // 初始化客户端
        client := pb.NewGreeterClient(conn)
        // 调用方法
        clientDeadline := time.Now().Add(
                time.Duration(2 * time.Second))
                ctx, cancel := context.WithDeadline(context.Background(), clientDeadline)
                defer cancel()
        // 设置延迟时间
        // time.Sleep(3 * time.Second)
        req := &pb.HelloRequest{
    
    Name: "gRPC"}
        res, err := client.SayHello(ctx, req)
        if err != nil {
    
    
                got := status.Code(err)
                log.Println(err)
                log.Fatalln("Error occured: %v ", got)
        }
        log.Println(res.Message)
}

执行 Server 端和 Client 端的程序,输出如下的结果:

2023/02/27 19:32:43 Hello gRPC.
Token info: appid=101010,appkey=i am key

若调用服务超过截止时间,则输出如下的结果:

2023/02/27 19:33:52 rpc error: code = DeadlineExceeded desc = context deadline exceeded
2023/02/27 19:33:52 Error occured: %v  DeadlineExceeded
exit status 1

命名解析器


简介

命名解析器(name resolver)接受一个服务的名称并返回后端 IP 的列表,可以看作是一个 map[service-name][]backend-ip ,它接收一个服务名称并返回后端的 IP 列表,gRPC 应用程序中根据目标字符串中的 scheme 选择名称解析器。

  • DNS 解析器

gRPC 应用程序中默认使用的名称解析器是 DNS ,即在 gRPC 客户端执行 grpc.Dial() 时提供域名,默认会将 DNS 解析出对应的 IP 列表返回,使用默认的 DNS 解析器的名称语法为:dns:[//authority/]host[:port] ,例如以下的代码:

conn, err := grpc.Dial("dns:///localhost:8972",
	grpc.WithTransportCredentials(insecure.NewCredentials()),
)
  • consul reslover

社区里有对应不同注册中心的 resolver ,例如下面使用 consul 作为注册中心的示例,其中使用了第三方的 grpc-consul-resolver 库作为 consul resolver :

package main

import _ "github.com/mbobakov/grpc-consul-resolver"

// ...

conn, err := grpc.Dial(
		// consul 服务
		"consul://192.168.1.11:8500/hello?wait=14s",
		grpc.WithTransportCredentials(insecure.NewCredentials()),
	)
  • 自定义解析器

除了使用内置和社区提供的名称解析器,还可以自定义一套自己的名称解析器,实现方式如以下关键的程序代码:

(1)核心接口

//该接口实时监听指定目标的状态,并及时更新配置
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
}

(2)常量定义

const (
		exampleScheme      = "example"
		exampleServiceName = "resolver.example.grpc.io"
		backendAddr = "localhost:50051"
)
// 最终的命名解析地址为:example:///resolver.example.grpc.io ;后端 node 一个节点:localhost:50051

(3)自定义 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()   

(4)自定义 Builder

// 命名解析器构建器
type exampleResolverBuilder struct{
    
    }

func (*exampleResolverBuilder) Build(target resolver.Target, cc resolver.ClientConn, opts resolver.BuildOptions) (resolver.Resolver, error) {
    
    
		// // 创建解析 lb.example.grpc.io 的示例解析器
		r := &exampleResolver{
    
    
				target: target,
				cc:     cc,
				addrsStore: map[string][]string{
    
    
						exampleServiceName: {
    
    backendAddr},  // 将 lb.example.grpc.io 解析为 localhost:50051 和 localhost:50052
				},
		}
		r.start()
		return r, nil
}

// 为 example 模式创建的解析器
func (*exampleResolverBuilder) Scheme() string {
    
     
		return exampleScheme
}

(5)加载命名服务

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{
    
    })
}

基于这个命名解析器实现,可以为所选的任意服务注册中心实现解析器( Consul、etcd 和 Zookeeper 等)。


程序示例

(1)在任意目录下,分别创建 serverclient 目录存放服务端和客户端文件,proto 目录用于编写 IDL 的 nameresoler.proto 文件,cert 目录存放证书文件,具体的目录结构如下所示:

NameResoler
├── client
│   └── proto
│       └── nameresoler.proto
└── server
    └── proto
        └── nameresoler.proto

nameresoler.proto 文件的具体内容如下所示:

syntax = "proto3"; // 版本声明,使用 Protocol Buffers v3 版

option go_package = "../proto";  // 指定生成的 Go 代码在项目中的导入路径

package nameresoler; // 包名


// 定义服务
service Greeter {
    	// SayHello 方法
    	rpc SayHello (HelloRequest) returns (HelloResponse) {}
}

// 请求消息
message HelloRequest {
    	string message = 1;
}

// 响应消息
message HelloResponse {
    	string message = 1;
}

(2)进入 proto 目录生成 gRPC 源代码程序,在 proto 目录下执行以下的命令:

protoc --go_out=. --go_opt=paths=source_relative --go-grpc_out=. --go-grpc_opt=paths=source_relative *.proto

正确生成后的目录结构如下所示:

NameResoler
├── client
│   └── proto
│       ├── nameresoler_grpc.pb.go
│       ├── nameresoler.pb.go
│       └── nameresoler.proto
└── server
    └── proto
        ├── nameresoler_grpc.pb.go
        ├── nameresoler.pb.go
        └── nameresoler.proto

(3)在 server 目录下初始化项目( go mod init server ),编写 Server 端程序重写定义的方法,该程序的具体代码如下:

package main

import (
        "context"
        "fmt"
        "log"
        pb "server/proto"
        "net"
        "google.golang.org/grpc"
)

const addr = "localhost:50051"

type server struct {
    
    
        pb.UnimplementedGreeterServer
        addr string
}

func (s *server) SayHello(ctx context.Context, req *pb.HelloRequest) (*pb.HelloResponse, error) {
    
    
        return &pb.HelloResponse{
    
    Message: fmt.Sprintf("%s (from %s)", req.Message, s.addr)}, nil
}


func main() {
    
    
        // 监听本地端口
        lis, err := net.Listen("tcp", addr)
        if err != nil {
    
    
                log.Printf("failed to listen: %v", err)
        }
        s := grpc.NewServer()                  // 创建gRPC服务器
        pb.RegisterGreeterServer(s, &server{
    
    addr: addr}) // 在gRPC服务端注册服务
        log.Println("Serving on %s ", addr)
        // 启动服务
        err = s.Serve(lis)
        if err != nil {
    
    
                log.Printf("failed to serve: %v", err)
        }
}

(4)在 client 目录下,编写 Client 端程序调用服务,该程序的具体代码如下:

package main

import (
        "context"
        "fmt"
        "log"
        "time"

        "google.golang.org/grpc"
        "google.golang.org/grpc/credentials/insecure"
        pb "client/proto"
        "google.golang.org/grpc/resolver"
)

const (
        exampleScheme      = "example"
        exampleServiceName = "resolver.example.grpc.io"

        backendAddr = "localhost:50051"
)

func callUnarySayHello(c pb.GreeterClient, message string) {
    
    
        ctx, cancel := context.WithTimeout(context.Background(), time.Second)
        defer cancel()
        r, err := c.SayHello(ctx, &pb.HelloRequest{
    
    Message: message})
        if err != nil {
    
    
                log.Fatalf("could not greet: %v", err)
        }
        fmt.Println(r.Message)
}

func makeRPCs(cc *grpc.ClientConn, n int) {
    
    
        hwc := pb.NewGreeterClient(cc)
        for i := 0; i < n; i++ {
    
    
                callUnarySayHello(hwc, "this is examples/name_resolving")
        }
}

func main() {
    
    
        passthroughConn, err := grpc.Dial(
                fmt.Sprintf("passthrough:///%s", backendAddr), // Dial to "passthrough:///localhost:50051"
                grpc.WithTransportCredentials(insecure.NewCredentials()),
        )
        if err != nil {
    
    
                log.Fatalf("did not connect: %v", err)
        }
        defer passthroughConn.Close()

        fmt.Printf("--- calling helloworld.Greeter/SayHello to \"passthrough:///%s\"\n", backendAddr)
        makeRPCs(passthroughConn, 3)

        fmt.Println()

        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, 3)
}

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() {
    
    
        // Register the example ResolverBuilder. This is usually done in a package's
        // init() function.
        resolver.Register(&exampleResolverBuilder{
    
    })
}

执行 Server 端和 Client 端的程序,输出如下的结果:

--- calling helloworld.Greeter/SayHello to "passthrough:///localhost:50051"
this is examples/name_resolving (from localhost:50051)
this is examples/name_resolving (from localhost:50051)
this is examples/name_resolving (from localhost:50051)

--- calling helloworld.Greeter/SayHello to "example:///resolver.example.grpc.io"
this is examples/name_resolving (from localhost:50051)
this is examples/name_resolving (from localhost:50051)
this is examples/name_resolving (from localhost:50051)

  • 参考链接:gRPC 教程

  • 参考链接:gRPC 官网

  • 参考书籍:《gRPC与云原生应用开发:以Go和Java为例》([斯里兰卡] 卡山 • 因德拉西里 丹尼什 • 库鲁普 著)

猜你喜欢

转载自blog.csdn.net/qq_46457076/article/details/129238302