Yelp天天要运行数百万个测试,确保开发人员提交的代码不会对已有的功能形成破坏。如此巨大规模的测试,他们是怎么作到的呢?如下内容翻译自 Yelp 的技术博客,并已得到翻译受权,查看原文 How Yelp Runs Millions of Tests Every Day 。html
开发速度对于一个公司的成败来讲是相当重要的。咱们老是经过减小测试、部署和监控变动的时间来提高开发效率。为了让开发者可以安全地提交代码,咱们天天经过内部的分布式系统 Seagull 运行了超过数百万个测试。python
Seagull 是什么?git
Seagull 是一个具有容错能力和弹性的分布式系统,咱们用它来并行执行咱们的测试套件。咱们使用了以下技术来构建 Seagull 。算法
挑战docker
在将咱们的单体 Web 应用 yelp-main 的新代码部署到生产环境以前,Yelp 的开发人员针对 yelp-main 的特定版本运行了整个测试套件。开发人员经过触发 seagull-run 做业来运行测试,这个做业将会在咱们的集群上安排调度以便运行测试用例。这里须要考虑两方面的因素。apache
咱们所面临的挑战是如何在分钟级别运行每个 seagull-run 做业,而不是按天来运行,同时还能保持较低的成本。编程
Seagull 的工做原理api
首先,开发人员在控制台触发 seagull-run,它会启动一个 Jenkins 做业,用于编译代码,并生成测试清单。这些测试清单被组合在一块儿,传递给一个调度器,调度器将会在 Seagull 集群上执行测试。最后,测试结果被保存到 Elasticsearch 和 S3 上。安全
1.一个开发人员为某个版本的代码(基于 git 某个分支的 SHA 值)触发了一个 seagull-job,咱们假设 git 分支的名字叫做 test_branch。服务器
2.为 test_branch 生成代码包和测试清单,并上传到 S3 上。
3.Bin Packer 获取测试清单和测试历史时间元数据,用于建立多个包含了测试用例的 bundle。如何进行有效的 bundle 实际上是一个装箱问题,我么使用了以下两种算法来解决这个问题。至于使用哪种算法,由开发人员传给 Seagull 的参数来决定。
线性编程(Linear Programming):若是出现了测试依赖,一个测试须要与同一个 bundle 里的另外一个测试一块儿运行。对于这种状况,咱们将会使用线性编程。线性编程方程式的目标函数和约束定义以下。
主要的约束:
咱们使用了 Pulp 来解开这个方程式。
# 目标函数: problem = LpProblem('Minimize bundles', LpMinimize) problem += lpSum([bundle[i] for i in range(max_bundles)]), 'Objective: Minimize bundles' # 其中的一个约束: for i in range(max_bundles): sum_of_test_durations = 0 for test in all_tests: sum_of_test_durations += test_bundle[test, i] * test_durations[test] problem += (sum_of_test_durations) <= bundle_max_duration * bundle[i], ''
在这里,bundle 和 test_bundle 是 LpVariable 类型,max_bundles 和 bundle_max_duration 是整数。
通常状况下,咱们会在线性编程约束里考虑测试用例的 setup 和 teardown 时长,不过为了简单起见,咱们在这里把它们忽略了。
4.在 Jenkins 服务器上启动一个调度器进程,它将会获取 bundle,而后启动一个 mesos 框架。咱们为每个 seagull-run 建立一个新的调度器。每次运行会生成300多个 bundle,每一个 bundle 大概须要 10 分钟的运行时间。调度器为每个 bundle 建立了一个 mesos 执行器,并被安排在 Seagull 集群上执行,只要 Mesos Master 可以提供可用的资源。
5.在执行器被安排到集群上以后,执行器内部将执行以下几个步骤。
每一个执行器启动一个沙箱,并从 S3 上下载软件包(它们是在第二步时上传到 S3 上的)。测试服务所依赖的 Docker 镜像被下载下来,用于启动 docker 容器(也就是服务)。在全部的容器都运行起来以后,开始执行测试。最后,测试结果和元数据被保存到 Elasticsearch(ES)和 S3 上。咱们使用了内部的代理服务 Apollo 将数据写到 ES 上。
若是你处在一个分布式环境里,那么遭遇主机崩溃是一件不可避免的事情。不过,Seagull 具备容错能力。
例如,假设一个调度器须要调度两个 bundle。Mesos 将代理(A1)的资源分配给调度器。假设调度器认为已经分配到足够的资源,那么两个 bundle 就会被安排在 A1 上。A1 由于某些缘由发生崩溃,那么 Mesos 会让调度器知道 A1 已经崩溃了。调度器的任务管理器决定进行重试,或者直接取消任务。若是进行了重试,当 Mesos 提供了足够的资源时(好比 A2),那么 bundle 就会从新被安排执行。若是任务被取消,调度器会将这些 bundle 的测试用例标记为未执行。
6.Seagull UI 经过 Apollo 从 ES 上获取测试结果,并将它们加载到一个 UI 上,让开发人员能够看到结果。若是测试经过,就能够进行部署!
咱们所谈论的规模是多大?
咱们天天有 300 多个 seagull-run,高峰期每小时有 30 到 40 个。它们为此天天启动超过 200 万个 Docker 镜像。为了应付这些场景,咱们的 Seagull 集群在高峰期须要差很少 1 万个 CPU 核心。
这种规模所带来的挑战
为了保持测试套件的及时性,特别是在高峰时期,咱们须要确保 Seagull 集群里有数百个可用的实例。咱们曾经使用过AWS ASG 和 AWS On-Demand 实例,不过它们对于咱们来讲太昂贵了。
为了下降成本,咱们开始使用一个叫做 FleetMiser 的内部工具来维护 Seagull 集群。FleetMiser 是一个自动扩展引擎,咱们用它基于一些信号来扩展集群,好比当前集群的使用状况、管道里运行的工做负荷数量,等等。它有两个主要的组件。
自动伸缩:几周前的容量数据
FleetMiser 为咱们节省了 80% 的集群成本。而在那以前,咱们的集群部署在 AWS On-Demand 实例上,没法进行自动伸缩。
咱们已经达成了什么样的目标?
Seagull 将测试结果的时间从 2 天下降到 30 分钟,并且减小了大量的运行成本。咱们的开发人员可以自信地提交代码,无需等待数个小时甚至数天来验证他们提交的变动没有形成任何破坏。