Grpc生态

简介

grpc是一个高性能、开源、通用的RPC框架,由Google推出,基于http2协议标准设计开发,默认采用protocol buffer数据序列化协议,支持多种开发语言。gRPC提供了一种简单的方法来精确的定义服务,并且为客户端和服务端自动生成可靠的功能库。

本文主要介绍除了基本的rpc通信功能外,grpc支持的扩展功能。

认证

gRPC默认内置了两种认证方式:

  • SSL/TLS认证方式(对服务端认证)
  • 基于Token的认证方式(对客户端认证)

同时,gRPC提供了接口用于扩展自定义认证方式。

Tls认证

服务端

// TLS认证
    creds, err := credentials.NewServerTLSFromFile("../../keys/server.pem", "../../keys/server.key")
    if err != nil {
        grpclog.Fatalf("Failed to generate credentials %v", err)
}
// 实例化grpc Server, 并开启TLS认证
    s := grpc.NewServer(grpc.Creds(creds))

客户端

// TLS连接
    creds, err := credentials.NewClientTLSFromFile("../../keys/server.pem", "server name")
    if err != nil {
        grpclog.Fatalf("Failed to create TLS credentials %v", err)
    }

    conn, err := grpc.Dial(Address, grpc.WithTransportCredentials(creds))

token认证

服务端

md, ok := metadata.FromContext(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)
    }

google.golang.org/grpc/credentials/oauth包已实现了用于Google APIoauthjwt验证的方法

客户端

每次调用,token信息会通过请求的metadata传输到服务端。

// 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
}

var opts []grpc.DialOption
// 使用自定义认证
    opts = append(opts, grpc.WithPerRPCCredentials(new(customCredential)))

conn, err := grpc.Dial(Address, opts...)

拦截器

grpc服务端和客户端都提供了interceptor功能,功能类似middleware,很适合在这里处理验证、日志等流程。

Grpc同时支持在客户端和服务端添加拦截器。

服务端

还是以token认证为例

 // auth 验证Token
func auth(ctx context.Context) error {
    md, ok := metadata.FromContext(ctx)
    if !ok {
        return 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 grpc.Errorf(codes.Unauthenticated, "Token认证信息无效: appid=%s, appkey=%s", appid, appkey)
    }

    return nil
}

// interceptor 拦截器
func interceptor(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
    err := auth(ctx)
    if err != nil {
        return nil, err
    }
    // 继续处理请求
    return handler(ctx, req)
}

// server启动时加载拦截器
var opts []grpc.ServerOption
// 注册interceptor
opts = append(opts, grpc.UnaryInterceptor(interceptor))
// 实例化grpc Server
    s := grpc.NewServer(opts...)

客户端

添加打印日志的拦截器为例

// interceptor 客户端拦截器
func interceptor(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error {
    start := time.Now()
    err := invoker(ctx, method, req, reply, cc, opts...)
    grpclog.Printf("method=%s req=%v rep=%v duration=%s error=%v\n", method, req, reply, time.Since(start), err)
    return err
}
// 拨号时指定客户端interceptor
    opts = append(opts, grpc.WithUnaryInterceptor(interceptor))
    conn, err := grpc.Dial(Address, opts...)

流式接口(streaming rpc)

streaming rpc相比于simple rpc来说可以很好的解决一个接口发送大量数据的场景。

比如一个订单导出的接口有20万条记录,如果使用simple rpc来实现的话。那么我们需要一次性接收到20万记录才能进行下一步的操作。但是如果我们使用streaming rpc那么我们就可以接收一条记录处理一条记录,直到所以的数据传输完毕。这样可以较少服务器的瞬时压力,也更有及时性

IDL

service StreamService {
    rpc OrderList(OrderSearchParams) returns (stream OrderList){}; //服务端流式,下发订单列表
    rpc UploadFile(stream ImageList) returns (uploadResponse){}; //客户端流式,上传图片
    rpc SumData(stream SumData) returns (stream SumData){}; //双向流式
}

服务端注册

	server := grpc.NewServer()
	proto.RegisterStreamServiceServer(server, &StreamServices{})

客户端注册

streamClient = proto.NewStreamServiceClient(connect)

grpc流式接口的底层实现基于http2.0的stream,我们回忆http2.0的基本概念,从顶而下分为stream, message, frame。其中一个stream由若干message组成,因此它超越了原先的一条请求一条答复的传统交互模式。

内置trace

grpc内置了客户端和服务端的请求追踪,基于golang.org/x/net/trace包实现,默认是开启状态,可以查看事件和请求日志,对于基本的请求状态查看调试也是很有帮助的,客户端与服务端基本一致

服务端代码

func startTrace() {
    trace.AuthRequest = func(req *http.Request) (any, sensitive bool) {
        return true, true
    }
    go http.ListenAndServe(":50051", nil)
    grpclog.Println("Trace listen on 50051")
}

这里我们开启一个http服务监听50051端口,用来查看grpc请求的trace信息

服务端事件查看

访问:localhost:50051/debug/events可以看到服务端注册的服务和服务正常启动的事件信息。

请求日志信息查看

访问:localhost:50051/debug/requests这里可以显示最近的请求状态,包括请求的服务、参数、耗时、响应,对于简单的状态查看还是很方便的,默认值显示最近10条记录。

服务注册与发现

Grpc原生并没有提供全套的服务注册与发现的组件,但在其api接口中仍体现了负载均衡的思想

Grpc生态_第1张图片

  1. 通过域名解析服务获取下游服务的ip,通常分为grpc服务地址和load balancer地址,以及负载均衡策略。如果是load balancer地址,走以下2、3、4步;
  2. 创建一个负载均衡策略实例,该实例负责挑选下一个请求地址;
  3. 所有grpc服务进行注册,负载均衡策略实例获取所有的server ip地址,同时感知其变化;
  4. 对每一个grpc服务建立连接;

在实现其上述思想时,通常需要借助zk,etcd等开源组件。

服务端

    err = grpclb.Register(*serv, "127.0.0.1", *port, *reg, time.Second*10, 15) // 服务注册

    grpclb.UnRegister() // 取消服务注册

 客户端

    r := grpclb.NewResolver(*serv) // watcher etcd
    b := grpc.RoundRobin(r) // 创建负载均衡实例
    conn, err := grpc.DialContext(ctx, *reg, grpc.WithInsecure(), grpc.WithBalancer(b)) // 与所有的下游服务建立连接

http网关

如果我们的grpc服务仍然需要接入部分http协议的请求,通常有两种做法:1是改造现有的服务,同时支持两种协议;2是引入http网关。Google官方提供一套流程直接支持第二种做法。

通过protobuf的自定义option实现了一个网关,服务端同时开启gRPCHTTP服务,HTTP服务接收客户端请求后转换为grpc请求数据,获取响应后转为json数据返回给客户端。

Grpc生态_第2张图片

使用这种方式,非常方便的接入了http协议请求。 

你可能感兴趣的:(架构,rpc,golang)