開源 serverless 產品原理剖析(二)
背景
本文是開源 serverless 產品原理剖析系列文章的第二篇,關於 serverless 背景知識的介紹可參考文章開源 serverless 產品原理剖析(一) - Kubeless,這裡不再贅述。
Fission 簡介
Fission 是由私有云服務提供商 Platform9 領導開源的 serverless 產品,它藉助 kubernetes 靈活強大的編排能力完成容器的管理排程工作,而將重心投入到 FaaS 功能的開發上,其發展目標是成為 AWS lambda 的開源替代品。從 CNCF 視角,fission 屬於 serverless 平臺型產品。
核心概念
Fission 包含 Function、Environment 、Trigger 三個核心概念,其關係如下圖所示:
- Function - 代表用特定語言編寫的需要被執行的程式碼片段。
- Environment- 用於執行使用者函式的特定語言環境。
- Trigger - 用於關聯函式和事件源。如果把事件源比作生產者,函式比作執行者,那麼觸發器就是聯絡兩者的橋樑。
關鍵元件
Fission 包含 Controller、Router、Executor 三個關鍵元件:
- Controller - 提供了針對 fission 資源的增刪改查操作介面,包括 functions、triggers、environments、Kubernetes event watches 等。它是 fission CLI 的主要互動物件。
- Router - 函式訪問入口,同時也實現了 HTTP 觸發器。它負責將使用者請求以及各種事件源產生的事件轉發至目標函式。
- Executor - fission 包含 PoolManager 和 NewDeploy 兩類執行器,它們控制著 fission 函式的生命週期。
原理剖析
本章節將從以下幾個方面介紹 fission 的基本原理:
- 函式執行器 - 它是理解 fission 工作原理的基礎。
- Pod 特化 - 它是理解 fission 如何根據使用者原始碼構建出可執行函式的關鍵。
- 觸發器 - 它是理解 fission 函式各種觸發原理的入口。
- 自動伸縮 - 它是理解 fission 如何根據負載動態調整函式個數的捷徑。
- 日誌處理 - 它是理解 fission 如何處理各函式日誌的有效手段。
本文所做的調研基於kubeless 0.12.0
和k8s 1.13
。
函式執行器
CNCF 對函式生命週期的定義如下圖所示,它描繪了函式構建、部署、執行的一般流程。
要理解 fission,首先需要了解它是如何管理函式生命週期的。Fission 的函式執行器是其控制函式生命週期的關鍵元件。Fission 包含 PoolManager 和 NewDeploy 兩類執行器,下面分別對兩者進行介紹。
PoolManager
Poolmgr 使用了池化技術,它通過為每個 environment 維持了一定數量的通用 pod 並在函式被觸發時將 pod 特化,大大降低了函式的冷啟動的時間。同時,poolmgr 會自動清理一段時間內未被訪問的函式,減少閒置成本。該執行器的原理如下圖所示。
此時,函式的生命週期如下:
-
使用 fission CLI 向 controller 傳送請求,建立函式執行時需要的特定語言環境。例如,以下命令將建立一個 python 執行環境。
fission environment create --name python --image fission/python-env
- Poolmgr 定期同步 environment 資源列表,參考 eagerPoolCreator。
- Poolmgr 遍歷 environment 列表,使用 deployment 為每個 environment 建立一個通用 pod 池,參考 MakeGenericPool。
-
使用 fission CLI 向 controller 傳送建立函式的請求。此時,controller 只是將函式原始碼等資訊持久化儲存,並未真正構建好可執行函式。例如,以下命令將建立一個名為 hello 的函式,該函式選用已經建立好的 python 執行環境,原始碼來自 hello.py,執行器為 poolmgr。
fission function create --name hello --env python --code hello.py --executortype poolmgr
- Router 接收到觸發函式執行的請求,載入目標函式相關資訊。
- Router 向 executor 傳送請求獲取函式訪問入口,參考 GetServiceForFunction。
- Poolmgr 從函式指定環境對應的通用 pod 池裡隨機選擇一個 pod 作為函式執行的載體,這裡通過更改 pod 的標籤讓其從 deployment 中“獨立”出來,參考 _choosePod。K8s 發現 deployment 所管理 pod 的實際副本數少於目標副本數後會對 pod 進行補充,這樣便實現了保持通用 pod 池中的 pod 個數的目的。
- 特化處理被挑選出來的 pod,參考 specializePod。
- 為特化後的 pod 建立 ClusterIP 型別的 service,參考 createSvc。
- 將函式的 service 資訊返回給 router,router 會將 serviceUrl 快取以避免頻繁向 executor 傳送請求。
- Router 使用返回的 serviceUrl 訪問函式。
- 請求最終被路由至執行函式的 pod。
- 如果該函式一段時間內未被訪問會被自動清理,包括該函式的 pod 和 service,參考 idleObjectReaper。
NewDeploy
Poolmgr 很好地平衡了函式的冷啟動時間和閒置成本,但無法讓函式根據度量指標自動伸縮。NewDeploy 執行器實現了函式 pod 的自動伸縮和負載均衡,該執行器的原理如下圖所示。
此時,函式的生命週期如下:
- 使用 fission CLI 向 controller 傳送請求,建立函式執行時需要的特定語言環境。
-
使用 fission CLI 向 controller 傳送建立函式的請求。例如,以下命令將建立一個名為 hello 的函式,該函式選用已經建立好的 python 執行環境,原始碼來自 hello.py,執行器為 newdeploy,目標副本數在 1 到 3 之間,目標 cpu 使用率是 50%。
fission fn create --name hello --env python --code hello.py --executortype newdeploy --minscale 1 --maxscale 3 --targetcpu 50
- Newdeploy 會註冊一個 funcController 持續監聽針對 function 的 ADD、UPDATE、DELETE 事件,參考 initFuncController。
-
Newdeploy 監聽到了函式的 ADD 事件後,會根據 minscale 的取值判斷是否立即為該函式建立相關資源。
- minscale > 0,則立即為該函式建立 service、deployment、HPA(deployment 管理的 pod 會特化)。
- minscale <= 0,延遲到函式被真正觸發時建立。
- Router 接收到觸發函式執行的請求,載入目標函式相關資訊。
- Router 向 newdeploy 傳送請求獲取函式訪問入口。如果函式所需資源已被建立,則直接返回訪問入口。否則,建立好相關資源後再返回。
- Router 使用返回的 serviceUrl 訪問函式。
- 如果該函式一段時間內未被訪問,函式的目標副本數會被調整成 minScale,但不會刪除 service、deployment、HPA 等資源,參考 idleObjectReaper。
執行器比較
實際使用過程中,使用者需要從延遲和閒置成本兩個角度考慮選擇何種型別的執行器。不同執行器的特點如下表所示。
執行器型別 | 最小副本數 | 延遲 | 閒置成本 |
---|---|---|---|
Newdeploy | 0 | 高 | 非常低 - pods 一段時間未被訪問會被自動清理掉。 |
Newdeploy | >0 | 低 | 中等 - 每個函式始終會有一定數量的 pod 在執行。 |
Poolmgr | 0 | 低 | 低 - 通用池中的 pod 會一直執行。 |
小結
Fission 將函式執行器的概念暴露給了使用者,增加了產品的使用成本。實際上可以將 poolmgr 和 newdeploy 技術相結合,通過建立 deployment 將特化後的 pod 管理起來,這樣可以很自然地利用 HPA 來實現對函式的自動伸縮。
Pod 特化
在介紹函式執行器時多次提到了 pod 特化,它是 fission 將環境容器變成函式容器的奧祕。Pod 特化的本質是通過向容器傳送特化請求讓其載入使用者函式,其原理如下圖所示。
一個函式 pod 由下面兩種容器組成:
- Fetcher - 下載使用者函式並將其放置在共享 volume 裡。不同語言環境使用了相同的 fetcher 映象,fetcher 的工作原理可參考程式碼 fetcher.go。
- Env - 使用者函式執行的載體。當它成功載入共享 volume 裡的使用者函式後,便可接收使用者請求。
具體步驟如下:
- 容器 fetcher 接收到拉取使用者函式的請求。
- Fetcher 從 K8s CRD 或 storagesvc 處獲取使用者函式。
- Fetcher 將函式檔案放置在共享的 volume 裡,如果檔案被壓縮還會負責解壓。
- 容器 env 接收到載入使用者函式的命令。
- Env 從共享 volume 中載入 fetcher 為其準備好的使用者函式。
- 特化流程結束,容器 env 開始處理使用者請求。
觸發器
前面的章節介紹了 fission 函式的構建、載入和執行的邏輯,本章節主要介紹如何基於各種事件源觸發 fission 函式的執行。CNCF 將函式的觸發方式分成了如下圖所示的幾種類別,關於它們的詳細介紹可參考連結 Function Invocation Types。
對於 fission 函式,最簡單的觸發方式是使用 fission CLI,另外還支援通過各種觸發器。下表展示了 fission 函式目前支援的觸發方式以及它們所屬的類別。
觸發方式 | 類別 |
---|---|
fission CLI | Synchronous Req/Rep |
HTTP Trigger | Synchronous Req/Rep |
Time Trigger | Job (Master/Worker) |
Message Queue Trigger 1. nats-streaming 2. azure-storage-queue 3. kafka |
Async Message Queue |
Kubernetes Watch Trigger | Async Message Queue |
下圖展示了 fission 函式部分觸發方式的原理:
HTTP trigger
所有發往 fission 函式的請求都會由 router 轉發,fission 通過為 router 建立 NodePort 或 LoadBalancer 型別的 service 讓其能夠被外界訪問。
除了直接訪問 router,還可以利用 K8s ingress 機制實現 http trigger。以下命令將為函式 hello 建立一個 http trigger,並指定訪問路徑為/echo
。
fission httptrigger create --url /echo --method GET --function hello --createingress --host example.com
該命令會建立如下 ingress 物件,可以參考 createIngress 深入瞭解 ingress 的建立邏輯。
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
# 該 Ingress 的名稱
name: xxx
...
spec:
rules:
- host: example.com
http:
paths:
- backend:
# 指向 router service
serviceName: router
servicePort: 80
# 訪問路徑
path: /echo
Ingress 只是用於描述路由規則,要讓規則生效、實現請求轉發,叢集中需要有一個正在執行的 ingress controller。想要深入瞭解 ingress 原理可參考系列文章第一篇中的 HTTP trigger 章節。
Time trigger
如果希望定期觸發函式執行,需要為函式建立 time trigger。Fission 使用 deployment 部署了元件 timer,該元件負責管理使用者建立的 timer trigger。Timer 每隔一段時間會同步一次 time trigger 列表,並通過 golang 中被廣泛使用的 cron 庫 robfig/cron 定期觸發和各 timer trigger 相關聯函式的執行。
以下命令將為函式 hello 建立一個名為halfhourly
的 time trigger,該觸發器每半小時會觸發函式 hello 執行一次。這裡使用了標準的 cron 語法定義執行計劃。
fission tt create --name halfhourly --function hello --cron "*/30 * * * *"
trigger 'halfhourly' created
Message queue trigger
為了支援非同步觸發,fission 允許使用者建立訊息佇列觸發器。目前可供選擇的訊息佇列有 nats-streaming、azure-storage-queue、kafka,下面以 kafka 為例描述訊息佇列觸發器的使用方法和實現原理。
以下命令將為函式 hello 建立一個基於 kafka 的訊息佇列觸發器hellomsg
。該觸發器訂閱了主題 input 中的訊息,每當有訊息到達它便會觸發函式執行。如果函式執行成功,會將結果寫入主題 output 中,否則將結果寫入主題 error 中。
fission mqt create --name hellomsg --function hello --mqtype kafka --topic input --resptopic output --errortopic error
Fission 使用 deployment 部署了元件 mqtrigger-kafka,該元件負責管理使用者建立的 kafka trigger。它每隔一段時間會同步一次 kafka trigger 列表,併為每個 trigger 建立 1 個用於執行觸發邏輯的 go routine,觸發邏輯如下:
- 消費 topic 欄位指定主題中的訊息;
- 通過向 router 傳送請求觸發函式執行並等待函式返回;
- 如果函式執行成功,將返回結果寫入 resptopic 欄位指定的主題中,並確認訊息已被處理;否則,將結果寫入 errortopic 欄位指定的主題中。
小結
- Fission 提供了一些常用觸發器,但缺少對 CNCF 規範裡提到的
Message/Record Streams
觸發方式的支援,該方式要求訊息被順序處理; - 如果有其它事件源想要接入可以參考 fission 觸發器的設計模式自行實現。
自動伸縮
K8s 通過 Horizontal Pod Autoscaler 實現 pod 的自動水平伸縮。對於 fission,只有通過 newdeploy 方式建立的函式才能利用 HPA 實現自動伸縮。
以下命令將建立一個名為 hello 的函式,執行該函式的 pod 會關聯一個 HPA,該 HPA 會將 pod 數量控制在 1 到 6 之間,並通過增加或減少 pod 個數使得所有 pod 的平均 cpu 使用率維持在 50%。
fission fn create --name hello --env python --code hello.py --executortype newdeploy --minmemory 64 --maxmemory 128 --minscale 1 --maxscale 6 --targetcpu 50
Fission 使用的是autoscaling/v1
版本的 HPA API,該命令將要建立的 HPA 如下:
apiVersion: autoscaling/v1
kind: HorizontalPodAutoscaler
metadata:
labels:
executorInstanceId: xxx
executorType: newdeploy
functionName: hello
...
# 該 HPA 名稱
name: hello-${executorInstanceId}
# 該 HPA 所在名稱空間
namespace: fission-function
...
spec:
# 允許的最大副本數
maxReplicas: 6
# 允許的最小副本數
minReplicas: 1
# 該 HPA 關聯的目標
scaleTargetRef:
apiVersion: extensions/v1beta1
kind: Deployment
name: hello-${executorInstanceId}
# 目標 CPU 使用率
targetCPUUtilizationPercentage: 50
想了解 HPA 的原理可參考系列文章第一篇中的自動伸縮章節,那裡詳細介紹了 K8s 如何獲取和使用度量資料以及目前採用的自動伸縮策略。
小結
- 和 kubeless 類似,fission 避免了將建立 HPA 的複雜細節直接暴露給使用者,但這是以犧牲功能為代價的;
- Fission 目前提供的自動伸縮功能過於侷限,只對通過 newdeploy 方式建立的函式有效,且只支援基於 cpu 使用率這一種度量指標(kubeless 支出基於 cpu 和 qps)。本質上是因為 fission 目前仍然使用的是 v1 版本的 HPA,如果使用者希望基於新的度量指標或者綜合多項度量指標可以直接使用 hpa-v2 提供的功能;
- 目前 HPA 的擴容縮容策略是基於既成事實被動地調整目標副本數,還無法根據歷史規律預測性地進行擴容縮容。
日誌處理
為了能更好地洞察函式的執行情況,往往需要對函式產生的日誌進行採集、處理和分析。Fission 日誌處理的原理如下圖所示。
日誌處理流程如下:
- 使用 DaemonSet 在叢集中的每個工作節點上部署一個 fluentd 例項用於採集當前機器上的容器日誌,參考 logger。這裡,fluentd 容器將包含容器日誌的宿主機目錄
/var/log/
和/var/lib/docker/containers
掛載進來,方便直接採集。 - Fluentd 將採集到的日誌儲存至 influxdb 中。
- 使用者使用 fission CLI 檢視函式日誌。例如,使用命令
fission function logs --name hello
可以檢視到函式 hello 產生的日誌。
小結
目前,fission 只做到了函式日誌的集中化儲存,能夠提供的查詢分析功能非常有限。另外,influxdb 更適合儲存監控指標類資料,無法滿足日誌處理與分析的多樣性需求。
函式是執行在容器裡的,因此函式日誌處理本質上可歸結為容器日誌處理。針對容器日誌,阿里雲日誌服務團隊提供了成熟完備的解決方案,欲知詳情可參考文章面向容器日誌的技術實踐。
總結
在介紹完 fission 的基本原理後,不妨從以下幾個方面將其和第一篇介紹的 kubeless 作一個簡單對比。
- 觸發方式 - 兩款產品都支援常用的觸發方式,但 kubeless 相比 fission 支援的更全面,且更方便接入新的資料來源。
- 自動伸縮 - 兩款產品的自動伸縮能力都還比較基礎,支援的度量指標都比較少,且底層都依賴於 K8s HPA。
- 函式冷啟動時間 - fission 通過池化技術降低了函式冷啟動時間,kubeless 在這一塊並未作過多優化。
- 高階功能 - fission 支援灰度釋出、自定義工作流等高階功能,kubeless 目前還不支援。