本文要點
- 儘管微服務架構(MSA)有很多好處,但管理數百個鬆耦合的微服務很快就會變得很麻煩。這就是設計基於單元格的架構(CBA)的原因。
- CBA是一種微服務架構模式,它的主要要求是將多個微服務(及其他組件)分組成稱爲單元格的構建塊,以方便管理和重用。
- 從零開始在容器編排平臺上創建CBA很費力。在撰寫本文時,Kubernetes是業界廣泛採用的容器編排平臺;然而,使用YAML編寫用於此目的的Kubernetes工件並不是一項簡單的任務。
- Cellery遵循代碼優先的方法,處理實現CBA的底層複雜性。
- Cellery包含一個SDK、一個運行時和一個管理框架。
Cellery簡介
Cellery到底是什麼,它如何幫助我們在Kubernetes上部署和管理應用程序?Cellery是一種在Kubernetes上構建、集成、運行和管理複合應用程序的代碼優先方法。這種複合應用程序的構建塊稱爲單元格——其名稱爲Cellery,而不是Celery。爲了幫你理解單元格和Cellery,讓我們看看如何使用Cellery部署、管理和觀察一個已有的由谷歌編寫的Kubernetes應用程序。但是,在此之前,讓我們先了解下單元格是什麼以及單元格的工作原理。
單元格是什麼?
讓我們看一下,爲什麼需要在微服務架構中使用複合組件。
微服務是構建複雜且不斷演化的應用程序的熱門選項,它可以縮短上市時間,加快創新速度。每個服務都可以由專門負責該服務的團隊獨立開發,並且他們可以自由選擇任何他們認爲合理的技術。最重要的是,微服務是可重用的,每個服務都可以獨立伸縮,使團隊可以使用最能滿足服務資源需求的最佳部署基礎設施。開發人員可以對其服務進行本地更改,並在測試完成後立即部署這些更改。那麼,這有什麼挑戰嗎?
微服務(包括無服務器函數)的使用正在快速增長,因爲組織的目標是提高開發速度和可伸縮性,而且它們還必須調整爲面向業務能力的團隊。在擁有數十個或數百個應用程序的企業中,管理如此多的鬆耦合微服務,不僅會成爲運營的噩夢,也在團隊溝通以及服務發現、版本控制和可觀察性等方面提出了挑戰。更多的服務、更多的溝通路徑、更復雜的網絡安排以及更多的潛在故障區。這就有了對高級結構的需求,將多個微服務和無服務器函數聚合到易於管理和重用的構建塊中。
基於單元格的架構是一種微服務架構模式,它將系統的微服務、數據和其他功能組件(包括前端應用程序、遺留服務、代理、網關和遺留系統適配器)分組爲內聚的、可單獨部署的架構單元(稱爲單元格)。
通常,組件分組的依據是作用範圍、所有權和組件之間的相互依賴關係。每個單元格都應該單獨設計和開發,並且應該可以獨立部署、管理和觀察。此外,單元格內的組件可以使用支持的傳輸協議在單元格內部實現相互通信。然而,所有傳入的服務請求必須首先通過單元格網關。該網關使用標準網絡協議通過受控的網絡端點提供安全API、事件或流。團隊可以通過自組織的方式生成可以持續部署和增量更新的單元格。下面是對單元格架構中單元格的一個簡單描述:
實現單元格架構的方法
Cellery設計用於基於單元格架構原則創建Kubernetes應用程序。使用Cellery,我們可以編寫代碼來定義單元格及其組件,方法是指向現有的容器鏡像(其中包含構成單元格的微服務及其他組件),並定義這些組件之間的關係、對其他單元格的依賴和單元格API(網關)。然後就可以使用單元格定義代碼生成單元格鏡像。實際上,一旦我們將單元格定義代碼提交到版本控制存儲庫中,就可以觸發CI/CD管道。像Jenkins這樣的CI/CD系統可以構建單元格鏡像,對其進行測試,並將其推入容器存儲庫。然後,CI/CD系統可以拉取單元格鏡像並將其部署到相應的生產環境。而且,我們還可以像在Kubernetes上部署的其他任何應用程序一樣更新、擴展和觀察部署好的單元格。
使用Cellery創建單元格
簡言之,Cellery包含了SDK、運行時和管理框架。安裝Cellery時,可以通過Cellery CLI運行命令,執行各種任務。
首先,你應該在本地機器上創建一個Kubernetes集羣,或者使用已有的Kubernetes集羣作爲Cellery運行時環境。輸入一個簡單的命令command cellery,就會看到一個提示,讓你通過交互式CLI選擇部署首選項,Cellery 將根據你的首選項爲你配置Kubernetes集羣。
設置好Cellery運行時環境之後,就可以開始用Cellery語言編寫單元格。Cellery語言基於Ballerina編程語言,因此可以使用VSCode和IntelliJIdea作爲IDE。要自動生成包含標準導入語句和所需函數的單元格定義文件,可以使用cellery init命令。接下來,你可以定義組件,完成構建,並使用Cellery語言運行邏輯。然後,Cellery編譯器將編譯代碼並使用一個簡單的命令cellery build創建相應的Kubernetes構件。
要在Cellery運行時環境上部署單元格,運行cellery run命令並提供必要的參數。你可以使用cellery push命令將構建好的鏡像推入單元格鏡像存儲庫,使用cellery pull命令從存儲庫中拉取單元格鏡像,並將構建和部署流集成到CI/CD管道。此外,你還可以通過cellery view命令查看單元格的可視化表示。Cellery還提供了單元格測試功能和可觀察性工具,讓你可以監控、記錄和跟蹤單元格。
爲什麼是Cellery?爲什麼不使用YAML爲單元格配置Kubernetes部署?
對於已經採用容器的組織,開發人員不僅要創建微服務,還要理解容器編制系統如Kubernetes的細微差別。此外,除了準備Kubernetes集羣、創建、部署和管理應用程序,從頭開始創建CBA涉及到配置服務網格、處理服務身份驗證和配置符合CBA原則的安全策略等任務。因此,使用標準Kubernetes資源配置單元格需要一些Kubernetes專業知識。
此外,Kubernetes資源(如pod、服務和部署)是通過YAML文件聲明性地創建的。因此,隨着部署規模的增加,面對不斷增長的、越來越複雜的YAML代碼,如果沒有成熟的IDE幫助他們提高生產效率,DevOps團隊就會陷入掙扎。而且,由於缺乏對函數、抽象和封裝等編程概念的支持,YAML本身就鼓勵大量重複代碼。因此,在Kubernetes上創建複雜的部署意味着DevOps團隊必須經歷一個冗長而令人畏懼的過程,編寫並維護可能長達數千行的YAML文件。這很容易出錯。
Cellery使用類型安全的、經過驗證的代碼而不是YAML來定義部署,而且,它還負責處理配置部署、單元格連接、服務、自動縮放等底層複雜性。此外,在默認情況下,使用Cellery編寫的單元格通過單點登錄、令牌、基於策略的訪問控制和mTLS等安全機制來確保安全。Cellery是圍繞DevOps實踐而設計的,因此,可以使用藍/綠部署和金絲雀部署無縫地進行構建、推送、拉取、測試、部署和更新。用戶還可以通過監控和跟蹤功能觀察部署。
簡言之,Cellery的目標是簡化Kubernetes上應用程序的配置、構建、測試和部署。如上所述,該項目試圖從不同的角度解決這個問題,包括開發、DevOps、安全性和可觀察性。
Cellery實例
讓我們看一個真實的微服務示例,你可以親自嘗試一下。(要了解如何使用Cellery編寫單元格代碼,可以查看Cellery語法並嘗試一些示例。)
爲此,我們使用了谷歌的“Hipster Shop”演示應用程序。這裏有原始Hipster Shop演示程序的詳細信息、源代碼、Docker文件等。這個示例適用於Cellery 0.3.0版本。
Hipster Shop應用程序是一個多層次、多語言的微服務應用程序,它是基於Web的電子商務應用程序。用戶可以瀏覽商品,將它們添加到購物車中,使用應用程序購買它們。Hipster Shop由前端和多個微服務組成,通過gRPC相互通信。服務架構如圖3所示,表1是Hipster Shop微服務的說明。
服務 | 語言 | 說明 |
---|---|---|
frontend | Go | 暴露一個HTTP服務器來爲網站提供服務。不需要註冊/登錄,會自動爲所有用戶生成會話ID。 |
cartservice | C# | 把用戶購物車中的物品保存在Redis中並檢索它。 |
productcatalogservice | Go | 從一個JSON文件提供產品列表以及搜索產品和獲取單個產品的功能。 |
currencyservice | Node.js | 將一種貨幣轉換成另一種貨幣。使用從歐洲央行獲取的真實值。這是QPS最高的服務。 |
paymentservice | Node.js | 從給定的信用卡(mock)扣除給定的金額,並返回一個交易ID。 |
shippingservice | Go | 根據購物車給出運輸成本估計。將物品發送到給定的地址(mock)。 |
emailservice | Python | 向用戶發送一封訂單確認郵件(mock)。 |
checkoutservice | Go | 檢索用戶購物車,準備訂單,並安排付款、發貨和電子郵件通知。 |
recommendationservice | Python | 根據購物車中的物品推薦其他產品。 |
adservice | Java | 基於給定上下文單詞提供文本廣告。 |
loadgenerator | Python/Locust | 不斷向前端發送模擬真實用戶購物流的請求。 |
爲了將Hipster Shop的微服務映射到基於單元格的架構,我們將這些微服務分組爲五個單元格:ads、products、cart、checkout和front-end。我們根據每個微服務單獨執行的任務以及它與單元格中其他微服務的關係密切程度設計了這種分類。一個單元格由一個團隊擁有,但是一個團隊可以擁有一個或多個單元格。定義單元格邊界可以基於其他標準,例如組件到組件的連接數量,而不僅限於功能和所有權。要了解更多關於單元格粒度的信息,請點擊這裏。
還有一點非常重要,爲了使用Cellery,我們沒有對Hipster Shop原來的微服務做任何更改;Cellery僅引用微服務的現有容器鏡像。表2列出了單元格及各自的組件,圖4進行了說明。
單元格 | 組件 |
---|---|
ads | adservice |
products | productcatalogservice、recommendationservice |
cart | cartservice、cacheservice |
checkout | checkoutservice、emailservice、paymentservice、shippingservice、currencyservice |
front-end | frontendservice |
單元格front-end包含前端應用程序,這是其唯一組件,HTTP流量通過其網關訪問單元格,而front-end通過gRPC與其他單元格交互。
單元格checkout是該架構中除front-end之外惟一與外部單元格(products和cart)通信的單元格,剩下的單元格products、ads和cart是獨立的單元格,在這些單元格中,只有其內部組件之間發生通信。可以點擊這裏查看所有完整的單元格定義文件(擴展名爲.bal的文件)以及運行和部署Hipster Shop單元格的說明。請注意,這個示例已經在Cellery 0.3.0版本上進行了測試。
創建單元格
讓我們看下ads的代碼,它包含一個組件:adservice。
import ballerina/config;
import celleryio/cellery;
public function build(cellery:ImageName iName) returns error? {
int adsContainerPort = 9555;
// Ad服務組件
// 基於給定上下文單詞提供文本廣告。
cellery:Component adsServiceComponent = {
name: "ads",
source: {
image: "gcr.io/google-samples/microservices-demo/adservice:v0.1.1"
},
ingresses: {
grpcIngress: <cellery:GRPCIngress>{
backendPort: adsContainerPort,
gatewayPort: 31406
}
},
envVars: {
PORT: {
value: adsContainerPort
}
}
};
// 單元格初始化
cellery:CellImage adsCell = {
components: {
adsServiceComponent: adsServiceComponent
}
};
return cellery:createImage(adsCell, untaint iName);
}
public function run(cellery:ImageName iName, map<cellery:ImageName> instances) returns error? {
cellery:CellImage adsCell = check cellery:constructCellImage(untaint iName);
return cellery:createInstance(adsCell, iName, instances);
}
首先,單元格定義以標準import 語句開始,幷包含兩個函數:build和run(如果你使用cellery init命令,那麼這些函數將自動生成)。當用戶分別執行cellery build和cellery run命令時,build函數和run函數將被調用。
在build 函數中,通過指向公共Docker鏡像URL(source)並定義網絡訪問入口點(ingresses)和環境變量(envVars),定義一個名爲adServiceComponent的組件來表示adservice。然後,單元格被初始化並使用名稱adsCell定義,之前定義的adServiceComponent被添加到它的組件列表中。然後,使用cellery:createImage方法創建單元格鏡像。
最後,run函數將獲取構建的單元格鏡像(cellery:ImageName iName),其中包含單元格鏡像和相應的實例名,並使用cellery:createInstance方法從單元格鏡像創建一個正在運行的實例。
單元格內組件間的通信
現在,我們已經看了基本單元格文件的代碼結構,讓我們看一下有兩個或多個組件的單元格的代碼,以及如何配置這些組件實現彼此通信。
單元格products有兩個組件:productcatalogservice和recommendationservice。如圖4所示,recommendationservice需要與productcatalogservice交互,因爲它根據購物車中的物品來推薦產品。單元格中組件之間的通信是通過環境變量實現的。
如下面的代碼片段所示,recommendationServiceComponent需要通過環境變量(envVars)PRODUCT_CATALOG_SERVICE_ADDR獲得productCatalogServiceComponent的地址。此外,productCatalogServiceComponent被標記爲dependencies字段下的一個依賴項,這可以確保productCatalogServiceComponent啓動並運行,並且可以用於解析依賴項。
..
// 推薦服務組件
// 根據購物車中的物品推薦其他產品
cellery:Component recommendationServiceComponent = {
name: "recommendations",
source: {
image: "gcr.io/google-samples/microservices-demo/recommendationservice:v0.1.1"
},
ingresses: {
grpcIngress: <cellery:GRPCIngress>{
backendPort: recommendationsContainerPort,
gatewayPort: 31407
}
},
envVars: {
PORT: {
value: recommendationsContainerPort
},
PRODUCT_CATALOG_SERVICE_ADDR: {
value: cellery:getHost(productCatalogServiceComponent) + ":" + productCatalogContainerPort
},
ENABLE_PROFILER: {
value: 0
}
},
dependencies: {
components: [productCatalogServiceComponent]
}
};
..
單元格以名稱productsCell定義並初始化,productCatalogServiceComponent和preferationservicecomponentare均被添加到其組件列表中,如下面的代碼片段所示。
..
// 單元格初始化
cellery:CellImage productsCell = {
components: {
productCatalogServiceComponent: productCatalogServiceComponent,
recommendationServiceComponent: recommendationServiceComponent
}
};
..
單元格間通信
討論完組件間通信,下面介紹一個單元格中的組件如何與另一個單元格中的組件通信。由於CBA要求所有外部傳入的通信必須通過單元格網關進行,因此單元格front-end的代碼的唯一組件是Web前端應用程序,並且必須與位於不同單元格中的各種組件通信。
..
cellery:Component frontEndComponent = {
name: "front-end",
source: {
image: "gcr.io/google-samples/microservices-demo/frontend:v0.1.1"
},
ingresses: {
portal: <cellery:WebIngress> { // Web ingress is exposed globally.
port: frontEndPort,
gatewayConfig: {
vhost: "my-hipstershop.com",
context: "/"
}
}
},
..
上面的代碼顯示了frontEndComponent如何暴露一個HTTP服務器爲Hipster Shop網站提供服務。爲了與相關的內外部微服務通信,同一個組件需要幾個環境變量的值。讓我們看一下讓frontendcomponent可以與單元格products的組件進行交互的代碼。
envVars: {
..
PRODUCT_CATALOG_SERVICE_ADDR: {
value: ""
},
RECOMMENDATION_SERVICE_ADDR: {
value: ""
},
..
},
如上面的代碼片段所示,frontEndServiceComponent需要通過環境變量(envVars)PRODUCT_CATALOG_SERVICE_ADDR和RECOMMENDATION_SERVICE_ADDR分別獲得productCatalogServiceComponent和recommendationServiceComponent的地址。
dependencies: {
cells: {
productsCellDep: <cellery:ImageName>{ org: "wso2cellery", name: "products-cell", ver: "latest"},
..
}
}
單元格front-end依賴於單元格products,這種依賴關係是通過上面介紹的frontEndComponent 中的dependencies字段定義的。
cellery:Reference productReference = cellery:getReference(frontEndComponent, "productsCellDep");
frontEndComponent.envVars.PRODUCT_CATALOG_SERVICE_ADDR.value = <string>productReference.gateway_host + ":" +<string>productReference.products_grpc_port;
frontEndComponent.envVars.RECOMMENDATION_SERVICE_ADDR.value = <string>productReference.gateway_host + ":" +<string>productReference.recommendations_grpc_port;
方法cellery: getReference (frontEndComponent productsCellDep)會提供一個指向已部署的products單元格實例的引用,藉助這個引用,我們可以解析出上述代碼中環境變量PRODUCT_CATALOG_SERVICE_ADDR和RECOMMENDATION_SERVICE_ADDR的值。類似地,front-end單元格通過上述方法與其他單元格通信。
剩下的兩個單元格定義文件遵循相同的原則,可以從GitHub庫中獲取。
cart.bal
單元格cart是一個包含兩個組件的獨立單元格。
checkout.bal
單元格checkout包含五個組件,爲了從checkoutservice調用cartservice和productcatalogservice,它需要分別與單元格cart和products通信。
在完成了所有單元格定義的編碼後,接下來可以構建和部署這些單元格了。你還可以將部署Hipster Shop微服務所需的完整Kubernetes YAML文件與Hipstershop Cellery代碼進行比較,後者不僅在Kubernetes上部署微服務,而且還圍繞這些微服務創建了基於單元格的架構。
構建和部署單元格
請按照這裏的說明構建和運行所有的Hipster Shop單元格。
運行獨立單元格
現在看一下如何構建和運行ads單元格,它是一個獨立的單元格。
打開終端,定位到ads.bal文件所在的位置,運行以下命令構建ads單元格:
$ cellery build ads.bal wso2cellery/ads-cell:latest
我們在Docker Hub中的組織名稱是wso2cellery,使用ads-cell作爲單元格鏡像的名稱,使用latest作爲標籤。執行build命令後可以看到如下輸出:
✔ Building image wso2cellery/ads-cell:latest
✔ Removing old Image
✔ Saving new Image to the Local Repository
✔ Successfully built cell image: wso2cellery/ads-cell:latest
What's next?
--------------------------------------------------------
Execute the following command to run the image:
$ cellery run wso2cellery/ads-cell:latest
--------------------------------------------------------
要以實例名ads-cell運行單元格鏡像wso2cellery/ads-cell:latest,運行以下命令:
$ cellery run wso2cellery/ads-cell:latest -n ads-cell
可以看到以下輸出:
✔ Extracting Cell Image wso2cellery/ads-cell:latest
Main Instance: ads-cell
✔ Reading Cell Image wso2cellery/ads-cell:latest
✔ Validating dependencies
Instances to be Used:
INSTANCE NAME CELL IMAGE USED INSTANCE SHARED
--------------- ----------------------------- --------------- --------
ads-cell wso2cellery/ads-cell:latest To be Created -
Dependency Tree to be Used:
No Dependencies
? Do you wish to continue with starting above Cell instances (Y/n)? y
✔ Starting main instance ads-cell
✔ Successfully deployed cell image: wso2cellery/ads-cell:latest
What's next?
--------------------------------------------------------
Execute the following command to list running cells:
$ cellery list instances
--------------------------------------------------------
運行依賴單元格
現在,以單元格front-end爲例,看看如何構建和運行依賴於其他單元格的單元格。build命令與執行單元格ads的命令類似。
$ cellery build front-end.bal wso2cellery/front-end-cell:latest
然而,在運行有依賴項的單元格鏡像時,還必須列出單元格所依賴的其他單元格的運行實例的名稱。這從單元格front-end的run命令可以看出來,如下所示。
$ cellery run wso2cellery/front-end-cell:latest -n front-end-cell -l cartCellDep:cart-cell -l productsCellDep:products-cell -l adsCellDep:ads-cell -l checkoutCellDep:checkout-cell -d
輸出如下:
✔ Extracting Cell Image wso2cellery/front-end-cell:latest
Main Instance: front-end-cell
✔ Reading Cell Image wso2cellery/front-end-cell:latest
⚠ Using a shared instance cart-cell for duplicated alias cartCellDep
⚠ Using a shared instance products-cell for duplicated alias productsCellDep
✔ Validating dependency links
✔ Generating dependency tree
✔ Validating dependency tree
Instances to be Used:
INSTANCE NAME CELL IMAGE USED INSTANCE SHARED
---------------- ----------------------------------- ---------------------- --------
checkout-cell wso2cellery/checkout-cell:latest Available in Runtime -
products-cell wso2cellery/products-cell:latest Available in Runtime Shared
ads-cell wso2cellery/ads-cell:latest Available in Runtime -
cart-cell wso2cellery/cart-cell:latest Available in Runtime Shared
front-end-cell wso2cellery/front-end-cell:latest To be Created -
Dependency Tree to be Used:
front-end-cell
├── checkoutCellDep: checkout-cell
├── productsCellDep: products-cell
├── adsCellDep: ads-cell
└── cartCellDep: cart-cell
? Do you wish to continue with starting above Cell instances (Y/n)? y
✔ Starting dependencies
✔ Starting main instance front-end-cell
✔ Successfully deployed cell image: wso2cellery/front-end-cell:latest
What's next?
--------------------------------------------------------
Execute the following command to list running cells:
$ cellery list instances
--------------------------------------------------------
還可以使用view命令查看單個單元格的圖形化表示及其依賴關係。例如,要查看單元格front-end,請鍵入以下命令:
cellery view wso2cellery/front-end-cell:latest
這會打開一個描述單元格front-end的Web頁面,如圖5所示。
可觀察性
爲了監控和排除已部署單元格的故障,Cellery提供了可觀察性工具,包括儀表板。Cellery儀表板顯示了許多單元格視圖,其中顯示了依賴關係圖、單元格的運行時指標、對通過網關的請求的端到端分佈式跟蹤以及單元格組件。所有指標都是從組件和網關收集的,其中包括與Kubernetes pod和節點(包括CPU、內存、網絡和文件系統使用情況)相關的系統指標和請求/響應指標(應用程序指標)。
真得需要Cellery嗎?
如果你正在尋找這個問題的答案,那麼你還需要問問自己,微服務項目是否將是雲原生的,以及該項目是否會隨着時間的推移而增長和進化。如果答案是肯定的,那麼你必須記住,管理數百個鬆耦合的微服務可能很快就會成爲一場噩夢。這就是爲什麼要設計基於單元格的架構,但是在容器編排平臺(如Kubernetes)上使用YAML文件從零開始創建CBA絕非易事。這就是Cellery的作用所在,它使開發人員能夠遵循代碼優先的方法,並處理實現CBA的潛在複雜性,從而真正利用雲原生微服務的優勢,避免其中的陷阱。
Cellery網站和GitHub庫中提供了更多有趣的內容和學習材料。
關於作者
Dakshitha Ratnayake是WSO2的企業架構師,他在軟件開發、解決方案架構和中間件技術方面有超過10年的經驗。這是作者爲InfoQ撰寫的第一篇文章。
原文鏈接:
Cellery: A Code-First Approach to Deploy Applications on Kubernetes