性能最佳实践
这是一本关于提高性能的用户指南,包含通用最佳实践和语言特定的最佳实践。
通用最佳实践
-
尽可能重用存根(stubs)和通道(channels) 在可能的情况下,重用存根和通道可以减少开销,提高性能。
-
使用 keepalive ping 保持 HTTP/2 连接的活跃状态 在活动期间使用 keepalive ping,可以确保在一段时间的不活跃期后,快速发起初始 RPC(例如,C++ 中的 channel 参数 GRPC_ARG_KEEPALIVE_TIME_MS)。
-
使用流式 RPC 处理长时间的数据流
如果客户端到服务器、服务器到客户端,或者双向的数据流需要持续较长时间,使用流式 RPC 可以避免不断重新发起 RPC。流式 RPC 可以避免客户端进行连接负载均衡、在传输层启动新的 HTTP/2 请求以及在服务器端调用用户定义的方法处理。
然而,流式 RPC 启动后无法进行负载均衡,并且调试流失败可能比较困难。虽然在小规模下,流式 RPC 可以提高性能,但由于负载均衡和复杂性,它可能会降低可扩展性,因此只有在对应用逻辑有显著性能或简化优势时才应使用流式 RPC。
此做法不适用于 Python(详情见 Python 部分)。
-
(专题)每个 gRPC 通道使用 0 或多个 HTTP/2 连接,而每个连接通常对并发流的数量有限制 当连接上的活动 RPC 数量达到此限制时,额外的 RPC 会在客户端排队,必须等到活动 RPC 完成后才能发送。高负载或长时间运行的流式 RPC 可能会因队列阻塞而导致性能问题。为此,有两种可能的解决方案:
-
为应用中的高负载区域创建单独的通道。
-
使用 gRPC 通道池,通过多个连接分发 RPC(通道必须使用不同的通道参数以防止重用,因此可以定义特定用途的通道参数,例如通道编号)。
-
gRPC 团队计划添加一个功能来解决这些性能问题(请参阅 grpc/grpc#21386 了解更多信息),因此任何涉及创建多个通道的解决方案都是一种临时的权宜之计,最终不需要使用。 |
C++ 性能最佳实践
-
不要在性能敏感的服务器中使用同步 API 如果性能和/或资源消耗不是问题,使用同步 API,因为它是最简单的实现方式,适用于低 QPS 服务。
-
优先使用回调 API 在大多数 RPC 中,优先使用回调 API,因为它能避免所有阻塞操作,或将阻塞操作移到单独的线程。回调 API 比异步的 completion-queue API 更易使用,但在真正的高 QPS 工作负载下会稍慢。
-
如果必须使用异步 completion-queue API,最佳的可扩展性折中是使用 numcpu 的线程数 完成队列的理想数量与线程数量之间的关系会随着 gRPC C++ 的发展而变化,但截至 gRPC 1.41(2021 年 9 月),使用每个完成队列 2 个线程似乎能够提供最佳性能。
-
对于异步 completion-queue API,确保注册足够的服务器请求以实现所需的并发水平 这样可以避免服务器一直陷入慢路径,从而导致几乎是串行请求处理。
-
(专题)gRPC::GenericStub 对于高竞争或在 proto 序列化上花费大量 CPU 时间的情况非常有用 该类允许应用程序直接发送原始的 gRPC::ByteBuffer 数据,而不是从 proto 序列化。这也可以帮助在相同数据多次发送时,通过一次显式的 proto 到 ByteBuffer 序列化,后续只发送多个 ByteBuffer。
Java 性能最佳实践
-
使用非阻塞存根并行化 RPC 通过使用非阻塞存根,可以使 RPC 并行执行,提高性能。
-
提供一个自定义执行器,限制线程数,根据工作负载调整(缓存线程池(默认)、固定线程池、forkjoin 等)
通过调整执行器,限制线程数,可以根据具体工作负载优化性能。