乐趣区

关于前端:lambda-nodejs-函数降低冷启动时间的最佳实践

lambda nodejs 函数升高冷启动工夫的最佳实际

  • lambda nodejs 函数升高冷启动工夫的最佳实际

    • 前言
    • 什么是冷启动工夫
    • 打包服务端 js
    • 什么是 inline
    • 进一步封装的打包工具
    • 存在的弊病以及解决方案
    • Next Chapter
    • 残缺示例及文章仓库地址

前言

本文章的思路,继承倒退自这两篇文章:

  • serverless 升高冷启动工夫的摸索 – 服务端打包 node_modules
  • Nodejs 云函数冷启动工夫的优化

这里要感激这 2 篇文章的作者:ice breaker2年前就提供了这么优良的思路和解决方案了,真是忍不住给他点赞呀。

首先在看这篇文章之前,我先必须给你介绍一个概念,就是 冷启动工夫

什么是冷启动工夫

这个个性各个服务商的 serverless 云函数都存在,这个和 函数容器的生命周期 非亲非故。

Lambda 为例,Lambda 生命周期能够分为三个阶段:

  • Init:在此阶段,Lambda 会尝试冻结之前的执行环境,若没有可冻结的环境,Lambda 会进行资源创立,下载函数代码,初始化扩大和 Runtime,而后开始运行初始化代码(主程序外的代码)。
  • Invoke:在此阶段,Lambda 接管事件后开始执行函数。函数运行到实现后,Lambda 会期待下个事件的调用。
  • Shutdown:如果 Lambda 函数在一段时间内没有接管任何调用,则会触发此阶段。在 Shutdown 阶段,Runtime 敞开,而后向每个扩大发送一个 Shutdown 事件,最初删除环境。

当您在触发 Lambda 时,若以后没有处于激活阶段的 Lambda 可供调用,则 Lambda 会下载函数的代码并创立一个 Lambda 的执行环境。从事件触发到新的 Lambda 环境创立实现这个周期通常称为“冷启动工夫”。显然,这个工夫必定是越短越好的。

这里能够参考 AWS 这篇博客 以获取更多信息。

其中 AWS 提供的几种升高冷启动工夫的形式有:

  • 抉择适合的编程语言(咱们大部分状况无奈更换)
  • 减小应用程序大小
  • 预热 (定时触发器避免回收和预置并发,保留实例)
  • JVM 分层编译(java 特供)

其中可行性最高的形式,就是本篇文章要探讨的 减小应用程序大小

打包服务端 js

回到正题,为什么要去打包服务端 js 代码呢? 用 layer 的形式不是蛮好吗?

这里必须要晓得的一点是,函数冷启动的工夫,是和整体运行以及其依赖的代码包大小,是非亲非故的。

比方上篇文章中的示例,咱们把 uuid 这个依赖给做成 layer 上传了下来,然而你有没有想过,既然 uuid 的所有实现都是 js,为什么不把它整个源代码,打入咱们的函数构建产物中呢?这样还省了依赖一个 layer 呢。

同样的情理,咱们函数也能够把 express,lodash 等等依赖,全副打入咱们的函数包里去,以减小整体代码包的体积。

这就像咱们在写前端我的项目那样,实质上也会把所有运行时代码,全副给 inline 打成一个一个 chunk 到各个 js 外面去,毕竟浏览器可没有什么 node_modules 的加载机制。你写 vue 还是写 react 都是间接加载它们 inline 的整个代码的。

什么是 inline

这里给一个例子: 原先你的 ts 代码可能是这样写的:

import express from 'express'

而后通过 tsc,产物变成了这样:

// commonjs format
const express = require('express')

而如果走 inline 那产物中就不会呈现 express,而是间接把 express 相干的代码全副给打了进来,成果相似于:

// 局部代码
var require_express = __commonJS({"../../node_modules/.pnpm/express@4.18.2/node_modules/express/lib/express.js"(exports, module2) {
    "use strict";
    var bodyParser2 = require_body_parser();
    var EventEmitter = require("events").EventEmitter;
    var mixin = require_merge_descriptors();
    var proto = require_application();
    var Route = require_route();
    var Router = require_router();
    var req = require_request2();
    var res = require_response2();
    exports = module2.exports = createApplication;
    function createApplication() {var app2 = function(req2, res2, next) {app2.handle(req2, res2, next);
      };
      mixin(app2, EventEmitter.prototype, false);
      mixin(app2, proto, false);
      app2.request = Object.create(req, {app: { configurable: true, enumerable: true, writable: true, value: app2}
      });
      app2.response = Object.create(res, {app: { configurable: true, enumerable: true, writable: true, value: app2}
      });
      app2.init();
      return app2;
    }
    exports.application = proto;
    exports.request = req;
    exports.response = res;
    exports.Route = Route;
    exports.Router = Router;
    exports.json = bodyParser2.json;
    exports.query = require_query();
    exports.raw = bodyParser2.raw;
    exports.static = require_serve_static();
    exports.text = bodyParser2.text;
    exports.urlencoded = bodyParser2.urlencoded;
    var removedMiddlewares = [
      "bodyParser",
      "compress",
      "cookieSession",
      "session",
      "logger",
      "cookieParser",
      "favicon",
      "responseTime",
      "errorHandler",
      "timeout",
      "methodOverride",
      "vhost",
      "csrf",
      "directory",
      "limit",
      "multipart",
      "staticCache"
    ];
    removedMiddlewares.forEach(function(name) {
      Object.defineProperty(exports, name, {get: function() {throw new Error("Most middleware (like" + name + ") is no longer bundled with Express and must be installed separately. Please see https://github.com/senchalabs/connect#middleware.");
        },
        configurable: true
      });
    });
  }
});
// ......

显然这种形式下,能够打出更小更繁多的包,因为所有的碎片化的 js 依赖,都被打成到了单文件外面去了,缩小了 io 的次数,而且还可能一起制订策略,如 split chunk or compress

要实现这种成果,其实基本上所有风行的打包工具都内置了这个性能。

比方 webpack/esbuild,当然 rollup 也有对应的插件反对,@rollup/plugin-node-resolve 就是它的实现形式之一。

其中笔者文章结尾提到的 2 篇文章里的实现形式,就是基于 rollup 这个工具去实现的,在此不再叙述。在 2 年后的明天,也很快乐看到了更多,基于它们的开箱即用的工具,使得咱们不须要装置大量的插件或者编写简单的打包配置,就能实现同样的成果,让咱们一起来看看吧。

进一步封装的打包工具

在过来,比拟构建库比拟风行 rollup 或者 esbuild,不过当初有了基于它们更进一步的打包工具: unbuild / tsup

其中 unbuild 这个工具先跳过,咱们会在 monorepo 章节中介绍它,这里咱们次要来介绍 tsup 在函数打包中的用法。

tsupesbuild 进一步封装而来。它太开箱即用了,甚至能够 0 config。它自身的打包配置,次要是基于约定的:

比方它会默认去 inline 咱们所有在运行时援用的,然而却是注册在 devDependencies 里的包

dependencies 里的包,则是被默认退出了 external 中,不进行 node resolve

这个约定实际上很简略却很实用,相似于这样 rollup 的配置:

// rollup.config.ts
import {readFileSync} from 'node:fs'
const pkg = JSON.parse(
  readFileSync('./package.json', {encoding: 'utf8'})
)
const dependencies = pkg.dependencies as Record<string, string> | undefined
const config: RollupOptions = {
  // ...
  plugins: [json(),
    nodeResolve(),
    commonjs(),
    typescript()],
  external: [...(dependencies ? Object.keys(dependencies) : [])]
}

显然比照起来 tsup.config.ts 文件的配置就 简洁 多了,见下:

// tsup.config.ts
import {defineConfig} from 'tsup'
const isDev = process.env.NODE_ENV === 'development'
export default defineConfig({entry: ['src/index.ts'],
  splitting: false,
  sourcemap: isDev,
  clean: true,
  // external: []})

接着注册指令即可 package.json 里的 npm scripts (这里我加了 cross-env 包和 NODE_ENV 环境变量是为了做更多,比方条件编译等等事件)

  "scripts": {
    "dev": "cross-env NODE_ENV=development tsup --watch",
    "build": "cross-env NODE_ENV=production tsup",
  },

这样执行这命令,你的函数就被打入 dist/inde.js 里了,连忙去检查一下产物吧。

存在的弊病以及解决方案

下面说的这种打包形式其实存在肯定的弊病:

  1. 首先,它扭转了第三方依赖的目录构造
  2. 其次它只能解决一些 js 依赖,不应用特定的插件,经常会呈现一些非 js 依赖的缺失

这个问题的严重性会导致一系列的问题,比方某些包源代码外面是依赖文件目录的:

// node_modules/some-lib/dist/index.js
const defaultDbFile = path.resolve(__dirname, '../data/ip2region.xdb')

那这行代码被打入咱们函数包就会有问题,因为目录构造被毁坏了,这会导致第三方包调用出错。

目录构造的变动如下:

(打包前)本地能够运行的目录构造

  • src

    • index.ts
  • node_modules

    • some-lib

      • dist

        • index.js
      • data

        • ip2region.xdb

(打包后)

  • dist

    • index.js
  • node_modules

    • some-lib

      • dist (用不到了)

        • index.js
      • data

        • ip2region.xdb

留神此时 node_modules/some-lib/dist/index.js 里的代码 inline 到了 dist/index.js 里去了。然而 defaultDbFile 的援用的门路却变动了,因为 __dirname 变动了,此时正确的门路实际上是 path.resolve(__dirname, '../node_modules/some-lib/data/ip2region.xdb')

那么如何解决呢?目前比拟好的解决方案,是应用 external 的形式,不去被动 inline 那些可能会导致问题的包,并把那些包挑出来,做成 layer 再进行绑定。幸好这种包是小概率会遇到的,测试环境很容易发现问题。

Next Chapter

当初你曾经学会了打包服务端代码的策略。

下一篇,《更灵便的 serverless framework 配置文件》中,将会具体介绍如何让你的部署配置文件变得灵便起来。

残缺示例及文章仓库地址

https://github.com/sonofmagic/serverless-aws-cn-guide

如果你遇到什么问题,或者发现什么勘误,欢送提 issue 给我

退出移动版