如何設計一個部署系統 Designing a Code Deployment System

2024年5月5日

💎 加入 E+ 成長計畫 與超過 350+ 位軟體工程師一同在社群中成長,並且獲得更多的軟體工程學習資源

本篇為部署平台 Zeabur 於 ExplainThis 的客座分享內容摘要

比起許多系統設計的題目需要專門的知識 (例如即時共編需要懂 CRDTs 這類算法,或者鄰近服務需要懂 Quad Tree),部署系統更需要對系統廣度有所理解。要能有效設計一個能處理高併發,同時速度快的部署系統,需要對部署過程中發生哪些事情,有全盤的理解,才能針對各個面向做優化。

因此在工作坊最開始,Zeabur 團隊先帶大家回顧部署過程中會發生什麼事。大家也可以先停下來想一想,今天當寫完程式碼,到讓程式碼被部署,會經過哪些流程?

直觀初步想,如果要有一個可以自動部署程式的系統,我們約莫需要經過以下的流程。程式碼推到遠端的程式碼存放處 (例如 GitHub),透過 Webhook 來通知部署系統的後端,後端根據最新的程式碼進行建構。

這邊要先停一下,當我們在提程式碼建構時,要先回答一個問題「建構出什麼東西可以被部署?」。大家不妨先停下來想一下,被面試官問這問題時,你會怎麼回答?

以目前業界常見會部署的東西,大致可以分為以下幾類:

  • 靜態檔案 (static files)
  • 容器映像檔 (container image)
  • 無服務函式 (serverless function)
  • 以及一些針對不同語言的檔案,例如 Java 有 JAR,或者用 Go、Rust 等語言可以部署二進制的檔案

這時下一個問題來了,上面這些建構出來的東西,分別會被放到哪裡? 因為總是需要放到某個地方,然後在我們綁定完域名 (bind domain) 後,客戶端才可以訪問到。大家不妨也思考一下,如果被面試官問到,你會怎麼回答?

上面的前三種類別,分別會放在以下地方:

  • 靜態檔案會放在 OSS (例如 AWS 的 S3)
  • 容器映像檔則會放在 Docker 或是用 Kubernetes 管理的多個節點
  • 無服務函式則會放在 AWS Lamda 這類放無服務函式的地方

從部署平台的角度概覽後,接著我們從使用者的角度來看。這邊一樣先來個常考的面試題目,當使用者在瀏覽器上輸入 https://explainthis.io (或任何網址),接著會發生什麼事?

假如只討論大方向的話,一開始會有 DNS 解析,然後拿到 CNAME record,這個 CNAME record 會隨著路由不斷指向下一個 CNAME record,然後最終會指向某個 A record,也就是最終的 IP 位置。

而這個 IP 位置就會指向上面提到的,存放部署好的東西的位置。以靜態檔案來說,就會是指向某個 OSS,然後拿到部署好的內容後,讓客戶端可以造訪。

假如把上面兩段連在一起,就會是這樣

image

上面有提到,自動化部署平台,需要有偵測到程式碼被推到遠端程式碼庫的機制。一般來說會是透過 Webhook 來做到。Webhook 在做的事情就是當今天發生某件事,例如遠端程式碼更新,會觸發某個通知,例如通知部署系統的後端,這時部署系統就可以進一步去做後面的操作。

具體來說,部署系統要能去分析要怎麼用了什麼程式語言、用了什麼框架、版本是什麼、有哪外部套件。因為如果能分析出來,就能夠自動去建構。

這時問題來了,要如何知道用什麼語言? 這其實很簡單,不同的語言會有相對應不同的檔案類型。舉例來說,如果是 Node.js 可以去找 package.json 檔案;或者如果是 Java 可以去找 pom.xml 或者 gradle.build。透過各個語言獨有的檔案,就能判斷是用什麼語言。而用什麼框架,則是可以進一步去該語言的相關檔案找,例如假如是用 Next.js,就可以在 package.json 裡面找到使用 Next.js。

因為每個語言都有其對應的建構方式,所以當能夠完成上面的分析,就有辦法去做到自動化建構。舉例來說,如果知道該專案使用 Node.js,那就可以用對應的 npm 來處理。這塊 Zeabur 有一個開源專案叫 zbpack,處理了各種程式語言的解析到建構,這邊我們先不展開細講,推薦有興趣的人可以去讀原始碼 (連結在此)開源專案可以做到。

如上面有提到的,當今天建構完,我們要進一步去部署。而靜態檔案與無服務函式會相對簡單處理,因為靜態檔案可以直接呼叫 S3 的 API (或其他 OSS 的 API),而無服務函式則可以直接呼叫 Lamda 的 API,來完成部署。

但假如要處理容器的映像檔,就有比較多要考量的。雖然說可以直接透過 Docker 來跑容器,但是如果今天要同時跑跑一千、一萬、十萬個容器,會需要好幾台伺服器一起跑,就需要額外的解決方案。Kubernetes 是目前業界常用的方案,它可以幫忙把好幾台伺服器合起,協助調度找到最適合的伺服器來跑。

總結目前為止提到的,從宏觀的流程上來說,會像下面這樣

image

從上方的流程圖可以看到,使用者綁定 GitHub 後,當把程式碼推到 GitHub 後,GitHub 的 Webhook 會通知部署系統的後端。這時系統的後端透過 Kubernetes 的 Create Job (一個一次性的容器) 去執行分析與建構。建構完把映像檔推到 Registry (可以理解成 Docker Hub)。

完成後,通知部署系統的後端,後端就可以去 Kubernetes 創建一個會一直跑的容器 (Create Deployment),這個容器會去 Registry 把映像檔拉回來,然後讓服務跑起來。

這時就有一個可以追問的面試問題。如果同時有成千上萬的人要部署時,上面的那個部分,容易成為系統的瓶頸? 推薦大家可以先想一想。

一般來說,會是建構 (build) 到推送映像檔 (Push Image) 這段。一般來說,建構一個前端可能是五分鐘,建構一個 Go 的伺服器可能要兩分鐘,當流量一大,同時多個併發的任務,這段比較可能被卡住。

這也是為什麼上面提到要用 Kubernetes,因為高併發時,後端可以同時開好幾個 Job,Kubernetes 會去做容器調度,這時有些 Job 會跑起來,有些會是沒辦法被排程 (因為集群滿了),要等到在正在跑的的跑完後,才會排下去跑。可以把這個設計理解成一個佇列 (queue),先進來的任務會先被處理,而處理端如果滿了,還沒被排程的任務就會在佇列中等,直到有多的機器有餘力處理,才會在依序被排入。

不過在一個部署系統中,會加入更多的要素在。舉例來說,會加入優先權 (priority),例如有些任務比較重要,會優先排到被處理 (像是付費使用者的任務可以被優先處理,直到有閒置運算資源才處理免費使用者)。又或者當想要更大量處理時,可以透過 Kubernetes 自動擴展 (auto scaling),同時開多個 Job 來加速。

以上我們基本走過了部署系統的整體架構,也談了在這個架構下的瓶頸為何、要如何處理。在 Zeabur 團隊的分享中,有進一步去談可能會被深入追問的問題,例如當部署完成後,假如用 k8s 這類工具,因為同時會有多台機器,那該如何處理 IP 位置該指向哪裡? 同時談了設計全球多區域部署、如何從部署平台角度抵擋 DDoS 攻擊。

這些更深入內容的文字整理,我們都放在 E+,該場直播的完整回放,也可以在 E+ 中看到。想要深入理解如何設計部署系統,歡迎加入 E+ (連結)。

🧵 如果你想收到最即時的內容更新,可以在 FacebookInstagram 上追蹤我們