GKE 上的 gRPC 性能基准测试
gRPC 性能基准测试现在已转移到在 GKE 上运行,结果相似,但灵活性大大提高。
背景
gRPC 性能测试需要一个测试驱动程序和工作器(一个或多个客户端和一个服务器),如gRPC 性能基准测试中所述。每个测试可能具有不同的配置或场景,该配置将传递给驱动程序并指定为 JSON 文件。以前,驱动程序由持续集成过程运行,而工作器则在长期存在的 GCE VM 上运行。这导致了以下几个限制:
测试按顺序运行,并且难以并行化,因为它们在(相同的)固定 VM 上运行。
无法保证每次测试开始时 VM 的状态都相同。
运行手动实验需要配置新的 VM,这是一个手动过程,或者重用现有 VM,但存在与其他用户冲突以及 VM 处于未知状态的风险。
在 Kubernetes 上进行基准测试
当前框架的核心是一个自定义控制器,用于管理类型为LoadTest的 Kubernetes 资源。必须先将此控制器部署到 Kubernetes 集群,然后才能在其上运行负载测试。该控制器使用kubebuilder实现。控制器的代码存储在测试基础架构存储库中。有关单个 LoadTest 字段的更多文档,请参见LoadTest 实现。
LoadTest 配置指定为测试创建的驱动程序、客户端和服务器 Pod。一旦将配置应用于集群(例如,使用kubectl apply -f
),控制器将创建 Pod 并运行测试。如果将多个配置应用于集群,则只要有可用资源,控制器就会创建 Pod,从而允许测试并行运行。
示例包括可以直接应用的基本配置和需要其他步骤和参数替换的模板。
基本配置依赖于与控制器的每个版本捆绑在一起的克隆、构建和运行时工作器镜像。克隆和构建镜像用于构建传递给运行时容器的 gRPC 二进制文件。这些配置适合作为示例和一次性测试。
模板配置依赖于在开始测试之前构建的工作器镜像。这些预构建镜像包含 gRPC 二进制文件,无需在每次测试之前克隆和构建。模板替换用于指向工作器镜像的位置。这些配置适合在同一 gRPC 版本上运行一批测试,或重复运行同一测试。
除了控制器之外,测试基础架构存储库还包含一组工具,包括测试运行器和用于构建和删除预构建工作器镜像的工具,以及仪表板实现。
与预构建工作器相关的工具在内部使用gcloud
,并且依赖于 GKE。除此之外,框架的所有组件都构建在 Kubernetes 本身之上,并且独立于 GKE。也就是说,应该可以在自定义 Kubernetes 集群或另一个云提供商的 Kubernetes 产品上部署控制器并运行测试。
集群设置
运行基准测试作业的集群必须配置节点池,以适应它应支持的并发测试数量。控制器使用pool
作为各种 Pod 类型的节点选择器。工作器 Pod 具有相互反亲和性,因此每个 Pod 需要一个节点。
例如,我们的持续集成设置中使用的节点池配置如下:
池名称 | 节点数量 | 机器类型 | Kubernetes 标签 |
---|---|---|---|
system | 2 | e2-standard-8 | default-system-pool:true, pool:system |
drivers-ci | 8 | e2-standard-2 | pool:drivers-ci |
workers-c2-8core-ci | 8 | c2-standard-8 | pool:workers-c2-8core-ci |
workers-c2-30core-ci | 8 | c2-standard-30 | pool:workers-c2-30core-ci |
由于我们测试中的每个场景都需要一个驱动程序和两个工作程序,因此此配置支持在 8 核机器上同时进行四个测试,在 30 核机器上同时进行四个测试。驱动程序需要的资源很少,并且没有相互反亲和性。我们发现将它们安排在双核机器上,并将节点数设置为所需的驱动程序数量,而不是在更大的共享机器上,这样做很方便,因为这样可以使驱动程序池的大小与工作程序池一起调整。控制器本身被安排在system
池中。
除了持续集成中使用的池之外,我们的集群还包含可用于临时测试的其他节点池
池名称 | 节点数量 | 机器类型 | Kubernetes 标签 |
---|---|---|---|
drivers | 8 | e2-standard-8 | default-driver-pool:true, pool:drivers |
workers-8core | 8 | e2-standard-8 | default-worker-pool:true, pool:workers-8core |
workers-32core | 8 | e2-standard-32 | pool:workers-32core |
一些池被标记为 default-*-pool
标签。这些标签指定如果 LoadTest 配置中未指定要使用的池。使用上面的配置,这些测试(例如,示例中指定的测试)将使用 drivers
和 workers-8core
池,并且不会干扰持续集成作业。默认标签在控制器构建过程中定义:如果未设置这些标签,控制器将仅运行显式指定了 pool
标签的测试。
控制器部署
构建和部署控制器的步骤在部署文档中有描述。
持续集成
我们的持续集成设置在 gRPC OSS 基准测试 README 中描述,该文档位于 gRPC Core 存储库中。主要的持续集成作业使用脚本 grpc_e2e_performance_gke.sh 来生成在链接到 gRPC 性能基准测试页面的仪表板上显示的数据。
每个持续集成运行都有三个阶段
- 生成测试配置。
- 构建和推送工作程序镜像。
- 运行测试。
每次持续集成运行都会使用 8 核工作程序池执行 122 个测试,使用 30 核工作程序池执行 98 个测试。每个测试都运行一个测试场景。使用 C++、C#、Java 和 Python 工作程序的测试在两个池中运行。使用 Node.js、PHP 和 Ruby 工作程序的测试仅在 8 核池中运行。所有这些组合的配置生成所需的时间可以忽略不计(约 1 秒)。
持续集成中使用的配置需要包含要测试的 gRPC 二进制文件的工作程序镜像。这些镜像仅依赖于工作程序的语言,因此这些预构建镜像会提前构建并推送到镜像存储库。此过程大约需要 20 分钟。
测试运行器管理将测试应用于集群的速度,收集测试结果和日志,并在测试成功完成后删除测试。每个池一次允许运行两个测试。此阶段大约需要 50 分钟才能完成。
每个测试场景都配置为运行 30 秒,加上 5 秒的预热期(Java 为 15 秒)。这为运行每个测试所需的时间设置了一个下限。在 8 核池中运行 122 个测试(其中 16 个在 Java 中),一次运行两个测试的观察到的运行时间表明,pod 创建和删除引入的开销很小,每个测试约为 12.8 秒。
配置生成
由于我们正在运行数百个主要共享相同组件(各种语言的驱动程序和工作程序)的测试,因此有必要生成包含重复的驱动程序和工作程序配置并且仅在被测场景中不同的配置。此外,每个配置都必须具有唯一的名称,因为这是应用于 Kubernetes 集群的资源的要求。
我们通过一个工具生成负载测试配置来处理这些问题。该工具存储在gRPC Core 存储库中,测试场景也在其中定义。
预构建镜像
为持续集成生成的配置使用一组预构建镜像。这些镜像在运行测试之前构建并推送到镜像存储库。镜像在每次测试运行结束时被删除。
有关用于准备和删除镜像的工具的详细信息,请参阅将预构建镜像与 gRPC OSS 基准测试一起使用。
测试运行器
测试运行器获取之前生成的测试配置,将每个配置应用于集群,轮询每个 LoadTest 资源以完成,收集结果和 pod 日志等工件,并且(可选)在每个测试成功完成后删除资源。
测试运行器维护单独的队列,用于需要在集群上使用相同资源的测试(例如 8 核或 30 核工作程序节点)。不属于同一队列的测试配置不会一次应用于集群,而是根据为每个队列设置的并发级别应用。我们的持续集成测试在两个队列中运行(分别对应于 8 核和 30 核工作程序节点)。每个队列的并发级别设置为 2。
一旦配置应用于集群,控制器将创建客户端、驱动程序和服务程序 pod 来运行测试,监视测试执行,并更新 LoadTest 资源的状态。
测试运行器的设计可以解释如下:
使用测试运行器允许持续集成作业等待所有测试完成,收集测试工件并准备包含结果的报告。
使用单独的队列(由每个测试配置中的注释指示)允许独立管理不需要相同集群资源的测试。
使用有限的并发级别减少了同时应用于集群的测试数量。这有几个好处:
测试运行器的负载减少了,因为集群上一次的 LoadTest 资源更少,并且运行器会定期轮询这些资源以完成。我们的持续集成中的轮询间隔设置为 5 秒。
控制器上的负载降低了,因为它需要同时控制的 LoadTest 资源更少。
每个测试可以有更短的超时时间,因为控制器启动每个测试所需的时间更可预测。超时对于处理客户端或服务器 Pod 挂起并阻止测试完成的错误情况是必要的。这些情况虽然很少见,但会累积并消耗集群资源,从而阻止其他测试运行。在我们的持续集成中,测试的超时时间为 15 分钟。
并发级别可以设置得低于集群的容量,允许用户在不阻止其他用户同时运行测试的情况下运行一批测试。
删除每个成功完成的测试(并在收集结果和日志后)的选项可以更好地控制每个测试的生命周期。
控制器的默认行为是将 LoadTest 资源和相关的 Pod 保留在集群上,直到达到设定的 TTL,然后将其删除。我们的持续集成为每个测试指定了 24 小时的 TTL。
属于已完成的 LoadTest 的 Pod 处于终止状态,因此不会消耗集群上的资源。但是,终止的 Pod 可以随时被垃圾回收。
如果我们让属于所有已完成测试的 Pod 留在我们的持续集成集群中,我们会发现它们会在一小时内被垃圾回收。
如果我们删除成功完成的测试的 LoadTest 资源,相关的 Pod 也会被删除。在这种情况下,属于不成功测试的 Pod(数量很少,可能对调试有用)会保留在集群上,直到达到 24 小时的 TTL。
仪表板
持续集成的测试结果将保存到 BigQuery。BigQuery 中存储的数据随后被复制到 Postgres 数据库,以便在仪表板上进行可视化。
仪表板的代码以及主持续集成仪表板的配置存储在 Test Infra 仓库中。这带来了以下好处
主仪表板是通过更新存储的配置来维护的,而不是直接在 UI 中更新。
用户可以使用自己的配置部署自己的仪表板。
这与之前使用 Perfkit Explorer 构建的基准测试仪表板形成对比,该仪表板是通过直接在 UI 中更新来维护的,用户无法轻松复制。
有关详细信息,请参阅 仪表板实现。


结果
可以根据 GKE 上 gRPC 基准测试的结果和用户体验得出以下观察结果
性能指标(延迟、QPS 等)产生了与 GCE 上旧基准测试相同或更好的结果。
在我们的基准测试集群中,GKE 中每次测试的 Pod 创建和删除的开销很小(小于 15 秒)。
测试镜像被 Docker 化并为每个测试重新启动,这带来了几个好处
结果更加一致。
运行时错误很少见。
系统被划分为定义明确的组件,从而简化了升级。
测试可以轻松并行化,从而加快了执行时间。
更容易进行实验。
从实验中得出的最佳实践和见解示例
客户端和服务器使用
c2
实例(实例类型对于观察到的延迟及其差异以及测量的吞吐量非常重要)。GKE Pod 到 Pod 的网络连接只有比原始 GCE 网络连接略小的开销。您可以通过为基准测试 Pod 设置
hostnetworking:true
来获得原始 GCE 网络性能。对于 Docker 下的 Java,JVM 可能无法自动检测可用的处理器数量。这可能会导致非常悲观的结果,因为 gRPC 使用检测到的处理器数量来调整用于处理事件的线程池大小。一种解决方法是显式设置处理器数量。此解决方法在 此处 实现。
运行您自己的测试
Test Infra 仓库中的代码允许任何用户创建集群、部署控制器、运行 gRPC 基准测试并在自己的仪表板上显示结果。如果您对性能感兴趣,并运行自己的基准测试,请 告诉我们!