每日最新頭條.有趣資訊

如何將代碼部署時間減少 95%?

作者丨Evan Limanto

譯者丨平川

策劃丨趙鈺瑩

本文作者所在的公司 Plaid 是一家金融科技公司,該公司搭建了一個技術平台,使應用程序能夠與用戶的銀行账戶建立聯繫。隨著公司的發展,基礎設施規模在不斷擴大。目前,這家公司運行著 20 多個內部服務,每天在核心服務上部署 50 多個代碼提交。因此,最小化部署時間對於最大化迭代速度至關重要,一個快速的部署過程能夠迅速進行 Bug 修複並運行平穩的連續部署系統。

幾個月前,我們注意到,銀行集成衣務部署緩慢正在影響團隊發布代碼的能力。工程師要花至少 30 分鐘才能通過多個過渡環境和生產環境構建、部署和監視變更,這將消耗大量寶貴的工程時間。隨著團隊越來越大,我們每天發布的代碼也越來越多,這一點變得越來越不可接受。

雖然我們計劃實現長期改進,比如將基於 Amazon ECS 服務的基礎設施遷移到 Kubernetes 上,但是,為了在短期內提高迭代速度,有必要快速解決下這個問題。因此,我們決定實踐自定義的“快速部署”機制。

1

Amazon ECS 部署的高延遲

我們的銀行集成衣務由 4000 個 Node.js 進程組成,這些進程運行在專用的 Docker 容器上,這些容器託管並部署在 Amazon 的容器編排服務 ECS 上。在分析了我們的部署過程之後,我們將增加的部署延遲歸結到三個不同的組件上:

啟動任務會導致延遲。除了應用程序啟動時間之外,ECS 健康檢查也會導致延遲,它決定容器何時準備好開始處理流量。控制這個過程的三個參數是 interval、retry 和 startPeriod。如果沒有對健康檢查進行仔細調優,容器可能會卡在“啟動”狀態,即使它們已經準備好為流量服務。

關閉任務會導致延遲。當我們運行 ECS 服務更新時,一個 SIGTERM 信號被發送到所有正在運行的容器。為了處理這個問題,我們在應用程序代碼中使用了一些邏輯,以便在完全關閉服務之前佔用現有資源。

我們啟動任務的速度限制了部署的並行性。儘管我們將 MaximumPercent 參數設置為 200%,但是 ECS start-taskAPI 調用的硬限制是每個調用只能執行 10 個任務,而且速度有限。我們需要調用 400 次才能將所有容器投入生產。

2

方法探索

我們考慮並試驗了一些不同的潛在解決方案,以逐步實現總體目標:

減少生產中運行的容器總數。這當然是可行的,但它涉及到對服務架構進行重大修改,以使其能夠處理相同的請求吞吐量,在進行這樣的修改之前,還需要進行更多研究。

通過修改健康檢查參數來調整 ECS 配置。我們嘗試通過減少 interval 和 startPeriod 的值來加強健康檢查,但是 ECS 在啟動時將健康的容器錯誤地標記為不健康,導致我們的服務永遠無法完全穩定在 100% 健康狀態。由於根本問題(ECS 部署緩慢)依然存在,對這些參數進行迭代是一個緩慢而費力的過程。

在 ECS 集群中啟動更多實例,以便可以在部署期間同時啟動更多任務。這樣做可以減少部署時間,但不會減少太多。從長遠來看,這也不劃算。

通過重構初始化和關機邏輯優化服務重啟時間。只需要做一些小小的修改,我們就能夠在每個容器中節省大約 5 秒的時間。

儘管這些更改將總體部署時間減少了幾分鐘,但是我們仍然需要將時間提高至少一個數量級,才能認為問題已解決。這將需要一個根本不同的解決方案。

3

初步解決方案:利用 Node Require Cache“熱重載”應用程序代碼

Node require cache 是一個 JavaScript 對象,它根據需要緩存模塊。這意味著多次執行 require(‘foo’) 或 import * as foo from 'foo’只會在第一次時請求 foo 模塊。神奇的是,刪除 require cache 中的條目(我們可以使用全局 require.cache 對象訪問)將迫使 Node 在下次導入模塊時從磁盤重新讀取該模塊。

為了繞過 ECS 部署過程,我們嘗試使用 Node 的 require cache 在運行時執行應用程序代碼的“熱重載”。一旦接收到外部觸發(我們將其實現為銀行集成衣務上的 gRPC 端點),應用程序將下載新代碼來替換現有的構建,清除 require cache,從而強製重新導入所有相關模塊。通過這種方法,我們能夠消除 ECS 部署中存在的大部分延遲,優化整個部署過程。

在 Plaiderdays (我們的內部黑客馬拉松)期間,來自不同團隊的一組工程師聚在一起,為我們所謂的“快速部署”實現了一個端到端的概念驗證。當我們一起設法構建一個原型時,有一件事似乎出了問題:如果下載新構建的 Node 代碼也試圖使失效緩存,那麽下載器代碼本身將如何重新加載就不清楚了。(有一種方法可以解決這個問題,就是使用 Node EventEmitter ,但是會給代碼增加相當大的複雜性)。更重要的是,還存在運行未同步代碼版本的風險,這可能導致應用程序意外失敗。

由於我們不願意在銀行集成衣務的可靠性上妥協,這種複雜性需要重新考慮“熱重載”方法。

4

最終解決方案:重新加載進程

在過去,為了在所有服務中運行一系列統一的初始化任務,我們編寫了自己的進程封裝器,它的名稱非常貼切,叫做 Bootloader。Bootloader 的核心包含設置日誌管道、轉發信號和讀取 ECS 元數據的邏輯。每個服務都是通過將應用程序可執行文件的路徑以及一系列標誌傳遞給 Bootloader 來啟動的,這些文件在執行初始化步驟之後會作為子進程執行。

我們沒有清除 Node 的 require cache,而是在下載預期的部署構建後,使用特殊的退出代碼來調用 process.exit 實現服務更新。我們還在 Bootloader 中實現了自定義邏輯,以觸發使用此代碼退出的任何子進程的進程重載。與“熱重載”方法類似,這使我們能夠繞過 ECS 部署的成本並快速引導新代碼,同時避免“熱重載”的陷阱。此外,Bootloader 層的這種“快速部署”邏輯允許我們將其推廣到在 Plaid 運行的任何其他服務。

下面是最終解決方案:

Jenkins 部署管道向銀行集成衣務的所有實例發送 RPC 請求,指示它們“快速部署”特定的提交散列。

應用程序接收 gRPC 請求進行快速部署,並根據接收到的提交散列從 Amazon S3 下載構建好的壓縮包。然後,它替換文件系統上的現有構建,並使用 Bootloader 識別的特殊退出代碼退出。

Bootloader 看到應用程序使用這個特殊的“Reload”退出代碼退出,然後重新啟動應用程序。

服務運行新的代碼。

下面這張圖簡單說明了這個過程。

5

結果

我們能夠在 3 周內交付這個“快速部署”項目,並將 90% 生產容器的部署時間從 30 多分鐘減少到 1.5 分鐘。

上圖顯示了我們為銀行集成衣務部署的容器數量(按提交表示為不同的顏色)。如果注意下黃線,就可以看到它在 12:15 左右增長趨於平穩,這代表我們的容器長尾仍然在佔用資源。

這個項目極大提高了 Plaid 集成工作的速度,允許我們更快地發布特性及進行 Bug 修複,並將浪費在上下文切換和監視儀表板上的工程時間最小化。這也證明了我們的工程文化,即通過黑客馬拉松得來的想法實現具有實質性影響的項目。

https://blog.plaid.com/how-we-reduced-deployment-times-by-95/

點個在看少個 bug

獲得更多的PTT最新消息
按讚加入粉絲團