- 華為Serverless核心技術與實踐
- 劉方明等
- 6601字
- 2022-05-06 18:19:57
1.4.1 平臺
本節將介紹典型的商用函數計算服務Lambda和Azure Function,以及兩個開源的系統OpenWhisk和Knative。
1.4.1.1 支持1∶1觸發的AWS Lambda
AWS于2014年11月推出Lambda函數計算服務,可響應事件運行代碼,并自動管理計算資源,從而輕松構建快速響應事件的應用程序。當時的AWS Lambda應用場景是圖片上傳、應用內事件、網站單擊或連接設備的輸出事件等,其目標是在這些事件發生后的1毫秒內開始自動啟動代碼運行并處理事件。此外,根據自定義請求自動觸發的后端服務也是其應用場景。Lambda可以實現自動彈性擴容,降低了基礎設施運維管理成本,也減輕了流量動態變化時的應用擴容和縮容負擔。除了架構創新,Lambda還創新了計費模式,將以往按照資源使用時長(以小時為單位)的計費模式,調整為以100ms(2020年AWS re:Invent大會宣布以1ms為計費單位)為計費單位,可以大幅降低一些后臺任務或請求量較低的后端服務的資源開銷。
Lambda是第一個商用的函數計算服務,接下來我們將以Lambda的架構為例,介紹事件在Lambda中處理的流程及其組件的基本功能,然后介紹其編程模型及當前的一些新特性和趨勢。
圖1-13是AWS Lambda的邏輯架構[2],分為控制面和數據面。控制面分為開發者工具和控制面接口,數據面主要用于支撐對函數的同步、異步調用,以及處理對函數的請求。

圖1-13 AWS Lambda的邏輯架構(詳見AWS Reinvent演講)
第一部分控制面包含如下內容。
? 開發者工具:Lambda Console是給開發人員使用的Web 控制臺,用來編輯、管理函數。SAM CLI是Lambda提供的命令行工具,同時抽象了函數部署的模型,方便開發人員使用命令行管理函數、自動化部署等。
? 控制面接口:提供給控制臺/SAM CLI調用,是進行函數生命周期管理、代碼包管理的接口。
第二部分數據面包含如下內容。
? Pollers/State Manager/Leasing Service:Pollers處理拉模型下的Poll觸發器,從SQS、Kinesis、Dynamodb服務中異步獲取事件,然后將事件拋給Frontend Invoke處理。State Manager和Leasing Service配合Pollers完成上述過程。
? FrontEnd Invoke:處理對函數的同步請求和異步請求,請求Worker Manager獲取空閑的函數實例,并將對函數的請求轉發給該函數實例。
? Counting Service:監控租戶在整個Region上的并發度,如果請求超過并發上限,可按照用戶的請求提高并發上限。
? Worker Manager:管理函數實例的狀態空閑或繁忙,并接收FrontEnd Invoke的請求,返回可用的實例信息。
? Worker:準備執行租戶函數代碼的安全環境。
? Placement Service:池化調度服務,將Worker分配給Worker Manager,并通過調度策略通知Worker啟動安全沙箱,下載函數代碼并執行。
Lambda采用1∶1的觸發模型,只要沒有空閑的函數實例,Lambda就會啟動新的函數實例來處理新的請求,下面是其典型的請求處理流程,如圖1-14所示。
① FrontEnd Invoke收到函數的請求,進行鑒權檢查,并判斷其是否超過該租戶的并發上限。
② FrontEnd Invoke沒有找到可用的函數實例,向Worker Manager申請新的函數實例。
③ Worker Manager沒有找到合適的Worker,向Placement Service申請新的Worker。
④ Placement Service分配新的Worker,并通知Worker Manager。
⑤ Worker Manager通知Worker拉起沙箱,下載函數代碼、初始化Runtime,完成函數實例準備,并通知FrontEnd Invoke,提供到函數實例的路由信息。
⑥ FrontEnd Invoke將請求轉給函數實例,函數實例完成消息處理后,通知Work Manager當前實例已空閑,可以接收下一次請求。

圖1-14 Lambda中請求處理的典型流程
如果函數實例未被系統回收,那么對函數的請求可以直接處理,不需要經歷②~⑤的流程。
從運行模型的角度看,Lambda采用輕量級虛擬機技術來保證邏輯多租時的安全性,并提供面向多語言的Runtime來執行不同的函數,如圖1-15所示。

圖1-15 Lambda的運行模型
Lambda的節點基于裸金屬服務器而非虛擬機,沙箱基于容器技術。對于物理多租的場景,比如容器服務出現逃逸攻擊等安全問題,其風險僅限于該租戶。而函數計算是邏輯多租的,某個租戶的函數出現安全問題會影響所有租戶。因此,Lambda自主研發了輕量級虛擬機(MicroVM)技術FireCracker,基于KVM(基于內核的虛擬機)并使用Rust語言開發實現,可以有效保證租戶函數的安全。FireCracker相比基于QEMU的虛擬機更為輕量,其基礎資源占用內存小于5MB,部署密度較高。不過,FireCracker的冷啟動時間還是相對比較高的,約為125ms,這是影響函數冷啟動時間的主要因素。Lambda的數據面在裸金屬服務器上運行,而不在EC2的虛擬機上運行,也緣于此。嵌套虛擬化的資源開銷過高,影響執行效率及成本。FireCracker提供安全、低開銷的隔離虛擬機,在虛擬機中運行容器沙箱,并針對不同語言(如Java、JavaScript等)提供函數的運行時環境。在Lambda最上層的為函數代碼,Runtime將事件轉給代碼處理,完成后再由Runtime返回。
Lambda提供了同步、異步編程模型,也提供了不同的錯誤自動處理機制。以Lambda文檔中JavaScript代碼為例,Lambda提供了invoke(params = {}, callback)接口完成對函數的同步調用和異步調用。該接口默認為同步調用,第二個參數為回調函數,處理成功或失敗的響應。當請求失敗時,Lambda最多可能會重試兩次,徹底失敗后拋出異常,下面是同步調用的代碼樣例。

異步調用使用相同接口,只是在請求的參數中,需要將InvocationType修改為Event。Lambda會把異步的請求先發送到消息隊列中,如果函數限于并發能力無法及時處理事件,則可能出現事件丟失甚至事件重復發送的情況,所以函數處理的業務最好保證冪等。Lambda提供了死信隊列功能,用于保存出錯的異步請求或沒有處理的事件。用戶配置好死信隊列后,異步請求出錯或未處理的事件會被發送到死信隊列(基于SQS服務),用戶可以根據自己的業務邏輯來繼續處理這些異步請求失敗的事件。下面是Lambda進行異步調用的代碼樣例。

DataDog是一個專注于云基礎設施監控和安全的公司,2020年DataDog在向AWS提供的Serverless服務調研報告(詳見DataDog官網)中指出,約有50%的AWS用戶使用了Lambda,而在使用容器的用戶中,有80%使用了Lambda。從DataDog的報告中可以發現,上云的客戶可能呈現從虛擬機到容器再到函數的使用趨勢。AWS在2020年re:Invent上發布的一些新特性會加速這一趨勢,具體分析如下。
? 計費單位改為1ms:過去Lambda的計費模型是以100ms為計費單位的,更換為1ms的計費單位后,用戶使用函數的成本可能會大幅降低。如果函數的實際執行時間為28ms,按照以前的計費方式,對于1GB的內存配置,6000萬次請求的資源開銷為100$,而按照以1ms為粒度的計費模式只需要28$,開銷降低了72%。
? 內存以1MB為步長計費:過去的Lambda函數的內存規格以128MB起步,以64MB為步長增加。如果函數只需要140MB的內存,可申請的規格也只能是192MB,有52MB的內存被浪費了。以1MB為計費步長,可以降低函數的使用成本。
? 最大支持6VCPU和10GB內存配置,并支持AVX2指令集:提升函數最大資源規格,可以將函數計算的應用范圍拓展到機器學習和推理、視頻處理、高性能計算、科學模擬及財經建模等豐富場景。AVX2指令集是Intel服務器CPU支持的指令集,可以在每個CPU周期進行更多整數和浮點數運算,對于圖片處理等場景可以提升30%的性能。Lambda不會對此收取額外費用,用戶只需要重新編譯依賴的類庫以增加對AVX2的支持。
? 自定義容器:Lambda以前只支持自定義Runtime,只能在層功能的基礎上實現。層(Layer)類似于容器的分層文件系統,用于在函數間共享代碼。自定義容器的自由度更大,開發者可以將業務代碼打包成容器,并集成Lambda Runtime API,將其發布到AWS的容器鏡像服務后,就可以在函數創建時將其指定為鏡像。自定義容器極大地方便了開發者在本地進行調測,也方便容器化的應用近似無縫地向Lambda遷移。
從Lambda的最新特性不難看出其正在通過以下三種方式加速開發者向Lambda遷移。
? 降低成本:計費形式的改變及對指令集的支持(提升性能會減少運行時間)都進一步降低了Lambda的使用成本。這是吸引開發者從虛擬機、容器向函數服務遷移的最大動力。
? 擴大應用場景:新的資源規格將Lambda的觸手伸向了功能和性能要求更高的領域。例如,近年來學術界出現了基于函數進行高性能計算、機器學習的研究案例。雖然函數的資源(CPU、內存、I/O和網絡帶寬等)是受限的,但是其擴展性強。例如,2017年伯克利大學的里斯實驗室推出了基于Lambda的PyWren框架[3],并發2800個函數實例,其峰值算力達到了40TFLOPS,峰值I/O達到了80GB/s(讀)、60GB/s(寫)。這意味著不用去申請和管理高性能計算集群,通過函數計算平臺也可以獲得高性能的算力。
? 降低容器應用遷移成本:自定義容器可以極大地降低容器應用遷移到Lambda的成本,開發者不用修改業務代碼,只需增加一段支持Lambda Runtime API的腳本。開發者也可以不使用Lambda的編程模型,繼續使用以往的開發框架及工具集,使本地調測更容易。在開發效率不降低的同時,發布和維護會大幅簡化。
從AWS Lambda推出的新特性不難看出,AWS在不斷地擴大Serverless的應用場景,同時不停降低其成本,讓基于容器、虛擬機的服務更容易地遷移到函數平臺上。這不禁會讓人遐想,Serverless的未來可能真會如伯克利所預言的那樣,成為云時代默認的計算范式。
1.4.1.2 支持數據綁定的Azure Function
Azure的函數計算平臺和Lambda的實現機制不同,在使用方式上也有差異,但整體也采用事件驅動的架構,并提供了多種事件源的接入,如圖1-16所示。

圖1-16 Azure Function邏輯結構
Azure和Lambda的具體差異如下。
? 觸發模型:Lambda是1∶1的觸發模型,即每個函數實例只處理一個事件,如果有新的事件,系統啟動新的函數實例來處理。而Azure Function則是1∶N的模型,一個函數實例處理多個請求。
? 管理粒度:Lambda管理的粒度是函數,而Azure管理的粒度是App,每個App下可以有多個函數,App對Function進行統一管理。同理,Azure的擴容粒度也是App。
? BaaS訪問:Lambda并沒有抽象BaaS(對象存儲服務、數據庫、緩存)的接口,函數訪問不同的BaaS服務需要開發者自己接入不同的SDK。Azure Function通過Data Binding抽象了不同的BaaS服務,開發人員只要使用配置文件就可以操作數據,簡化了BaaS服務使用的方式,如圖1-17所示。Data Binding將函數對數據的操作抽象為in/out,開發人員只需要通過function.json配置數據in/out的信息,比如要讀取的數據源是哪種BaaS服務(是消息隊列還是NoSQL數據庫)、對應的表是什么,進而在函數中直接操作配置Binding的對象即可,無須再調用BaaS服務SDK、創建連接的客戶端等。同理,函數只需要返回對象,剩余的操作都由Azure Function運行時代為處理。
? 可移植性:Azure Function可以直接基于Kubernetes運行,結合KEDA(詳見KEDA官網),可以讓Azure的函數代碼運行在任何Kubernetes的環境中,其可移植性比Lambda更好。

圖1-17 Azure Function的編程模型
下面是Azure Function文檔中Data Bindings使用示例的配置文件function.json的內容。

Bindings的配置文件有兩部分,第一部分是輸入即消息隊列觸發器的消息,包含接收事件源的消息隊列Topic為myqueue-items,映射到代碼中的對象名稱為order;第二部分是函數輸出的信息,函數會將處理完的返回值寫到Azure的Table Storage中,所以direction是out。配置信息表示函數的返回值將會寫到outTable中。
配置文件中的Connection是包含連接字符串的為應用程序設置的名稱,用來表示對消息隊列和Table Storage的連接。
Data Bindings示例對應的Javascript函數代碼如下。

order是消息隊列myqueue-items的一個事件,它是一個JSON對象。代碼將分區的鍵改為Order,然后隨機生成了一個ID并將其作為RowKey的值,再將order返回。
在這個過程中,代碼沒有創建讀寫BaaS服務的客戶端,沒有管理BaaS服務連接信息,編碼量大大降低。同時,開發人員不需要去了解不同BaaS服務的SDK,Bindings的配置會隱藏這些接口,同時Azure Function的Runtime會完成剩余的實際數據操作。
1.4.1.3 支持服務型和事件型應用的Knative
Knative是基于Kubernetes生態的開源Serverless項目,其目的是提供一個容器平臺,既可以支持開發者運行Serverless容器,幫助開發者解決服務型應用的負載均衡、路由及彈性擴容和縮容(Scale to 0),又可以支持事件驅動型應用的Serverless化運行。簡單來說它就是服務網格和Lambda的結合體,只是它沒有編程模型的約束。2019年,Google發布基于Knative的新服務Cloud Run,這也是開源FaaS平臺中少數實現商用的項目。
Knative基于Kubernetes,主要由兩大部分組成:Serving(應用容器運行,基于Istio能力實現負載均衡、流量管理及自動擴容)和Eventing(支持事件驅動的應用,對接云服務商或其他事件源),如圖1-18所示。本節主要介紹Eventing的相關內容。

圖1-18 Knative的組成和基本功能
Knative Eventing旨在滿足微服務開發的通用需求,提供可組合的方式綁定事件源和事件消費者,其設計目標如下。
? 提供松耦合服務,可獨立開發和部署。松耦合服務可跨平臺使用(如Kubernetes、VM、SaaS、FaaS)。
? 事件的生產者(事件源)和消費者相互獨立。事件的生產者可以于事件的消費者監聽之前產生事件;同樣,事件的消費者可以于事件產生之前監聽事件。
? 支持第三方的服務對接。
? 確保跨服務/云平臺的互操作性,遵循CNCF的Cloud Event規范,詳見1.4.3節。
Eventing主要由事件源(EventSource)、事件處理(Flow)及事件消費者(Event Consumer)三部分構成,如圖1-19所示,Eventing支持的事件源較多,包括典型的消息隊列(Kafka、RabbitMQ)、數據庫(CouchDB)、WebHooks(Gitlab、Github)、WebSocket,以及第三方事件源(如AWS云側服務、Slack等)。

圖1-19 Eventing的組成
如圖1-20所示,Eventing支持三種事件處理方式。
? 事件直接處理:通過事件源直接轉發到單一事件消費者。
? 事件轉發和持久化:通過事件通道及事件訂閱轉發事件,以保證事件不丟失并可進行緩沖處理。訂閱事件可以將事件發給多個消費者處理(扇出)。
? 事件過濾:由Broker接收事件源發送的事件,通過事先定義好的一個或多個Trigger將事件發送給消費者,并且可以按照事件的屬性進行過濾。

圖1-20 Eventing的三種事件處理方式
事件消費者是用來最終接收事件的,Eventing定義了兩個通用的接口作為事件消費者。
? Addressable:提供可用于事件接收和發送的HTTP請求地址,并通過status.address.hostname字段定義。
? Callable:接收并轉換事件,可以按照處理來自外部事件源事件的方式,對這些返回的事件做進一步處理,以用于事件轉發的場景。
Eventing中的組件都以自定義資源的方式部署,其擴展性較好,如果需要支持新的事件源、Broker及Trigger,只需要按照接口實現部署即可。對于熟悉Kubernetes生態的開發者,Knative Eventing是一個不錯的選擇。
1.4.1.4 支持多平臺部署的OpenWhisk
OpenWhisk是IBM發起的開源Serverless平臺,現已被捐獻給Apache基金會,在2019年7月份晉升為Apache基金會頂級項目。
與IBM云平臺同源的OpenWhisk,有很多優秀的特性,比如支持多平臺部署(Docker、Kubernetes、Openshift、Mesos等),開發者通過docker-compose就可以將整個平臺在自己的PC上運行起來。OpenWhisk支持多語言,支持同步、異步編程方式,提供Composer來實現函數的編排(基于函數的編程模型),降低了整體的學習成本。在擴展性方面,OpenWhisk支持按照請求的彈性伸縮。
OpenWhisk是事件驅動的編程模型,包含Action、Trigger、Rule、Feed等概念。OpenWhisk的編程模型如圖1-21所示。
? Event Source(事件源):生成事件的服務,事件通常反映數據的變化或本身攜帶的數據,如消息隊列中的消息、數據庫中數據的變化、網站或Web應用交互、對API的調用等。
? Feed(事件流):屬于某個觸發器的事件流。OpenWhisk支持以鉤子、輪詢和長連接的方式處理事件流。
? Trigger(觸發器):接收來自事件源的一類事件的管道,每個事件只能發給一個觸發器。
? Rule(規則):關聯觸發器和函數的規則。與其他Serverless平臺不同,OpenWhisk可以通過規則讓一個觸發器綁定不同的函數,以原生支持Fan-out(扇出)。
? Action(函數):Action是在OpenWhisk上執行的無狀態、短生命周期的函數。

圖1-21 OpenWhisk的編程模型(詳見OpenWhisk官方文檔)
圖1-22是OpenWhisk架構圖,OpenWhisk的組件及詳細作用如下。

圖1-22 OpenWhisk架構圖
? Nginx:Nginx是進入OpenWhisk的第一個組件,在整個系統中起著網關的作用。作為系統的反向代理,Nginx將消息轉發給Controller,同時完成SSL卸載。
? Controller:Controller作為OpenWhisk的控制組件,負責函數的調用、請求負載均衡,以及函數和觸發器的管理API(函數和觸發器的增、刪、改、查等)。Controller會通過CouchDB對請求進行鑒權,鑒權通過后再觸發對函數的請求。不同于其他Serverless平臺,OpenWhisk的Controller使用Scala語言開發。Controller包含Load Balancer和Activator兩個組件。Load Balancer用于選擇合適的Invoker來執行函數,通過健康檢查感知OpenWhisk系統中可用的Invoker,進而通過哈希算法選擇合適的Invoker處理請求。這樣做的好處是可以將相同的函數調度到同一個Invoker上,最大程度重用緩存、容器等資源,避免容器的創建及初始化等操作開銷。Activator用于處理觸發器的事件,可根據規則調用觸發器綁定的函數。
? Kafka:和其他Serverless平臺只對異步請求使用消息隊列的方式不同,OpenWhisk對于同步請求和異步請求都使用Kafka消息隊列,借助Kafka的持久化能力,當系統崩潰時消息不會丟失,并且消息隊列可以作為高負載下的緩沖隊列,降低系統的內存占用。
? Invoker:Invoker是OpenWhisk的核心模塊,用于從Kafka中讀取需要觸發的函數代碼和參數,并根據參數啟動執行函數的容器,然后返回結果。考慮到執行函數的隔離性和安全性,它使用Docker來啟動運行時,執行具體的函數。
? CouchDB:CouchDB是OpenWhisk的狀態存儲數據庫,用來存儲鑒權認證信息、函數、觸發器、規則等定義及函數的運行響應信息。
OpenWhisk的事件處理流程如圖1-23所示,當消息通過Nginx進入Controller后,Controller根據哈希算法選擇對應的Invoker來執行函數,同時為該請求生成全局唯一的ActivationID(在CouchDB中創建數據項),并將請求的事件、ActivationID等信息寫入Invoker注冊的消息隊列中。Invoker先獲取要觸發的函數信息及參數,再從CouchDB獲取函數代碼及其元數據,然后啟動對應語言運行時的容器執行函數。首次啟動的容器會調用/init接口進行初始化,然后調用/run接口執行Action。根據請求的類型不同,Invoker會將結果返回到不同的地方。如果是異步請求,則Invoker會將執行結果存入CouchDB,客戶端可以根據ActivationID從CouchDB中查詢到結果。如果是同步請求,則Invoker會將執行結果直接寫到完成的消息隊列中,Controller會注冊到該隊列中并獲得相關消息。根據消息中的ActivationID,Controller可以將正確的響應內容返回給客戶端。

圖1-23 OpenWhisk的事件處理流程
OpenWhisk設計架構簡單、易理解,支持多平臺操作,且原生支持編排(參見1.4.4)也降低了開發者的學習成本。此外,OpenWhisk的開發者生態也較為完備,支持多語言,文檔完善,提供自動化工具CLI,這些方面優于其他開源Serverless平臺。