FaaS, Fission and K8S_Kubernetes中文社群
雲端計算時代出現了大量XaaS形式的概念,從IaaS、PaaS、SaaS到容器雲引領的CaaS,再到火熱的微服務架構,以及現在越來越多被談起的Serverless和FaaS,我們正在經歷一個技術飛速變革的時代。
什麼是FaaS
Serverless的概念剛剛出現在HackerNews時並不為大眾所接受。後來隨著微服務和事件驅動架構的發展才慢慢引起關注。Serverless並不是說沒有伺服器參與,它通過將複雜的伺服器架構透明化,使開發者專注於“要做什麼”,從而強調了減少開發者對伺服器等計算資源的關注、工作粒度從伺服器切換到任務的思想。2006年第一個支援“隨用隨付”的程式碼執行平臺Zimki問世。2014年亞馬遜AWS推出了Lambda成為最主要的無服務架構的代表。接著Google、IBM和Microsoft也紛紛推出了各自支援Serverless的平臺。
微服務架構近年來是一個非常火爆的話題,大大小小的公司都開始逐步分拆原來的單體應用,試著轉換到由各個模組服務組合成大型的複雜應用。Serverless可以看作是比微服務架構更細粒度的架構模式,即FaaS。Lambda也是FaaS的典型代表,它允許使用者僅僅上傳程式碼而無需提供和管理伺服器,由它負責程式碼的執行、高可用擴充套件,支援從別的AWS服務或其他Web應用直接呼叫等。以電子商務應用為例,微服務中可以將瀏覽商品、新增購物車、下單、支付、檢視物流等拆分為解耦的微服務。在FaaS裡,它可以拆分到使用者的所有CRUD操作程式碼。當發生“下單”事件時,將觸發相應的Functions,交由Lambda執行。人們在越來越多的場景裡將Serverless和FaaS等同起來。
假設現在有下面的JavaScript程式碼:
module.exports = function(context, callback) { callback(200, "Hello, world!"); }
顯然它是一個函式,通過FaaS的方式,我們可以通過訪問一個URL的方式呼叫這個函式。
$ curl -XGET localhost:8080
Hello, world!
FaaS擁有下面的特點:
- FaaS裡的應用邏輯單元都可以看作是一個函式,開發人員只關注如何實現這些邏輯,而不用提前考慮效能優化,讓工作聚焦在這個函式裡,而非應用整體。
- FaaS是無狀態的,天生滿足雲原生(Cloud Native App)應用該滿足的12因子(12 Factors)中對狀態的要求。無狀態意味著本地記憶體、磁盤裡的資料無法被後續的操作所使用。大部分的狀態需要依賴於外部儲存,比如資料庫、網路儲存等。
- FaaS的函式應當可以快速啟動執行,並擁有短暫的生命週期。函式在有限的時間裡啟動並處理任務,並在返回執行結果後終止。如果它執行時間超過了某個閾值,也應該被終止。
- FaaS函式啟動延時受很多因素的干擾。以AWS Lambda為例,如果採用了JS或Python實現了函式,它的啟動時間一般不會超過10~100毫秒。但如果是實現在JVM上的函式,當遇到突發的大流量或者呼叫間隔過長的情況,啟動時間會顯著變長。
- FaaS需要藉助於API Gateway將請求的路由和對應的處理函式進行對映,並將響應結果代理返回給呼叫方。
比如對於一個簡單的3層Web應用,在這裡後端系統實現了大部分業務邏輯:認證、搜尋、事務等,它的架構如下:
如果採用Serverless架構,將認證、資料庫等採用第三方的服務,從原來的單體後端裡分拆出來(可能需要在原來的客戶端里加入一些業務邏輯)。對於大部分的任務,通過函式的形式進行執行,而不再使用一直線上的伺服器進行支援,如此一來它的架構看起來就清晰多了:
這樣的拆分除了讓各個元件(函式)間充分解耦,每個都很好地實現了單一職責原則(SRP, Single Responsibility Principle)以外,它的好處還有:
- 減少開支。通過購買共享的基礎設施,同時減少了花費在運維上的人力成本,最終減少了開支。
- 減輕負擔。不再需要重複造輪子,需要什麼功能直接整合呼叫即可,也無需考慮整體的效能,只專注於業務程式碼的實現。
- 易於擴充套件。雲上提供了自動的彈性擴充套件,用了多少計算資源,就購買多少,完全按需付費。
- 簡化管理。自動化的彈性擴充套件、減少了打包和部署的複雜度、可以快速推向市場,這些都讓管理變得簡單高效。
- 環保計算。即使在雲的環境上,仍習慣於購買多餘的伺服器,最終導致空閒。Serverless杜絕了這種情況。
在Martin Flower的專欄文章Serverless Architectures曾這樣定義Serverless架構:
“Serverless architectures refer to applications that significantly depend on third-party services(AKA Backend as a Service or “BaaS”) or on custom code that is run ephmemeral containers (Function as a Service or “FaaS”)”
正如前面提到了FaaS的每個函式都擁有快速啟動和短暫生命週期的特性,讓容器作為任務函式執行的基本單位,是不是非常適合FaaS的場景?同樣,作為最熱門的容器編排工具的Kubernetes又該怎樣應對FaaS呢?
K8s與FaaS
Fission是一款基於Kubernetes的FaaS框架。通過Fission可以輕而易舉地將函式釋出成HTTP服務。它通過讀取使用者的原始碼,抽象出容器映象並執行。同時它幫助減輕了Kubernetes的學習負擔,開發者無需瞭解太多K8s也可以搭建出實用的服務。Fission目前主要支援NodeJS和Python,預支援C# .NET,對Golang的支援也在進行中。Fission可以與HTTP路由、Kubernetes Events和其他的事件觸發器結合,所有這些函式都只有在執行的時候才會消耗CPU和記憶體。
Kubernetes提供了強大的彈性編排系統,並且擁有易於理解的後端API和不斷髮展壯大的社群。所以Fission將容器編排功能交給了K8s,讓自己專注於FaaS的特性。
對於FaaS來說,它最重要的兩個特性是將函式轉換為服務,同時管理服務的生命週期。有很多辦法可以實現這兩個特性,但需要考慮一些問題,比如“框架執行在原始碼級?還是Docker映象?”,“第一次執行的負載多少能接受”,不同的選擇會影響到平臺的擴充套件性、易用性、資源使用以及效能等問題。
為了使Fission足夠易用,它選擇在原始碼級工作。使用者不再參與映象構建、推倉庫、映象認證、映象版本等過程。但原始碼級的介面不允許使用者打包二進位制依賴。Fission採用的方式是在映象內部放置動態的函式載入工具,讓使用者可以在原始碼層操作,同時在需要的時候可以定製映象。這些映象在Fission裡叫做“環境映象”,它包含了特定語言的執行時、一組常用的依賴和函式的動態載入工具。如果這些依賴已經足夠滿足需求,就直接使用這個映象,否則的話需要重新匯入依賴並構建映象。環境映象是Fission中唯一與語言相關的部分。可以把它看做是框架裡其餘部分的統一介面。所以Fission可以更加容易擴充套件(這看起來就像VFS一樣)。
FaaS優化了函式執行時的資源使用,它的目標是在執行的時候才消費資源。但在冷啟動的時候可能會有些資源使用過載,比如對於使用者登入的過程,無論多等幾秒都是不可接受的。為了改變這個問題,Fission維持了一個面向任何環境容器池。當有函式進來時,Fission無需啟動新容器,直接從池裡取一個,將函式拷貝到容器裡,執行動態載入,並將請求路由到對應的例項。
除了安裝在本地的Fission主程式外,Fission-bundle設計為一組微服務構成:
- Controller: 記錄了函式、HTTP路由、事件觸發器和環境映象
- Pool Manager: 管理環境容器,載入函式到容器,函式例項空閒時殺掉
- Router: 接受HTTP請求,並路由到對應的函式例項,必要的話從Pool Manager中請求容器例項
在Kubernetes上,這些元件都以Deployment的方式執行,並對外暴露Service。除了這三個Fission特有的元件外,還用了Etcd作為資源和對映的儲存,同樣也以Deployment的方式啟動。Controller支援Fission的API,其他的元件監視controller的更新。Router暴露為K8s裡的LoadBalancer或NodePort型別的服務(這取決於K8s叢集放在哪裡)。
目前,Fission將一個函式對映為一個容器,對於自動擴充套件為多個例項的特性在後續版本里。以及重用函式Pods來支援多個函式也在計劃中(在這種情況下隔離不是必須的)。Fission文件簡單介紹了它的工作原理:
“當Router收到外部請求,它先去快取Cache裡檢視是否在請求一個已經存在的服務。如果沒有,要訪問請求對映的服務函式,需要向Pool Manager申請一個容器例項執行函式。Pool Manager擁有一個空閒Pod池。它選擇一個Pod,並把函式載入到裡面(通過向容器裡的Sidecar傳送請求實現),並且把Pod的地址返回給Router。Router將外部請求代理轉發到該Pod,並將響應結果返回。Pod會被快取起來以應對後續的請求。如果空閒了幾分鐘,它就會被殺死”
對於較小的REST API來說,Fission是個很好的選擇,通過實現webhooks,可以為Slack或其他服務編寫chatbots。
Fission同時還支援根據Kubernetes的Watch機制來出發函式的執行。例如你可以設定一個函式來watch某個名稱空間下所有滿足某個標籤的pod,這個函式將得到序列化的物件和這個上下文的Watch Event型別(added/removed/updated)。又如通過設定事件處理函式可以將它應用於簡單的監控,指定當任意一個服務新增到叢集時向Slack傳送一條訊息。當然也有更復雜的應用,例如編寫一個watching Kubernetes第三方資源(Third Party Resource)的自定義controller。
在Fission的官網上有個入門的使用示例:
$ cat hello.js module.exports = function(context, callback) { callback(200, "Hello, world!\n");} # Upload your function code to fission $ fission function create --name hello --env nodejs --code hello.js # Map GET /hello to your new function $ fission route create --method GET --url /hello --function hello # Run the function. This takes about 100msec the first time. $ curl http://$FISSION_ROUTER/hello Hello, world!
如果是第一次執行,需要先準備NodeJS的執行環境:
# Add the stock NodeJS env to your Fission deployment
$ fission env create --name nodejs --image fission/node-env
通過閱讀Fission的原始碼,可以很清晰地看到它的執行過程:
1. fission env create --name nodejs --image fission/node-env
由fission主程式執行命令env和子命令create,通過–name指定語言為NodeJS,通過–image指定映象為fission/node-env,通過HTTP的POST方法請求controller的/v1/environments併發送環境資訊JSON。controller拿到這個JSON後先獲取一個UUID進行標記,然後將放到ETCD裡。由此完成了環境資源的儲存。
2. fission function create --name hello --env nodejs --code hello.js
同樣,由fission主程式執行命令function和子命令create,通過–name引數指定函式名為hello,–env引數確定環境,–code引數確定要執行的函式程式碼。通過POST向/v1/functions發出請求,攜帶函式資訊的JSON。controller拿到JSON後進行函式資源的儲存。首先將拿到UUID,然後寫到檔名為該UUID的檔案裡。接著向ETCD的API傳送HTTP請求,在file/name路徑下有序存放UUID。最後類似上面env命令,將UUID和序列化後的JSON資料寫到ETCD裡。
3. fission route create --method GET --url /hello --function hello
fission通過引數–method指定請求所需方法為GET,–url指定API路由為hello,–function指定對應執行的函式為hello。通過POST向/v1/triggers/http發出請求,將路由和函式的對映關係資訊傳送到controller。controller會在已有的trigger列表裡進行重名檢查,如果不重複,才會獲取UUID並將序列化後的JSON資料寫到etcd裡。
前面的都是由本地的fission程式完成的。我們已經預先建立了fission-bundle的Deployment和Service。它建立了名為fission的名稱空間,並在裡面啟動4個Deployment,分別是controller, router, poolMgr, etcd,並建立NodePort型別的Service: controller和router,分別監聽埠31313和31314。同時建立另一個名為fission-function的名稱空間用來執行執行函式的Pod.
router使用Cache維護著一份function到service的對映,同時還有trigger集合(有個goroutine通過controller保持對這個trigger集合的更新),在啟動時按照新增trigger裡的url和針對對應函式的handler初始化路由。
4. curl http://$FISSION_ROUTER/hello
當執行該curl時,請求傳送至router容器。收到請求後會轉發到兩個對應的handler。一個是使用者定義的面向外部的,一個是內部的。實際上它們執行的是同一個handler。任何handler都會先根據funtion名去Cache裡查詢對應的service名。如果沒有命中,將通過poolmgr為函式建立新的Service,並把記錄新增到Cache。然後生成一個反向代理,接收外部請求,然後轉發至Kubernetes Service。
Poolmgr在建立新的service時,會根據env建立Pod pool(初始大小為3個副本的deployment),然後從中隨機選擇一個Ready的Pod。接著為此建立對應的Service。
Fission是一個開源專案,由Platform 9和社群進行開發。社群正在努力讓Kubernetes上的FaaS更加易用和輕鬆整合。在未來幾個月將新增單元測試、與Git整合、函式監控和日誌聚合等特性,同時也會跟其他的Events進行整合,對了,還有為更多的語言建立環境。在今年1月份,Fission釋出了alpha版。
後記
容器技術的出現改變了軟體交付的思維,微服務和Serverless雖然沒有減少軟體生命週期中的環節,但確實改變了下游軟體部署和維護的理念,提高了軟體開發人員的效率。FaaS是未來的一種可能的走勢,但一定不會是最終的未來。總有一天FaaS又會被其他技術所代替。生活在這個資訊爆炸、技術飛速更迭的時代很煩惱也很幸福。這就是我們所在的時代,我們正在親身經歷著未來。