vlambda博客
学习文章列表

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

原文地址: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文件来描述我们的服务。

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; // 字符串类型}

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

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


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

p

python -m grpc_tools.protoc --python_out=. --grpc_python_out=. time.proto


创建服务器脚本 server.py。

import timefrom concurrent import futures
import grpc
# import 生成的代码import time_pb2import 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()

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

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

Add client code to theclient.pyfile.

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

import grpc
import time_pb2import 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()

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

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

更多细节


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

支援4种不同类型的RPCs:

  • Unary RPC - a single request followed by a sing单一的 RPC ——一个请求后跟一个来自服务器的响应。我们的 TimeService 示例使用单一的 RPC。 

rpc GetTime (TimeRequest) returns (TimeReply) {}
  • 服务器流 RPC-客户端发送一个请求并获得一个可读取的流。

 rpc GetTime (TimeRequest) returns (stream TimeReply) {}
  • 客户端流式 RPC-客户端写入一个消息序列。 

rpc GetTime (stream TimeRequest) returns (TimeReply) {}
  •  双向流式 RPC——双方使用读写流发送一系列消息。

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文件。

service TimeService {
string get_time()
}


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

thrift -r --gen py time_service.thrift


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


import sysimport time
from thrift.protocol import TBinaryProtocolfrom thrift.server import TServerfrom thrift.transport import TSocket, TTransportsys.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 中编写以下代码。

import sys
from thrift import Thriftfrom thrift.protocol import TBinaryProtocolfrom thrift.transport import TSocket, TTransportsys.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方法来创建服务器和客户端代码。例如,看看下面的服务器和客户端代码。

服务器代码

import time
import thriftpyfrom 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()

客户端代码

import thriftpyfrom 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

import time
from rpyc import Servicefrom 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()

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

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

client.py

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

附加注释的客户端代码:

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

优缺点

优点:

  • 可能是最容易开始的,不需要理解Protocol Buffer或Thrift的语法

  • 极为灵活。不需要正式使用IDL(接口定义语言)来定义客户-服务器接口。只需开始实现你的代码--它拥抱了python的Duck Typing。

缺点:

  • 缺少多种客户机语言

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

gRPC vs Thrift vs RPyC 比较


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


gRPC

Thrift

RPyC

入门指南

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

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

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

文档

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

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

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

语言支持

C++, Python,..2. c + + ,Python,.

C++, Python,..2. c + + ,Python,.

Python Only只能用 Python

可维护性

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

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

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

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

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

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

没有 IDL 也能工作

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

上表的注释:

  • 我发现要让基本的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/


相关文章