1. 程式人生 > 其它 >Serverless 工程實踐 | Serverless 應用開發觀念的轉變

Serverless 工程實踐 | Serverless 應用開發觀念的轉變

前言:在 Serverless 架構下,雖然更多精力是關注業務程式碼,但是實際上對一些配置和成本也是需要關注的,並且必要的時候還需要根據配置與成本對 Serverless 應用進行配置和程式碼優化。

Serverless 應用開發觀念的轉變


Serverless 架構帶來的除了一種新的架構、一種新的程式設計正規化,還包括思路上的轉變,尤其是開發過程中的一些思路轉變。有人說要把 Serverless 架構看成一種天然的分散式架構,需要用分散式架構的思路去開發 Serverless 應用。誠然,這種說法是正確的。但是在一些情況下,Serverless 還有一些特性,所以要轉變開發觀念。

1、檔案上傳方法


在傳統 Web 框架中,上傳檔案是非常簡單和便捷的,例如 Python 的 Flask 框架:

f = request.files['file']
f.save('my_file_path')


但是在 Serverless 架構下,檔案卻不能直接上傳,原因如下:

  • 一般情況下,一些雲平臺的API閘道器觸發器會將二進位制檔案轉換成字串,不便直接獲取和儲存;
  • 一般情況下,API 閘道器與 FaaS 平臺之間傳遞的資料包有大小限制,很多平臺限制資料包大小為 6MB 以內;
  • FaaS 平臺大多是無狀態的,即使儲存到當前例項中,也會隨著例項釋放而使檔案丟失。

所以,傳統 Web 框架中常用的上傳檔案方案不太適合在 Serverless 架構中直接使用。在 Serverless 架構中,上傳檔案的方法通常有兩種:一種是轉換為 Base64 格式後上傳,將檔案持久化到物件儲存或者 NAS 中,但 API 閘道器與 FaaS 平臺之間傳遞的資料包有大小限制,所以此方法通常適用於上傳頭像等小檔案的業務場景。

另一種上傳方法是通過物件儲存等平臺來上傳,因為客戶端直接通過金鑰等來將檔案直傳到物件儲存是有一定風險的,所以通常是客戶端發起上傳請求,函式計算根據請求內容進行預簽名操作,並將預簽名地址返給客戶端,客戶端再使用指定的方法上傳,上傳完成之後,通過物件儲存觸發器等來對上傳結果進行更新等,如下圖所示。


在 Serverless 架構下檔案上傳檔案示例

以阿里雲函式計算為例,針對上述兩種常見的上傳方法通過 Bottle 來實現。在函式計算中,先初始化物件儲存相關的物件等:

AccessKey = {
   "id": '',
   "secret": ''
}
OSSConf = {
    'endPoint': 'oss-cn-hangzhou.aliyuncs.com',
    'bucketName': 'bucketName',
    'objectSignUrlTimeOut': 60
}
#獲取/上傳檔案到OSS的臨時地址
auth = oss2.Auth(AccessKey['id'], AccessKey['secret'])
bucket = oss2.Bucket(auth, OSSConf['endPoint'], OSSConf['bucketName'])
#物件儲存操作
getUrl = lambda object, method: bucket.sign_url(method, object, OSSConf['object
    SignUrlTimeOut'])
getSignUrl = lambda object: getUrl(object, "GET")
putSignUrl = lambda object: getUrl(object, "PUT")

#獲取隨機字串
randomStr = lambda len: "".join(random.sample('abcdefghijklqrstuvwxyz123456789
    ABCDEFGZSA' * 100, len))


第一種上傳方法,通過 Base64 上傳之後,將檔案持久化到物件儲存:

#檔案上傳
# URI: /file/upload
# Method: POST
@bottle.route('/file/upload', "POST")
def postFileUpload():
    try:
    pictureBase64 = bottle.request.GET.get('picture', '').split("base64,")[1]
    object = randomStr(100)
    with open('/tmp/%s' % object, 'wb') as f:
        f.write(base64.b64decode(pictureBase64))
        bucket.put_object_from_file(object, '/tmp/%s' % object)
        return response({
        "status": 'ok',
        })
    except Exception as e:
    print("Error: ", e)
    return response(ERROR['SystemError'], 'SystemError')


第二種上傳方法,獲取預簽名的物件儲存地址,再在客戶端發起上傳請求,直傳到物件儲存:

#獲取檔案上傳地址
# URI: /file/upload/url
# Method: GET
@bottle.route('/file/upload/url', "GET")
def getFileUploadUrl():
    try:
        object = randomStr(100)
        return response({        
            "upload": putSignUrl(object),    
            "download": 'https://download.xshu.cn/%s' % (object)    
         })    
     except Exception as e:  
         print("Error: ", e)     
         return response(ERROR['SystemError'], 'SystemError')


HTML 部分:


通過 Base64 上傳的客戶端 JavaScript 實現:

function UpladFileFC() {  
    const oFReader = new FileReader();  
    oFReader.readAsDataURL(document.getElementById("fileFc").files[0]);   
    oFReader.onload = function (oFREvent) {  
        const xmlhttp = window.XMLHttpRequest ? (new XMLHttpRequest()) : (new        
            ActiveXObject("Microsoft.XMLHTTP"))   
        xmlhttp.onreadystatechange = function () {    
            if (xmlhttp.readyState == 4 && xmlhttp.status == 200) {            
                alert(xmlhttp.responseText)
            }      
        }        
        const url = "https://domain.com/file/upload"   
        xmlhttp.open("POST", url, true);      
        xmlhttp.setRequestHeader("Content-type", "application/json");       
        xmlhttp.send(JSON.stringify({       
            picture: oFREvent.target.result     
        })); 
    }
}


客戶端通過預簽名地址,直傳到物件儲存的客戶端 JavaScript 實現:

function doUpload(bodyUrl) {  
    const xmlhttp = window.XMLHttpRequest ? (new XMLHttpRequest()) : (new Active       
        XObject("Microsoft.XMLHTTP"));    
    xmlhttp.open("PUT", bodyUrl, true);  
    xmlhttp.onload = function () {   
        alert(xmlhttp.responseText)  
    };   
    xmlhttp.send(document.getElementById("fileOss").files[0]);
    }
    
    
function UpladFileOSS() {
    const xmlhttp = window.XMLHttpRequest ? (new XMLHttpRequest()) : (new Active
        XObject("Microsoft.XMLHTTP"))
    xmlhttp.onreadystatechange = function () {
        if (xmlhttp.readyState == 4 && xmlhttp.status == 200) {            
            const body = JSON.parse(xmlhttp.responseText)         
            if (body['url']) {  
                doUpload(body['url'])   
            }
        }    
    }    
    const getUploadUrl = 'https://domain.com/file/upload/url'   
    xmlhttp.open("POST", getUploadUrl, true); 
    xmlhttp.setRequestHeader("Content-type", "application/json"); 
    xmlhttp.send();
}


整體效果如圖中所示。


Serverless 架構下檔案上傳實驗 Web 端效果

此時,我們可以在當前頁面進行不同型別的檔案上傳方案實驗。

2、檔案讀寫與持久化方法


應用在執行過程中,可能會涉及檔案的讀寫操作,或者是一些檔案的持久化操作。在傳統的雲主機模式下,可以直接讀寫檔案,或者將檔案在某個目錄下持久化,但是在 Serverless 架構下並不是這樣的。

由於 FaaS 平臺是無狀態的,並且用過之後會被銷燬,因此檔案並不能直接持久化在例項中,但可以持久化到其他的服務中,例如物件儲存、NAS 等。

同時,在不配置 NAS 的情況下,FaaS 平臺通常情況下只具備 /tmp 目錄可寫許可權,所以部分臨時檔案可以快取在 /tmp 資料夾下。

3、慎用部分 Web 框架的特性


(1) 非同步

函式計算是請求級別的隔離,所以可以認為這個請求結束了,例項就有可能進入一個靜默狀態。而在函式計算中,API 閘道器觸發器通常是同步呼叫(以阿里雲函式計算為例,通常只在定時觸發器、OSS 事件觸發器、MNS 主題觸發器和 IoT 觸發器等幾種情況下是非同步觸發)。

這就意味著當 API 閘道器將結果返給客戶端的時候,整個函式就會進入靜默狀態,或者被銷燬,而不是繼續執行完非同步方法。所以通常情況下像 Tornado 等框架就很難在 Serverless 架構下發揮其非同步的作用。當然,如果使用者需要非同步能力,可以參考雲廠商所提供的非同步方法。

以阿里雲函式計算為例,阿里雲函式計算為使用者提供了一種非同步呼叫能力。當函式的非同步呼叫被觸發後,函式計算會將觸發事件放入內部佇列,並返回請求 ID,而不會返回具體的呼叫情況及函式執行狀態。如果使用者希望獲得非同步呼叫的結果,可以通過配置非同步呼叫目標來實現,如圖所示。


函式非同步功能原理簡圖

(2) 定時任務

在 Serverless 架構下,應用一旦完成當前請求,就會進入靜默狀態,甚至例項會被銷燬,這就導致一些自帶定時任務的框架沒有辦法正常執行定時任務。函式計算通常是由事件觸發,不會自主定時啟動。例如 Egg 專案中設定了一個定時任務,但是在實際的函式計算中如果沒有通過觸發器觸發該函式,該函式不會被觸發,也不會從內部自動啟動來執行定時任務,此時可以使用定時觸發器,通過定時觸發器觸發指定方法來替代定時任務。

4、要注意應用組成結構


(1) 靜態資源與業務邏輯

在 Serverless 架構下,靜態資源更應該在物件儲存與 CDN 的加持下對外提供服務,否則所有的資源都在函式中。通過函式計算對外暴露,不僅會讓函式的業務邏輯併發度降低,也會造成更多的成本。尤其是將一些已有的程式遷移到 Serverless 架構上,例如 Wordpress 等,更要注意將靜態資源與業務邏輯進行拆分,否則在高併發情況下,效能與成本都將會受到比較嚴峻的考驗。

(2) 業務邏輯的拆分

在眾多雲廠商中,函式的收費標準都是依靠執行時間、配置的記憶體以及產生的流量收費的。如果一個函式的記憶體設定不合理,會導致成本成倍增加。想要保證記憶體設定合理,更要保證業務邏輯結構的可靠性。

以阿里雲函式計算為例,一個應用有兩個對外介面,其中有一個介面的記憶體消耗在 128MB 以下,另一個介面的記憶體消耗穩定在 3000MB 左右。這兩個介面平均每天會被觸發 10000 次,並且時間消耗均在 100 毫秒。如果兩個介面寫到一個函式中,那麼這個函式可能需要將記憶體設定在 3072MB,同時使用者請求記憶體消耗較少的介面在冷啟動情況下難以得到較好的效能;如果兩個介面分別寫到函式中,則兩個函式記憶體分別設定成 128MB 以及 3072MB 即可,如表所示。



通過上表可以明確看出合理、適當地拆分業務會在一定程度上節約成本。上面例子的成本節約近 50%。

_關於作者:劉宇(江昱)國防科技大學電子資訊專業在讀博士,阿里雲 Serverless 產品經理,阿里雲 Serverless 雲佈道師,CIO 學院特聘講師。

新書推薦




本書會通過多個開源專案、多個雲廠商的多款雲產品,以及多種途徑向讀者介紹什麼是 Serverless 架構、如何上手 Serverless 架構、不同領域中 Serverless 架構的應用以及如何從零開發一個 Serverless 應用等。本書可以幫助讀者將 Serverless 架構融入到自己所在的領域,把 Serverless 專案真實落地,獲得 Serverless 架構帶來的技術紅利。