Serverless 架構中的無狀態性指的是什麼?

每個接觸過 Serverless 的人應該都聽過這樣一句話:“Serverless 是無狀態的。”顧名思義,無狀態就是沒有狀態,我們無法使用它來儲存狀態,用完即銷燬。那麼,在 Serverless 架構下(這裡特指 FaaS 平臺),函式的前一次執行和這一次執行,不會有聯絡呢?前一次執行的結果也不會影響這一次呢?

函式的無狀態探索

首先,需要明確的是 Serverless 的關鍵特徵:執行成本更低、自動擴縮容、事件驅動、無狀態性。其中,無狀態性是說開發者可以直接將服務業務邏輯程式碼部署,執行在第三方提供的無狀態計算容器中。

那麼,前一次執行情況是否會影響這一次呢?準確來說,只有在容器沒有被複用的情況下是這樣的。但是在實際的專案中,為了降低冷啟動率,提高瞬時產生的高併發應對能力,往往會採用容器複用,而這可能會讓“無狀態性“變得比較複雜。

我們以騰訊雲的 SCF 為例,在控制檯建立一個函式,使用以下的程式碼測試一下具體情況:

 複製程式碼

# -*- coding: utf8 -*-importjsondefmain_handler(event, context):print(“Test”)return(“Hello World”)

Serverless 架構中的無狀態性指的是什麼?

我們可以看到,透過點選測試按鈕,輸出了日誌: Test ,接下來,多次點選:

Serverless 架構中的無狀態性指的是什麼?

Serverless 架構中的無狀態性指的是什麼?

可以看到,隨著我們點選測試按鈕,每次都在日誌準確輸出了 Test 。接下來,我們變換一下程式碼:

 複製程式碼

# -*- coding: utf8 -*-importjsonprint(“Not in main_handler”)defmain_handler(event, context):print(“Test”)return(“Hello World”)

同樣的方法,連續點選三次測試,並且記錄結果:

Serverless 架構中的無狀態性指的是什麼?

Serverless 架構中的無狀態性指的是什麼?

Serverless 架構中的無狀態性指的是什麼?

透過這一組測試,我們發現,這三個結果有點不太一樣:只有第一次請求的時候,執行了這條語句:

 複製程式碼

print(“Not in main_handler”)

為什麼後幾次都沒有執行這條語句呢?是沒執行到這裡?還是因為容器複用的原因,在接下來的幾次跳過了這個步驟?為什麼會跳過這個步驟?為了搞清楚具體情況,我們再來做個測試:

 複製程式碼

# -*- coding: utf8 -*-importjsonprint(“ 此處給 tempNumber 賦值 ”)tempNumber =100defmain_handler(event, context):print(“temp number: ”, tempNumber)return(“Hello World”)

Serverless 架構中的無狀態性指的是什麼?

Serverless 架構中的無狀態性指的是什麼?

Serverless 架構中的無狀態性指的是什麼?

可以看到,在第一次測試的時候,這個程式先執行了:

 複製程式碼

print(“ 此處給 tempNumber 賦值 ”)tempNumber =100

執行完成之後, tempNumber 這個變數就會存在,在接下來的幾次呼叫中,都直接取了這個值。

Serverless 架構中的無狀態性指的是什麼?

也就是說,函式在複用容器的情況下被執行(或者說是被觸發),實際上可以認為是已經有一個程序被啟動,每次觸發是透過這個程序來呼叫入口方法,所以在方法之外的各種操作,實際上是冷啟動的時候,在啟動程序時會被執行。

因此,函式的無狀態性並不是前一次操作對後一次被觸發沒有影響。那麼,所謂的無狀態到底指的是什麼呢?

在 CNCF 釋出的 Serverlss 白皮書中,是這樣描述的:Serverless 架構通常是無狀態、不可變和短暫的。每個函式都以指定的角色和明確定義有限的資源訪問許可權執行。什麼樣的程式或者服務適合 Serverless 架構?白皮書中是這樣表述的:無狀態,短暫的,對瞬間冷啟動時間沒有過多需求的程式適合使用 Serverless 架構。

所以,函式的無狀態實際上可以認為是:函式是執行在第三方提供的無狀態計算容器中的,並且在容器無複用、存在冷啟動的情況下,函式可以認為是無狀態;由於各個廠商的容器降低冷啟動方案是不同的,容器複用方案也都是未公開的,所以什麼時候可能會複用容器,怎麼複用也是未知的,這就要求我們函式的功能本身要保證是無狀態的。例如,在函式中,儲存某些資料到快取中,下次觸發的時候從快取中獲得對應內容就是容易產生異常的操作,因為雲廠商無法保證這次請求是否複用了已有容器,以及複用的已有容器是否就是上次進行快取的容器。

拓展

根據上面討論的內容,我們可以進行一些實踐化的應用:

1。 透過容器複用,完成初始化操作

剛剛說過了,如果在容器複用的前提下,在函式外面執行的內容是可以直接使用的,所以我們實際上是可以在外層進行一些初始化的,例如:

Serverless 架構中的無狀態性指的是什麼?

以上圖的程式碼為例,透過這樣的初始化,就不用每次呼叫函式都進行一次資料庫的初始化 / 連結等,而是可以複用已有的連結。如果是在 main_handler 中進行資料庫的初始化 / 連結,會影響函式效能,在高併發的情況下更容易把資料庫的連結打滿,造成惡劣影響。

2。 小心容器複用,不要掉進坑裡

我之前寫過一個 SCF 打包 Python 依賴的小工具,執行在 SCF 中,測試的時候是好好的,但是專案上線之後,我發現了一個問題:只有冷啟動的情況下,依賴是可以被打包的,如果出現容器複用的情況,就會出現依賴打包失敗的問題。

經過仔細排查才發現,原來是因為一個物件在使用完成之後未被清理,由於容器是被複用,或者說是“這個物件也被複用了”,在執行指定方法的時候,看到物件已存在,就會直接用這個物件,導致本次函式的觸發使用了上次殘留的物件,發生異常。

所以說,當程式在雲函式中連續執行多次的時候,開始成功後來失敗,很可能就是由於某些資源複用,導致程式出錯。

3。 我就想要一種狀態

有的人在使用雲函式的時候,可能真的需要有一種狀態來記錄某些事情,例如部落格系統判斷管理員使用者是否登入,本來可以直接放到快取中的操作,此時不能放進去,那應該怎麼處理,如何記錄管理員是否已經登陸了後臺,或者說如何確定這個使用者是否為管理員?

這種情況是很常見的,我們可以融合兩套方案:

方案 1: 採用 Token 機制

方案 2: 採用快取機制

所謂的採用 Token 機制和快取機制融合方案,就是在管理員使用者登陸之後,會生成一個 Token,這個 Token 就記錄到資料庫中,同時這個 Token 也會被寫到快取中。當用戶請求發起後,函式會先嚐試在快取中獲取結果,如果沒獲取到,就連線資料庫進行獲取。

總結

Serverless 架構可以被看成是一個新的技術,一種新的框架,很多時候,我們不能用已有的態度去衡量新鮮事物。同樣,一個特性也很難直接用好壞去形容,就無狀態性來說,真的是有幾種鍾愛,就有幾種迷茫。

作者介紹:劉宇,騰訊 Serverless 團隊後臺研發工程師。