其实很久没有搞过服务这个东西了,以前用的是TCP、http这种,后来用了Netty

今天我们再说个grpc服务:

为什么会用到这个呢,我说一下我的场景:

做机器学习部署模型,有这么几种模型部署方式,

1、tfserver -- 只支持tensorflow的模型代码

2、pmml 模型服务,这种封装的比较死,灵活度不高,但是小公司用起来也足够了

3、grpc -- 也是今天我们要说的这个

grpc一种server - client模式,这个就比较熟悉了,我们常用的netty、socket都是这种

grpc是什么?

所谓RPC(remote procedure call 远程过程调用)框架实际是提供了一套机制,使得应用程序之间可以进行通信,而且也遵从server/client模型。使用的时候客户端调用server端提供的接口就像是调用本地的函数一样。

grpc支持拦截器吗_Nginx

gRPC有什么好处以及在什么场景下需要用gRPC

既然是server/client模型,那么我们直接用restful api不是也可以满足吗,为什么还需要RPC呢?下面我们就来看看RPC到底有哪些优势

gRPC vs. Restful API

gRPC和restful API都提供了一套通信机制,用于server/client模型通信,而且它们都使用http作为底层的传输协议(严格地说, gRPC使用的http2.0,而restful api则不一定)。不过gRPC还是有些特有的优势,如下:

  • gRPC可以通过protobuf来定义接口,从而可以有更加严格的接口约束条件。关于protobuf可以参见 这篇文章  Google Protobuf简明教程
  • 另外,通过protobuf可以将数据序列化为二进制编码,这会大幅减少需要传输的数据量,从而大幅提高性能。
  • gRPC可以方便地支持流式通信(理论上通过http2.0就可以使用streaming模式, 但是通常web服务的restful api似乎很少这么用,通常的流式数据应用如视频流,一般都会使用专门的协议如HLS,RTMP等,这些就不是我们通常web服务了,而是有专门的服务器应用。)

使用场景

  • 需要对接口进行严格约束的情况,比如我们提供了一个公共的服务,很多人,甚至公司外部的人也可以访问这个服务,这时对于接口我们希望有更加严格的约束,我们不希望客户端给我们传递任意的数据,尤其是考虑到安全性的因素,我们通常需要对接口进行更加严格的约束。这时gRPC就可以通过protobuf来提供严格的接口约束。
  • 对于性能有更高的要求时。有时我们的服务需要传递大量的数据,而又希望不影响我们的性能,这个时候也可以考虑gRPC服务,因为通过protobuf我们可以将数据压缩编码转化为二进制格式,通常传递的数据量要小得多,而且通过http2我们可以实现异步的请求,从而大大提高了通信效率。

但是,通常我们不会去单独使用gRPC,而是将gRPC作为一个部件进行使用,这是因为在生产环境,我们面对大并发的情况下,需要使用分布式系统来去处理,而gRPC并没有提供分布式系统相关的一些必要组件。而且,真正的线上服务还需要提供包括负载均衡,限流熔断,监控报警,服务注册和发现等等必要的组件。不过,这就不属于本篇文章讨论的主题了,我们还是先继续看下如何使用gRPC。

这里写个简单的Python gRPC示例,能实现加法和乘法的计算器:

开始环境准备

安装gRPC相关的库,grpcio-tools主要用根据我们的protocol buffer定义来生成Python代码,官方解释是Protobuf code generator for gRPC。protocolbuffers/protobuf是Google开发的一种序列化数据结构的协议。具体结构和语法超纲了,现在还不多用做太多理解,只要会用就行了。

pip install grpcio grpcio-tools

这个是代码结构:

grpc支持拦截器吗_python_02

定义服务:使用protocolbuffers/protobuf格式来创建结构化数据文件SimpleCal.proto,内容如下:

至于这个怎么编写,可以借鉴这篇文章,上面说的比较详细


syntax = "proto3";
     
    service Cal {
      rpc Add(AddRequest) returns (ResultReply) {}
      rpc Multiply(MultiplyRequest) returns (ResultReply) {}
    }
     
    message AddRequest {
      int32 number1  = 1;
      int32 number2  = 2;
    }
     
    message MultiplyRequest {
      int32 number1  = 1;
      int32 number2  = 2;
    }
     
    message ResultReply {
      int32 number = 1;
    }

在SimpleCal.proto 文件中定义了一个服务Cal,定义了2个RPC方法:Add和Multiply,需要分别在gRPC的服务端中实现加法和乘法。

同时我们也定义了2个方法的参数,Add方法的参数是AddRequest,包含number1和number2两个整数参数。Multiply方法的参数是MultiplyRequest,里面也有number1和number2两个整数参数。两个函数的返回结构都是ResultReply,内容是一个整数。

根据上面的定义,生成Python代码:(在.protoc文件目录下执行这个会自己生成两个文件的

$ python3 -m grpc_tools.protoc -I. --python_out=. --grpc_python_out=. ./SimpleCal.proto
$ ls
SimpleCal_pb2_grpc.py  SimpleCal_pb2.py  SimpleCal.proto

使用python3 -m grpc_tools.protoc --hel能获得命令的参数含义。ls可以看到grpc_tools 帮我们

自动生成了 SimpleCal_pb2_grpc.py, SimpleCal_pb2.py这2个文件。这2个文件会在后面的客户端和服务端代码中被引用。

服务端和客户端样例

下面是服务端代码 hello_server.py:

from concurrent import futures
import grpc
import SimpleCal_pb2
import SimpleCal_pb2_grpc
 
class CalServicer(SimpleCal_pb2_grpc.CalServicer):
  def Add(self, request, context):   # Add函数的实现逻辑
    print("Add function called")
    return SimpleCal_pb2.ResultReply(number=request.number1 + request.number2)
 
  def Multiply(self, request, context):   # Multiply函数的实现逻辑
    print("Multiply service called")
    return SimpleCal_pb2.ResultReply(number=request.number1 * request.number2)
 
def serve():
  server = grpc.server(futures.ThreadPoolExecutor(max_workers=5))
  SimpleCal_pb2_grpc.add_CalServicer_to_server(CalServicer(),server)
  server.add_insecure_port("[::]:50051")
  server.start()
  print("grpc server start...")
  server.wait_for_termination()
 
if __name__ == '__main__':
  serve()

这里的重点在于CalServicer类中对Add和Multiply两个方法的实现。逻辑很简单,从request中读取number1和number2,然后相加。注意,这里的所有变量都需要完整名称:request.number1和request.number2, 不能使用位置参数。Multiply 的实现和Add一样,不多说了。serve函数里定义了gRPC的运行方式,使用5个worker的线程池。

客户端代码 hello_client.py:

import SimpleCal_pb2
import SimpleCal_pb2_grpc
import grpc
 
def run(n, m):
  channel = grpc.insecure_channel('localhost:50051') # 连接上gRPC服务端
  stub = SimpleCal_pb2_grpc.CalStub(channel)
  response = stub.Add(SimpleCal_pb2.AddRequest(number1=n, number2=m))  # 执行计算命令
  print(f"{n} + {m} = {response.number}")
  response = stub.Multiply(SimpleCal_pb2.MultiplyRequest(number1=n, number2=m))
  print(f"{n} * {m} = {response.number}")
 
if __name__ == "__main__":
  run(100, 300)

客户端的逻辑更加简单,就连上gRPC服务,然后发起调用。

下面开启服务端,并执行客户端代码调用gRPC服务,结果如下:

$ python3 cal_server.py  &
$ python3 cal_client.py
100 + 300 = 400
100 * 300 = 30000

执行结果表明客户端和服务端已经都运行正常。更多的gRPC样例可以访问gRPC官网的Example, grpc/grpc 。

https://github.com/grpc/grpc/tree/master/examples/python

使用Nginx来代理gRPC

gRPC是基于HTTP/2协议的,Nginx在1.9.5里开始支持HTTP/2,在1.13.10里开始支持gRPC。为了反向代理gRPC服务,编译Nginx的时候必须要添加这两个参数:--with-http_ssl_module --with-http_v2_module

给Nginx添加如下的server配置:

server {
        listen 80 http2;
     
        location / {
          grpc_pass grpc://localhost:50051;
        }
      }

把这段server的配置添加到Nginx的http段里,配置和启动好Nginx之后,然后把cal_client.py里的channel = grpc.insecure_channel('localhost:50051')

接着往下挖掘gRPC的HTTP2.0接口细节的话,可以打开SimpleCal_pb2_grpc.py你可以看到在CalStub这个类的__init__方法里,定义了Add和Multiply两个函数对应的uri。

grpc支持拦截器吗_grpc支持拦截器吗_03

查看Nginx的日志也能表明这一点:

127.0.0.1 - - [18/Nov/2019:20:09:25 +0800] "POST /Cal/Add HTTP/2.0" 200 8 "-" "grpc-python/1.25.0 grpc-c/8.0.0 (manylinux; chttp2; game)"
127.0.0.1 - - [18/Nov/2019:20:09:25 +0800] "POST /Cal/Multiply HTTP/2.0" 200 9 "-" "grpc-python/1.25.0 grpc-c/8.0.0 (manylinux; chttp2; game)"

如果部署了多个gRPC服务端,也可以使用Nginx的upstream来做多个后端的负载均衡。

最后,用wireshark来对http2的流量进行抓包分析。

grpc支持拦截器吗_grpc_04