作者 | Chris Price
譯者 | 明知山
策劃 | 丁曉昀
什麽是蜂窩架構?蜂窩架構是一種有助于在多租戶應用程序中實現高可用性的設計模式。其目標是在設計應用程序時將所有組件部署到一個完全自給自足的隔離“單元”中,然後創建許多這種“單元”的離散部署,它們之間沒有任何依賴關系。
每個單元都是應用程序的一個完全可操作的自治實例,隨時准備爲流量提供服務,不依賴于任何其他單元,也不與其他單元交互。
用戶的流量可以分布到這些單元中,如果一個單元發生故障,它只會影響該單元中的用戶,其他單元仍然完全可用。這最大程度地減小了服務可能經曆的故障“爆炸半徑”,並確保故障不會影響大多數用戶的 SLA。
關于如何組織單元以及將哪些流量路由到哪個單元,有許多不同的策略。有關蜂窩架構的好處以及這些策略的一些示例,可以觀看 Peter Voshall 在 re:Invent 2018 的演講:“AWS 如何最小化故障的爆炸半徑”。
管理一個具有許多獨立單元的應用程序可能令人望而生畏。因此,對于創建和維護單元所需的常見基礎設施任務來說,進行盡可能多的自動化是非常有價值的。在本文的其余部分,我們將較少關注蜂窩架構的“爲什麽”,而更多地關注“如何”進行這種自動化。有關“爲什麽”的更多信息,請查看 Peter 的演講和文章末尾的其他資源鏈接!
自動化你的蜂窩架構在實現蜂窩基礎設施自動化的過程中,有五個關鍵問題需要解決:
隔離:如何確保單元之間的明確邊界?新單元:如何持續有效地讓它上線?部署:如何將最新的代碼變更傳送到每個單元?權限:如何確保單元是安全的,並有效地管理其入站和出站權限?監控:運維人員如何一目了然地確定所有單元的健康狀況,並輕松地識別哪些單元受到故障的影響?有許多工具和策略可用于解決這些問題。本文將討論 Momento 公司所使用的工具和解決方案。
在解決這些具體問題之前,讓我們先談一談標准化。
標准化對構建 / 測試 / 部署生命周期的某些部分進行標准化,可以更輕松地圍繞它們構建通用的自動化。而通用自動化將大大簡化在每個單元所有組件之間重用基礎設施代碼的工作。
需要注意的是,我們討論的是標准化,而不是同質化。大多數現代雲應用程序都不是同質化的。你的應用程序可能由五種不同的微服務組成,運行在 Kubernetes、AWS Lambda 和 EC2 等平台上。要爲這些不同類型的組件構建通用的自動化,我們只需要標准化它們生命周期的幾個特定部分。
標准化——部署模板那麽,我們需要標准化什麽?我們來看一下通常將代碼變更部署到生産環境所涉及的步驟。它們可能是這樣的:
開發人員提交代碼變更到版本控制存儲庫。我們使用最新的變更構建二進制構件,可能是一個 Docker 鏡像,一個 JAR 文件,一個 ZIP 文件或其他一些構件。構件被發布:Docker 鏡像被推送到 Docker 存儲庫,JAR 文件被推送到 Maven 存儲庫,ZIP 文件被推送到雲存儲的某個位置,等等。構件被部署到生産環境。這通常涉及逐個部署到每個單元。因此,對于應用程序的任何一個給定組件,這是部署過程的大致模板:![](http://image.uc.cn/s/wemedia/s/upload/2024/5dde911878c2c5e816e4a49bbf4f071a.png)
圖 1:最小化的部署模板
蜂窩架構的目標之一是最小化故障的爆炸半徑,而故障最有可能發生的一個時間點是在部署之後。因此,在實踐中,我們希望在部署過程中添加一些保護措施,如果檢測到問題,可以停止部署變更,直到解決問題爲止。爲此,我們添加了一個“暫存(staging)”單元,並在部署到後續單元之間添加一個“烘烤(bake)”周期,這是一個好主意。在烘烤期間,我們可以監視指標和警報,如果有任何異常就停止部署。現在給定的組件的部署模板可能看起來像這樣:
![](http://image.uc.cn/s/wemedia/s/upload/2024/7ba2379fa074a624352cfc1fdbe2ca9d.png)
圖 2:帶有“烘烤”階段的部署模板
現在,我們的目標是通用化我們的自動化,對于任何一個應用程序組件都可以輕松實現這一組部署步驟,無論這些組件是基于什麽樣的技術構建的。
有許多工具可以自動執行上述的步驟。在本文的其余部分,我們將使用一些基于 Momento 選擇的工具,但你也可以使用更適合你特定環境的工具來實現這些步驟。
Momento 的大部分基礎設施都部署在 AWS 上,因此我們傾向于使用 AWS 工具。對于在 EC2 上運行並通過 CloudFormation 部署的應用程序組件,我們使用:
AWS CodePipeline 用于定義和執行階段;AWS CodeBuild 用于執行各個構建步驟;AWS Elastic Container Registry 用于發布組件的新 Docker 鏡像;AWS CloudFormation 用于將新版本部署到每個單元;AWS Step Functions 用于在“烘烤”階段監視警報,並決定是否可以安全地將變更部署到下一個單元。圖 3:部署階段實現——基于 CloudFormation
對于基于 Kubernetes 的組件,我們稍微做一些修改即可實現相同的步驟:我們使用 AWS Lambda 調用 k8s API 將新鏡像部署到單元中。
![](http://image.uc.cn/s/wemedia/s/upload/2024/740c9539ec33d3bd998c31934ef5d864.png)
圖 4:部署階段實現——基于 Kubernetes
盡管應用程序組件的技術棧存在差異,但我們可以定義一個通用的模板來描述變更的部署步驟。然後,我們可以使用相同的工具鏈實現這些步驟,並對特定步驟進行微小的修改。將構建生命周期中的一些東西標准化,有助于我們以一種通用的方式爲這些步驟構建自動化,這意味著我們可以重複使用大量的基礎設施代碼,我們的部署在所有組件中都將是一致和可識別的。
標准化——構建目標那麽,我們如何在各種組件之間標准化所需的步驟呢?一個有價值的策略是定義一些標准化的構建目標,並在所有組件中重用它們。在 Momento,我們使用了一種經過驗證的技術:Makefiles。
Makefiles 相當簡單,而且它們已經存在很久了。它們在這方面有非常好的效果。
![](http://image.uc.cn/s/wemedia/s/upload/2024/caf52fc4ed1c92c4fa2ee47490b5246e.png)
圖 5:使用 Makefiles 標准化構建目標
在左邊,你可以看到我們的一個 Kotlin 微服務的 Makefile 片段。右邊是一個 Rust 服務的 Makefile 片段。構建命令不同,但關鍵在于在這兩個文件中有著完全相同的構建目標。
例如,我們有一個 pipeline-build 目標,它用于控制服務在 build 步驟所發生的事情。然後,我們有一些用于“單元引導”和“GCP 單元引導”的目標,因爲我們可以部署到 AWS 單元或 GCP 單元。Makefile 的目標名稱是相同的;在這些單獨服務之外運行的基礎設施的其他部分現在有了這個共同的生命周期,它們知道它們可以依賴于每個組件內部的存在,在進行部署時,它們需要與這些組件交互。
標准化——單元注冊表幫助我們標准化自動化的另一個構建塊是“單元注冊表”。它是一種機制,爲我們提供所有單元的清單和它們的基本元數據。
![](http://image.uc.cn/s/wemedia/s/upload/2024/6168fe45dbb7042c36f4161f489cd9ed.png)
圖 6:用于單元注冊表的 TypeScript 模型
在 Momento,我們使用 TypeScript 構建單元注冊表。我們使用了大約 100 行 TypeScript 代碼定義了一些簡單的接口,我們可以使用它們來表示所有單元的數據。我們有一個 CellConfiguration 接口,它可能是最重要的一個,能捕獲給定單元的所有重要信息。比如,它是一個生産單元還是開發單元?它在哪個區域?這個單元中端點的 DNS 名稱是什麽?這是 AWS 單元還是 GCP 單元?
我們還有一個 MomentoOrg 接口,它包含了一個 CellConfiguration 數組。
使用這些接口提供的模型,我們可以編寫更多的 TypeScript 代碼來實例化它們,並創建單元的數據。下面是這些代碼的一個片段:
![](http://image.uc.cn/s/wemedia/s/upload/2024/6f86bf605a39ab5efc1855e7a972123c.png)
圖 7:我們“alpha”單元的單元注冊表數據
這是我們的“alpha”單元的數據,有單元名稱、帳戶 ID、區域、DNS 配置等。現在,每當我們想要添加新單元時,只需要輸入這個單元注冊表代碼,並向這個數組添加一個新條目。
現在,我們有了所有單元的數據,我們需要將其發布到某個地方,這樣就可以從基礎設施的其他部分訪問它。根據不同的情況,你可能會做一些複雜一點的事情,比如將數據存儲在可以查詢的數據庫中。我們不需要這些東西,所以只需將數據以 JSON 的形式存儲在 S3 中。
單元注冊表的最後一個組件是一個小型的 TypeScript 庫,它知道如何從 S3 檢索這些數據,並將其轉換成 TypeScript 對象。我們將該庫發布到私有 npm 存儲庫,可以在我們基礎設施的代碼中使用它。這使得我們可以在我們的基礎設施自動化過程中構建一些通用的模式,我們可以遍曆所有單元並爲每個單元配置相同的自動化。
標准化——單元引導腳本我們用來通用自動化的最後一個標准化部分是“單元引導腳本”。將應用程序的所有組件部署到一個新單元可能非常具有挑戰性、耗時且容易出錯,而單元引導腳本可以簡化這個過程,並確保單元之間的一致性。
假設你的應用程序組件的代碼都位于一個 git 存儲庫中,那麽,根據上述構建塊,引導新單元的邏輯就可以像下面這樣簡單:
使用單元注冊表查找我們在此單元中所需的元數據(例如,AWS 帳戶 ID、DNS 配置等)。對于每個應用程序組件:克隆該組件的 git 存儲庫;運行 Makefile 中標准化的 cell-bootstrap 目標。![](http://image.uc.cn/s/wemedia/s/upload/2024/a61ac9b58ff14d31939837e6e3bdb34e.png)
圖 8:單元引導腳本
這個腳本僅用五行代碼爲我們提供了一個通用且可擴展的用于部署應用程序新單元的解決方案。如果你向應用程序引入新組件,腳本仍然是適用的,並確保簡單且一致的部署流程。
將所有部分組合起來
我們已經定義了一些標准化的構建塊來幫助我們組織單元的信息,並對應用程序組件的各種生命周期任務進行了通用化,現在是時候重新審視一下我們需要解決的自動化基礎設施的五個問題。
隔離在 AWS 環境中確保單元隔離性最簡單的方法是爲每個單元創建一個單獨的 AWS 帳戶。起初,如果你不習慣于管理多個帳戶,那麽這看起來有點令人生畏。然而,現在的 AWS 工具已經非常成熟,因此這比你想象的要容易得多。
使用專有的 AWS 帳戶部署單元可以確保默認與其他單元隔離,但你必須爲一個單元與另一個單元的交互設置複雜的跨帳戶 IAM 策略。反過來,如果你使用一個 AWS 帳戶部署多個單元,就必須設置複雜的 IAM 策略來防止單元之間的交互。IAM 策略管理是使用 AWS 最具挑戰性的部分之一,所以任何時候你都可以選擇避免這麽做,爲你節省時間和減少痛點。
使用多個帳戶的另一個好處是你可以使用 AWS Organizations 將這些帳戶鏈接在一起,然後使用 AWS Cost Explorer 可視化和分析每個單元的成本。如果你選擇使用單個 AWS 帳戶部署多個單元,就必須仔細標記與每個單元相關的資源,以便查看每個單元的成本。使用多個帳戶可以免費獲得這個功能。
![](http://image.uc.cn/s/wemedia/s/upload/2024/0e8001af6a28bd475965dd3b4eafe1f6.png)
圖 9:使用 AWS Cost Explorer 查看每個單元帳戶的成本
與蜂窩架構隨之而來的一個挑戰是路由。如果你有多個隔離的單元,並且在每個單元中運行應用程序的一個副本,你就必須選擇一種策略,將用戶的流量從用戶路由到目標單元。
如果用戶通過 SDK 或你提供的其他客戶端軟件與應用程序交互,那麽將流量路由到某個單元的一種簡單方法是爲每個單元使用唯一的 DNS 名。這是我們在 Momento 使用的方法。在爲用戶創建身份驗證令牌時,我們將目標單元的 DNS 名作爲令牌內部的聲明包含在內,然後我們的客戶端庫就可以根據這個信息路由流量。
不過這種方法只適用于某些場景。如果你的用戶通過網絡浏覽器與服務交互,你可能希望爲他們提供一個可以在浏覽器中訪問的 DNS 名,這樣他們就不需要知道單元的信息。對于這種情況就有必要創建一個薄路由層來引導流量。
![](http://image.uc.cn/s/wemedia/s/upload/2024/f0cd17cdc192053d7d4d4bbb41ff0c02.png)
圖 10:用于單元隔離的路由層
路由層應該盡可能小。它應該包含用于識別用戶的最基本的邏輯(根據請求中的一些信息),確定應該將流量路由到哪個單元,然後相應地代理或重定向請求。
這個路由層提供了更簡單的用戶體驗(用戶不需要知道單元的信息),但代價是你必須維護和監控這個新的全局組件。它還變成了一個單點故障點,但你可以通過蜂窩架構在很大程度上避免這種情況。這就是爲什麽你應該努力使讓它變得盡可能小而簡單。
擁有這樣一個路由層的一個好處是,它可以透明地將用戶從一個單元遷移到另一個單元。假設一個用戶需要更大的或不那麽擁擠的單元,你可以爲他們准備好新單元,然後部署一個改變路由邏輯 / 配置的變更,在他們無感的情況下重定向他們的流量。
新單元如果按照上面的標准化部分進行操作,你會發現,我們已經完成了大部分工作,解決了如何創建新單元的問題。我們所需要做的就是:
在 Organization 中創建一個新的 AWS 賬戶;將賬戶添加到單元注冊表中;運行單元引導腳本來構建和部署所有組件。就這樣,我們有了一個新的單元。由于我們在 Makefiles 中對每個組件的構建生命周期步驟進行了標准化,所以部署邏輯非常通用,幾乎不需要花費什麽功夫就可以啓動一個新的單元。
部署部署可能是應用程序架構需要解決的最具挑戰性的問題,蜂窩架構尤其如此。所幸的是,在最近幾年,基礎設施即代碼工具所取得的重大進展使這些挑戰變得更容易解決。
在過去的幾年裏,大多數 IaC 工具都使用聲明性配置語法(例如 YAML 或 JSON)來定義用戶希望創建的資源。而最近的一種趨勢是爲開發人員提供一種使用真正的編程語言來表達基礎設施定義的方式。開發人員現在可以使用他們熟悉的編程語言來定義基礎設施組件,不必糾結于複雜且冗長的配置文件。下面是這類工具的一些示例:
AWS CDK(雲開發工具包)——用于部署 CloudFormation 基礎設施;AWS cdk8s——用于部署 Kubernetes 基礎設施;CDKTF(Terraform 的 CDK)——用于通過 HashiCorp Terraform 部署基礎設施。我們可以在這些工具中使用 for 循環之類的構造來消除大量的 YAML/JSON 樣板配置代碼。![](http://image.uc.cn/s/wemedia/s/upload/2024/ea4f2e1d9c437dc42fae7135b84e3cdf.png)
圖 11:CloudFormation JSON 與 CDK TypeScript
使用編程語言,比如 TypeScript,來表達基礎設施的另一個好處是,我們可以將 npm 庫作爲依賴項。這意味著我們的 IaC 項目可以在單元注冊表庫中添加依賴項,可以訪問包含所有單元元數據的數組。然後,我們可以循環遍曆這個數組,定義每個單元所需的基礎設施步驟。在添加新單元和更新單元注冊表時,基礎設施也將自動更新!
AWS CDK 和 AWS CodePipeline 的組合功能非常強大,我們可以使用通用模式爲每個應用程序組件定義管道,並在共享大部分代碼的同時爲每個組件設置必要的構建和部署步驟。
在 Momento,我們爲可能需要添加到 AWS CodePipeline 中的每種類型的階段編寫了一些 TypeScript CDK 代碼(例如,構建項目、推送 Docker 鏡像、部署 CloudFormation 棧、將新鏡像部署到 Kubernetes 集群等)。我們可以將這些階段放到數組中,然後循環遍曆它,將階段添加到每個管道中:
![](http://image.uc.cn/s/wemedia/s/upload/2024/91f0038dd044ead1b2aabf16d8a37219.png)
圖 12:將階段添加到 CodePipeline 的 CDK 代碼
我們創建了一個特殊的管道,叫作“管道的管道”。它是一個“元”管道,負責爲每個應用程序組件創建單獨的管道。
![](http://image.uc.cn/s/wemedia/s/upload/2024/45d4e489d1b67c073da726df98976a5e.png)
圖 13:管道的管道
這個存儲庫作爲我們所有部署邏輯的單一事實來源。每當開發人員需要更改部署基礎設施的內容時,都可以在這裏完成。我們對部署步驟列表(例如,更改單元的順序或使用更複雜的“烘焙”步驟)所做的任何更改都將自動反映在所有組件管道中。在添加新單元時,管道的管道會運行並更新所有組件管道,將新單元添加到部署步驟列表中。
爲了幫助改進可用性,我們仔細考慮了部署到生産單元的順序。單元根據大小、重要性和流量級別進行分組。在第一階段,我們會部署到預生産單元,在這裏對變更進行測試,然後才會被推送到生産單元。如果這些部署進展順利,我們會逐漸部署到越來越大的生産單元中。這種分階段的部署方法讓變更部署變得可控,並增加在問題影響更多客戶之前捕獲它們的可能性。
權限爲了管理單元的訪問權限,我們主要依賴 AWS 的 SSO(現在的 IAM Identity Center)。這個服務爲我們提供了一個單點登錄頁面,所有開發人員都可以使用他們的 Google 身份登錄,然後訪問他們有權限訪問的 AWS 控制台。它還通過命令行和 AWS SDK 的方式提供對目標賬戶的訪問,使自動化操作任務變得容易。
管理接口提供了對每個帳戶內的用戶訪問的細粒度控制。例如,在單元賬戶內定義了“只讀”和“單元操作員”等角色,授予不同級別的權限。
![](http://image.uc.cn/s/wemedia/s/upload/2024/78ed1df0286dde5d70df532bea36eaba.png)
圖 14:AWS SSO 賬戶權限
將 AWS SSO 的角色映射能力與 CDK 和我們的單元注冊表結合起來,我們就可以完全自動化每個單元賬戶的入站和出站權限。
對于入站權限,我們可以循環遍曆注冊表中所有開發人員和單元賬戶,並使用 CDK 授予適當的角色。在向單元注冊表添加新賬戶時,自動化機制會自動設置正確的權限。我們對注冊表中的每個單元進行循環遍曆,根據需要對資源(如 ECR 鏡像或私有 VPC)授予訪問權限,以獲得出站權限。
監控監控大量的單元可能很困難。關鍵在于要有一種監控手段,確保運維人員可以在單個視圖中評估所有單元內服務的運行狀況。指望運維人員查看每個單元賬戶中的指標不是一個可擴展的解決方案。
爲了解決這個問題,你只需要選擇一種集中式的指標解決方案,你可以導出所有單元賬戶的指標。這種解決方案還必須支持通過維度對指標進行分組,比如單元名稱。
許多指標解決方案提供了這種功能,可以將多個賬戶的指標聚合到中央監控賬戶的 CloudWatch 指標中。還有許多第三方選擇,例如 Datadog、New Relic、LightStep 和 Chronosphere。
下面是 LightStep 儀表盤的截圖,其中 Momento 的指標按單元名稱分組:
![](http://image.uc.cn/s/wemedia/s/upload/2024/a3409cdba9c4952a24f5439375f8029a.png)
圖 15:指標儀表盤,按單元名稱分組的指標
額外的好處我們已經介紹了蜂窩架構如何幫助實現高可用性,以及現代基礎設施和基礎設施工具如何幫助我們自動化蜂窩基礎設施,現在來看一下我們可以從這種自動化中獲得的額外好處。
一個關鍵的好處是能夠非常快速地啓動新單元。我們可以使用本文描述的單元引導腳本從零開始在幾小時內部署一個新單元。如果沒有基礎的標准化和自動化,這個過程的大多數步驟都必須手動完成,甚至需要花費數周的時間。對于初創公司和小型公司來說,能夠快速添加新單元來滿足用戶的需求可能是一個巨大的價值主張。它可能成爲是否能夠達成重要交易的關鍵因素。
另一個巨大的價值在于開發人員可以在自己的開發賬戶中創建個人單元。有時候,如果沒有真實的環境,根本無法測試和調試依賴多個服務或組件之間交互的複雜功能。
一些工程組織會嘗試使用共享的開發環境來解決這個問題,但這需要開發人員之間的密切協作,並且容易發生沖突和停機。相反,使用我們的單元引導腳本,開發人員可以在一天內創建和銷毀完整的應用程序開發部署環境。這種敏捷的方法最大程度地減少了中斷,提升了生産效率,使開發人員能夠專注于他們的任務,而不會在無意中影響到其他人。
沒有一刀切的解決方案在本文中,我們分享了我們選擇的幾種工具和技術來實現蜂窩基礎設施的自動化。需要注意的是,對于本文中提到的技術,都有大量的替代選擇。例如,雖然 Momento 使用了一些 AWS 工具,但其他主要的雲提供商,如 GCP 和 Azure,也爲每個相關的任務提供了類似的産品。
此外,你可能會選擇自動化其中的部分內容,或者可能選擇自動化更多超出本文分享的內容!關鍵在于你要選擇適合你業務和環境的工具和自動化級別。
總結蜂窩架構可以提升可用性,並確保達成你的 SLA。對業務的敏捷性和工程速度也很有價值。自動化這些過程只需要解決本文中提出的一些關鍵問題,並在應用程序組件之間標准化一些東西。
基礎設施即代碼領域的一些進展讓自動化變得更加簡單,只要你利用這些機會來標准化一些關于如何定義組件的東西。
其他資源Peter Vosshall 在 re:Invent 2018 上的演講: AWS 如何最小化故障的影響範圍AWS Well-Architected:蜂窩架構Slack 遷移到蜂窩架構的經驗原文鏈接: