基於 Node.js 的輕量級雲函數功能實現

導語

在萬物皆可雲的時代,你的應用甚至不須要服務器。雲函數功能在各大雲服務中均有提供,那麼,如何用「無所不能」的 node.js 實現呢?javascript

1、什麼是雲函數?

雲函數是誕生於雲服務的一個新名詞,顧名思義,雲函數就是在雲端(即服務端)執行的函數。各個雲函數相互獨立,簡單且目的單一,執行環境相互隔離。使用雲函數時,開發者只須要關注業務代碼自己,其它的諸如環境變量、計算資源等,均由雲服務提供。html

2、爲何須要雲函數?

程序員說不想買服務器,因而便有了雲服務;
程序員又說連 server 都不想寫了,因而便有了雲函數。前端

Serverless 架構

一般咱們的應用,都會有一個後臺程序,它負責處理各類請求和業務邏輯,通常都須要跟網絡、數據庫等 I/O 打交道。而所謂的無服務器架構,就是把除了業務代碼外的全部事情,都交給執行環境處理,開發者不須要知道 server 怎麼跑起來,數據庫的 api 怎麼調用——一切交給外部,在「溫室」裏寫代碼便可。java

FaaS

而云函數,正是 serverless 架構得以實現的途徑。咱們的應用,將是一個個獨立的函數組成,每個函數裏,是一個小粒度的業務邏輯單元。沒有服務器,沒有 server 程序,「函數即服務」(Functions as a Service)。node

3、如何實現?

因爲本實現是應用在一個 CLI 工具裏面的,函數聲明在開發者的項目文件裏,於是大體過程以下:程序員

雲函數存取

一、函數聲明與存儲

聲明

咱們的目標是讓雲函數的聲明和通常的 js 函數沒什麼兩樣:數據庫

module.exports = async function (ctx) {
    return 'hahha'
  }
};

因爲雲函數的執行一般伴隨着接口的調用,因此應該要能支持聲明 http 方法:api

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 類型的字段裏便可。promise

二、函數執行

url

若是用於前端調用,每一個雲函數須要有一個對應的 url,以上述聲明文件的文件名爲雲函數的惟一名稱的話,能夠簡單將 url 設計爲:

/f/:funcname

構造獨立做用域(重點)

在 js 世界裏,執行一個字符串類型的函數體,有如下這麼一些途徑:

  1. eval 函數
  2. new Function
  3. vm 模塊

那麼要選哪種呢?讓咱們回顧雲函數的特色:各自獨立,互不影響,運行在雲端
關鍵是將每一個雲函數放在一個獨立的做用域執行,而且沒有訪問執行環境的權限,所以,最優選擇是 nodejs 的 vm 模塊。關於該模塊的使用,可參考官方文檔
至此,雲函數的執行能夠分爲三步:

  1. 從數據庫獲取函數體
  2. 構造 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);
  1. 執行函數獲得結果
const code = `func = ${funcBody}; promise = func(ctx);`;
vm.runInContext(code, sandbox);
const data = await sandbox.promise;

NPM社區的 vm2 模塊針對 vm 模塊的一些安全缺陷作了改進,也可用此模塊,思路大抵相同。

三、引用

雖說原則上雲函數應當互相獨立,各不相欠,可是爲了提升靈活性,咱們仍是決定支持函數間的相互引用,便可以在某雲函數中調用另一個雲函數。

聲明

很簡單,加個函數名稱的數組字段就好:

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('\n')}
    }`;

    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]);
        });
    }
}

四、性能

上述方案中,每次雲函數執行的時候,都須要進行一下幾步:

  1. 獲取函數體
  2. 編譯代碼
  3. 構造做用域和獨立環境
  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
}

4、參考資料

相關文章

什麼是Serverless(無服務器)架構?

業界的 serverless

騰訊雲 - 無服務雲函數
阿里雲 - 函數計算
AWS - Lambda
Azure - Azure Functions

相關文章
相關標籤/搜索