探索
接觸Serverless架構(gòu)的人,或者說接觸函數(shù)計(jì)算的人,很多都會聽過這樣一句話:Serverless是無狀態(tài)。眾所周知,無狀態(tài)就是沒有狀態(tài)的意思,也就是說我們沒辦法用它保存狀態(tài),因?yàn)橛猛昙翠N毀。那么這句話是不是說,在Serverless架構(gòu)下(此處特指FaaS平臺)函數(shù)的前一次運(yùn)行和這一次運(yùn)行,不會有聯(lián)系呢?或者前一次運(yùn)行的結(jié)果不會影響這一次呢?這里的無狀態(tài)指的是什么?
首先要明白,Serverless的幾個(gè)關(guān)鍵特性:運(yùn)行成本更低、自動擴(kuò)縮容、事件驅(qū)動、無狀態(tài)性。這里面的無狀態(tài)性是說開發(fā)者可以直接將服務(wù)業(yè)務(wù)邏輯代碼部署,運(yùn)行在第三方提供的無狀態(tài)計(jì)算容器中。這里的無狀態(tài)如果強(qiáng)行說前一次不影響后一次,沒有狀態(tài)的話,也只能是說在容器沒有被復(fù)用的情況下,是這樣的。但是在實(shí)際的項(xiàng)目中,為了降低冷啟動率,提高瞬時(shí)產(chǎn)生的高并發(fā)應(yīng)對能力,容器的復(fù)用可能會讓這個(gè)“無狀態(tài)性“變得比較撲朔迷離,此處以騰訊云的SCF為例,我們在控制臺創(chuàng)建一個(gè)函數(shù):

然后我們使用以下的代碼進(jìn)行測試:
# -*- coding: utf8 -*-
import json
def main_handler(event, context):
print("Test")
return("Hello World")

可以看到,隨著我們點(diǎn)擊測試按鈕,每次都在日志準(zhǔn)確輸出了Test。接下來,我們變換一下代碼:
# -*- coding: utf8 -*-
import json
print("Not in main_handler")
def main_handler(event, context):
print("Test")
return("Hello World")
接下來同樣的方法多次點(diǎn)擊測試按鈕:

我們可以看到這個(gè)時(shí)候只有第一次請求的時(shí)候,執(zhí)行了這條語句:
print("Not in main_handler")
那么為什么后幾次都沒有執(zhí)行這條語句呢?是沒走到這里?還是因?yàn)槿萜鲝?fù)用的原因,在接下來的幾次跳過了這個(gè)步驟?為什么會跳過這個(gè)步驟?為了讓程序更加有趣,我們來做這樣一個(gè)測試:
# -*- coding: utf8 -*-
import json
print("此處給tempNumber賦值")
tempNumber = 100
def main_handler(event, context):
print("temp number: ", tempNumber)
return("Hello World")

可以看到,在第一次測試的時(shí)候,我們這個(gè)程序執(zhí)行的時(shí)候,先執(zhí)行了:
print("此處給tempNumber賦值")
tempNumber = 100
執(zhí)行完成之后,tempNumber這個(gè)變量就會存在,在接下來的幾次調(diào)用中,都直接取了這個(gè)值。所以可以這樣認(rèn)為:

也就是說,實(shí)際上函數(shù)在復(fù)用容器的情況下被執(zhí)行(或者說是被觸發(fā)),實(shí)際上可以認(rèn)為是已經(jīng)有一個(gè)進(jìn)程被啟動,每次觸發(fā),是通過這個(gè)進(jìn)程來調(diào)用我們的入口方法,所以在方法之外寫的各種操作,實(shí)際上是冷啟動的時(shí)候,在啟動進(jìn)程的時(shí)候,會被執(zhí)行。
所以說,實(shí)際上函數(shù)的無狀態(tài)性,并不是說函數(shù)的前一次操作對后一次被觸發(fā)沒有影響。那么所謂的無狀態(tài)是什么?
在CNCF發(fā)布的serverlss白皮書中,這樣描述過Serverless架構(gòu)的優(yōu)點(diǎn):Serverless架構(gòu)通常是無狀態(tài)、不可變和短暫的。每個(gè)函數(shù)都以指定的角色和明確定義有限的資源訪問權(quán)限運(yùn)行。同時(shí)在白皮書中,也說了什么樣的程序或者服務(wù)適合Serverless架構(gòu),其中有這樣一個(gè)描述:無狀態(tài),短暫的,對瞬間冷啟動時(shí)間沒有過多需求的程序適合使用Serverless架構(gòu)。
所以說,這里的函數(shù)是無狀態(tài)實(shí)際上可以認(rèn)為是:函數(shù)是運(yùn)行在第三方提供的無狀態(tài)計(jì)算容器中的,并且在無復(fù)用的情況下,函數(shù)會存在冷啟動,這個(gè)時(shí)候函數(shù)可以認(rèn)為是無狀態(tài);因?yàn)楦鱾€(gè)廠商的不同容器降低冷啟動方案,以及容器復(fù)用方案也都是未公開的,所以什么時(shí)候可能會復(fù)用這個(gè)容器,怎么復(fù)用也是未知的,這就要求我們函數(shù)的功能本身要保證是無狀態(tài)的。例如說,在函數(shù)中,保存某些數(shù)據(jù)到緩存中,下次觸發(fā)的時(shí)候從緩存中獲得對應(yīng)內(nèi)容就是容易產(chǎn)生異常的操作,因?yàn)樵茝S商無法保證這次請求,是否復(fù)用了已有容器,以及復(fù)用的已有容器是否就是上次進(jìn)行緩存的容器。
拓展
那么根據(jù)我們上面討論的內(nèi)容,在進(jìn)行實(shí)踐化的應(yīng)用:
-
通過容器復(fù)用,做一些初始化操作
剛剛說過了,如果在容器復(fù)用的前提下,那么在函數(shù)外面執(zhí)行的內(nèi)容是可以直接使用的,所以這里我們實(shí)際上是可以在外層進(jìn)行一些初始化的,例如:
image
以上圖的代碼為例,通過這樣的初始化,就不用每次調(diào)用函數(shù)的時(shí)候,都進(jìn)行一次數(shù)據(jù)庫的初始化/鏈接等而是可以復(fù)用已有的鏈接,如果在main_handler中進(jìn)行數(shù)據(jù)庫的初始化/鏈接,會影響函數(shù)性能,在高并發(fā)的情況下更容易把數(shù)據(jù)庫的鏈接打滿,造成極其惡劣的影響。
小心容器復(fù)用不要掉進(jìn)坑里
之前寫了一個(gè)SCF打包Python依賴的小工具,運(yùn)行在SCF中,我在測試之處是好好的,但是項(xiàng)目上線之后,我發(fā)現(xiàn)這樣一個(gè)問題:只有冷啟動的情況下,依賴是可以被打包的,如果出現(xiàn)容器復(fù)用的情況,就會出現(xiàn)依賴打包失敗的問題。
經(jīng)過仔細(xì)排查才發(fā)現(xiàn),實(shí)際上是一個(gè)對象在使用完成之后未被清理,由于容器是被復(fù)用,或者說是“這個(gè)對象也被復(fù)用了”,在執(zhí)行指定方法的時(shí)候,看到對象已存在,就會直接用這個(gè)對象,導(dǎo)致本次函數(shù)的觸發(fā)使用了上次殘留的對象,導(dǎo)致異常的發(fā)生。
所以說,當(dāng)我們的程序在云函數(shù)中,連續(xù)執(zhí)行多次的時(shí)候,開始成功后來失敗,很可能就是由于某些資源復(fù)用,導(dǎo)致我們程序出錯(cuò)。-
我就想要一種狀態(tài)
有的人在使用云函數(shù)的時(shí)候,可能真的就需要有一種狀態(tài)來記錄某些事情,例如我的博客系統(tǒng)判斷管理員用戶是否登錄。本來可以直接放到緩存中的操作,此時(shí)不能放進(jìn)去,那應(yīng)該怎么處理,我怎么記錄管理員是否已經(jīng)登陸了后臺,或者說我怎么確定這個(gè)用戶是否是管理員?
這種情況其實(shí)就比較常見了,我們完全可以融合兩套方案:- 方案1: 采用Token機(jī)制
- 方案2: 采用緩存機(jī)制
所謂的采用Token機(jī)制和緩存機(jī)制融合方案,就是說管理員用戶登陸之后,會生成一個(gè)Token,這個(gè)Token就記錄到數(shù)據(jù)庫中,同時(shí)這個(gè)Token也會被寫到緩存中。當(dāng)用戶請求發(fā)起后,函數(shù)先嘗試在緩存中獲取結(jié)果,如果沒獲得到,就連接數(shù)據(jù)庫進(jìn)行獲取。
總結(jié)
Serverless架構(gòu)可以被看成是一個(gè)新的技術(shù),一種新的框架,很多時(shí)候,我們真的不能用已有的態(tài)度去衡量這樣的新鮮事物。同樣,一個(gè)特性也很難直接用好壞去形容。就這個(gè)無狀態(tài)性來說真的是有幾種鐘愛,有幾種迷茫。

