RSS

在 GKE 上进行 gRPC 性能基准测试

gRPC 性能基准测试现已迁移到 GKE 上运行,结果相似但灵活性大大提高。

背景

gRPC 性能基准测试中所述,gRPC 性能测试需要一个测试驱动器和工作节点(一个或多个客户端和一个服务器)。每个测试可能有不同的配置,或称为*场景*,这些配置会传递给驱动器,并以 JSON 文件指定。以前,驱动器由持续集成流程运行,而工作节点则在长期运行的 GCE VM 上运行。这导致了几个限制:

  1. 测试按顺序运行,难以并行化,因为它们在(相同的)固定 VM 上运行。

  2. 无法保证 VM 的状态在每次测试开始时都相同。

  3. 运行手动实验需要配置新的 VM,这是一个手动过程;或者重复使用现有 VM,存在与其他用户冲突和 VM 处于未知状态的风险。

在 Kubernetes 上进行基准测试

当前框架的核心是一个自定义控制器,用于管理类型为 LoadTest 的 Kubernetes 资源。在运行负载测试之前,必须将此控制器部署到 Kubernetes 集群。该控制器使用 kubebuilder 实现。控制器的代码存储在 Test Infra 仓库中。有关各个 LoadTest 字段的更多文档,请参阅 LoadTest 实现

LoadTest 配置指定要为测试创建的驱动器、客户端和服务器 pod。将配置应用于集群后(例如,使用 kubectl apply -f),控制器将创建 pod 并运行测试。如果多个配置应用于集群,控制器将在资源可用时创建 pod,从而允许测试并行运行。

示例包括可以直接应用的基本配置,以及需要额外步骤和参数替换的模板。

  • 基本配置依赖于随控制器每个版本捆绑的 clonebuildruntime 工作节点镜像。clone 和 build 镜像用于构建 gRPC 二进制文件,然后传递给 runtime 容器。这些配置适合作为示例和用于一次性测试。

  • 模板配置依赖于在开始测试之前构建的工作节点镜像。这些预构建镜像包含 gRPC 二进制文件,无需在每次测试前进行克隆和构建。模板替换用于指向工作节点镜像的位置。这些配置适合用于对同一 gRPC 版本运行批量测试,或重复运行同一测试。

除了控制器之外,Test Infra 仓库还包含一套工具,包括一个测试运行器以及构建和删除预构建工作节点镜像的工具,还有一个仪表板实现。

与预构建工作节点相关的工具在内部使用 gcloud,因此依赖于 GKE。除此之外,该框架的所有组件都构建在 Kubernetes 本身之上,并且独立于 GKE。也就是说,应该可以在自定义 Kubernetes 集群或另一个云提供商的 Kubernetes 产品上部署控制器并运行测试。

集群设置

运行基准测试作业的集群必须配置好节点池,其规模应与应支持的同时测试数量相匹配。控制器使用 pool 作为各种 pod 类型的节点选择器。工作节点 pod 具有相互的反亲和性,因此每个 pod 需要一个节点。

例如,我们持续集成设置中使用的节点池配置如下:

节点池名称节点数机器类型Kubernetes 标签
system2e2-standard-8default-system-pool:true, pool:system
drivers-ci8e2-standard-2pool:drivers-ci
workers-c2-8core-ci8c2-standard-8pool:workers-c2-8core-ci
workers-c2-30core-ci8c2-standard-30pool:workers-c2-30core-ci

由于我们测试中的每个场景都需要一个驱动器和两个工作节点,此配置支持在 8 核机器上同时运行四个测试,在 30 核机器上同时运行四个测试。驱动器所需的资源很少,并且不具有相互反亲和性。我们发现在双核机器上调度它们,并将节点数设置为所需的驱动器数量,比在更大的共享机器上调度它们更方便,因为这允许驱动器池与工作节点池一起调整大小。控制器本身被调度在 system 节点池中。

除了持续集成中使用的节点池之外,我们的集群还包含额外的节点池,可用于临时测试:

节点池名称节点数机器类型Kubernetes 标签
drivers8e2-standard-8default-driver-pool:true, pool:drivers
workers-8core8e2-standard-8default-worker-pool:true, pool:workers-8core
workers-32core8e2-standard-32pool:workers-32core

一些节点池带有 default-*-pool 标签。这些标签指定如果在 LoadTest 配置中未指定,则使用哪个节点池。使用上述配置,这些测试(例如,示例中指定的测试)将使用 driversworkers-8core 节点池,并且不会干扰持续集成作业。默认标签作为控制器构建的一部分定义:如果未设置这些标签,控制器将仅运行明确指定 pool 标签的测试。

控制器部署

构建和部署控制器的步骤在部署文档中描述。

持续集成

我们的持续集成设置在 gRPC Core 仓库的 gRPC OSS 基准测试 README 中描述。主要的持续集成作业使用脚本 grpc_e2e_performance_gke.sh 来生成链接到gRPC 性能基准测试页面的仪表板上显示的数据。

每次持续集成运行有三个阶段:

  1. 生成测试配置。
  2. 构建并推送工作节点镜像。
  3. 运行测试。

每次持续集成运行使用 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 日志等 artifacts,并(可选地)在每个测试成功完成后删除资源。

测试运行器为需要集群上相同资源(例如 8 核或 30 核工作节点)的测试维护单独的*队列*。属于同一队列的测试配置不会立即全部应用于集群,而是根据为每个队列设置的*并发级别*进行应用。我们的持续集成测试在两个队列中运行(对应于 8 核和 30 核工作节点)。每个队列的并发级别都设置为二。

配置应用于集群后,控制器会创建客户端、驱动器和服务器 pod 以运行测试,监控测试执行,并更新 LoadTest 资源的状态。

测试运行器的设计可解释如下:

  1. 使用测试运行器允许持续集成作业等待所有测试完成,收集测试 artifacts,并准备一份包含结果的报告。

  2. 使用单独的队列(由每个测试配置中的注释指示)允许不需要相同集群资源的测试彼此独立管理。

  3. 使用有限的并发级别减少了同时应用于集群的测试数量。这有几个好处:

    1. 测试运行器的负载降低,因为集群上同时存在的 LoadTest 资源较少,并且运行器会定期轮询这些资源以检查完成状态。我们持续集成中的轮询间隔设置为 5 秒。

    2. 控制器的负载降低,因为它同时需要控制的 LoadTest 资源较少。

    3. 每个测试可以有更短的超时时间,因为控制器启动每个测试所需的时间更容易预测。超时是必要的,用于处理客户端或服务器 pod 挂起并阻止测试完成的错误情况。这些情况很少发生,但可能累积并消耗集群资源,阻止其他测试运行。我们持续集成中的测试超时时间为 15 分钟。

    4. 并发级别可以设置为低于集群容量,允许用户在不阻止其他用户同时运行测试的情况下运行批量测试。

  4. 测试成功完成后(并在收集结果和日志后)删除每个测试的选项提供了对每个测试生命周期的更好控制。

    1. 控制器的默认行为是保留 LoadTest 资源及其关联的 pod 在集群上,直到达到设定的 TTL,然后将其删除。我们的持续集成为每个测试指定了 24 小时的 TTL。

    2. 属于已完成 LoadTest 的 pod 处于终止状态,因此不消耗集群资源。但是,终止的 pod 随时可能被垃圾回收。

    3. 如果我们将所有已完成测试的 pod 保留在我们的持续集成集群中,我们发现它们会在一小时内被垃圾回收。

    4. 如果删除成功完成的测试的 LoadTest 资源,则关联的 pod 也会被删除。在这种情况下,属于*未成功*测试的 pod(数量很少,可能对调试有用)将保留在集群上,直到达到 24 小时的 TTL。

仪表板

持续集成的测试结果保存到BigQuery。然后将存储在 BigQuery 中的数据复制到 Postgres 数据库,以便在仪表板上进行可视化。

仪表板的代码以及主要持续集成仪表板的配置存储在Test Infra 仓库中。这带来了以下好处:

  1. 主要仪表板通过更新存储的配置进行维护,而不是直接在 UI 中更新。

  2. 用户可以部署自己的仪表板,使用自己的配置。

这与先前基准测试的仪表板(使用 Perfkit Explorer 构建)形成对比,后者是通过直接在 UI 中更新来维护的,并且用户不容易复制。

有关详细信息,请参阅仪表板实现

Dashboard snapshot

结果

关于在 GKE 上进行 gRPC 基准测试的结果和用户体验,可以得出以下观察结论:

  1. 性能指标(延迟、QPS 等)产生与在 GCE 上旧基准测试相同或更好的结果。

  2. 在我们的基准测试集群中,GKE 中每个测试的 pod 创建和删除开销很小(少于 15 秒)。

  3. 测试镜像已被 Docker 化,并且每个测试都会重新启动,这带来以下好处:

    1. 结果更加一致。

    2. 运行时错误很少见。

    3. 系统被分解为定义明确的组件,从而简化了升级。

    4. 测试易于并行化,从而加快了执行时间。

    5. 实验更容易进行。

实验中获得的最佳实践和见解示例:

  1. 对客户端和服务器使用 c2 实例(实例类型对观察到的延迟及其方差以及测量的吞吐量影响很大)。

  2. GKE pod 到 pod 网络相对于原始 GCE 网络只有很小的开销。您可以通过为基准测试 pod 设置 hostnetworking:true 来获得原始 GCE 网络性能。

  3. 对于 Docker 下的 Java,JVM 可能无法自动检测可用处理器数量。这可能导致非常悲观的结果,因为 gRPC 使用检测到的处理器数量来调整事件处理线程池的大小。一个解决方法是明确设置处理器数量。此解决方法在此实现。

自行运行

Test Infra 仓库中的代码允许任何用户创建集群、部署控制器、运行 gRPC 基准测试,并在其自己的仪表板上显示结果。如果你对性能感兴趣并运行自己的基准测试,请告诉我们!