作者 | 張挺
這次分享的内容分為兩部分, 一塊是 Midway Serverless 的能力介紹,第二塊是這些能力的設計、思考、沉澱。

Background
目前業界的 Serverless 化方向如火突入,有如阿裡正在利用 Serverless 将原有業務遷移,降低成本的,也有正在向這些方向努力前進的,我首先會介紹一下目前 Serverless 的一些背景,前端使用 Serverless 的一些場景和方向。第二塊會簡單介紹一下 Midway Serverless 的一些基礎上手和使用。第三塊會介紹 Midway Serverless 在抹平平台差異,架構防腐層的一些設計與心得,最後是對未來的一些期望,方向的思考。
Midway 從 2014 年開始一直在集團承擔 Node.js 應用的基礎開發架構,最開始是 express,到後面的 koa,egg 體系等,将集團業務承載至今。最開始的前後端分離,到如今的函數化,都在不斷的開拓前端職能,讓業務更聚焦,開發更提效。
之前的 midway v1 版本,我們認為 midway 是一個 Web 全棧架構,提供 Web 服務,增加了依賴注入之後,也适合于大型應用的開發,靈活性和應用的可維護性也得到了驗證。
而到了現在 Midway Serverless 時期,整個 Midway 架構的定位在逐漸的變化。
首先,Midway Serverless 是一個 serverless framework,可以在讓代碼在多雲平台部署,在使用者選擇時可以減少一些顧慮。
第二是能夠友善的讓傳統的應用遷移上現有的彈性服務,畢竟在集團内,還有非常多的傳統應用,不管是在什麼場景,這些應用都還需要人維護,需要占用大量的資源,如果能上彈性,對節省成本有非常大的好處。
第三是讓應用本身能夠在傳統應用和函數之間切換,傳統的 midway 是基于裝飾器加上依賴注入的特性建構出來的,在函數體系下上,也可以這樣做,甚至于通過建構将不同的場景結合到一起,我們希望最後能達到代碼不變的情況下,不同場景都可用的狀态。
結構
這是我們經典的目錄結構,最簡單的,抛開 ts 的一些檔案,隻需要 f.yml 這個配置以及 index.ts 這個邏輯檔案,而複雜一點,也隻是增加了目錄,增加了不同邏輯的分層,和傳統的寫法契合。
這裡的 f.yml 就承載了之前的路由層的功能,在 Serverless(FaaS)體系中,路由交給了網關處理,那麼我們隻需要在項目代碼中寫對應的原 Controller 的内容即可。
如圖所示,f.yml 中每一個服務都會對應一個接口,每個接口都由一個方法承載由 handler字段去映射綁定,而實際運作中,通過依賴注入的方式,架構隻根據目前執行的邏輯動态初始化其中方法,是以也不需要擔心執行的性能問題。
f.yml 通過标準化适配多雲平台,最簡單的來說,可以通過定義 http 觸發器的 path 和 method 具體的指定接口位址,也可以簡化到預設值,自動變為通配路由等等。
工具鍊和能力
除了 f.yml 這套标準定義檔案,我們還提供了 faas-cli ,一個精簡的本地開發工具,幫助函數體系開發的更好。在開發層面,我們隻精簡的提供了 create,invoke,test,deploy 四個指令,對應了整個研發流程的四個周期,而剩下的部分,則交給了對應平台自身的能力來完成,同時,我們後續也會提供一些後置管理,讓 Node.js 開發本身更加的高效。
從 v1.0 之後,我們也提供了一系列示例,不管是和前端內建的 React/Vue,還是場景化示例,部落格,Todo list 等等。
原了解析
雖然給大家展示了開發的工具鍊,開發的标準,解釋了運作時機制,大家是不是還是很疑惑,依賴注入是如何把 f.yml 中的 handler 字段如何與代碼中對應的裝飾器連接配接的,而函數整個原來的參數是如何和雲平台對接,做到一套代碼跨多平台的?
為了友善了解,我們拿 Midway v1 裡的依賴注入容器來解釋。
整個 Midway v1 是基于 EggJS 往上擴充,增加 IoC(依賴注入) 容器的初始化部分,并且将裝飾器的能力注冊到其中,和整個路由體系結合到一起。
右邊是我們核心的僞代碼,在初始化時,容器會做一次掃描,把目前使用者的代碼都加載到記憶體中,并分析其中的裝飾器組成一個”依賴圖“,在每次執行邏輯的時候,從其中拿到對應的執行個體(get),并将其依賴,子依賴統一初始化。
路由部分也是這種邏輯的其中一層,在調用路由時,擷取到對應的 Controller key,找到對應的方法,整個 Midway v1 都是如此運作起來的。
在之後的疊代過程中,我們發現這樣和單一架構依賴會比較深,很難去靈活的調整功能,并且在 Web 場景的能力,很難去适配到其他場景,這就給邏輯的複用和擴充造成了不少困難。
我們希望不同的場景的代碼,能夠在一定程度上能夠複用,比如常見的 router/orm/graphql 等等,都是可以橫跨不同的場景去複用的,甚至于使用者的服務層代碼本身也是可以去多處複用的。另外一塊,我們希望傳統全棧到 Serverless 的過程是有延續性的,不希望代碼的寫法有比較大的差別,既能在不同的平台通用,又能在不同的技術棧大部分通用。
這也迫使我們的去思考不同的代碼設計,找到最佳的路徑。
Design
我們從最原始的函數寫法給大家講起。
架構防腐
整個原始的入口函數,社群的寫法都非常簡單,是一個傳統的方法,其參數在不同平台根據不同觸發器略有不同。
在執行時,通過網關排程到其内部的運作時,然後拼裝參數執行到使用者的入口函數中。
為了和之前的架構結合,以及屏蔽不同平台之間的差異,我們在社群的運作時執行之後,使用者入口函數之前,做了一層架構抽象,即我們所謂的”防腐層“。這層一共包括兩個功能,一是運作時防腐的部分,屏蔽出入參數差異,屏蔽異步差異,錯誤處理等等。第二部分是 API 傳承,将傳統的 Midway v1 的容器初始化,根據 yml 裡的資訊執行個體化對應的函數方法。
這樣說有一點抽象,我們找一段實際的代碼來了解一下。
下面是實際生成的代碼入口 index.js 的示例。
初始化的時候,我們會做兩個事情,一個就是每個平台的擴充卡,會自動根據 f.yml 中配置的 provider.name 來生成,我們會自動提供支援的平台啟動器(現在已經有阿裡雲,騰訊雲,以及即将完成的 aws)。
另外一個就是 Midway Serverless 架構的入口(FaaSStarter),通過它,來調用到實際的使用者代碼(src/index.ts)。其餘的 asyncWrapper 和 asyncEvent 則是用于對異步函數的包裹,讓代碼可以統一用上 async 關鍵字。
看到這裡,大家是否好奇我們的運作時擴充卡(防腐層)内部是如何運作的?這就來稍微詳細一點的講講。
整個運作時處在最中間的部分,往上承接事件帶來的資料,接收,中轉,往下調用到業務代碼,把中轉的參數傳入,在整個容器中占了非常重要的部分 。一般來說,整個運作時包括幾個部分,一是語言的 VM,基建的 SDK 等等,比如 Node.js 10/12,日志采集子產品等等,二是運維的腳本,用來控制啟停,健康檢查等等,第三塊就是運作時實際代碼,簡單的實作的話,可以了解起了個 http 服務,并在其中加載業務函數的代碼。
生命周期
傳統的社群平台都會預設埋入自己的運作時,而我們的運作時則是在這些平台内置的運作時之上的封裝,并且将運作時和業務代碼通過自定義生命周期進行關聯,将整個代碼 run 起來。
整個生命周期分為幾部分階段,外圍運作時包括 RuntimeStart、FunctionStart、Invoke、Close 等階段,而在這些周期中,還提供了 before 和 after 的鈎子,友善對這些階段進行擴充。
我們來一下實際的運作時擴充的例子,看看我們是如何抹平不同的雲平台的。
這是一個阿裡雲運作時擴充卡的例子,我們接着上面的業務代碼調用的路徑來觀察,asyncEvent 用來包裹真實的入參,在接受到參數之後,我們做了一些不同觸發器的類型判斷,将其分為了 Web 和非 Web 兩種類型。
在 Web 的處理方法中,進一步細化内容,比如判斷是否是網關的類型,構造出一個類似 koa ctx 的結構,處理 body 參數等。
做完這些事情之後,就開始把規則化好的參數傳遞給使用者的真正的邏輯了,這個時候,由于生命周期的存在,開始執行才看到的 invoke 過程,并在内部調用 before 和 after 過程。
除此之外,我們還提供了一個預設執行攔截的能力,這個能力在傳統應用遷移的時候起了巨大的作用。
應用遷移
現在,所有的擴充(Layer)都可以複用在所有的運作時适配上,整個函數體系和應用遷移體系基于這套生命周期和擴充機制,将能力複用,結構分層表現的淋漓盡緻。
而使用者所需要做的,僅僅增加一個 f.yml,寫入這 4 行即可。整個 midway 建構器 會生成所有需要的入口和适配代碼。
這套函數和應用統一的方案,如果在企業内部,私有化部署也是非常适合的,阿裡集團内部的函數體系也是如此被加載起來,和社群保持了高度的一緻,也減少了很多的維護成本。
小結
從 midway serverless 的基礎到入口的生成,生命周期以及應用遷移的方案介紹,這裡涵蓋的是 midway serverless 的架構防腐的一小部分,後續也将會有其他文章介紹不同的部分,感謝大家閱讀,也歡迎大家關注 Midway。
關注「Alibaba F2E」
把握阿裡巴巴前端新動向