摘要:各個云函數相互獨立,簡單且目的單一,執行環境相互隔離。而云函數,正是架構得以實現的途徑。獲取所有依賴的函數依賴循環既然可以相互依賴,那必然會可能出現這種循環的依賴情況,所以需要在開發者提交云函數的時候,檢測依賴循環。
導語
在萬物皆可云的時代,你的應用甚至不需要服務器。云函數功能在各大云服務中均有提供,那么,如何用“無所不能”的 node.js 實現呢?
一、什么是云函數?云函數是誕生于云服務的一個新名詞,顧名思義,云函數就是在云端(即服務端)執行的函數。各個云函數相互獨立,簡單且目的單一,執行環境相互隔離。使用云函數時,開發者只需要關注業務代碼本身,其它的諸如環境變量、計算資源等,均由云服務提供。
二、為什么需要云函數?程序員說不想買服務器,于是便有了云服務;
程序員又說連 server 都不想寫了,于是便有了云函數。
通常我們的應用,都會有一個后臺程序,它負責處理各種請求和業務邏輯,一般都需要跟網絡、數據庫等 I/O 打交道。而所謂的無服務器架構,就是把除了業務代碼外的所有事情,都交給執行環境處理,開發者不需要知道 server 怎么跑起來,數據庫的 api 怎么調用——一切交給外部,在“溫室”里寫代碼即可。
FaaS而云函數,正是 serverless 架構得以實現的途徑。我們的應用,將是一個個獨立的函數組成,每一個函數里,是一個小粒度的業務邏輯單元。沒有服務器,沒有 server 程序,“函數即服務”(Functions as a Service)。
三、如何實現?由于本實現是應用在一個 CLI 工具里面的,函數聲明在開發者的項目文件里,因而大致過程如下:
1、函數聲明與存儲 聲明我們的目標是讓云函數的聲明和一般的 js 函數沒什么兩樣:
module.exports = async function (ctx) { return "hahha" } };
由于云函數的執行通常伴隨著接口的調用,所以應該要能支持聲明 http 方法:
module.exports = { method: "POST", handler: async function (ctx) { return "hahha" } };存儲
由于有 method 等配置,因此編譯的時候,需要把上述聲明文件 require 進來,此時,handler 字段是一個 Function 類型的對象。可以調用其 toString 方法,得到字符串類型的函數體:
const f = require("./func.js"); const method = f.method; const body = f.handler.toString(); // async function (ctx) { // return "hahha" // }
有了字符串的函數體,存儲就很簡單了,直接存在數據庫 string 類型的字段里即可。
2、函數執行 url如果用于前端調用,每個云函數需要有一個對應的 url,以上述聲明文件的文件名為云函數的唯一名稱的話,可以簡單將 url 設計為:
/f/:funcname構造獨立作用域(重點)
在 js 世界里,執行一個字符串類型的函數體,有以下這么一些途徑:
eval 函數
new Function
vm 模塊
那么要選哪一種呢?讓我們回顧云函數的特點:各自獨立,互不影響,運行在云端。
關鍵是將每個云函數放在一個獨立的作用域執行,并且沒有訪問執行環境的權限,因此,最優選擇是 nodejs 的 vm 模塊。關于該模塊的使用,可參考官方文檔。
至此,云函數的執行可以分為三步:
從數據庫獲取函數體
構造 context
// ctx 為 koa 的上下文對象 const sandbox = { ctx: { params: ctx.params, query: ctx.query, body: ctx.request.body, userid: ctx.userid, }, promise: null, console: console } vm.createContext(sandbox);
執行函數得到結果
const code = `func = ${funcBody}; promise = func(ctx);`; vm.runInContext(code, sandbox); const data = await sandbox.promise;
NPM社區的 vm2 模塊針對 vm 模塊的一些安全缺陷做了改進,也可用此模塊,思路大抵相同。
3、引用雖然說原則上云函數應當互相獨立,各不相欠,但是為了提高靈活性,我們還是決定支持函數間的相互引用,即可以在某云函數中調用另外一個云函數。
聲明很簡單,加個函數名稱的數組字段就好:
module.exports = { method: "POST", use: ["func1", "func2"], handler: async function (ctx) { return "hahha" } };注入
也很簡單,根據依賴鏈把函數都找出來,全部掛載在 ctx 下就好,深度優先或者廣度優先都可以。
if (func.use) { const funcs = {}; const fnames = func.use; for (let i = 0; i < fnames.length; i++) { const fname = fnames[i]; await getUsedFuncs(ctx, fname, funcs); } const funcCode = `{ ${Object.keys(funcs).map(fname => `${fname}:${funcs[fname]}`).join(" ")} }`; code = `ctx.methods=${funcCode};${code}`; } else { code = `ctx.methods={};${code}`; } // 獲取所有依賴的函數 const getUsedFuncs = async (ctx, funcName, methods) => { const func = getFunc(funcName); methods[funcName] = func.body; if (func.use) { const uses = func.use.split(","); for (let i = 0; i < uses.length; i++) { await getUsedFuncs(ctx,uses[i], methods); } } }依賴循環
既然可以相互依賴,那必然會可能出現 a→b→c→a 這種循環的依賴情況,所以需要在開發者提交云函數的時候,檢測依賴循環。
檢測的思路也很簡單,在遍歷依賴鏈的過程中,每一個多帶帶的鏈條都記錄下來,如果發現當前遍歷到的函數在鏈條里出現過,則發生循環。
const funcMap = {}; flist.forEach((f) => { funcMap[f.name] = f; }); const chain = []; flist.forEach((f) => { getUseChain(f, chain); }); function getUseChain(f, chain) { if (chain.includes(f.name)) { throw new Error(`函數發生循環依賴:${[...chain, f.name].join("→")}`); } else { f.use.forEach((fname) => { getUseChain(funcMap[fname], [...chain, f.name]); }); } }4、性能
上述方案中,每次云函數執行的時候,都需要進行一下幾步:
獲取函數體
編譯代碼
構造作用域和獨立環境
執行
步驟3,因為每次執行的參數都不一樣,也會有不同請求并發執行同一個函數的情況,所以作用域 ctx 無法復用;步驟4是必須的,那么可優化點就剩下了1和2。
代碼緩存vm 模塊提供了代碼編譯和執行分開處理的接口,因此每次獲取到函數體字符串之后,先編譯成 Script 對象:
// ...get code const script = new vm.Script(code);
執行的時候可以直接傳入編譯好的 Script 對象:
// ...get sandbox vm.createContext(sandbox); script.runInContext(sandbox); const data = await sandbox.promise;函數體緩存
簡單的緩存,不需要很復雜的更新機制,定一個時間閾值,超過后拉取新的函數體并編譯得到 Script 對象,然后緩存起來即可:
const cacheFuncs = {}; // ...get script cacheFuncs[funcName] = { updateTime: Date.now(), script, }; // cache time: 60 sec const cacheFunc = cacheFuncs[cacheKey]; if (cacheFunc && (Date.now() - cacheFunc.updateTime) <= 60000) { const sandbox = { /*...*/ } vm.createContext(sandbox); cacheFunc.script.runInContext(sandbox); const data = await saandbox.promise; return data; } else { // renew cache }四、參考資料 相關文章
什么是Serverless(無服務器)架構?
業界的 serverless騰訊云 - 無服務云函數
阿里云 - 函數計算
AWS - Lambda
Azure - Azure Functions
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/105385.html
Node.js從2009年誕生至今,已經發展了兩年有余,其成長的速度有目共睹。從在github的訪問量超過Rails,到去年底Node.jsS創始人Ryan Dalh加盟Joyent獲得企業資助,再到今年發布Windows移植版本,Node.js的前景獲得了技術社區的肯定。InfoQ一直在關注Node.js的發展,在今年的兩次Qcon大會(北京站和杭州站)都有專門的講座。為了更好地促進Node.j...
摘要:因為路由層面受業務影響很大,經常修改一些功能的行為,所以后來大部分測試都是針對層面的單元測試。在我了解的過程中,我發現中文網絡上對的討論非常分散,于是我創建了中文社區,到年末已經有個注冊用戶和個帖子了。 https://jysperm.me/2016/02/programming-of-2015/ 從 2014 年末開始開發的一個互聯網金融項目終于在今年三月份上線了,這是一個 Node...
閱讀 3573·2019-08-30 15:55
閱讀 1372·2019-08-29 16:20
閱讀 3655·2019-08-29 12:42
閱讀 2660·2019-08-26 10:35
閱讀 1010·2019-08-26 10:23
閱讀 3404·2019-08-23 18:32
閱讀 897·2019-08-23 18:32
閱讀 2891·2019-08-23 14:55