前面談的都是長時間運行運算的系統設計:處理使用者請求的伺服器永遠開著。但有一類應用,可能只需要為了處理單一請求而暫時存在,或只需回應特定事件。這種請求/事件驅動的應用設計,隨著大型公有雲供應商推出函式即服務(FaaS, function-as-a-service)產品而蓬勃發展;近來也出現跑在私有雲或實體環境叢集編排器之上的 FaaS 實作。多數情況下,FaaS 是更大架構中的一個元件,而非完整解決方案。
- 無伺服器適用範圍更廣——多租戶容器編排器(container-as-a-service)是無伺服器但不是事件驅動。
- 跑在你自有、自管實體機叢集上的開源 FaaS 是事件驅動但不是無伺服器。
分清這一點,才能判斷你的應用該選事件驅動、無伺服器,還是兩者兼具。
判斷 FaaS 何時合理#
事件驅動處理容易被當成萬用鐵鎚,但它其實只最適合特定一組問題——硬套到所有應用或系統上,只會得到過度複雜、脆弱的設計。
FaaS 的好處#
- 對開發者的簡化:大幅縮短「程式碼到運行服務」的距離。除了原始碼本身,沒有其他產出物要建構或推送——從筆電或瀏覽器上的程式碼到雲端運行,非常簡單。
- 自動管理與擴展:流量增加時自動建立更多函式實例;函式因應用或機器故障而失敗時,自動在其他機器重啟。
- 更細粒度的建構區塊:函式是無狀態的,建構在函式之上的系統天生比單一二進位檔更模組化、更解耦——但這同時也是 FaaS 開發的挑戰所在。
FaaS 的挑戰#
- FaaS 強迫你把服務的每一塊徹底解耦:每個函式完全獨立、只透過網路溝通、實例不能有本地記憶體——所有狀態都得存在儲存服務裡。開發敏捷度提升了,但營運可能顯著複雜化。
- 很難獲得服務的全貌:各函式如何整合、何時出錯、為何出錯,都不易掌握。
有些問題在 FaaS 下極難偵測。書中的例子:
functionA()呼叫functionB(),functionB()呼叫functionC(),functionC()又呼叫回functionA()——任何一個函式收到請求就會啟動無限迴圈,直到原始請求逾時(甚至可能不會停),或你的請求費用燒光為止。因為函式之間徹底解耦,系統中沒有任何地方表達函式間的依賴或互動關係,這種問題在程式碼裡很難看出來。採用 FaaS 時必須嚴格建立監控與告警,在問題變大之前偵測並修正——而監控帶來的複雜度,恰恰與 FaaS 部署的簡單性相牴觸,這是開發者必須克服的摩擦。
需要背景處理的場合#
- FaaS 本質上是事件驅動的應用模型,函式實例的執行時間通常有時間上限,因此不適合需要長時間處理的場景——轉檔影片、壓縮日誌檔等低優先、長時間運算。
- 可以設定排程觸發器(scheduled trigger)定期合成事件——這適合回應時間性事件(如發簡訊鬧鐘叫人起床),但仍不足以支撐一般性的背景處理。真正的背景處理需要支援長時間行程的環境,通常也意味著那部分應用要從「按請求計費」改為「按用量計費」。
需要在記憶體中保存資料的場合#
- 有些服務(如文件搜尋索引)需要把大量資料載入記憶體才能服務請求。即使儲存層很快,載入時間仍可能遠超過期望的回應時間。
- FaaS 的函式可能是在使用者等待時才動態啟動的,大量載入會直接反映在使用者感受到的延遲上。函式建立後可以分攤到大量請求——但若請求多到足以讓函式一直活著,你按請求付費多半已經付太多了。
持續性請求處理的成本#
- 公有雲 FaaS 按請求計價:請求稀疏(每分鐘或每小時幾個)時很划算——閒置時不付錢;反之長時間運行的容器或虛擬機,大多數時間是在為「等請求的處理器週期」付費。
- 但服務成長後,請求多到能讓處理器持續忙碌時,按請求計價的經濟性開始惡化,且只會越來越糟:虛擬機的單位成本隨核心數增加遞減(還有預留、持續使用折扣),按請求的成本卻隨請求數線性成長。
服務成長後的一條理想路徑:改跑在 Kubernetes 等容器編排器上的開源 FaaS——保留 FaaS 的開發者體驗,同時享有虛擬機的計價模式。
FaaS 的模式#
以下是把 FaaS 納入分散式系統的幾個典型模式。(本章範例使用部署在 Kubernetes 上的 kubeless FaaS 框架:安裝 kubeless 二進位檔後執行 kubeless install;它以 Kubernetes 原生第三方 API 安裝,之後可用原生 kubectl get functions 查看已部署的函式。)
裝飾器模式:請求或回應轉換#
- FaaS 很適合部署「接收輸入、轉換成輸出、傳給其他服務」的簡單函式——可用來增強或**裝飾(decorate)**進出其他服務的 HTTP 請求。這與 Python 的 decorator 概念非常相似。
- 裝飾轉換通常無狀態、又常是服務演進後才追加的,非常適合用 FaaS 實作;FaaS 的輕量也讓你能先實驗多種裝飾器,確定後再完整併入服務實作。

圖 8-1:套用在 HTTP API 上的裝飾器模式
- 典型例子:為 HTTP RESTful API 的輸入補預設值。經典 JSON 難以表達「欄位預設為 true」(缺欄位就是 null,通常被理解為 false)。把補值邏輯放進 API 伺服器前端或應用程式碼(
if (field == null) field = true)都不漂亮——補值在概念上與請求處理相當獨立。用 FaaS 裝飾器在使用者與服務實作之間轉換請求更乾淨。
為什麼不用第一部講的轉接器容器打包補值邏輯?完全可以,但那會把補值服務的規模與 API 服務綁在一起。補值是輕量操作,所需的實例數多半遠少於服務本身。
動手做:在請求處理前加上預設值
用 Python 寫補值函式:
# Simple handler function for adding default values
def handler(context):
# Get the input value
obj = context.json
# If the 'name' field is not present, set it randomly
if obj.get("name", None) is None:
obj["name"] = random_name()
# If the 'color' field is not present, set it to 'blue'
if obj.get("color", None) is None:
obj["color"] = "blue"
# Call the actual API, potentially with the new default
# values, and return the result
return call_my_api(obj)存成 defaults.py(call_my_api 要改成指向實際要呼叫的 API),再部署為 kubeless 函式:
kubeless function deploy add-defaults \
--runtime python27 \
--handler defaults.handler \
--from-file defaults.py \
--trigger-http測試也用 kubeless 工具:
kubeless function call add-defaults --data '{"name": "foo"}'處理事件#
- 請求 vs. 事件(作者的區分):請求屬於一連串更大的互動或工作階段(session)——每個使用者請求都是與整個 Web 應用或 API 互動的一部分。事件則傾向單一實例、非同步:從主互動中被拋出、稍後才被回應。
- 事件的例子:使用者註冊新服務(觸發歡迎信)、有人上傳檔案到共享資料夾(通知所有有權限的人)、機器即將重開機(通知操作員或自動化系統採取行動)。
- 事件大多彼此獨立、無狀態,且事件量的波動可能很大——是事件驅動與 FaaS 架構的理想候選。這種角色下,FaaS 常伴隨正式應用伺服器部署,作為主要使用者體驗的增強、或處理某種回應式背景工作。
- 函式部署的輕量,也很配「服務常會動態新增事件」的特性;每個事件在概念上獨立,函式系統的強制解耦反而降低概念複雜度——開發者只需專注於處理單一事件型別的步驟。
動手做:實作雙因子驗證
**雙因子驗證(two-factor authentication)**要求使用者同時具備「知道的東西」(密碼)與「持有的東西」(手機)——比單靠密碼安全得多,因為得同時發生兩種安全事故(密碼外洩+手機被偷)才構成真正的問題。
挑戰在於:產生隨機碼、向登入服務註冊該碼、發送簡訊——這段邏輯放哪?塞進主登入伺服器既複雜又單體化,還會讓有延遲的簡訊發送內嵌在渲染登入頁的程式碼裡,拖累使用者體驗。更好的選擇:登入伺服器只對 FaaS 發一個非同步 webhook 請求,由 FaaS 處理「註冊雙因子碼+發簡訊」這種偏慢的非同步任務:
def two_factor(context):
# Generate a random six digit code
code = random.randint(100000, 999999)
# Register the code with the login service
user = context.json["user"]
register_code_with_login_service(user, code)
# Use the twillio library to send texts
account = "my-account-sid"
token = "my-token"
client = twilio.rest.Client(account, token)
user_number = context.json["phoneNumber"]
msg = "Hello {} your authentication code is: {}.".format(user, code)
message = client.api.account.messages.create(to=user_number,
from_="+12065251212",
body=msg)
return {"status": "ok"}以 kubeless 部署:
kubeless function deploy add-two-factor \
--runtime python27 \
--handler two_factor.two_factor \
--from-file two_factor.py \
--trigger-http使用者成功輸入密碼後,客戶端 JavaScript 非同步呼叫此函式;Web UX 立即顯示輸入驗證碼的頁面,使用者收到簡訊後填入即可——驗證碼早已透過 FaaS 註冊完成。
事件驅動管線#
- 有些應用天生就適合用「解耦事件的管線(pipeline)」來思考——像老式流程圖,可表示為相連事件節點的有向圖:每個節點是一個函式或 webhook,連接圖的邊是 HTTP 或其他網路呼叫。
- 管線各段之間一般沒有共享狀態,但可能有一個 context 或其他參考點,用來在共享儲存中查找資訊。
- 與微服務架構的兩個核心差異:
- 事件管線本質上是事件驅動的;微服務架構則是一組長時間運行的服務。
- 事件管線可以高度非同步、連接的東西五花八門——很難想像「人類在 Jira 裡核准工單」如何整合進微服務應用,但把這個事件納入事件驅動管線卻很自然。
- 例子:程式碼提交進版控系統(原始事件)→ 觸發建構(可能跑數分鐘)→ 完成後發事件給建構分析函式 → 成功則開工單等人核准推正式環境,工單關閉這個事件觸發實際的上線;失敗則對失敗開 bug、管線終止。
動手做:實作新使用者註冊管線
新使用者建立時,有些事必做(寄歡迎信)、有些事選做(訂閱產品更新信,俗稱「垃圾信」)。全部塞進單一的使用者建立伺服器,意味著一個團隊擁有整個服務、整個體驗以單一服務部署——實驗或改動使用者體驗都更困難。
改用一系列 FaaS 組成事件管線:使用者建立函式不需要知道後續細節,只維護兩張清單——必做動作清單與選做動作清單,每個動作都是一個 FaaS、清單其實就是 webhook 清單:
def create_user(context):
# For required event handlers, call them universally
for key, value in required.items():
call_function(value.webhook, context.json)
# For optional event handlers, check and call them
# conditionally
for key, value in optional.items():
if context.json.get(key, None) is not None:
call_function(value.webhook, context.json)各個處理器同樣以 FaaS 實作:
def email_user(context):
# Get the user name
user = context.json['username']
msg = 'Hello {} thanks for joining my awesome service!".format(user)
send_email(msg, contex.json['email])
def subscribe_user(context):
# Get the user name
email = context.json['email']
subscribe_user(email)這樣切分後,每個 FaaS 都很簡單——只有幾行程式碼、專注實作一項特定功能。這種微服務式做法寫起來簡單,但若真要部署管理三個不同的微服務就會複雜起來——這正是 FaaS 的亮點:託管這些小程式碼片段輕而易舉。把使用者建立流程視覺化為事件驅動管線後,只要順著 context 在各函式間的流動,就能對「使用者註冊時到底發生什麼」有高階的理解。