騰訊雲雲函數 SCF 最近新發布了 Node.js 12.16 的 runtime,也是國內首家支持 Node.js 12.x 的主流雲服務商。
Node.js 版本的升級帶來了新的特性以及性能方面的提升,有興趣的同學可以參考國外一博主總結的文章《Node.js 12: The future of server-side JavaScript》了解具體內容。
其中比較重要的一點是啟動速度提升,通過 v8 code cache
的支持,構建時提前為內置庫生成代碼緩存,提升 30% 的啟動耗時。
騰訊雲雲函數 SCF 為了讓 Serverless 更加符合 Node.js 原生的使用體驗,針對 Node.js runtime 做了針對性的優化。
借這個機會,我想和大家分享一下如何使用騰訊雲雲函數來開發 Node.js 應用以及 scf 的 Node.js runtime 實現的原理。
入口函數的參數
首先我們看一下最基本的 Node.js 入口函數:
exports.main_handler = (event, context, callback) => {
console.log("Hello World");
console.log(event);
console.log(context);
callback(null, event);
};
runtime 會將三個參數傳遞到處理程序方法。
第一個參數是 event,用來傳遞觸發事件數據
包含來自調用程序的信息。調用程序在調用時將該信息作為 JSON 格式字符串傳遞,事件結構因服務而異。
-
定時觸發器的 event 對象就包括了觸發的時間,觸發器的名稱
{Message: "", Time: "2020-05-08T14:30:00Z", TriggerName: "time_5", Type: "Timer"}
-
apigateway 觸發器的 event 對象透傳了 http 請求的完整內容以及 apigateway 定制化的 http 請求頭部信息
{"headerParameters":{},"headers":{...},"httpMethod":"GET","path":"/params_log","pathParameters":{},"queryString":{},"queryStringParameters":{},"requestContext":{"httpMethod":"ANY","identity":{},"path":"/params_log","serviceId":"service-9khp96qy","sourceIp":"120.229.9.165","stage":"release"}}
- cos 觸發器的 event 對象包括了觸發執行的具體 cos 操作以及 cos 對象
{"Records":[{"cos":{"cosBucket":{"appid":"1251133793","name":"test","region":"gz"},"cosNotificationId":"unkown","cosObject":{"key":"/1251133793/test/xxx.png","meta":{"Content-Type":"image/png","x-cos-request-id":"NWViNTZmMmFfOTJhODQwYV80MGZmXzI0Y2ZkYmM="},"size":6545739,"url":"...","vid":""},"cosSchemaVersion":"1.0"},"event":{"eventName":"cos:ObjectCreated:Put","eventQueue":"qcs:0:scf:ap-guangzhou:appid/1251133793:default.params_log.$DEFAULT","eventSource":"qcs::cos","eventTime":1588948779,"eventVersion":"1.0","reqid":1038862404,"requestParameters":{"requestHeaders":{"Authorization":"..."},"requestSourceIP":"120.229.9.165"},"reservedInfo":""}}]}
第二個參數 context,函數運行時信息
我們來看一下一個完整的 context 包含的內容:
callbackWaitsForEmptyEventLoop: true,
getRemainingTimeInMillis: 200,
memory_limit_in_mb: 128,
time_limit_in_ms: 3000,
environment: "{"SCF_NAMESPACE":"demo","TENCENTCLOUD_SECRETID":"...","TENCENTCLOUD_SECRETKEY":"...","TENCENTCLOUD_SESSIONTOKEN":"..."}"
function_name: "params",
function_version: "$LATEST",
namespace: "demo",
request_id: "ab42b693-8bfd-4dc1-b228-60360a63e06c",
tencentcloud_appid: "...",
tencentcloud_region: "ap-chengdu",
tencentcloud_uin: "..."
從上面的內容可以看到,該對象包含的內容有:
- 函數配置信息,比如設置的內容大小,超時時間等
- 執行身份認證信息。如果設置了函數的運行角色(角色必須要包含對應操作的授權策略),在環境變量中會注入 secretId,secretKey,sessionToken,在訪問第三方雲服務,比如 cos、自定義監控數據上報時就可以使用這幾個值直接調用雲 api,而不用在代碼里面去 hard code 各種密鑰信息
- 環境變量:包括了用戶自定義的環境變量以及一些系統環境變量
- 執行環境基本信息:包括了當前函數調用的地域,用戶的 appId,uin
第三個參數 callback 是一個可選參數,在非異步函數中返回執行結果
回調函數采用兩個參數:一個 Error 和一個返回。返回對象必須與 JSON.stringify
兼容。異步函數將忽略 callback 的返回,必須通過 return、throw exception 或者 promise 來處理返回或錯誤
const https = require('https')
let url = "https://cloud.tencent.com/"
exports.main_handler = function(event, context, callback) {
https.get(url, (res) => {
callback(null, res.statusCode)
}).on('error', (e) => {
callback(Error(e))
})
}
函數返回
我們來看一下,針對異步場景(async 函數)和非異步場景,雲函數怎么把返回值傳遞出去
異步函數
對於異步函數,可以使用 return 和 throw 來發送返回或錯誤。函數必須使用 async 關鍵字。在異步函數中,第三個參數 callback 沒有定義
示例:異步函數
const https = require('https')
let url = "https://cloud.tencent.com/"
const httpRequest = url => {
const promise = new Promise(function(resolve, reject) {
https
.get(url, res => {
resolve(res.statusCode)
})
.on('error', e => {
reject(Error(e))
})
})
return promise
}
exports.handler = async function(event, context) {
try{
const result = await httpRequest(url)
// 在async函數中callback未定義
// callback(null, result)
return result
}catch(e) {
throw e
}
}
同步函數
還是上面的例子,發起一個 http 請求,如果用同步函數實現,參照以下示例
示例:同步函數,callback 返回
const https = require('https')
let url = "https://cloud.tencent.com/"
exports.handler = function(event, context, callback) {
https.get(url, (res) => {
// 只能通過callback返回,return會被忽略
callback(null, res.statusCode)
}).on('error', (e) => {
callback(Error(e))
})
}
返回的時機
正常的 Node.js web framework 在 response 返回后,異步邏輯還是繼續在執行的。而 Serverless 場景下,由於機制和 framework的差別,對於已經返回 responese 的情況,一種是等着異步都處理完再來返回,這樣保證了一次調用的完整性。另外一種就是在返回后就直接結束當次調用,直接掛起異步處理。
騰訊雲雲函數針對 Node.js 的異步場景,實現了返回和結束分離的特殊機制。
- 入口函數的同步執行過程完成及返回后,雲函數的調用將立刻返回,並將代碼的返回信息返回給函數調用方
- 同步流程處理並返回后,代碼中的異步邏輯可以繼續執行和處理,直到異步事件執行完成后,雲函數的實際執行過程才完成和退出。
默認情況下,函數執行會等待所有異步執行結束才算一次調用結束,但也給用戶提供了關閉事件循環等待的選項,用戶可以關閉事件循環等待來自行控制函數的返回時機。通過在 callback 回調執行前設置 context.callbackWaitsForEmptyEventLoop = false
,可以使雲函數在執行返回后立刻凍結進程,不再等待異步循環內的事件
比如一下示例代碼,代碼里面發起了一個異步 http 請求,另外有一個 2s 后執行的 setTimeout
const https = require('https')
let url = "https://cloud.tencent.com/"
const httpRequest = url => {
const promise = new Promise(function(resolve, reject) {
https
.get(url, res => {
resolve(res.statusCode)
})
.on('error', e => {
reject(Error(e))
})
})
return promise
}
exports.main_handler = async function(event, context) {
// 設置該選項為false會不等待異步隊列執行完,而是在返回后直接凍結process
//context.callbackWaitsForEmptyEventLoop = false
try{
const result = await httpRequest(url)
setTimeout(() => {
console.log('timeout log')
}, 2000)
return result
}catch(e) {
throw e
}
}
在 http 請求完成后,會立即返回給調用方,不會等待 setTimeout 的異步實踐執行完。而在返回后,程序會繼續執行,直到 setTimeout 的事件執行完才算本次調用結束。
在設置了 context.callbackWaitsForEmptyEventLoop = false
后,在 return 后進程會被凍結,setTimeout 里面的執行邏輯會被掛起
完整流程圖
以下是單實例內 runtime 運行的完整流程圖
針對 Node.js應用,有以下幾個實踐建議:
- 日志:runtime重寫了 console 的幾個主要方法,而且是在 require 用戶文件之后,所以用戶自定義日志選項會無效
- 緩存復用:在入口函數外可以定義變量,存儲可以復用的緩存對象,比如數據庫的連接等
Node.js 的模塊實現邏輯中,如果一個 module 被 require 過,該模塊就會被 cache 到內存中,再次被 require時不會重新初始化。針對這一特性,如果實例一直再復用,那么在入口文件中,入口函數外定義的變量都不會被銷毀,可以達到復用的效果 - 內置部分 npm 包,可以直接使用,具體參照文檔。部署雲函數代碼時推薦
npm install --production
,減少代碼包的體積,提升上傳速度和執行速度 - 執行角色:配置執行角色,從 context 中可以獲取臨時的密鑰信息,可以用了訪問有相應權限的第三方服務,而不用在代碼內寫死密鑰信息
Serverless Framework 30 天試用計划
我們誠邀您來體驗最便捷的 Serverless 開發和部署方式。在試用期內,相關聯的產品及服務均提供免費資源和專業的技術支持,幫助您的業務快速、便捷地實現 Serverless!
One More Thing
3 秒你能做什么?喝一口水,看一封郵件,還是 —— 部署一個完整的 Serverless 應用?
復制鏈接至 PC 瀏覽器訪問:https://serverless.cloud.tencent.com/deploy/express
3 秒極速部署,立即體驗史上最快的 Serverless HTTP 實戰開發!
傳送門:
- GitHub: github.com/serverless
- 官網:serverless.com
歡迎訪問:Serverless 中文網,您可以在 最佳實踐 里體驗更多關於 Serverless 應用的開發!