原文地址: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。
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文件。
代码语言: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吧。
其他要注意的重要事项:
- 我没有比较速度,对于某些人来说,这可能是最相关的指标
- 我没有处理非常大的服务的经验。我不是评论每个框架的可维护性的合适人选。然而,这是决定选择哪种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/