性能最佳实践
一份包含通用和特定语言最佳实践的用户指南,旨在提高性能。
性能最佳实践
通用
尽可能复用 Stub 和 Channel。
使用 Keepalive 探测包以在非活动期间保持 HTTP/2 连接活跃,从而使首次 RPC 能够快速发出而没有延迟(即 C++ Channel 参数 GRPC_ARG_KEEPALIVE_TIME_MS)。
在处理客户端到服务器、服务器到客户端或双向的长期逻辑数据流时,使用流式 RPC。流可以避免持续的 RPC 初始化,这包括客户端的连接负载均衡、在传输层启动新的 HTTP/2 请求以及在服务器端调用用户定义的方法处理程序。
然而,流一旦启动就无法进行负载均衡,并且难以调试流失败问题。它们可能在小规模下提高性能,但由于负载均衡和复杂性问题,可能会降低可伸缩性,因此只有在它们为应用逻辑带来显著的性能或简洁性优势时才应使用。使用流是为了优化应用程序,而不是 gRPC。
旁注: 这不适用于 Python(详情请参阅 Python 部分)。
(专题) 每个 gRPC Channel 使用 0 个或多个 HTTP/2 连接,每个连接通常对并发流的数量有限制。当连接上的活跃 RPC 数量达到此限制时,额外的 RPC 会在客户端排队,必须等待活跃 RPC 完成后才能发送。高负载或长期流式 RPC 的应用程序可能会因此排队而出现性能问题。有两种可能的解决方案:
为应用程序中每个高负载区域创建单独的 Channel。
使用 gRPC Channel 池将 RPC 分布到多个连接上(Channel 必须具有不同的 Channel 参数以防止重复使用,因此请定义一个特定用途的 Channel 参数,例如 Channel 编号)。
旁注: gRPC 团队计划添加一个功能来解决这些性能问题(详情请参阅 grpc/grpc#21386),因此涉及创建多个 Channel 的任何解决方案都是一个临时权宜之计,最终将不再需要。
C++
对于性能敏感的服务器,不要使用同步 API。如果性能和/或资源消耗不是问题,可以使用同步 API,因为它是实现低 QPS 服务的ost简单的 API。
对于大多数 RPC,优先使用回调 API 而非其他 API,前提是应用程序可以避免所有阻塞操作,或者阻塞操作可以移到单独的线程中。回调 API 比完成队列异步 API 更易于使用,但目前对于真正的高 QPS 工作负载来说速度较慢。
如果必须使用异步完成队列 API,最佳可伸缩性权衡是拥有
numcpu
个线程。 完成队列数量与线程数量的理想比例可能会随时间变化(随着 gRPC C++ 的发展),但截至 gRPC 1.41(2021 年 9 月),每个完成队列使用 2 个线程似乎能获得最佳性能。对于异步完成队列 API,请确保注册足够的服务器请求以达到期望的并发级别,避免服务器持续陷入慢路径导致本质上是串行请求处理。
(专题) gRPC::GenericStub 在某些情况下非常有用,特别是当存在高争用/CPU 时间花费在 proto 序列化上时。此类允许应用程序直接发送原始 gRPC::ByteBuffer 作为数据,而不是从某个 proto 进行序列化。如果相同的数据需要发送多次,先进行一次明确的 proto 到 ByteBuffer 序列化,然后发送多个 ByteBuffer,这种方式也很有帮助。
Java
使用非阻塞 Stub 来并行化 RPC。
根据您的工作负载提供一个限制线程数量的自定义执行器(如 cached(默认)、fixed、forkjoin 等)。
Python
流式 RPC 会创建额外的线程用于接收和可能发送消息,这使得 gRPC Python 中的流式 RPC 比一元 RPC 慢得多,这与其他 gRPC 支持的语言不同。
使用 asyncio 可以提高性能。
在同步栈中使用 Future API 会导致额外线程的创建。如果可能,请避免使用 Future API。
(实验性) 通过 SingleThreadedUnaryStream Channel 选项 提供了一种实验性的单线程一元流实现,每条消息最多可节省 7% 的延迟。