原文链接:万字长文 | 从实际到原理,带你参透 gRPC

大家好,我是煎鱼。

gRPC 在 Go 语言中大放异彩,越来越多的小伙伴在应用,最近也在公司安利了一波,心愿这一篇文章能带你一览 gRPC 的奇妙之处,本文篇幅比拟长,请做好阅读准备。

本文目录如下:

简述

gRPC 是一个高性能、开源和通用的 RPC 框架,面向挪动和 HTTP/2 设计。目前提供 C、Java 和 Go 语言版本,别离是:grpc, grpc-java, grpc-go. 其中 C 版本反对 C, C++, Node.js, Python, Ruby, Objective-C, PHP 和 C# 反对。

gRPC 基于 HTTP/2 规范设计,带来诸如双向流、流控、头部压缩、单 TCP 连贯上的多复用申请等个性。这些个性使得其在挪动设施上体现更好,更省电和节俭空间占用。

调用模型

1、客户端(gRPC Stub)调用 A 办法,发动 RPC 调用。

2、对申请信息应用 Protobuf 进行对象序列化压缩(IDL)。

3、服务端(gRPC Server)接管到申请后,解码申请体,进行业务逻辑解决并返回。

4、对响应后果应用 Protobuf 进行对象序列化压缩(IDL)。

5、客户端承受到服务端响应,解码申请体。回调被调用的 A 办法,唤醒正在期待响应(阻塞)的客户端调用并返回响应后果。

调用形式

一、Unary RPC:一元 RPC

Server

type SearchService struct{}func (s *SearchService) Search(ctx context.Context, r *pb.SearchRequest) (*pb.SearchResponse, error) {    return &pb.SearchResponse{Response: r.GetRequest() + " Server"}, nil}const PORT = "9001"func main() {    server := grpc.NewServer()    pb.RegisterSearchServiceServer(server, &SearchService{})    lis, err := net.Listen("tcp", ":"+PORT)    ...    server.Serve(lis)}

  • 创立 gRPC Server 对象,你能够了解为它是 Server 端的形象对象。
  • 将 SearchService(其蕴含须要被调用的服务端接口)注册到 gRPC Server。的外部注册核心。这样能够在承受到申请时,通过外部的 “服务发现”,发现该服务端接口并转接进行逻辑解决。
  • 创立 Listen,监听 TCP 端口。
  • gRPC Server 开始 lis.Accept,直到 Stop 或 GracefulStop。

Client

func main() {    conn, err := grpc.Dial(":"+PORT, grpc.WithInsecure())    ...    defer conn.Close()    client := pb.NewSearchServiceClient(conn)    resp, err := client.Search(context.Background(), &pb.SearchRequest{        Request: "gRPC",    })    ...}

  • 创立与给定指标(服务端)的连贯句柄。
  • 创立 SearchService 的客户端对象。
  • 发送 RPC 申请,期待同步响应,失去回调后返回响应后果。

二、Server-side streaming RPC:服务端流式 RPC

Server

func (s *StreamService) List(r *pb.StreamRequest, stream pb.StreamService_ListServer) error {    for n := 0; n <= 6; n++ {        stream.Send(&pb.StreamResponse{            Pt: &pb.StreamPoint{                ...            },        })    }    return nil}

Client

func printLists(client pb.StreamServiceClient, r *pb.StreamRequest) error {    stream, err := client.List(context.Background(), r)    ...    for {        resp, err := stream.Recv()        if err == io.EOF {            break        }        ...    }    return nil}

三、Client-side streaming RPC:客户端流式 RPC

Server

func (s *StreamService) Record(stream pb.StreamService_RecordServer) error {    for {        r, err := stream.Recv()        if err == io.EOF {            return stream.SendAndClose(&pb.StreamResponse{Pt: &pb.StreamPoint{...}})        }        ...    }    return nil}

Client

func printRecord(client pb.StreamServiceClient, r *pb.StreamRequest) error {    stream, err := client.Record(context.Background())    ...    for n := 0; n < 6; n++ {        stream.Send(r)    }    resp, err := stream.CloseAndRecv()    ...    return nil}

四、Bidirectional streaming RPC:双向流式 RPC

Server

func (s *StreamService) Route(stream pb.StreamService_RouteServer) error {    for {        stream.Send(&pb.StreamResponse{...})        r, err := stream.Recv()        if err == io.EOF {            return nil        }        ...    }    return nil}

Client

func printRoute(client pb.StreamServiceClient, r *pb.StreamRequest) error {    stream, err := client.Route(context.Background())    ...    for n := 0; n <= 6; n++ {        stream.Send(r)        resp, err := stream.Recv()        if err == io.EOF {            break        }        ...    }    stream.CloseSend()    return nil}

客户端与服务端是如何交互的

在开始剖析之前,咱们要先 gRPC 的调用有一个初始印象。那么最简略的就是对 Client 端调用 Server 端进行抓包去分析,看看整个过程中它都做了些什么事。如下图:

  • Magic
  • SETTINGS
  • HEADERS
  • DATA
  • SETTINGS
  • WINDOW_UPDATE
  • PING
  • HEADERS
  • DATA
  • HEADERS
  • WINDOW_UPDATE
  • PING

咱们略加整顿发现共有十二个行为,是比拟重要的。在开始剖析之前,倡议你本人先想一下,它们的作用都是什么?大胆猜想一下,带着疑难去学习效果更佳。

行为剖析

Magic

Magic 帧的次要作用是建设 HTTP/2 申请的前言。在 HTTP/2 中,要求两端都要发送一个连贯前言,作为对所应用协定的最终确认,并确定 HTTP/2 连贯的初始设置,客户端和服务端各自发送不同的连贯前言。

而上图中的 Magic 帧是客户端的前言之一,内容为 PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n,以确定启用 HTTP/2 连贯。

SETTINGS

SETTINGS 帧的次要作用是设置这一个连贯的参数,作用域是整个连贯而并非繁多的流。

而上图的 SETTINGS 帧都是空 SETTINGS 帧,图一是客户端连贯的前言(Magic 和 SETTINGS 帧别离组成连贯前言)。图二是服务端的。另外咱们从图中能够看到多个 SETTINGS 帧,这是为什么呢?是因为发送完连贯前言后,客户端和服务端还须要有一步互动确认的动作。对应的就是带有 ACK 标识 SETTINGS 帧。

HEADERS

HEADERS 帧的次要作用是存储和流传 HTTP 的标头信息。咱们关注到 HEADERS 里有一些眼生的信息,别离如下:

  • method:POST
  • scheme:http
  • path:/proto.SearchService/Search
  • authority::10001
  • content-type:application/grpc
  • user-agent:grpc-go/1.20.0-dev

你会发现这些货色十分眼生,其实都是 gRPC 的根底属性,实际上远远不止这些,只是设置了多少展现多少。例如像平时常见的 grpc-timeoutgrpc-encoding 也是在这里设置的。

DATA

DATA 帧的次要作用是装填主体信息,是数据帧。而在上图中,能够很显著看到咱们的申请参数 gRPC 存储在外面。只须要理解到这一点就能够了。

HEADERS, DATA, HEADERS

在上图中 HEADERS 帧比较简单,就是通知咱们 HTTP 响应状态和响应的内容格局。

在上图中 DATA 帧次要承载了响应后果的数据集,图中的 gRPC Server 就是咱们 RPC 办法的响应后果。

在上图中 HEADERS 帧次要承载了 gRPC 状态 和 gRPC 状态音讯,图中的 grpc-status 和 grpc-message 就是咱们的 gRPC 调用状态的后果。

其它步骤

WINDOW_UPDATE

次要作用是治理和流的窗口管制。通常状况下关上一个连贯后,服务器和客户端会立刻替换 SETTINGS 帧来确定流控制窗口的大小。默认状况下,该大小设置为约 65 KB,但可通过收回一个 WINDOW_UPDATE 帧为流控制设置不同的大小。

PING/PONG

次要作用是判断以后连贯是否依然可用,也罕用于计算往返工夫。其实也就是 PING/PONG,大家对此应该很熟。

小结

  • 在建设连贯之前,客户端/服务端都会发送连贯前言(Magic+SETTINGS),确立协定和配置项。
  • 在传输数据时,是会波及滑动窗口(WINDOW_UPDATE)等流控策略的。
  • 流传 gRPC 附加信息时,是基于 HEADERS 帧进行流传和设置;而具体的申请/响应数据是存储的 DATA 帧中的。
  • 申请/响应后果会分为 HTTP 和 gRPC 状态响应两种类型。
  • 客户端发动 PING,服务端就会回应 PONG,反之亦可。

这块 gRPC 的根底应用,你能够看看我另外的 《gRPC 入门系列》,置信对你肯定有帮忙。

浅谈了解

服务端

为什么四行代码,就可能起一个 gRPC Server,外部做了什么逻辑。你有想过吗?接下来咱们一步步分析,看看外面到底是何方神圣。

一、初始化

// grpc.NewServer()func NewServer(opt ...ServerOption) *Server { opts := defaultServerOptions for _, o := range opt {  o(&opts) } s := &Server{  lis:    make(map[net.Listener]bool),  opts:   opts,  conns:  make(map[io.Closer]bool),  m:      make(map[string]*service),  quit:   make(chan struct{}),  done:   make(chan struct{}),  czData: new(channelzData), } s.cv = sync.NewCond(&s.mu) ... return s}

这块比较简单,次要是实例 grpc.Server 并进行初始化动作。波及如下:

  • lis:监听地址列表。
  • opts:服务选项,这块蕴含 Credentials、Interceptor 以及一些根底配置。
  • conns:客户端连贯句柄列表。
  • m:服务信息映射。
  • quit:退出信号。
  • done:实现信号。
  • czData:用于存储 ClientConn,addrConn 和 Server 的 channelz 相干数据。
  • cv:当优雅退出时,会期待这个信号量,直到所有 RPC 申请都解决并断开才会持续解决。

二、注册

pb.RegisterSearchServiceServer(server, &SearchService{})

步骤一:Service API interface

// search.pb.gotype SearchServiceServer interface { Search(context.Context, *SearchRequest) (*SearchResponse, error)}func RegisterSearchServiceServer(s *grpc.Server, srv SearchServiceServer) { s.RegisterService(&_SearchService_serviceDesc, srv)}

还记得咱们平时编写的 Protobuf 吗?在生成进去的 .pb.go 文件中,会定义出 Service APIs interface 的具体实现束缚。而咱们在 gRPC Server 进行注册时,会传入利用 Service 的性能接口实现,此时生成的 RegisterServer 办法就会保障两者之间的一致性。

步骤二:Service API IDL

你想乱传糊弄一下?不可能的,请乖乖定义与 Protobuf 统一的接口办法。然而那个 &_SearchService_serviceDesc 又有什么作用呢?代码如下:

// search.pb.govar _SearchService_serviceDesc = grpc.ServiceDesc{ ServiceName: "proto.SearchService", HandlerType: (*SearchServiceServer)(nil), Methods: []grpc.MethodDesc{  {   MethodName: "Search",   Handler:    _SearchService_Search_Handler,  }, }, Streams:  []grpc.StreamDesc{}, Metadata: "search.proto",}

这看上去像服务的形容代码,用来向外部表述 “我” 都有什么。波及如下:

  • ServiceName:服务名称
  • HandlerType:服务接口,用于检查用户提供的实现是否满足接口要求
  • Methods:一元办法集,留神构造内的 Handler 办法,其对应最终的 RPC 解决办法,在执行 RPC 办法的阶段会应用。
  • Streams:流式办法集
  • Metadata:元数据,是一个形容数据属性的货色。在这里次要是形容 SearchServiceServer 服务

步骤三:Register Service

func (s *Server) register(sd *ServiceDesc, ss interface{}) {    ... srv := &service{  server: ss,  md:     make(map[string]*MethodDesc),  sd:     make(map[string]*StreamDesc),  mdata:  sd.Metadata, } for i := range sd.Methods {  d := &sd.Methods[i]  srv.md[d.MethodName] = d } for i := range sd.Streams {  ... } s.m[sd.ServiceName] = srv}

在最初一步中,咱们会将先前的服务接口信息、服务形容信息给注册到外部 service 去,以便于后续理论调用的应用。波及如下:

  • server:服务的接口信息
  • md:一元服务的 RPC 办法集
  • sd:流式服务的 RPC 办法集
  • mdata:metadata,元数据

小结

在这一章节中,次要介绍的是 gRPC Server 在启动前的整顿和注册行为,看上去很简略,但其实一切都是为了后续的理论运行的事后筹备。因而咱们整顿一下思路,将其串联起来看看,如下:

三、监听

接下来到了整个流程中,最重要也是大家最关注的监听/解决阶段,外围代码如下:

func (s *Server) Serve(lis net.Listener) error { ... var tempDelay time.Duration for {  rawConn, err := lis.Accept()  if err != nil {   if ne, ok := err.(interface {    Temporary() bool   }); ok && ne.Temporary() {    if tempDelay == 0 {     tempDelay = 5 * time.Millisecond    } else {     tempDelay *= 2    }    if max := 1 * time.Second; tempDelay > max {     tempDelay = max    }    ...    timer := time.NewTimer(tempDelay)    select {    case <-timer.C:    case <-s.quit:     timer.Stop()     return nil    }    continue   }   ...   return err  }  tempDelay = 0  s.serveWG.Add(1)  go func() {   s.handleRawConn(rawConn)   s.serveWG.Done()  }() }}

Serve 会依据内部传入的 Listener 不同而调用不同的监听模式,这也是 net.Listener 的魅力,灵活性和扩展性会比拟高。而在 gRPC Server 中最罕用的就是 TCPConn,基于 TCP Listener 去做。接下来咱们一起看看具体的解决逻辑,如下:

  • 循环解决连贯,通过 lis.Accept 取出连贯,如果队列中没有需解决的连贯时,会造成阻塞期待。
  • 若 lis.Accept 失败,则触发休眠机制,若为第一次失败那么休眠 5ms,否则翻倍,再次失败则一直翻倍直至下限休眠工夫 1s,而休眠结束后就会尝试去取下一个 “它”。
  • 若 lis.Accept 胜利,则重置休眠的工夫计数和启动一个新的 goroutine 调用 handleRawConn 办法去执行/解决新的申请,也就是大家很喜爱说的 “每一个申请都是不同的 goroutine 在解决”。
  • 在循环过程中,蕴含了 “退出” 服务的场景,次要是硬敞开和优雅重启服务两种状况。

客户端

一、创立拨号连贯

// grpc.Dial(":"+PORT, grpc.WithInsecure())func DialContext(ctx context.Context, target string, opts ...DialOption) (conn *ClientConn, err error) { cc := &ClientConn{  target:            target,  csMgr:             &connectivityStateManager{},  conns:             make(map[*addrConn]struct{}),  dopts:             defaultDialOptions(),  blockingpicker:    newPickerWrapper(),  czData:            new(channelzData),  firstResolveEvent: grpcsync.NewEvent(), } ... chainUnaryClientInterceptors(cc) chainStreamClientInterceptors(cc) ...}

grpc.Dial 办法实际上是对于 grpc.DialContext 的封装,区别在于 ctx 是间接传入 context.Background。其次要性能是创立与给定指标的客户端连贯,其承当了以下职责:

  • 初始化 ClientConn
  • 初始化(基于过程 LB)负载平衡配置
  • 初始化 channelz
  • 初始化重试规定和客户端一元/流式拦截器
  • 初始化协定栈上的根底信息
  • 相干 context 的超时管制
  • 初始化并解析地址信息
  • 创立与服务端之间的连贯

连没连

之前听到有的人说调用 grpc.Dial 后客户端就曾经与服务端建设起了连贯,但这对不对呢?咱们先鸟瞰全貌,看看正在跑的 goroutine。如下:

咱们能够有几个外围办法始终在期待/解决信号,通过剖析底层源码可得悉。波及如下:

func (ac *addrConn) connect()func (ac *addrConn) resetTransport()func (ac *addrConn) createTransport(addr resolver.Address, copts transport.ConnectOptions, connectDeadline time.Time)func (ac *addrConn) getReadyTransport()

在这里次要剖析 goroutine 提醒的 resetTransport 办法,看看都做了啥。外围代码如下:

func (ac *addrConn) resetTransport() { for i := 0; ; i++ {  if ac.state == connectivity.Shutdown {   return  }  ...  connectDeadline := time.Now().Add(dialDuration)  ac.updateConnectivityState(connectivity.Connecting)  newTr, addr, reconnect, err := ac.tryAllAddrs(addrs, connectDeadline)  if err != nil {   if ac.state == connectivity.Shutdown {    return   }   ac.updateConnectivityState(connectivity.TransientFailure)   timer := time.NewTimer(backoffFor)   select {   case <-timer.C:    ...   }   continue  }  if ac.state == connectivity.Shutdown {   newTr.Close()   return  }  ...  if !healthcheckManagingState {   ac.updateConnectivityState(connectivity.Ready)  }  ...  if ac.state == connectivity.Shutdown {   return  }  ac.updateConnectivityState(connectivity.TransientFailure) }}

在该办法中会一直地去尝试创立连贯,若胜利则完结。否则一直地依据 Backoff 算法的重试机制去尝试创立连贯,直到胜利为止。从论断上来讲,单纯调用 DialContext 是异步建设连贯的,也就是并不是马上失效,处于 Connecting 状态,而正式下要达到 Ready 状态才可用。

真的连了吗

在抓包工具上提醒一个包都没有,那么这算真正连贯了吗?我认为这是一个表述问题,咱们应该尽可能的谨严。如果你真的想通过 DialContext 办法就买通与服务端的连贯,则须要调用 WithBlock 办法,尽管会导致阻塞期待,但最终连贯会达到 Ready 状态(握手胜利)。如下图:

二、实例化 Service API

type SearchServiceClient interface { Search(ctx context.Context, in *SearchRequest, opts ...grpc.CallOption) (*SearchResponse, error)}type searchServiceClient struct { cc *grpc.ClientConn}func NewSearchServiceClient(cc *grpc.ClientConn) SearchServiceClient { return &searchServiceClient{cc}}

这块就是实例 Service API interface,比较简单。

三、调用

// search.pb.gofunc (c *searchServiceClient) Search(ctx context.Context, in *SearchRequest, opts ...grpc.CallOption) (*SearchResponse, error) { out := new(SearchResponse) err := c.cc.Invoke(ctx, "/proto.SearchService/Search", in, out, opts...) if err != nil {  return nil, err } return out, nil}

proto 生成的 RPC 办法更像是一个包装盒,把须要的货色放进去,而实际上调用的还是 grpc.invoke 办法。如下:

func invoke(ctx context.Context, method string, req, reply interface{}, cc *ClientConn, opts ...CallOption) error { cs, err := newClientStream(ctx, unaryStreamDesc, cc, method, opts...) if err != nil {  return err } if err := cs.SendMsg(req); err != nil {  return err } return cs.RecvMsg(reply)}

通过概览,能够关注到三块调用。如下:

  • newClientStream:获取传输层 Trasport 并组合封装到 ClientStream 中返回,在这块会波及负载平衡、超时管制、 Encoding、 Stream 的动作,与服务端基本一致的行为。
  • cs.SendMsg:发送 RPC 申请进来,但其并不承当期待响应的性能。
  • cs.RecvMsg:阻塞期待承受到的 RPC 办法响应后果。

连贯

// clientconn.gofunc (cc *ClientConn) getTransport(ctx context.Context, failfast bool, method string) (transport.ClientTransport, func(balancer.DoneInfo), error) { t, done, err := cc.blockingpicker.pick(ctx, failfast, balancer.PickOptions{  FullMethodName: method, }) if err != nil {  return nil, nil, toRPCErr(err) } return t, done, nil}

在 newClientStream 办法中,咱们通过 getTransport 办法获取了 Transport 层中形象进去的 ClientTransport 和 ServerTransport,实际上就是获取一个连贯给后续 RPC 调用传输应用。

四、敞开连贯

// conn.Close()func (cc *ClientConn) Close() error { defer cc.cancel()    ... cc.csMgr.updateState(connectivity.Shutdown)    ... cc.blockingpicker.close() if rWrapper != nil {  rWrapper.close() } if bWrapper != nil {  bWrapper.close() } for ac := range conns {  ac.tearDown(ErrClientConnClosing) } if channelz.IsOn() {  ...  channelz.AddTraceEvent(cc.channelzID, ted)  channelz.RemoveEntry(cc.channelzID) } return nil}

该办法会勾销 ClientConn 上下文,同时敞开所有底层传输。波及如下:

  • Context Cancel
  • 清空并敞开客户端连贯
  • 清空并敞开解析器连贯
  • 清空并敞开负载平衡连贯
  • 增加跟踪援用
  • 移除以后通道信息

Q&A

1. gRPC Metadata 是通过什么传输?

2. 调用 grpc.Dial 会真正的去连贯服务端吗?

会,然而是异步连贯的,连贯状态为正在连接。但如果你设置了 grpc.WithBlock 选项,就会阻塞期待(期待握手胜利)。另外你须要留神,当未设置 grpc.WithBlock 时,ctx 超时管制对其无任何成果。

3. 调用 ClientConn 不 Close 会导致泄露吗?

会,除非你的客户端不是常驻过程,那么在利用完结时会被动地回收资源。但如果是常驻过程,你又真的遗记执行 Close 语句,会造成的泄露。如下图:

3.1. 客户端

3.2. 服务端

3.3. TCP

4. 不管制超时调用的话,会呈现什么问题?

短时间内不会呈现问题,然而会一直积蓄泄露,积蓄到最初当然就是服务无奈提供响应了。如下图:

5. 为什么默认的拦截器不能够传多个?

func chainUnaryClientInterceptors(cc *ClientConn) { interceptors := cc.dopts.chainUnaryInts if cc.dopts.unaryInt != nil {  interceptors = append([]UnaryClientInterceptor{cc.dopts.unaryInt}, interceptors...) } var chainedInt UnaryClientInterceptor if len(interceptors) == 0 {  chainedInt = nil } else if len(interceptors) == 1 {  chainedInt = interceptors[0] } else {  chainedInt = func(ctx context.Context, method string, req, reply interface{}, cc *ClientConn, invoker UnaryInvoker, opts ...CallOption) error {   return interceptors[0](ctx, method, req, reply, cc, getChainUnaryInvoker(interceptors, 0, invoker), opts...)  } } cc.dopts.unaryInt = chainedInt}

当存在多个拦截器时,取的就是第一个拦截器。因而论断是容许传多个,但并没有用。

6. 真的须要用到多个拦截器的话,怎么办?

能够应用 go-grpc-middleware 提供的 grpc.UnaryInterceptor 和 grpc.StreamInterceptor 链式办法,方便快捷省心。

单单会用还不行,咱们再深剖一下,看看它是怎么实现的。外围代码如下:

func ChainUnaryClient(interceptors ...grpc.UnaryClientInterceptor) grpc.UnaryClientInterceptor { n := len(interceptors) if n > 1 {  lastI := n - 1  return func(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error {   var (    chainHandler grpc.UnaryInvoker    curI         int   )   chainHandler = func(currentCtx context.Context, currentMethod string, currentReq, currentRepl interface{}, currentConn *grpc.ClientConn, currentOpts ...grpc.CallOption) error {    if curI == lastI {     return invoker(currentCtx, currentMethod, currentReq, currentRepl, currentConn, currentOpts...)    }    curI++    err := interceptors[curI](currentCtx, currentMethod, currentReq, currentRepl, currentConn, chainHandler, currentOpts...)    curI--    return err   }   return interceptors[0](ctx, method, req, reply, cc, chainHandler, opts...)  } }    ...}

当拦截器数量大于 1 时,从 interceptors[1] 开始递归,每一个递归的拦截器 interceptors[i] 会一直地执行,最初才真正的去执行 handler 办法。同时也常常有人会问拦截器的执行程序是什么,通过这段代码你得出结论了吗?

7. 频繁创立 ClientConn 有什么问题?

这个问题咱们能够反向验证一下,假如不专用 ClientConn 看看会怎么样?如下:

func BenchmarkSearch(b *testing.B) { for i := 0; i < b.N; i++ {  conn, err := GetClientConn()  if err != nil {   b.Errorf("GetClientConn err: %v", err)  }  _, err = Search(context.Background(), conn)  if err != nil {   b.Errorf("Search err: %v", err)  } }}

输入后果:

    ... connection error: desc = "transport: Error while dialing dial tcp :10001: socket: too many open files"    ... connection error: desc = "transport: Error while dialing dial tcp :10001: socket: too many open files"    ... connection error: desc = "transport: Error while dialing dial tcp :10001: socket: too many open files"    ... connection error: desc = "transport: Error while dialing dial tcp :10001: socket: too many open files"FAILexit status 1

当你的利用场景是存在高频次同时生成/调用 ClientConn 时,可能会导致系统的文件句柄占用过多。这种状况下你能够变更应用程序生成/调用 ClientConn 的模式,又或是池化它,这块能够参考 grpc-go-pool 我的项目。

8. 客户端申请失败后会默认重试吗?

会一直地进行重试,直到上下文勾销。而重试工夫方面采纳 backoff 算法作为的重连机制,默认的最大重试工夫距离是 120s。

9. 为什么要用 HTTP/2 作为传输协定?

许多客户端要通过 HTTP 代理来拜访网络,gRPC 全副用 HTTP/2 实现,等到代理开始反对 HTTP/2 就能通明转发 gRPC 的数据。不光如此,负责负载平衡、访问控制等等的反向代理都能无缝兼容 gRPC,比起本人设计 wire protocol 的 Thrift,这样做迷信不少。@ctiller @滕亦飞

10. 在 Kubernetes 中 gRPC 负载平衡有问题?

gRPC 的 RPC 协定是基于 HTTP/2 规范实现的,HTTP/2 的一大个性就是不须要像 HTTP/1.1 一样,每次发出请求都要从新建设一个新连贯,而是会复用原有的连贯。

所以这将导致 kube-proxy 只有在连贯建设时才会做负载平衡,而在这之后的每一次 RPC 申请都会利用本来的连贯,那么实际上后续的每一次的 RPC 申请都跑到了同一个中央。

注:应用 k8s service 做负载平衡的状况下

总结

  • gRPC 基于 HTTP/2 + Protobuf。
  • gRPC 有四种调用形式,别离是一元、服务端/客户端流式、双向流式。
  • gRPC 的附加信息都会体现在 HEADERS 帧,数据在 DATA 帧上。
  • Client 申请若应用 grpc.Dial 默认是异步建设连贯,过后状态为 Connecting。
  • Client 申请若须要同步则调用 WithBlock(),实现状态为 Ready。
  • Server 监听是循环期待连贯,若没有则休眠,最大休眠工夫 1s;若接管到新申请则起一个新的 goroutine 去解决。
  • grpc.ClientConn 不敞开连贯,会导致 goroutine 和 Memory 等泄露。
  • 任何内/外调用如果不加超时管制,会呈现透露和客户端一直重试。
  • 特定场景下,如果不对 grpc.ClientConn 加以调控,会影响调用。
  • 拦截器如果不必 go-grpc-middleware 链式解决,会笼罩。
  • 在抉择 gRPC 的负载平衡模式时,须要审慎。

参考

  • http://doc.oschina.net/grpc
  • https://github.com/grpc/grpc/...
  • https://juejin.im/post/5b88a4...
  • https://www.ibm.com/developer...
  • https://github.com/grpc/grpc-...
  • https://www.zhihu.com/questio...


微信公众号【程序员黄小斜】作者是前蚂蚁金服Java工程师,专一分享Java技术干货和求职成长心得,不限于BAT面试,算法、计算机根底、数据库、分布式、spring全家桶、微服务、高并发、JVM、Docker容器,ELK、大数据等。关注后回复【book】支付精选20本Java面试必备精品电子书。