幾個月前,我們留意到,銀行集成服務部署緩慢正在影響團隊發(fā)布代碼的能力。工程師要花至少 30 分鐘才能通過多個過渡環(huán)境和生產(chǎn)環(huán)境構建、部署和監(jiān)視變更,這將消耗大量寶貴的工程時間。隨著團隊越來越大,我們天天發(fā)布的代碼也越來越多,這一點變得越來越不可接受。
固然我們計劃實現(xiàn)長期改進,比如將基于 Amazon ECS 服務的基礎設施遷移到 Kubernetes 上,但是,為了在短期內(nèi)進步迭代速度,有必要快速解決下這個題目。因此,我們決定實踐自定義的“快速部署”機制。
我們的銀行集成服務由 4000 個 Node.js 進程組成,這些進程運行在專用的 Docker 收留器上,這些收留器托管并部署在 Amazon 的收留器編排服務 ECS 上。在分析了我們的部署過程之后,我們將增加的部署延遲回結(jié)到三個不同的組件上:
啟動任務會導致延遲。除了應用程序啟動時間之外,ECS 健康檢查也會導致延遲,它決定收留器何時預備好開始處理流量??刂七@個過程的三個參數(shù)是 interval、retry 和 螺螄粉 startPeriod。假如沒有對健康檢查進行仔細調(diào)優(yōu),收留器可能會卡在“啟動”狀態(tài),即使它們已經(jīng)預備好為流量服務。封閉任務會導致延遲。當我們運行 ECS 服務更新時,一個 SIGTERM 信號被發(fā)送到所有正在運行的收留器。為了處理這個題目,我們在應用程序代碼中使用了一些邏輯,以便在完全封閉服務之前占用現(xiàn)有資源。我們啟動任務的速度限制了部署的并行性。盡管我們將 MaximumPercent 參數(shù)設置為 200%,但是 ECS start-taskAPI 調(diào)用的硬限制是每個調(diào)用只能執(zhí)行 10 個任務,千航國際,而且速度有限。我們需要調(diào)用 400 次才能將所有收留器投進生產(chǎn)。
我們考慮并試驗了一些不同的潛伏解決方案,以逐步實現(xiàn)總體目標:
減少生產(chǎn)中運行的收留器總數(shù)。這當然是可行的,但它涉及到對服務架構進行重大修改,以使其能夠處理相同的請求吞吐量,在進行這樣的修改之前,還需要進行更多研究。通過修改健康檢查參數(shù)來調(diào)整 ECS 配置。我們嘗試通過減少 interval 和 startPeriod 的值來加強健康檢查,但是 ECS 在啟動時將健康的收留器錯誤地標記為不健康,導致我們的服務永遠無法完全穩(wěn)定在 100% 健康狀態(tài)。由于根本題目(ECS 部署緩慢)依然存在,對這些參數(shù)進行迭代是一個緩慢而費力的過程。在 ECS 集群中啟動更多實例,以便可以在部署期間同時啟動更多任務。這樣做可以減少部署時間,但不會減少太多。從長遠來看,這也不劃算。通過重構初始化和關機邏輯優(yōu)化服務重啟時間。只需要做一些小小的修改,我們就能夠在每個收留器中節(jié)省大約 5 秒的時間。
盡管這些更改將總體部署時間減少了幾分鐘,但是我們?nèi)匀恍枰獙r間進步至少一個數(shù)目級,才能以為題目已解決。這將需要一個根本不同的解決方案。
Node require cache 是一個 JavaScript 對象,它根據(jù)需要緩存模塊。這意味著多次執(zhí)行 require(‘foo’) 或 import * as 螺螄粉 foo from 'foo’只會在第一次時請求 foo 模塊。神奇的是,刪除 require cache 中的條目(我們可以使用全局 require.cache 對象訪問)將迫使 Node 在下次導進模塊時從磁盤重新讀取該模塊。
為了繞過 ECS 部署過程,我們嘗試使用 Node 的 require cache 在運行時執(zhí)行應用程序代碼的“熱重載”。一旦接收到外部觸發(fā)(我們將實在現(xiàn)為銀行集成服務上的 gRPC 端點),應用程序?qū)⑾螺d新代碼來替換現(xiàn)有的構建,清除 require cache,從而強制重新導進所有相關模塊。通過這種方法,我們能夠消除 ECS 部署中存在的大部分延遲,優(yōu)化整個部署過程。
在 Plaiderdays (我們的內(nèi)部黑客馬拉松)期間,來自不同團隊的一組工程師聚在一起,為我們所謂的“快速部署”實現(xiàn)了一個端到真?zhèn)€概念驗證。當我們一起想法構建一個原型時,有一件事似乎出了題目:假如下載新構建的 Node 代碼也試圖使失效緩存,跨境鐵路 國際物流,那么下載器代碼本身將如何重新加載就不清楚了。(有一種方法可以解決這個題目,就是使用 Node EventEmitter ,但是會給代碼增加相當大的復雜性)。更重要的是,千航國際,還存在運行未同步代碼版本的風險,這可能導致應用程序意外失敗。
由于我們不愿意在銀行集成服務的可靠性上妥協(xié),這種復雜性需要重新考慮“熱重載”方法。
在過往,為了在所有服務中運行一系列同一的初始化任務,我們編寫了自己的進程封裝器,它的名稱非常貼切,叫做 Bootloader。Bootloader 的核心包含設置日志管道、轉(zhuǎn)發(fā)信號和讀取 ECS 元數(shù)據(jù)的邏輯。每個服務都是通過將應用程序可執(zhí)行文件的路徑以及一系列標志傳遞給 Bootloader 來啟動的,這些文件在執(zhí)行初始化步驟之后會作為子進程執(zhí)行。
我們沒有清除 Node 的 require cache,而是在下載預期的部署構建后,使用特殊的退出代碼來調(diào)用 process.exit 實現(xiàn)服務更新。我們還在 Bootloader 中實現(xiàn)了自定義邏輯,以觸發(fā)使用此代碼退出的任何子進程的進程重載。與“熱重載”方法類似,這使我們能夠繞過 ECS 部署的本錢并快速引導新代碼,同時避免“熱重載”的陷阱。此外,Bootloader 層的這種“快速部署”邏輯答應我們將其推廣到在 Plaid 運行的任何其他服務。
下面是終極解決方案:
Jenkins 部署管道向銀行集成服務的所有實例發(fā)送 RPC 請求,指示它們“快速部署”特定的提交散列。應用程序接收 gRPC 請求進行快速部署,并根據(jù)接收到的提交散列從 Amazon S3 下載構建好的壓縮包。然后,它替換文件系統(tǒng)上的現(xiàn)有構建,并使用 Bootloader 識別的特殊退出代碼退出。Bootloader 看到應用程序使用這個特殊的“Reload”退出代碼退出,然后重新啟動應用程序。服務運行新的代碼。
下面這張圖簡單說明了這個過程。
結(jié)果
我們能夠在 3 周內(nèi)交付這個“快速部署”項目,并將 90% 生產(chǎn)收留器的部署時間從 30 多分鐘減少到 1.5 分鐘。
上圖顯示了我們?yōu)殂y行集成服務部署的收留器數(shù)目(按提交表示為不同的顏色)。假如留意下黃線,就可以看到它在 12:15 左右增長趨于平穩(wěn),這代表我們的收留器長尾仍然在占用資源。
這個項目極大進步了 Plaid 集成工作的速度,答應我們更快地發(fā)布特性及進行 Bug 修復,并將浪費在上下文切換和監(jiān)視儀表板上的工程時間最小化。這也證實了我們的工程文化,即通過黑客馬拉松得來的想法實現(xiàn)具有實質(zhì)性影響的項目。
最后,我自己是一名從事了多年開發(fā)的JAVA老程序員,辭職目前在做自己的java私人定制課程,今年年初我花了一個月整理了一份最適合2019年學習的java學習干貨,可以送給每一位喜歡java的小伙伴,想要獲取的可以關注我的頭條號并在后臺私信我:java,即可免費獲取。
本文轉(zhuǎn)載至微信公眾號——InfoQ,如有侵權請聯(lián)系立刪!
鄭重聲明:本文版權歸原作者所有,轉(zhuǎn)載文章僅為傳播更多信息之目的,如作者信息標記有誤,請第一時間聯(lián)系我們修改或刪除,多謝。
千航國際 |
國際空運 |
國際海運 |
國際快遞 |
跨境鐵路 |
多式聯(lián)運 |