一个 Python 团队用 gRPC 解决内部通信顽疾的真实经历

B站影视 韩国电影 2025-10-09 06:59 1

摘要:在后端设计的世界里,基于 HTTP/JSON 的微服务通信听起来像是一个标准答案。它简单、直观,易于调试,甚至可以用 curl 命令轻松测试。几个月前,我们的团队也是这么想的。我们有多个 Python 服务,它们之间需要传递用户会话、交易记录和日志事件流等数据

一个 Python 团队用 gRPC 解决内部通信顽疾的真实经历

在后端设计的世界里,基于 HTTP/JSON 的微服务通信听起来像是一个标准答案。它简单、直观,易于调试,甚至可以用 curl 命令轻松测试。几个月前,我们的团队也是这么想的。我们有多个 Python 服务,它们之间需要传递用户会话、交易记录和日志事件流等数据。

起初,一切运转良好。然而,这套架构“在它还能用的时候确实很好用”。

随着系统规模的扩大,问题接踵而至。首先是延迟,我们感受到了明显的性能瓶颈。紧接着,是数据载荷的不一致性问题悄然出现。然后,一些难以追踪的错误开始频繁发生,而与此同时,服务间的 API 端点数量像野草一样疯狂增长。 我们意识到,原先作为解决方案的通信层,现在正逐渐演变成系统的核心负债。

就在这时,团队里有人提议使用 gRPC。说实话,我一开始是有些抗拒的。在项目冲刺阶段引入 Protobuf 和一套全新的技术栈,听起来并不是个好主意。但事实证明,这次技术转型,恰恰是我们摆脱困境所需要的关键一步。

在我们深入探讨 gRPC 之前,有必要先回顾一下为什么原有的 RESTful API + JSON 的方案会在内部服务间通信的场景下举步维艰。需要明确的是,REST 本身并非不好,尤其是在面向浏览器的公开 API 场景下,它依然是王者。 但在服务与服务之间的高频内部调用中,它的弊端被放大了。

我们遇到的问题可以归结为以下几点:

臃肿的数据格式:JSON 是一种冗长的文本格式。当 Python 服务反复序列化(倾倒)和反序列化(解析)这些充满键名的文本时,性能开销是巨大的。随着业务发展,JSON 的体积会不断膨胀,进一步拖慢了整个系统的响应速度。脆弱的“口头约定”:调试过程严重依赖于所有团队成员对数据字段(keys)的共识。 比如,A 服务发布了一个新字段,但 B 服务的开发人员没有及时同步,这就可能导致解析失败或数据丢失。这种依赖“人”来保障一致性的方式,在快速迭代的团队中是极不可靠的。缺失的校验约束:字段该如何校验?这完全取决于各个团队的自觉性。 同一个用户 ID 字段,一个服务可能期望它是整数,而另一个服务可能会传一个字符串。这种缺乏强制性契约约束的情况,是数据不一致和潜在错误的温床。模糊的错误信息:当错误发生时,我们收到的往往只是一个字符串。 “处理失败”或者“参数错误”这样的信息几乎没有任何价值。我们无法从中得知是哪个字段错了,错在哪里,也无法建立一套结构化的错误处理机制。

当这些问题在一个分布式系统中被放大时,服务间的通信就从协作的桥梁,变成了系统的 liability(负债)。 我们迫切需要一种方案,能提供结构化的数据、更快的传输速度,并从根本上减少因开发人员“自由发挥”而导致的 bug。

gRPC 从一开始就给了我们完全不同的感受。它的核心在于“契约先行”——你首先需要在一个.proto文件中定义服务和消息(数据结构)。

这份文件,就是服务间通信的唯一“法律”。无论客户端或服务端使用什么编程语言,所有人都必须基于这份共同的契约来编译生成代码。 仅此一点,就解决了我们过去大约 30%的沟通和协同问题。

这种方式强制团队遵守纪律。开发人员不再需要争论 API 的路径应该怎么设计,某个字段应该是用下划线还是驼峰命名。一切都由.proto文件说了算。你编译它,然后使用它,而不是去“解读”它。

除了规范性,grpc 带来的另一个巨大好处是性能。这主要归功于 Protobuf(Protocol Buffers)和 HTTP/2 这两项核心技术。

Protobuf 是 gRPC 默认使用的数据序列化格式。与 JSON 不同,它是一种二进制格式,因此极其紧凑。 这意味着同样的数据,用 Protobuf 表示比用 JSON 表示的体积要小得多。更小的数据体积,意味着更少的网络传输时间和更低的网络带宽消耗。

当这种高效的二进制格式与现代的 HTTP/2 协议结合时,性能提升是惊人的。HTTP/2 支持多路复用等特性,允许在单个 TCP 连接上同时处理多个请求和响应,大大减少了连接建立的开销。

最终反映到我们的实际业务上,就是许多服务调用的响应时间从原先的 200 毫秒,降低到了 35 毫秒甚至更少。

空谈不如实干。接下来,我将展示如何从零开始,用 Python 快速构建一个可以工作的 gRPC 服务。这个过程没有任何复杂的框架和“黑魔法”。

第一步:安装必要的 Python 库

你需要安装两个核心库:grpcio是 gRPC 的 Python 运行时,grpcio-tools则包含了根据.proto文件生成代码的编译器。

pip install grpcio grpcio-tools

第二步:定义服务契约(hello.proto)

创建一个名为hello.proto的文件。这是我们服务的核心定义。

syntax = "proto3";// 定义一个名为 Greeter 的服务service Greeter { // 定义一个名为 SayHello 的远程调用方法 (RPC) // 它接收一个 Hellorequest 类型的消息,并返回一个 HelloReply 类型的消息 rpc SayHello (HelloRequest) returns (HelloReply);}// 定义请求消息的结构message HelloRequest { string name = 1; // 一个名为 name 的字符串字段,字段编号为 1}// 定义响应消息的结构message HelloReply { string message = 1; // 一个名为 message 的字符串字段,字段编号为 1}

这个文件非常简洁易读,它清晰地定义了服务、方法以及输入输出的数据结构。

第三步:编译.proto 文件生成 Python 代码

在终端中运行以下命令,将.proto文件编译成 Python 代码。

python -m grpc_tools.protoc -I. --python_out=. --grpc_python_out=. hello.proto

执行成功后,你会发现当前目录下自动生成了两个文件:

hello_pb2.py:包含了我们定义的消息类(HelloRequest和HelloReply)。hello_pb2_grpc.py:包含了服务的存根(stub),供客户端和服务器使用。

第四步:编写 gRPC 服务端代码

现在,我们来创建server.py文件,实现我们在.proto中定义的服务。

from concurrent import futuresimport grpcimport hello_pb2import hello_pb2_grpc# 创建一个类,继承自生成的 GreeterServicer 类class Greeter(hello_pb2_grpc.GreeterServicer): # 实现 .proto 文件中定义的 SayHello 方法 def SayHello(self, request, context): # request 参数就是 HelloRequest 对象 # 返回一个 HelloReply 对象 return hello_pb2.HelloReply(message=f"Hello, {request.name}")def run_server: # 创建一个 gRPC 服务器,使用线程池处理请求 server = grpc.server(futures.ThreadPoolExecutor(max_workers=5)) # 将我们的服务实现注册到服务器中 hello_pb2_grpc.add_GreeterServicer_to_server(Greeter, server) # 监听 50051 端口,这里使用非安全模式 server.add_insecure_port('[::]:50051') # 启动服务器 server.start print("Server is running on port 50051") # 等待服务器终止 server.wait_for_terminationif __name__ == '__main__': run_server

第五步:编写 gRPC 客户端代码

接着,创建client.py文件,用于调用服务。

import grpcimport hello_pb2import hello_pb2_grpcdef run_client: # 创建一个到服务器的通道(channel),指定地址和端口 channel = grpc.insecure_channel('localhost:50051') # 使用通道创建一个客户端存根(stub) stub = hello_pb2_grpc.GreeterStub(channel) # 调用远程方法,就像调用本地方法一样 # 传入一个 HelloRequest 对象 response = stub.SayHello(hello_pb2.HelloRequest(name='Reader')) # 打印收到的响应消息 print("Received:", response.message)if __name__ == '__main__': run_client

打开两个终端窗口。

在第一个终端中,启动服务器:

python server.py

你会看到输出 Server is running on port 50051。

在第二个终端中,运行客户端:

python client.pyReceived: Hello, Reader

至此,你已经成功地端到端构建并运行了一个基于 gRPC 的 Python 服务。

引入 gRPC 后,除了性能提升和规范统一,我们还收获了一些意料之外的好处:

调试出乎意料地简单:原以为调试二进制的 Protobuf 会很困难,但实际上因为字段都是预先编译和强类型的,反而更容易了。你不会再遇到因为字段名拼写错误或数据类型不匹配导致的低级 bug。版本迭代更平滑:.proto文件支持将字段标记为optional(可选)或deprecated(已弃用),这让内部 API 的版本管理变得异常轻松。你可以添加新字段而不破坏旧服务的正常运行。持续集成(CI)流水线更高效:很多服务间的“契约破坏”问题,在代码编译阶段就能被发现,而不是等到运行时才暴露。这让我们的 CI 流程变得更快、更可靠。解锁了新的通信模式:gRPC 对流(Streaming)的内置支持,为我们探索新的数据交互模式打开了大门,尽管我们还在学习这部分内容。

而最大的胜利,莫过于我们的团队再也不用为请求和响应的数据格式应该长什么样而争论不休了。 答案永远只有一个:去看.proto文件。

gRPC 是解决所有问题的“银弹”吗?当然不是。

如果你的 API 是直接面向外部用户或浏览器的,那么 REST 可能仍然是更好的选择。

但是,如果你的场景是服务与服务之间的内部通信,并且你对性能、规范性和长期可维护性有较高的要求,那么 gRPC 绝对值得你投入时间去研究。 特别是对于 Python 开发者来说,它的同步编程模型非常自然,开发体验也相当稳定和可预测。

我的建议是:从构建一个简单的服务开始,运行它,连接它,然后在此基础上逐步扩展。 自从引入 gRPC 后,我编写 API 文档的方式也彻底改变了——因为我的 API 文档,就是那份.proto文件,它自己“写”好了自己。

来源:高效码农

相关推荐