獐子岛扇贝 發表於 2021-9-29 19:00:00

【Go】Golang实现gRPC的Proxy的原理

<h1>背景</h1>
<p>gRPC是Google开始的一个RPC服务框架, 是英文全名为Google Remote Procedure Call的简称。</p>
<p>广泛的应用在有RPC场景的业务系统中,一些架构中将gRPC请求都经过一个gRPC服务代理节点或网关,进行服务的权限限制,限流,服务调用监控,增加请求统计等等诸多功能。</p>
<p>如下以Golang和gRPC为例,简要分析gRPC的转发原理。</p>
<p>&nbsp;</p>
<h1>gRPC Proxy原理</h1>
<p>基本原理如下</p>
<ul>
<li>基于TCP启动一个gRPC代理服务端</li>
<li>拦截gRPC框架的服务,能将gRPC请求的服务拦截到转发代理的一个函数中执行。</li>
<li>接收客户端的请求,处理业务指标后转发给服务端。</li>
<li>接收服务端的响应,处理业务指标后转发给客户端。</li>
</ul>
<p>基于如上原理描述,如下图所示,gRPC的客户端将所有的请求都发给gRPC Server Proxy,这个代理网关实现请求转发。</p>
<p>将gRPC Client的请求流转发到gRPC 服务实现的节点上。并将服务处理结果响应返回给客户端。</p>
<p><img src="https://img2020.cnblogs.com/blog/449477/202109/449477-20210929172544546-1681221119.png" alt="" loading="lazy"></p>
<p>&nbsp;</p>
<p>&nbsp;在这个图中的转发需要回答如下几个问题</p>
<ul>
<li>Proxy怎么知道哪些请求转发到哪些服务节点上,转发的依据是什么?</li>
<li>Proxy是否需要解析gRPC协议?</li>
<li>Proxy上没有服务的实现,该如何转发?</li>
</ul>
<p>&nbsp;</p>
<h2>简化的gRPC服务处理流程</h2>
<p>在回答如下问题之前,我们先简单的分析一下gRPC服务器的实现原理和流程。</p>
<ul>
<li>编写自己的服务实现,例子中以HelloWorld为例。</li>
<li>把自己的服务实现HelloWorldServer注册到gRPC框架中</li>
<li>创建一个TCP的服务端监听</li>
<li>基于TCP监听启动一个gRPC服务</li>
<li>gRPC服务接收gRPC客户端的TCP请求</li>
<li>解析gRPC的头部信息,找出服务名</li>
<li>根据服务名找到第一步注册的服务和方法实现处理器handler</li>
<li>处理函数执行</li>
<li>返回处理结果</li>
</ul>
<p>简化的注册服务处理器函数,启动gRPC服务,调用请求和执行数据流图如下所示:</p>
<p><img src="https://img2020.cnblogs.com/blog/449477/202109/449477-20210929170823726-882868933.png" alt="" loading="lazy"></p>
<p>&nbsp;</p>
<p>&nbsp;</p>
<p>&nbsp;</p>
<h2>详细的gRPC服务运行原理</h2>
<p><strong>第一步,定义和编写HelloWorld的IDL文件</strong></p>
<div class="cnblogs_code">
<pre>syntax = <span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">proto3</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(0, 0, 0, 1)">;

package demoapi;


</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> HelloWorld Service</span>
<span style="color: rgba(0, 0, 0, 1)">service HelloWorldService {
   rpc HelloWorld(HelloWorldRequest) returns (HelloWorldResponse){};
}

</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> Request message</span>
<span style="color: rgba(0, 0, 0, 1)">message HelloWorldRequest {
   </span><span style="color: rgba(0, 0, 255, 1)">string</span>request = <span style="color: rgba(128, 0, 128, 1)">1</span><span style="color: rgba(0, 0, 0, 1)">;
}

</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> Response message</span>
<span style="color: rgba(0, 0, 0, 1)">message HelloWorldResponse {
   </span><span style="color: rgba(0, 0, 255, 1)">string</span> respose = <span style="color: rgba(128, 0, 128, 1)">1</span><span style="color: rgba(0, 0, 0, 1)">;
}</span></pre>
</div>
<p>在这个简单的IDL中,定义了一个HelloWorldService的gRPC服务Service,这个服务中有一个HelloWorld方法Method。</p>
<p>&nbsp;</p>
<p><strong>第二步,编译IDL文件</strong></p>
<p>将IDL的proto文件编译成helloworld.pb.go的gRPC代码文件。</p>
<p>生成的代码文件中,我们可以看到如下信息</p>
<div class="cnblogs_code">
<pre><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> Hello World的客户端接口</span>
type HelloWorldServiceClient <span style="color: rgba(0, 0, 255, 1)">interface</span><span style="color: rgba(0, 0, 0, 1)"> {
    HelloWorld(ctx context.Context, </span><span style="color: rgba(0, 0, 255, 1)">in</span> *HelloWorldRequest, opts ...grpc.CallOption) (*<span style="color: rgba(0, 0, 0, 1)">HelloWorldResponse, error)
}

</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> Hello World的服务端接口</span>
type HelloWorldServiceServer <span style="color: rgba(0, 0, 255, 1)">interface</span><span style="color: rgba(0, 0, 0, 1)"> {
    HelloWorld(context.Context, </span>*HelloWorldRequest) (*<span style="color: rgba(0, 0, 0, 1)">HelloWorldResponse, error)
}

</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> HelloWorld的服务注册处理器函数Handler</span>
func _HelloWorldService_HelloWorld_Handler(srv <span style="color: rgba(0, 0, 255, 1)">interface</span>{}, ctx context.Context, dec func(<span style="color: rgba(0, 0, 255, 1)">interface</span>{}) error, interceptor grpc.UnaryServerInterceptor) (<span style="color: rgba(0, 0, 255, 1)">interface</span><span style="color: rgba(0, 0, 0, 1)">{}, error) {
    </span><span style="color: rgba(0, 0, 255, 1)">in</span> := <span style="color: rgba(0, 0, 255, 1)">new</span><span style="color: rgba(0, 0, 0, 1)">(HelloWorldRequest)
    </span><span style="color: rgba(0, 0, 255, 1)">if</span> err := dec(<span style="color: rgba(0, 0, 255, 1)">in</span>); err !=<span style="color: rgba(0, 0, 0, 1)"> nil {
      </span><span style="color: rgba(0, 0, 255, 1)">return</span><span style="color: rgba(0, 0, 0, 1)"> nil, err
    }
    </span><span style="color: rgba(0, 0, 255, 1)">if</span> interceptor ==<span style="color: rgba(0, 0, 0, 1)"> nil {
      </span><span style="color: rgba(0, 0, 255, 1)">return</span> srv.(HelloWorldServiceServer).HelloWorld(ctx, <span style="color: rgba(0, 0, 255, 1)">in</span><span style="color: rgba(0, 0, 0, 1)">)
    }
    info :</span>= &amp;<span style="color: rgba(0, 0, 0, 1)">grpc.UnaryServerInfo{
      Server:   srv,
      FullMethod: </span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">/demoapi.HelloWorldService/HelloWorld</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(0, 0, 0, 1)">,
    }
    handler :</span>= func(ctx context.Context, req <span style="color: rgba(0, 0, 255, 1)">interface</span>{}) (<span style="color: rgba(0, 0, 255, 1)">interface</span><span style="color: rgba(0, 0, 0, 1)">{}, error) {
      </span><span style="color: rgba(0, 0, 255, 1)">return</span> srv.(HelloWorldServiceServer).HelloWorld(ctx, req.(*<span style="color: rgba(0, 0, 0, 1)">HelloWorldRequest))
    }
    </span><span style="color: rgba(0, 0, 255, 1)">return</span> interceptor(ctx, <span style="color: rgba(0, 0, 255, 1)">in</span><span style="color: rgba(0, 0, 0, 1)">, info, handler)
}

</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> gRPC服务注册的服务描述信息
</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> gRPC服务注册时,会建立以ServiceName为Key,Methods为Value的一个Map映射
</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> Methods中的Handler就是如上的服务处理Handler</span>
<span style="color: rgba(0, 0, 255, 1)">var</span> _HelloWorldService_serviceDesc =<span style="color: rgba(0, 0, 0, 1)"> grpc.ServiceDesc{
    ServiceName: </span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">demoapi.HelloWorldService</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(0, 0, 0, 1)">,
    HandlerType: (</span>*<span style="color: rgba(0, 0, 0, 1)">HelloWorldServiceServer)(nil),
    Methods: []grpc.MethodDesc{
      {
            MethodName: </span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">HelloWorld</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(0, 0, 0, 1)">,
            Handler:    _HelloWorldService_HelloWorld_Handler,
      },
    },
    Streams:[]grpc.StreamDesc{},
    Metadata: </span><span style="color: rgba(128, 0, 0, 1)">"demo</span><span style="color: rgba(128, 0, 0, 1)">api/HelloWorld.proto</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(0, 0, 0, 1)">,
}</span></pre>
</div>
<p>如上代码中有如下几个关键信息需要解释</p>
<ul>
<li>服务Service名称 demoapi.HelloWorldService,对应IDL文件的package包名.service服务名称</li>
<li>方法Method名称 HelloWorld,对应IDL文件的rpc方法</li>
</ul>
<p>&nbsp;</p>
<p><strong>第三步,注册HelloWorld服务到gRPC的服务映射中</strong></p>
<ul>
<li>grpc.ServiceDesc是 gRPC服务注册的服务描述信息。</li>
<li>gRPC服务注册时,会建立以ServiceName为Key,包装Methods为Value的一个Map映射m。</li>
<li>Methods中的Handler就是如上的服务处理Handler。</li>
</ul>
<p>对应的注册代码如下</p>
<div class="cnblogs_code">
<pre><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 注册gRPC服务</span>
func RegisterHelloWorldServiceServer(s *<span style="color: rgba(0, 0, 0, 1)">grpc.Server, srv HelloWorldServiceServer) {
    s.RegisterService(</span>&amp;<span style="color: rgba(0, 0, 0, 1)">_HelloWorldService_serviceDesc, srv)
}

</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> Server is a gRPC server to serve RPC requests.</span>
type Server <span style="color: rgba(0, 0, 255, 1)">struct</span><span style="color: rgba(0, 0, 0, 1)"> {
       </span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> ...</span>
    m      map[<span style="color: rgba(0, 0, 255, 1)">string</span>]*service <span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> service name -&gt; service info</span>
<span style="color: rgba(0, 0, 0, 1)">}

</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> gRPC service.go的服务注册</span>
func (s *Server) register(sd *ServiceDesc, ss <span style="color: rgba(0, 0, 255, 1)">interface</span><span style="color: rgba(0, 0, 0, 1)">{}) {
    srv :</span>= &amp;<span style="color: rgba(0, 0, 0, 1)">service{
      server: ss,
      md:   make(map[</span><span style="color: rgba(0, 0, 255, 1)">string</span>]*<span style="color: rgba(0, 0, 0, 1)">MethodDesc),
      sd:   make(map[</span><span style="color: rgba(0, 0, 255, 1)">string</span>]*<span style="color: rgba(0, 0, 0, 1)">StreamDesc),
      mdata:sd.Metadata,
    }
    </span><span style="color: rgba(0, 0, 255, 1)">for</span> i :=<span style="color: rgba(0, 0, 0, 1)"> range sd.Methods {
      d :</span>= &amp;<span style="color: rgba(0, 0, 0, 1)">sd.Methods
      srv.md </span>=<span style="color: rgba(0, 0, 0, 1)"> d
    }
    </span><span style="color: rgba(0, 0, 255, 1)">for</span> i :=<span style="color: rgba(0, 0, 0, 1)"> range sd.Streams {
      d :</span>= &amp;<span style="color: rgba(0, 0, 0, 1)">sd.Streams
      srv.sd </span>=<span style="color: rgba(0, 0, 0, 1)"> d
    }
    s.m </span>=<span style="color: rgba(0, 0, 0, 1)"> srv
}</span></pre>
</div>
<p><strong>第四步,接收客户端gRPC请求并处理</strong></p>
<p>在这一步中,会进行如下几个步骤和函数的调用,也会回答前面的第一个问题。</p>
<ul>
<li>gRPC客户端通过TCP链接,连接到gRPC服务端</li>
<li>gRPC的Serve函数触发TCP的Accept函数调用,生成一个和客户端的网络连接</li>
<li>grpc框架代码执行handleRawConn方法,将这个网络连接设置打破gRPC的传输层,做为网络的读和写实现</li>
<li>依次调用grpc流的handlerStream方法,用于处理gRPC数据流</li>
<li>这个函数中会接收gRPC请求的头信息,并解析得到服务名 如第二步中的服务名 demoapi.HelloWorldService</li>
<li>通过如下的服务名中的方法名HelloWorld,并在Method的map中找到这个方法的处理器函数Handler,并执行这个Handler函数,实现gRPC服务的调用</li>
<li>最后将处理结果返回</li>
</ul>
<p><strong>&nbsp;整体的数据流整理如下:</strong>&nbsp;</p>
<p><img src="https://img2020.cnblogs.com/blog/449477/202109/449477-20210929172157787-1075484567.png" alt="" loading="lazy"></p>
<p>我们发现在gRPC框架代码中的handleStream存在两类服务,<span style="color: rgba(255, 0, 0, 1)">一类是已知服务 knownService, 第二类是unknownService</span></p>
<p>这两个有什么区别呢?</p>
<p>已知服务 knownService就是gRPC服务端代码注册到gRPC框架中的服务,叫做已知服务,其他没有注册的服务叫做未知服务。</p>
<p>为什么我们要提到这个未知服务unknownService呢?着就是我们实现gRPC服务代码的关键所在,是前面问题三的答案,</p>
<p>&nbsp;</p>
<p><span style="color: rgba(255, 0, 0, 1)">要实现gRPC服务代理,我们在创建grpc服务grpc.NewServer时,传递一个未知服务的handler,将未知服务的处理进行接管,然后通过注册的这个Handler实现gRPC代理转发的逻辑。</span></p>
<p>基于如下描述,gRPC代理的原理如下图所示:</p>
<ul>
<li>创建grpc服务时,注册一个未知服务处理器Handler和一个自定义的编码Codec编码和解码,此处使用proto标准的Codec(回答前面第二个问题)</li>
<li>这个handle给业务方预留一个director的接口,用于代理重定向转发的grpc连接获取,这样proxy就可以通过redirector得到gRPCServer的grpc连接。</li>
<li>proxy接收gRPC客户端的连接,并使用gRPC的RecvMsg方法,接收客户端的消息请求</li>
<li>proxy将接收到的gRPC客户端消息请求,通过SendHeader和SendMsg方法发送给gRPC服务端。</li>
<li>同样的方法,RecvMsg接收gRPC服务端的响应消息,使用SendMsg发送给gRPC客户端。</li>
<li>至此gRPC代码服务就完成了消息的转发功能,企业的限流,权限等功能可以通过转发的功能进行拦截处理。</li>
</ul>
<p><img src="https://img2020.cnblogs.com/blog/449477/202109/449477-20210929170903074-1424219286.png" alt="" loading="lazy"></p>
<p>gRPC Proxy的实现逻辑如下图所示:</p>
<p><img src="https://img2020.cnblogs.com/blog/449477/202109/449477-20210929171021724-2043840522.png" alt="" loading="lazy"></p>
<p>&nbsp;gRPC 代理服务的关键代码如下所示:</p>
<p>服务端到客户端的转发</p>
<div class="cnblogs_code">
<pre><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 转发服务端的数据流到客户端</span>
func (s *<span style="color: rgba(0, 0, 0, 1)">handler) forwardServerToClient(src grpc.ServerStream, dst grpc.ClientStream) chan error {
    ret :</span>= make(chan error, <span style="color: rgba(128, 0, 128, 1)">1</span><span style="color: rgba(0, 0, 0, 1)">)
    go func() {
      f :</span>= &amp;<span style="color: rgba(0, 0, 0, 1)">frame{}
      </span><span style="color: rgba(0, 0, 255, 1)">for</span> i := <span style="color: rgba(128, 0, 128, 1)">0</span>; ; i++<span style="color: rgba(0, 0, 0, 1)"> {
            </span><span style="color: rgba(0, 0, 255, 1)">if</span> err := src.RecvMsg(f); err !=<span style="color: rgba(0, 0, 0, 1)"> nil {
                ret </span>&lt;- err <span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> this can be io.EOF which is happy case</span>
                <span style="color: rgba(0, 0, 255, 1)">break</span><span style="color: rgba(0, 0, 0, 1)">
            }
            </span><span style="color: rgba(0, 0, 255, 1)">if</span> err := dst.SendMsg(f); err !=<span style="color: rgba(0, 0, 0, 1)"> nil {
                ret </span>&lt;-<span style="color: rgba(0, 0, 0, 1)"> err
                </span><span style="color: rgba(0, 0, 255, 1)">break</span><span style="color: rgba(0, 0, 0, 1)">
            }
      }
    }()
    </span><span style="color: rgba(0, 0, 255, 1)">return</span><span style="color: rgba(0, 0, 0, 1)"> ret
}</span></pre>
</div>
<p>客户端到服务端的转发</p>
<div class="cnblogs_code">
<pre><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 转发客户端的数据流到服务端</span>
func (s *<span style="color: rgba(0, 0, 0, 1)">handler) forwardClientToServer(src grpc.ClientStream, dst grpc.ServerStream) chan error {
    ret :</span>= make(chan error, <span style="color: rgba(128, 0, 128, 1)">1</span><span style="color: rgba(0, 0, 0, 1)">)
    go func() {
      f :</span>= &amp;<span style="color: rgba(0, 0, 0, 1)">frame{}
      </span><span style="color: rgba(0, 0, 255, 1)">for</span> i := <span style="color: rgba(128, 0, 128, 1)">0</span>; ; i++<span style="color: rgba(0, 0, 0, 1)"> {
            </span><span style="color: rgba(0, 0, 255, 1)">if</span> err := src.RecvMsg(f); err !=<span style="color: rgba(0, 0, 0, 1)"> nil {
                ret </span>&lt;- err <span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> this can be io.EOF which is happy case</span>
                <span style="color: rgba(0, 0, 255, 1)">break</span><span style="color: rgba(0, 0, 0, 1)">
            }
            </span><span style="color: rgba(0, 0, 255, 1)">if</span> i == <span style="color: rgba(128, 0, 128, 1)">0</span><span style="color: rgba(0, 0, 0, 1)"> {
                </span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> This is a bit of a hack, but client to server headers are only readable after first client msg is
                </span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> received but must be written to server stream before the first msg is flushed.
                </span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> This is the only place to do it nicely.</span>
                md, err :=<span style="color: rgba(0, 0, 0, 1)"> src.Header()
                </span><span style="color: rgba(0, 0, 255, 1)">if</span> err !=<span style="color: rgba(0, 0, 0, 1)"> nil {
                  ret </span>&lt;-<span style="color: rgba(0, 0, 0, 1)"> err
                  </span><span style="color: rgba(0, 0, 255, 1)">break</span><span style="color: rgba(0, 0, 0, 1)">
                }
                </span><span style="color: rgba(0, 0, 255, 1)">if</span> err := dst.SendHeader(md); err !=<span style="color: rgba(0, 0, 0, 1)"> nil {
                  ret </span>&lt;-<span style="color: rgba(0, 0, 0, 1)"> err
                  </span><span style="color: rgba(0, 0, 255, 1)">break</span><span style="color: rgba(0, 0, 0, 1)">
                }
            }
            </span><span style="color: rgba(0, 0, 255, 1)">if</span> err := dst.SendMsg(f); err !=<span style="color: rgba(0, 0, 0, 1)"> nil {
                ret </span>&lt;-<span style="color: rgba(0, 0, 0, 1)"> err
                </span><span style="color: rgba(0, 0, 255, 1)">break</span><span style="color: rgba(0, 0, 0, 1)">
            }
      }
    }()
    </span><span style="color: rgba(0, 0, 255, 1)">return</span><span style="color: rgba(0, 0, 0, 1)"> ret
}</span></pre>
</div>
<p>&nbsp;</p>
<h1>参考材料</h1>
<p>https://github.com/grpc/grpc</p>
<p>https://github.com/mwitkow/grpc-proxy</p>
<p>grpc-proxy demo 可以参考我实现的一个简单的 demo 例子&nbsp;https://github.com/mygityf/grpc-proxy</p>
<p>工作原理如下:</p>
<div class="cnblogs_code">
<pre>    gRpchelloClient ----&gt; gRpcProxy ----&gt;<span style="color: rgba(0, 0, 0, 1)"> gRpcHelloServer
                        </span>^                  |
                        |______register______|</pre>
</div>
<p>&nbsp;</p>
<p>done。</p>
<p>祝玩的开心~</p><br><br>
来源:https://www.cnblogs.com/voipman/p/15352001.html
頁: [1]
查看完整版本: 【Go】Golang实现gRPC的Proxy的原理