Yelp每天要運行數百萬個測試,確保開發人員提交的代碼不會對已有的功能造成破壞。如此巨大規模的測試,他們是怎么做到的呢?以下內容翻譯自 Yelp 的技術博客,并已獲得翻譯授權,查看原文 How Yelp Runs Millions of Tests Every Day 。
開發速度對于一個公司的成敗來說是至關重要的。我們總是通過減少測試、部署和監控變更的時間來提升開發效率。為了讓開發者能夠安全地提交代碼,我們每天通過內部的分布式系統 Seagull 運行了超過數百萬個測試。
Seagull 是什么?
Seagull 是一個具備容錯能力和彈性的分布式系統,我們用它來并行執行我們的測試套件。我們使用了如下技術來構建 Seagull 。
- Apache Mesos(用于管理 Seagull 集群的資源)
- AWS EC2(為 Seagull 和 Jenkins 集群提供實例)
- AWS DynamoDB(保存調度器的元數據)
- Docker(為測試要用到的服務提供隔離)
- Elasticsearch(跟蹤測試的運行時間和集群數據的使用情況)
- Jenkins(構建代碼并運行 Seagull 調度器)
- Kibana 和 SignalFx(用于監控和告警)
- AWS S3(作為測試日志的真實來源)
挑戰
在將我們的單體 Web 應用 yelp-main 的新代碼部署到生產環境之前,Yelp 的開發人員針對 yelp-main 的特定版本運行了整個測試套件。開發人員通過觸發 seagull-run 作業來運行測試,這個作業將會在我們的集群上安排調度以便運行測試用例。這里需要考慮兩方面的因素。
- 性能:每一個 seagull-run 作業都有將近 10 萬個測試用例,如果逐個運行它們需要差不多 2 天的時間。
- 規模:一般情況下,每天會有超過 300 個 seagull-run 作業被觸發,高峰期有 30 到 40 個作業并行運行。
我們所面臨的挑戰是如何在分鐘級別運行每一個 seagull-run 作業,而不是按天來運行,同時還能保持較低的成本。
Seagull 的工作原理
首先,開發人員在控制臺觸發 seagull-run,它會啟動一個 Jenkins 作業,用于編譯代碼,并生成測試清單。這些測試清單被組合在一起,傳遞給一個調度器,調度器將會在 Seagull 集群上執行測試。最后,測試結果被保存到 Elasticsearch 和 S3 上。
1.一個開發人員為某個版本的代碼(基于 git 某個分支的 SHA 值)觸發了一個 seagull-job,我們假設 git 分支的名字叫作 test_branch。
2.為 test_branch 生成代碼包和測試清單,并上傳到 S3 上。
3.Bin Packer 獲取測試清單和測試歷史時間元數據,用于創建多個包含了測試用例的 bundle。如何進行有效的 bundle 其實是一個裝箱問題,我么使用了如下兩種算法來解決這個問題。至于使用哪一種算法,由開發人員傳給 Seagull 的參數來決定。
- 貪婪算法(Greedy Algorithm):測試用例先是按照它們的歷史測試時長來排序,然后我們開始按照 10 分鐘的工作量(測試用例)來填充 bundle。
-
線性編程(Linear Programming):如果出現了測試依賴,一個測試需要與同一個 bundle 里的另一個測試一起運行。對于這種情況,我們將會使用線性編程。線性編程方程式的目標函數和約束定義如下。
- 目標函數:最小化生成 bundle 的數量
-
主要的約束:
- 單個 bundle 的運行時長不超過 10 分鐘
- 每個測試只能存在于一個 bundle 里
- 具有依賴關系的測試需要被放在同一個 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 是一個自動擴展引擎,我們用它基于一些信號來擴展集群,比如當前集群的使用情況、管道里運行的工作負荷數量,等等。它有兩個主要的組件。
- AWS Spot Fleet:AWS 的 Spot 實例比 On-Demand 實例要便宜,Spot Fleet 為使用 Spot 實例提供了簡單易用的接口。
- 自動伸縮:我們對集群的使用是動態變化的,主要集中在太平洋時間 10:00 到 19:00,開發人員的主要工作都集中在這段時間。為了能夠自動伸縮,FleetMiser 使用了集群的當前和歷史數據。每天,Seagull 集群在 1500 個 CPU 核心到 10000 個 CPU 核心之間伸縮。
自動伸縮:幾周前的容量數據
FleetMiser 為我們節省了 80% 的集群成本。而在那之前,我們的集群部署在 AWS On-Demand 實例上,無法進行自動伸縮。
我們已經達成了什么樣的目標?
Seagull 將測試結果的時間從 2 天降低到 30 分鐘,而且減少了大量的運行成本。我們的開發人員能夠自信地提交代碼,無需等待數個小時甚至數天來驗證他們提交的變更沒有造成任何破壞。
文章列表