【译】基于python 的 RPC 框架比较: gRPC vs Thrift vs RPyC

2021-08-18 17:32:59 浏览数 (1)

原文地址:https://www.hardikp.com/2018/07/28/services/

引言

那一年是2015年。我正在写一堆ML训练脚本以及几个生产脚本。他们都需要金融数据。数据分散在多个表和多个数据存储中。日内市场数据以不同方式存储在cassandra集群中,而每日/每月的数据则在MySQL数据库中。同样地,不同类型的证券(期货、期权、股票等)被存储在不同的位置。

所以,我决定写一个可以在我的脚本中使用的数据操作库。结果这个数据操作库在我的团队中相当受欢迎。它拥有我们当时需要的所有东西:

  • 所有数据类型的单一接口 - 来自不同交易所的期货、股票、ETF、货币、指数和基金。
  • 易于使用的接口
  • 在支持的数据间隔方面很灵活。它在日内、跨日和跨月的时间段里工作得完美无缺
  • 它既可用于实时数据获取/使用,也可用于历史数据需求
  • 很容易支持一种新的数据类型——例如,宏观经济指标

然而,它有一些我当时无法预见的致命的缺陷。随着时间的推移,依赖这个库的生产脚本的数量成倍增长。而我们的数据操作库直接调用数据库查询。

  • 更改数据库中的任何内容都会破坏现有的生产流程。因此,没有办法在不造成停机的情况下更改数据库
  • 此外,迅速增加的生产进程对数据库造成了压力。由于数据库的访问被细化到代码库的其他部分,所以不可能进行适当的优化或负载平衡。

大约一年前,有人问我,我们是否应该把那个代码库转换为服务。我对此置之不理--完全没有意识到在接下来的一年里我将面临的问题。说实话,那时候我还没有完全理解服务或微服务--这让我对它用于数据获取这样的事情持怀疑态度。我仍然相信,将这些代码作为一个库是灵活性和快速变化的保证。

但是,几天前我终于开始重新审视这些服务。在过去的几天里,我看了gRPC、Thrift和RPyC。我在这篇文章中总结了我的初步结论。因为我主要是用python来做所有事情,所以我是从这个角度来看待这些框架的。

您可以在这个链接中找到后续示例的代码。

gRPC

gGPC使用Protocal Buffers 进行序列化和反序列化。它是由谷歌开发的--他们在重写内部框架stubby的时候将其作为一个开源软件发布。目前,包括Netflix和Square在内的一些公司正在使用这个框架来实现他们的服务。

让我们直接跳到最简单的例子中。

我们将为所有3个框架使用相同的玩具示例:

  • 我们将定义一个名为Time 的服务。
  • 它实现了一个单一的 RPC 调用:GetTime.
  • GetTime 不接受任何参数并以字符串格式返回当前的服务器时间。

简单的 gRPC 示例

创建一个 time.proto Protocol Buffers文件来描述我们的服务。

代码语言:javascript复制
syntax = "proto3"; 
package time; 


service Time { // Time 服务名
    // GetTime RPC 调用
    // TimeRequest RPC 输入类型
    // TimeReply   RPC 输出类型
    rpc GetTime (TimeRequest) returns (TimeReply) {} 
} 

// Empty Request Message 
message TimeRequest {
} 

// The response message containing the time 
message TimeReply {
    string message = 1; // 字符串类型
}

下面是对上面代码的一点解释。

现在,使用上面的 protobuf 文件生成 python 文件 time_pb2.py 和 time_pb2_grpc.py。我们将在服务器和客户端代码中使用它们。下面是执行此操作的命令行代码(您将需要 grpcio-tools python 包) :

p

代码语言:javascript复制
python -m grpc_tools.protoc --python_out=. --grpc_python_out=. time.proto

创建服务器脚本 server.py。

代码语言:javascript复制
import time
from concurrent import futures

import grpc

# import 生成的代码
import time_pb2
import time_pb2_grpc

_ONE_DAY_IN_SECONDS = 60 * 60 * 24


# 定义 Timer 类
class Timer(time_pb2_grpc.TimeServicer):
    def GetTime(self, request, context): # 定义RPC 调用
        return time_pb2.TimeReply(message=time.ctime()) # 返回当前时间


def serve():
    # 创建一个线程池,添加我们的服务实例并启动服务器
    server = grpc.server(futures.ThreadPoolExecutor(max_workers=10))
    time_pb2_grpc.add_TimeServicer_to_server(Timer(), server)
    server.add_insecure_port('[::]:50051') 
    server.start()
    try:
        while True:# sleep 避免主线程退出
            time.sleep(_ONE_DAY_IN_SECONDS)
    except KeyboardInterrupt:
        server.stop(0)


if __name__ == '__main__':
    serve()

下面是带注释的服务器代码:

Add client code to theclient.pyfile.

将客户端代码添加到 client.py 文件。

代码语言:javascript复制
import grpc

import time_pb2
import time_pb2_grpc


def run():
    channel = grpc.insecure_channel('localhost:50051') # 连接服务器
    stub = time_pb2_grpc.TimeStub(channel)
    response = stub.GetTime(time_pb2.TimeRequest()) # 调用RPC
    print('Client received: {}'.format(response.message))


if __name__ == '__main__':
    run()

我在下面添加了注释客户机代码。

更多细节

gRPC 使用 HTTP/2进行客户机-服务器通信,每个 RPC 调用都是同一个 TCP/IP 连接中的单独的流。

支援4种不同类型的RPCs:

  • Unary RPC - a single request followed by a sing单一的 RPC ——一个请求后跟一个来自服务器的响应。我们的 TimeService 示例使用单一的 RPC。
代码语言:javascript复制
rpc GetTime (TimeRequest) returns (TimeReply) {}
  • 服务器流 RPC-客户端发送一个请求并获得一个可读取的流。
代码语言:javascript复制
 rpc GetTime (TimeRequest) returns (stream TimeReply) {}
  • 客户端流式 RPC-客户端写入一个消息序列。
代码语言:javascript复制
rpc GetTime (stream TimeRequest) returns (TimeReply) {}
  • 双向流式 RPC——双方使用读写流发送一系列消息。
代码语言:javascript复制
rpc GetTime (stream TimeRequest) returns (stream TimeReply) {}

带有内置的超时功能,这在实践中相当方便。许多应用程序要求在一定的时间间隔内做出响应。

优缺点

优点:

  • 为服务器和客户端提供多语言支持
  • 默认情况下,连接使用 HTTP/2
  • 丰富的文档
  • 这个项目得到了谷歌和其他公司的积极支持

缺点:

  • 灵活性较低(特别是与rpyc).

链接:

  • 官方网站及教程 -https://grpc.io/docs/guides/.
  • gRPC Concepts.

Thrift

Thrift在Facebook和Hadoop/Java服务世界中相当流行。它是在Facebook创建的,他们在某个时候把它作为一个Apache项目开源了。

简单的thrift例子

使用Thrift接口描述语言(IDL)创建描述接口的time_service.thrift文件。

代码语言:javascript复制
service TimeService {
    string get_time()
}

运行以下命令生成 python 代码。它将创建一个 gen-py 目录。我们将使用它来构建服务器和客户端脚本。

代码语言:javascript复制
thrift -r --gen py time_service.thrift

用 server.py 编写以下服务器代码。

代码语言:javascript复制
import sys
import time

from thrift.protocol import TBinaryProtocol
from thrift.server import TServer
from thrift.transport import TSocket, TTransport
sys.path.append('gen-py')
from time_service import TimeService


class TimeHandler:
    def __init__(self):
        self.log = {}

    def get_time(self):
        return time.ctime()


if __name__ == '__main__':
    handler = TimeHandler()
    processor = TimeService.Processor(handler)
    transport = TSocket.TServerSocket(host='127.0.0.1', port=9090)
    tfactory = TTransport.TBufferedTransportFactory()
    pfactory = TBinaryProtocol.TBinaryProtocolFactory()

    server = TServer.TSimpleServer(processor, transport, tfactory, pfactory)

    print('Starting the server...')
    server.serve()
    print('done.')

在 client.py 中编写以下代码。

代码语言:javascript复制
import sys

from thrift import Thrift
from thrift.protocol import TBinaryProtocol
from thrift.transport import TSocket, TTransport
sys.path.append('gen-py')
from time_service import TimeService


def main():
    # 创建 socket
    transport = TSocket.TSocket('localhost', 9090)

    # Buffering 是关键. 原始套接字非常慢
    transport = TTransport.TBufferedTransport(transport)

    # 以协议方式包装
    protocol = TBinaryProtocol.TBinaryProtocol(transport)

    # 创建一个client 
    client = TimeService.Client(protocol)

    # Connect!
    transport.open()

    ts = client.get_time()
    print('Client Received {}'.format(ts))

    # Close!
    transport.close()


if __name__ == '__main__':
    try:
        main()
    except Thrift.TException as tx:
        print('%s' % tx.message)

简单的 thriftPy 例子

thriftPy似乎比默认的python thrift 库更受欢迎。它也解决了默认的python thrift 库的一些常见问题--这包括用更多的pythonic方法来创建服务器和客户端代码。例如,看看下面的服务器和客户端代码。

服务器代码

代码语言:javascript复制
import time

import thriftpy
from thriftpy.rpc import make_server

class Dispatcher(object):
    def get_time(self):
        return time.ctime()

time_thrift = thriftpy.load('time_service.thrift', module_name='time_thrift')
server = make_server(time_thrift.TimeService, Dispatcher(), '127.0.0.1', 6000)
server.serve()

客户端代码

代码语言:javascript复制
import thriftpy
from thriftpy.rpc import make_client

time_thrift = thriftpy.load('time_service.thrift', module_name='time_thrift')
client = make_client(time_thrift.TimeService, '127.0.0.1', 6000)
print(client.get_time())

优缺点

优点:

  • Thrift支持容器类型list、set和map。也支持常量。这是protocol Buffers 所不支持的。然而,rpyc支持所有的python和python库类型--你甚至可以在RPC调用中发送一个numpy数组。(编辑:proto3也支持这些类型。感谢Barak Michener指出这一点)。)

Cons:

缺点:

  • Python感觉不是Thrift的主要语言。不得不添加sys.path.append('gen-py'),这并不能带来流畅的python体验。
  • 与gRPC相比,文档和在线讨论相对匮乏

RPyC

RPyC 是一个纯粹的 python RPC 框架。它不支持多种语言。如果您的整个代码库都使用 python,那么这将是一个简单而灵活的框架。

简单的 rpyc 示例

server.py

代码语言:javascript复制
import time

from rpyc import Service
from rpyc.utils.server import ThreadedServer

# 定义 TimeService 类
class TimeService(Service):
    def exposed_get_time(self): # 在RPC 调用 名字加 exposed_ 前缀
        return time.ctime()


if __name__ == '__main__':
    s = ThreadedServer(TimeService, port=18871) # 启动服务
    s.start()

下面是注释的服务器代码:

client.py

代码语言:javascript复制
import rpyc

conn = rpyc.connect('localhost', 18871) # 连接服务
print('Time is {}'.format(conn.root.get_time()))

附加注释的客户端代码:

优缺点

优点:

  • 可能是最容易开始的,不需要理解Protocol Buffer或Thrift的语法
  • 极为灵活。不需要正式使用IDL(接口定义语言)来定义客户-服务器接口。只需开始实现你的代码--它拥抱了python的Duck Typing。

缺点:

  • 缺少多种客户机语言
  • 如果代码库变得足够大,缺乏正式定义的服务接口可能会导致维护问题

gRPC vs Thrift vs RPyC 比较

在深入讨论每个框架的细节之前,让我在这里总结一下。

gRPC

上表的注释:

  • 我发现要让基本的Thrift例子工作起来比较困难。我发现的几个python例子都是针对较早的thrift版本(和python2)。
  • 我对 "可维护性 "的看法是基于这样一个事实:RPyC没有IDL(gRPC使用protobuf,Thrift使用Thrift IDL)--它拥抱鸭子的类型。虽然这使得它非常容易上手,但在维护方面,它可能是一件坏事。

我的偏好是:

  • 如果Python是我要使用的唯一语言,我个人更倾向于使用RPyC。
  • 如果我的服务需要稳健性、可靠性和可扩展性,我更愿意使用gPRC。
  • Thrift最好的一点是它支持更多语言。如果这是你的目标,就选择Thirft吧。

其他要注意的重要事项:

  1. 我没有比较速度,对于某些人来说,这可能是最相关的指标
  2. 我没有处理非常大的服务的经验。我不是评论每个框架的可维护性的合适人选。然而,这是决定选择哪种RPC框架的一个重要标准。

你可以在这个代码库中找到上面例子的代码。

参考链接:

  • 原文连接:https://www.hardikp.com/2018/07/28/services/
  • 代码库:https://github.com/hardikp/service_demo
  • https://thrift.apache.org/
  • https://rpyc.readthedocs.io/en/latest/
  • https://grpc.io/

0 人点赞