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