玩转gRPC和Python的实例教程

307 阅读3分钟

当我们与微服务一起工作时,通常我们需要以某种方式在它们之间进行通信。基本上,我们有两种选择。同步(API)和异步通信(消息队列)。REST APIs是一种相当直接的方式来创建一个通信渠道。我们有很多框架和微框架来创建REST APIs。例如,在Python中,我们可以使用Flask。REST很简单,它可以适用于很多情况,但有时是不够的。REST API是一种HTTP服务,HTTP是建立在TCP之上的协议。当我们创建一个REST连接时,我们要打开一个TCP连接到服务器,我们发送请求有效载荷,我们接收响应,然后关闭连接。如果我们需要执行大量的连接,也许我们会面临一个瓶颈。此外,我们还有有效载荷。我们需要定义我们要如何对信息进行编码。我们通常使用JSON(我们也可以使用XML)。用几乎所有的语言对JSON进行编码/解码很容易,但JSON是纯文本。通过TCP连接的大的有效载荷意味着缓慢的响应时间。

为了解决这种情况,我们的工具箱中还有一个工具。这个工具就是gRPC。通过gRPC,我们在客户端和服务器之间建立了一个持久的连接(而不是像REST那样打开和关闭连接),同时我们使用二进制的有效载荷来减少大小,提高性能。

首先,我们需要定义我们要使用的协议。这是我们在HTTP APIs中不需要做的事情(我们使用JSON,我们忘记了其他的)。这是一个额外的步骤。并不复杂,但也是一个额外的步骤。我们需要用一个proto文件来定义我们的服务和变量的类型。

// api.proto
syntax = "proto3";
package api;

service Api {
  rpc sayHello (HelloRequest) returns (Hello) {}
  rpc getAll (ApiRequest) returns (api.Items) {}
  rpc getStream (ApiRequest) returns (stream api.Item) {}
}

message ApiRequest {
  int32 length = 1;
}

message Items {
  repeated api.Item items = 1;
}

message Item {
  int32 id = 1;
  string name = 2;
}

message HelloRequest {
  string name = 1;
}

message Hello {
  string message = 1;
}


通过我们的proto文件(与语言无关),我们可以使用我们的编程语言创建我们的服务的封装器。在我的例子中是python。

python -m grpc_tools.protoc -I./protos --python_out=. --grpc_python_out=. ./protos/api.proto

当然,我们可以用一种语言创建客户端,用另一种语言创建服务器。两者都使用同一个proto文件。

它创建了两个文件。我们不需要打开这些文件。我们将导入这些文件来创建我们的客户端和服务器。我们可以直接使用那些文件,但我更喜欢使用一个额外的包装器。不需要重新发明轮子,只是让我容易使用客户端/和服务器。

import grpc

from api_pb2 import Items, Item, Hello, HelloRequest, ApiRequest
from api_pb2_grpc import ApiServicer, ApiStub


class ApiServer(ApiServicer):
    def getAll(self, request, context):
        data = []
        for i in range(1, request.length + 1):
            data.append(Item(id=i, name=f'name {i}'))
        return Items(items=data)

    def getStream(self, request, context):
        for i in range(1, request.length + 1):
            yield Item(id=i, name=f'name {i}')

    def sayHello(self, request, context):
        return Hello(message=f'Hello {request.name}!')


class ApiClient:
    def __init__(self, target):
        channel = grpc.insecure_channel(target)
        self.client = ApiStub(channel)

    def sayHello(self, name):
        response = self.client.sayHello(HelloRequest(name=name))
        return response.message

    def getAll(self, length):
        response = self.client.getAll(ApiRequest(length=length))
        return response.items

    def getStream(self, length):
        response = self.client.getStream(ApiRequest(length=length))
        return response


现在我可以创建一个服务器。

import logging
from concurrent import futures

import grpc

import settings
from api import ApiServer
from api_pb2_grpc import add_ApiServicer_to_server


def serve():
    server = grpc.server(futures.ThreadPoolExecutor(max_workers=10))
    add_ApiServicer_to_server(ApiServer(), server)
    server.add_insecure_port(f'[::]:{settings.BACKEND_PORT}')
    server.start()
    server.wait_for_termination()


if __name__ == '__main__':
    logging.basicConfig()
    serve()


还有一个客户端。在我的例子中,我将使用一个消耗gRPC服务器的flask前端。

from flask import Flask, render_template

import settings
from api import ApiClient

app = Flask(__name__)

app.config["api"] = ApiClient(f"{settings.BACKEND_HOST}:{settings.BACKEND_PORT}")


@app.route("/")
def home():
    api = app.config["api"]
    return render_template(
        "index.html",
        name=api.sayHello("Gonzalo"),
        items=api.getAll(length=10),
        items2=api.getStream(length=5)
    )

我们可以把这个例子部署在docker服务器中。这里是docker-compose.yml

version: '3.6'

services:
  frontend:
    build:
      context: .
      dockerfile: Dockerfile
    environment:
      BACKEND_HOST: backend
    ports:
      - 5000:5000
    command: gunicorn -w 4 app:app -b 0.0.0.0:5000
  backend:
    build:
      context: .
      dockerfile: Dockerfile
    command: python server.py


源代码可在我的github上找到