開源 serverless 產品原理剖析(二) - Fission

背景

本文是開源 serverless 產品原理剖析系列文章的第二篇,關於 serverless 背景知識的介紹可參考文章開源 serverless 產品原理剖析(一) - Kubeless,這裏再也不贅述。node

Fission 簡介

Fission 是由私有云服務提供商 Platform9 領導開源的 serverless 產品,它藉助 kubernetes 靈活強大的編排能力完成容器的管理調度工做,而將重心投入到 FaaS 功能的開發上,其發展目標是成爲 AWS lambda 的開源替代品。從 CNCF 視角,fission 屬於 serverless 平臺型產品。python

核心概念

Fission 包含 FunctionEnvironment 、Trigger 三個核心概念,其關係以下圖所示:git

  1. Function - 表明用特定語言編寫的須要被執行的代碼片斷。
  2. Environment- 用於運行用戶函數的特定語言環境。
  3. Trigger - 用於關聯函數和事件源。若是把事件源比做生產者,函數比做執行者,那麼觸發器就是聯繫二者的橋樑。

關鍵組件

Fission 包含 Controller、Router、Executor 三個關鍵組件:github

  1. Controller - 提供了針對 fission 資源的增刪改查操做接口,包括 functions、triggers、environments、Kubernetes event watches 等。它是 fission CLI 的主要交互對象。
  2. Router - 函數訪問入口,同時也實現了 HTTP 觸發器。它負責將用戶請求以及各類事件源產生的事件轉發至目標函數。
  3. Executor - fission 包含 PoolManager 和 NewDeploy 兩類執行器,它們控制着 fission 函數的生命週期。

原理剖析

本章節將從如下幾個方面介紹 fission 的基本原理:golang

  1. 函數執行器 - 它是理解 fission 工做原理的基礎。
  2. Pod 特化 - 它是理解 fission 如何根據用戶源碼構建出可執行函數的關鍵。
  3. 觸發器 - 它是理解 fission 函數各類觸發原理的入口。
  4. 自動伸縮 - 它是理解 fission 如何根據負載動態調整函數個數的捷徑。
  5. 日誌處理 - 它是理解 fission 如何處理各函很多天志的有效手段。

本文所作的調研基於kubeless 0.12.0k8s 1.13docker

函數執行器

CNCF 對函數生命週期的定義以下圖所示,它描繪了函數構建、部署、運行的通常流程。apache

要理解 fission,首先須要瞭解它是如何管理函數生命週期的。Fission 的函數執行器是其控制函數生命週期的關鍵組件。Fission 包含 PoolManager 和 NewDeploy 兩類執行器,下面分別對二者進行介紹。設計模式

PoolManager

Poolmgr 使用了池化技術,它經過爲每一個 environment 維持了必定數量的通用 pod 並在函數被觸發時將 pod 特化,大大下降了函數的冷啓動的時間。同時,poolmgr 會自動清理一段時間內未被訪問的函數,減小閒置成本。該執行器的原理以下圖所示。api

此時,函數的生命週期以下:緩存

  1. 使用 fission CLI 向 controller 發送請求,建立函數運行時須要的特定語言環境。例如,如下命令將建立一個 python 運行環境。

    fission environment create --name python --image fission/python-env
  2. Poolmgr 按期同步 environment 資源列表,參考 eagerPoolCreator
  3. Poolmgr 遍歷 environment 列表,使用 deployment 爲每一個 environment 建立一個通用 pod 池,參考 MakeGenericPool
  4. 使用 fission CLI 向 controller 發送建立函數的請求。此時,controller 只是將函數源碼等信息持久化存儲,並未真正構建好可執行函數。例如,如下命令將建立一個名爲 hello 的函數,該函數選用已經建立好的 python 運行環境,源碼來自 hello.py,執行器爲 poolmgr。

    fission function create --name hello --env python --code hello.py --executortype poolmgr
  5. Router 接收到觸發函數執行的請求,加載目標函數相關信息。
  6. Router 向 executor 發送請求獲取函數訪問入口,參考 GetServiceForFunction
  7. Poolmgr 從函數指定環境對應的通用 pod 池裏隨機選擇一個 pod 做爲函數執行的載體,這裏經過更改 pod 的標籤讓其從 deployment 中「獨立」出來,參考 _choosePod。K8s 發現 deployment 所管理 pod 的實際副本數少於目標副本數後會對 pod 進行補充,這樣便實現了保持通用 pod 池中的 pod 個數的目的。
  8. 特化處理被挑選出來的 pod,參考 specializePod
  9. 爲特化後的 pod 建立 ClusterIP 類型的 service,參考 createSvc
  10. 將函數的 service 信息返回給 router,router 會將 serviceUrl 緩存以免頻繁向 executor 發送請求。
  11. Router 使用返回的 serviceUrl 訪問函數。
  12. 請求最終被路由至運行函數的 pod。
  13. 若是該函數一段時間內未被訪問會被自動清理,包括該函數的 pod 和 service,參考 idleObjectReaper

NewDeploy

Poolmgr 很好地平衡了函數的冷啓動時間和閒置成本,但沒法讓函數根據度量指標自動伸縮。NewDeploy 執行器實現了函數 pod 的自動伸縮和負載均衡,該執行器的原理以下圖所示。

此時,函數的生命週期以下:

  1. 使用 fission CLI 向 controller 發送請求,建立函數運行時須要的特定語言環境。
  2. 使用 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
  3. Newdeploy 會註冊一個 funcController 持續監聽針對 function 的 ADD、UPDATE、DELETE 事件,參考 initFuncController
  4. Newdeploy 監聽到了函數的 ADD 事件後,會根據 minscale 的取值判斷是否當即爲該函數建立相關資源。

    1. minscale > 0,則當即爲該函數建立 service、deployment、HPA(deployment 管理的 pod 會特化)。
    2. minscale <= 0,延遲到函數被真正觸發時建立。
  5. Router 接收到觸發函數執行的請求,加載目標函數相關信息。
  6. Router 向 newdeploy 發送請求獲取函數訪問入口。若是函數所需資源已被建立,則直接返回訪問入口。不然,建立好相關資源後再返回。
  7. Router 使用返回的 serviceUrl 訪問函數。
  8. 若是該函數一段時間內未被訪問,函數的目標副本數會被調整成 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 裏的用戶函數後,即可接收用戶請求。

具體步驟以下:

  1. 容器 fetcher 接收到拉取用戶函數的請求。
  2. Fetcher 從 K8s CRD 或 storagesvc 處獲取用戶函數。
  3. Fetcher 將函數文件放置在共享的 volume 裏,若是文件被壓縮還會負責解壓。
  4. 容器 env 接收到加載用戶函數的命令。
  5. Env 從共享 volume 中加載 fetcher 爲其準備好的用戶函數。
  6. 特化流程結束,容器 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-streamingazure-storage-queuekafka,下面以 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,觸發邏輯以下:

  1. 消費 topic 字段指定主題中的消息;
  2. 經過向 router 發送請求觸發函數執行並等待函數返回;
  3. 若是函數執行成功,將返回結果寫入 resptopic 字段指定的主題中,並確認消息已被處理;不然,將結果寫入 errortopic 字段指定的主題中。

小結

  1. Fission 提供了一些經常使用觸發器,但缺乏對 CNCF 規範裏提到的Message/Record Streams觸發方式的支持,該方式要求消息被順序處理;
  2. 若是有其它事件源想要接入能夠參考 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 如何獲取和使用度量數據以及目前採用的自動伸縮策略。

小結

  1. 和 kubeless 相似,fission 避免了將建立 HPA 的複雜細節直接暴露給用戶,但這是以犧牲功能爲代價的;
  2. Fission 目前提供的自動伸縮功能過於侷限,只對經過 newdeploy 方式建立的函數有效,且只支持基於 cpu 使用率這一種度量指標(kubeless 支出基於 cpu 和 qps)。本質上是由於 fission 目前仍然使用的是 v1 版本的 HPA,若是用戶但願基於新的度量指標或者綜合多項度量指標能夠直接使用 hpa-v2 提供的功能;
  3. 目前 HPA 的擴容縮容策略是基於既成事實被動地調整目標副本數,還沒法根據歷史規律預測性地進行擴容縮容。

日誌處理

爲了能更好地洞察函數的運行狀況,每每須要對函數產生的日誌進行採集、處理和分析。Fission 日誌處理的原理以下圖所示。

日誌處理流程以下:

  1. 使用 DaemonSet 在集羣中的每一個工做節點上部署一個 fluentd 實例用於採集當前機器上的容器日誌,參考 logger。這裏,fluentd 容器將包含容器日誌的宿主機目錄/var/log//var/lib/docker/containers掛載進來,方便直接採集。
  2. Fluentd 將採集到的日誌存儲至 influxdb 中。
  3. 用戶使用 fission CLI 查看函很多天志。例如,使用命令fission function logs --name hello能夠查看到函數 hello 產生的日誌。

小結

目前,fission 只作到了函很多天志的集中化存儲,可以提供的查詢分析功能很是有限。另外,influxdb 更適合存儲監控指標類數據,沒法知足日誌處理與分析的多樣性需求。

函數是運行在容器裏的,所以函很多天志處理本質上可歸結爲容器日誌處理。針對容器日誌,阿里雲日誌服務團隊提供了成熟完備的解決方案,欲知詳情可參考文章面向容器日誌的技術實踐

總結

在介紹完 fission 的基本原理後,不妨從如下幾個方面將其和第一篇介紹的 kubeless 做一個簡單對比。

  1. 觸發方式 - 兩款產品都支持經常使用的觸發方式,但 kubeless 相比 fission 支持的更全面,且更方便接入新的數據源。
  2. 自動伸縮 - 兩款產品的自動伸縮能力都還比較基礎,支持的度量指標都比較少,且底層都依賴於 K8s HPA。
  3. 函數冷啓動時間 - fission 經過池化技術下降了函數冷啓動時間,kubeless 在這一塊並未做過多優化。
  4. 高級功能 - fission 支持灰度發佈自定義工做流等高級功能,kubeless 目前還不支持。

參考資料

原文連接 

相關文章
相關標籤/搜索