美團 Serverless 的探索與實踐

Serverless 是目前比較熱門的技術話題,各大雲平臺以及互聯網大廠內部都在積極建設 Serverless 產品。本文將介紹美團 Serverless 產品在落地過程中的一些實踐經驗,其中包括技術選型的考量、系統的詳細設計、系統穩定性優化、產品的周邊生態建設以及在美團的落地情況。雖然各個公司的背景不盡相同,但總有一些可以相互借鑑的思路或方法,希望能給大家帶來一些啓發或者幫助。

1 背景

Serverless 一詞於 2012 年被提出,2014 年由於亞馬遜的 AWS Lambda 無服務器計算服務的興起,而被大家廣泛認知。Serverless 通常被直譯成 “無服務器”,無服務器計算是可以讓用戶在不考慮服務器的情況下構建並運行應用程序。使用無服務器計算,應用程序仍在服務器上運行,但所有服務器管理工作均由 Serverless 平臺負責。如機器申請、代碼發佈、機器宕機、實例擴縮容、機房容災等都由平臺幫助自動完成,業務開發只需考慮業務邏輯的實現即可。

回顧計算行業的發展歷程,基礎設施從物理機到虛擬機,再從虛擬機到容器;服務架構從傳統單體應用架構到 SOA 架構,再從 SOA 架構到微服務架構。從基礎設施和服務架構兩條主線來看整體技術發展趨勢,大家可能會發現,不論是基礎設施還是服務架構,都是從大往小或者由巨到微的方向上演進,這種演變的本質原則無非是解決資源成本或者研發效率的問題。當然,Serverless 也不例外,它也是用來解決這兩個方面的問題:

雖然 AWS 在 2014 年就推出了第一個 Serverless 產品 Lambda,但 Serverless 技術在國內的應用一直不溫不火。不過近兩三年,在容器、Kubernetes 以及雲原生等技術的推動下,Serverless 技術迅速發展,國內各大互聯網公司都在積極建設 Serverless 相關產品,探索 Serverless 技術的落地。在這種背景下,美團也於 2019 年初開始了 Serverless 平臺的建設,內部項目名稱爲 Nest

截止到目前,Nest 平臺已經過兩年的建設,回顧整體的建設過程,主要經歷了以下三個階段:

2 快速驗證,落地 MVP 版本

2.1 技術選型

建設 Nest 平臺,首要解決的就是技術選型問題,Nest 主要涉及三個關鍵點的選型:演進路線、基礎設施、開發語言。

2.1.1 演進路線

起初 Serverless 服務主要包含 FaaS(Function as a Service)和 BaaS(Backend as a Service),近幾年 Serverless 的產品領域有所擴張,它還包含面向應用的 Serverless 服務。

面向應用的 Serverless 服務:如 Knative,它提供了從代碼包到鏡像的構建、部署以及實例彈性伸縮等全面的服務託管能力,公有云產品有 Google Cloud Run(基於 Knative)、阿里雲的 SAE(Serverless Application Engine)。

在美團內部,BaaS 產品其實就是內部的中間件以及底層服務等,它們經過多年的發展,已經非常豐富且成熟了。因此,在美團的 Serverless 產品演進主要在函數計算服務和麪嚮應用的 Serverless 服務兩個方向上。那究竟該如何演進呢?當時主要考慮到在業界 FaaS 函數計算服務相對於面向應用的 Serverless 服務來說,更加成熟且確定。因此,我們決定 “先建設 FaaS 函數計算服務,再建設面向應用的 Serverless 服務” 這樣一條演進路線。

2.1.2 基礎設施

由於彈性伸縮是 Serverless 平臺必備的能力,因此 Serverless 必然涉及到底層資源的調度和管理。這也是爲什麼當前業界有很多開源的 Serverless 產品(如 OpenFaaS、Fission、Nuclio、Knative 等)是基於 Kubernetes 來實現的,因爲這種選型能夠充分利用 Kubernetes 的基礎設施的管理能力。在美團內部基礎設施產品是 Hulk,雖然 Hulk 是基於 Kubernetes 封裝後的產品,但 Hulk 在落地之初考慮到落地難度以及各種原因,最終未按照原生的方式來使用 Kubernetes,並且在容器層採用的也是富容器模式。

在這種歷史背景下,我們在做基礎設施選型時就面臨兩種選項:一是使用公司的 Hulk 來作爲 Nest 的基礎設施(非原生 Kubernetes),二是採用原生 Kubernetes 基礎設施。我們考慮到當前業界使用原生 Kubernetes 是主流趨勢並且使用原生 Kubernetes 還能充分利用 Kubernetes 原生能力,可以減少重複開發。因此,最終考量的結果是我們採用了原生 Kubernetes 作爲我們的基礎設施

2.1.3 開發語言

雖然無論在雲原生領域,還是 Kubernetes 的生態中,Golang 都更加主流,但在美團 Java 纔是使用最廣泛的語言,相比 Golang,Java 在公司內部生態比較好。因此,在語言的選型上我們選擇了 Java 語言。在 Nest 產品開發之初,Kubernetes 社區的 Java 客戶端還不夠完善,但隨着項目的推進,社區的 Java 客戶端也逐漸豐富了起來,目前已經完全夠用了。另外,我們也在使用過程中,也貢獻了一些 Pull Request,反哺了社區。

2.2 架構設計

基於以上的演進路線、基礎設施、開發語言的選型,我們進行了 Nest 產品的架構設計。

在整體的架構上,流量由 EventTrigger(事件觸發源,如 Nginx、應用網關、定時任務、消息隊列、RPC 調用等)觸發到 Nest 平臺,Nest 平臺內會根據流量的特徵路由到具體函數實例,觸發函數執行,而函數內部代碼邏輯可以調用公司內的各個 BaaS 服務,最終完成函數的執行,返回結果。

圖 1 FaaS 架構圖

在技術實現上,Nest 平臺使用 Kubernetes 作爲基礎底座並適當參考了一些 Knative 的優秀設計,在其架構內部主要由以下幾個核心部分組成:

圖 2 Nest 架構圖

2.3 流程設計

在具體的 CI/CD 流程上,Nest 又與傳統的模式有何區別呢?爲了說明這個問題,我們先來看一看在 Nest 平臺上函數的整體生命週期怎樣的?具體有以下四個階段:構建、版本、部署、伸縮。

就這四個階段來看,Nest 與傳統的 CI/CD 流程本質區別在於部署和伸縮:傳統的部署是感知機器的,一般是將代碼包發佈到確定的機器上,但 Serverless 是要向用戶屏蔽機器的(在部署時,可能函數的實例數還是 0);另外,傳統的模式一般是不具備動態擴縮容的,而 Serverless 則不同,Serverless 平臺會根據業務的自身流量需要,進行動態擴縮容。後續章節會詳細講解彈性伸縮,因此這裏我們只探討部署的設計。

部署的核心點在於如何向用戶屏蔽機器?對於這個問題,我們抽象了機器,提出了分組的概念,分組是由 SET(單元化架構的標識,機器上會帶有該標識)、泳道(測試環境隔離標識,機器上會帶有該標識)、區域(上海、北京等)三個信息組成。用戶部署只需在相應的分組上進行操作,而不用涉及到具體機器。能夠做到這些的背後,是由 Nest 平臺幫助用戶管理了機器資源,每次部署會根據分組信息來實時初始化相應的機器實例。

圖 3 函數生命週期

2.4 函數觸發

函數的執行是由事件觸發的。完成函數的觸發,需要實現以下四個流程:

圖 4 函數觸發

2.5 函數執行

函數不同於傳統的服務,傳統的服務是個可執行的程序,但函數不同,函數是代碼片段,自身是不能單獨執行的。那流量觸發到函數實例後,函數是如何執行的呢?

函數的執行的首要問題是函數的運行環境:由於 Nest 平臺是基於 Kubernetes 實現的,因此函數一定是運行在 Kubernetes 的 Pod(實例)內,Pod 內部是容器,容器的內部是運行時,運行時是函數流量接收的入口,最終也是由運行時來觸發函數的執行。一切看起來是那麼的順利成章,但我們在落地時是還是遇到了一些困難,最主要的困難是讓開發同學可以在函數內無縫的使用公司內的組件,如 OCTO(服務框架)、Celler(緩存系統)、DB 等。

在美團的技術體系中,由於多年的技術沉澱,很難在一個純粹的容器(沒有任何其他依賴)中運行公司的業務邏輯。因爲公司的容器中沉澱了很多環境或服務治理等能力,如服務治理的 Agent 服務以及實例環境配置、網絡配置等。

因此,爲了業務在函數內無縫的使用公司內的組件,我們複用公司的容器體系來降低業務編寫函數的成本。但複用公司的容器體系也沒那麼簡單,因爲在公司內沒有人試過這條路,Nest 是公司第一個基於原生 Kubernetes 建設的平臺,“第一個喫螃蟹的人” 總會遇到一些坑。對於這些坑,我們只能在推進過程中 “逢山開路,遇水搭橋”,遇到一個解決一個。總結下來,其中最核心的是在容器的啓動環節打通的 CMDB 等技術體系,讓運行函數的容器與開發同學平時申請的機器用起來沒有任何區別。

圖 5 函數執行

2.6 彈性伸縮

彈性伸縮的核心問題主要有三個:什麼時候伸縮,伸縮多少,伸縮的速度快不快?也就是伸縮時機、伸縮算法、伸縮速度的問題。

除了基本的擴縮容能力,我們還支持了伸縮到 0,支持配置最大、最小實例數(最小實例即預留實例)。伸縮到 0 的具體實現是,我們在事件網關內部增加了激活器模塊,當函數無實例時,會將函數的請求流量緩存在激活器內部,然後立即通過流量的 Metrics 去驅動彈性伸縮組件進行擴容,等擴容的實例啓動完成後,激活器再將緩存的請求重試到擴容的實例上觸發函數執行。

圖 6 彈性伸縮

3 優化核心技術,保障業務穩定性

3.1 彈性伸縮優化

上面提到的伸縮時機、伸縮算法、伸縮速度這三要素都是理想情況下的模型,尤其是伸縮速度,當前技術根本做不到毫秒級別的擴縮容。因此,在線上實際場景中,彈性伸縮會存在一些不符合預期的情況,比如實例伸縮比較頻繁或者擴容來不及,導致服務不太穩定的問題。

下圖展示的是線上彈性伸縮的真實案例(配置的最小實例數爲 4,單實例閾值 100,閾值使用率 0.7),其中上半部分是業務每秒的請求數,下半部分是擴縮實例的決策圖,可以看到在成功率 100% 的情況下,業務完美應對流量高峯。

圖 7 彈性伸縮案例

3.2 冷啓動優化

冷啓動是指在函數調用鏈路中包含了資源調度、鏡像 / 代碼下載、啓動容器、運行時初始化、用戶代碼初始化等環節。當冷啓動完成後,函數實例就緒,後續請求就能直接被函數執行。冷啓動在 Serverless 領域至關重要,它的耗時決定了彈性伸縮的速度。

所謂 “天下武功,無堅不破,唯快不破”,這句話在 Serverless 領域也同樣受用。試想如果拉起一個實例足夠快,快到毫秒級別,那幾乎所有的函數實例都可以縮容到 0,等有流量時,再擴容實例處理請求,這對於存在高低峯流量的業務將極大的節省機器資源成本。當然,理想很豐滿,現實很骨感。做到毫秒級別幾乎不可能。但只要冷啓動時間越來越短,成本自然就會越來越低,另外,極短的冷啓動時間對伸縮時函數的可用性以及穩定性都有莫大的好處。

圖 8 冷啓動的各個階段

冷啓動優化是個循序漸進的過程,我們對冷啓動優化主要經歷了三個階段:鏡像啓動優化、資源池優化、核心路徑優化。

圖 9 鏡像啓動優化成果

圖 10 資源池優化成果

3.3 高可用保障

說到高可用,對於一般的平臺,指的就是平臺自身的高可用,但 Nest 平臺有所不同,Nest 的高可用還包含託管在 Nest 平臺上的函數。因此,Nest 的高可用保障需要從平臺和業務函數兩個方面着手。

3.3.1 平臺高可用

對平臺的高可用,Nest 主要從架構層、服務層、監控運營層、業務視角層面都做了全面的保障。

圖 11 部署架構

3.3.2 業務高可用

對於業務高可用,Nest 主要從服務層、平臺層兩個層面做了相關的保障。

圖 12 業務監控

3.4 容器穩定性優化

前文已提到,Serverless 與傳統模式在 CI/CD 流程上是不同的,傳統模式都是事先準備好機器然後部署程序,而 Serverless 則是根據流量的高低峯實時彈性擴縮容實例。當新實例擴容出來後,會立即處理業務流量。這聽起來貌似沒什麼毛病,但在富容器生態下是存在一些問題的:我們發現剛擴容的機器負載非常高,導致一些業務請求執行失敗,影響業務可用性。

分析後發現主要是因爲容器啓動後,運維工具會進行 Agent 升級、配置修改等操作,這些操作非常耗 CPU。同在一個富容器中,自然就搶佔了函數進程的資源,導致用戶進程不穩定。另外,函數實例的資源配置一般比傳統服務的機器要小很多,這也加劇了該問題的嚴重性。基於此,我們參考業界,聯合容器設施團隊,落地了輕量級容器,將運維的所有 Agent 放到 Sidecar 容器中,而業務的進程單獨放到 App 容器中。採用這種容器的隔離機制,保障業務的穩定性。同時,我們也推動了容器裁剪計劃,去掉一些不必要的 Agent。

圖 13 輕量級容器

4 完善生態,落實收益

Serverless 是個系統工程,在技術上涉及到 Kubernetes、容器、操作系統、JVM、運行時等各種技術,在平臺能力上涉及到 CI/CD 各個流程的方方面面。

爲了給用戶提供極致的開發體驗,我們爲用戶提供了開發工具的支持,如 CLI(Command Line Interface)、WebIDE 等。爲了解決現有上下游技術產品的交互的問題,我們與公司現有的技術生態做了融合打通,方便開發同學使用。爲了方便下游的集成平臺對接,我們開放了平臺的 API,實現 Nest 賦能各下游平臺。針對容器過重,系統開銷大,導致低頻業務函數自身資源利用率不高的問題,我們支持了函數合併部署,成倍提升資源利用率。

4.1 提供研發工具

開發工具能夠降低平臺的使用成本,幫助開發同學快速的進行 CI/CD 流程。目前 Nest 提供了 CLI 工具,幫助開發同學快速完成創建應用、本地構建、本地測試、Debug、遠程發佈等操作。Nest 還提供了 WebIDE,支持在線一站式完成代碼的修改、構建、發佈、測試。

4.2 融合技術生態

僅支持這些研發工具還是不夠的,項目推廣使用後,我們很快就發現開發同學對平臺有了新的需求,如無法在 Pipeline 流水線、線下服務實例編排平臺上完成對函數的操作,這對我們項目的推廣也形成了一些阻礙。因此,我們融合這些公司的成熟技術生態,打通了 Pipeline 流水線等平臺,融入到現有的上下游技術體系內,解決用戶的後顧之憂。

4.3 開放平臺能力

有很多 Nest 的下游解決方案平臺,如 SSR(Server Side Render)、服務編排平臺等,通過對接 Nest 的 OpenAPI,實現了生產力的進一步解放。例如,不用讓開發同學自己去申請、管理和運維機器資源,就能夠讓用戶非常快速的實現一個 SSR 項目或者編排程序從 0 到 1 的創建、發佈與託管。

Nest 除了開放了平臺的 API,還對用戶提供了自定義資源池的能力,擁有了該項能力,開發同學可以定製自己的資源池,定製自己的機器環境,甚至可以下沉一些通用的邏輯,實現冷啓動的進一步優化。

4.4 支持合併部署

合併部署指的是將多個函數部署在一個機器實例內。合併部署的背景主要有兩個:

基於這兩個背景,我們考慮支持合併部署,將一些低頻的函數部署到同一個機器實例內,來提升預留實例中業務進程的資源利用率。

在具體實現上,我們參考 Kubernetes 的設計方案,設計了一套基於 Sandbox 的函數合併部署體系(每個 Sandbox 就是一個函數資源),將 Pod 類比成 Kubernetes 的 Node 資源,Sandbox 類比成 Kubernetes 的 Pod 資源,Nest Sidecar 類比成 Kubelet。爲了實現 Sandbox 特有的部署、調度等能力,我們還自定義了一些 Kubernetes 資源(如 SandboxDeployment、SandboxReplicaSet、SandboxEndpoints 等)來支持函數動態插拔到具體的 Pod 實例上。

圖 14 合併部署架構

除此之外,在合併部署的形態下,函數之間的隔離性也是不可迴避的問題。爲了儘可能的解決函數(合併在同一個實例中)之間的互相干擾問題,在 Runtime 的實現上,我們針對 Node.js 和 Java 語言的特點採取了不同的策略:Node.js 語言的函數使用不同的進程來實現隔離,而 Java 語言的函數,我們採用類加載隔離。採用這種策略的主要原因是由於 Java 進程佔用內存空間相較於 Node.js 進程會大很多。

5 落地場景、收益

目前 Nest 產品在美團前端 Node.js 領域非常受歡迎,也是落地最廣泛的技術棧。當前 Nest 產品在美團前端已實現了規模化落地,幾乎涵蓋了所有業務線,接入了大量的 B/C 端的核心流量。

5.1 落地場景

具體的落地前端場景有:BFF(Backend For Frontend)、CSR(Client Side Render)/SSR(Server Side Render)、後臺管理平臺、定時任務、數據處理等。

5.2 落地收益

Serverless 的收益是非常明顯的,尤其在前端領域,大量的業務接入已是最好的說明。具體收益,從以下兩個方面分別來看:

6 未來規劃

作者簡介

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/n5JVLJ4Vf5stWR7SjVchLw