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 部分:
- <div style="width: 70%"> <div style="text-align: center"> <h3>Web端上傳文件</h3> </div> <hr> <div> <p> 方案1:上傳到函數計算進行處理再轉存到對象存儲,這種方法比較直觀,問題是 FaaS 平臺與 API 網關處有數據包大小上限,而且對二進制文件處理并不好。 </p> <input type="file" name="file" id="fileFc"/> <input type="button" onclick="UpladFileFC()" value="上傳"/> </div> <hr> <div> <p> 方案2:直接上傳到對象存儲。流程是先從函數計算獲得臨時地址并進行數據存儲(例如將文件信息存到 Redis 等),然后再從客戶端將文件上傳到對象存儲,之后通過對象存儲觸發器觸發函數,從存儲系統(例如已經存儲到Redis)讀取到信息,再對圖像進行處理。 </p> <input type="file" name="file" id="fileOss"/> <input type="button" onclick="UpladFileOSS()" value="上傳"/> </div></div>
通過 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%。