• 依赖项的处理与层的创建与注册


    Image

    依赖项的处理与层的创建与注册

    新问题

    在一个 nodejs 函数里,我们往往会去安装和使用各种各样的依赖包,来辅助我们项目的开发。

    这些依赖以及对应的版本都被注册在了 package.json里 (由 npm init创建)。在安装依赖之后,它们会被存放在当前项目的 node_modules 目录里。而 sls deploy 默认也会把 node_modules 里所有文件,打包压缩后,和代码一起上传到云上。这点很好理解,没有依赖项,函数压根跑不起来。

    但是当我们 node_modules 依赖足够大,足够深之后,整个 node_modules 就会变成一个黑洞。随随便便就有 1GB/2GB 甚至更多,这时候去进行 sls deploy 就会变成一种折磨,因为要压缩 node_modules,这个行为耗时太长了,即使压缩好了,上传到 S3 对象存储也要花费很久的时间。

    如何解决? 这时候就需要让我们的 layer(层)出场了。

    什么是 layer?

    layer 你可以理解成,预先放置在我们 Lambda 函数容器中的文件包,你可以在里面放一些代码,库,数据文件,配置文件甚至是一些自定义语言运行时。比如我在使用的时候,往往会把 运行时 依赖的包,打成 zip 上传上去,又或者有些爬虫函数,需要使用 chromium 来模仿用户的行为,那么这种case则需要在 layer 里面直接内置一个 headless chromium

    这些文件最终都会被挂载到函数容器中的 /opt 目录。

    layer 的创建与注册

    这里我给出一个示例,假设运行时的依赖只有 uuid (假设我们只依赖这一个包)

    我们现在项目中安装 npm i uuid 并使用它:

    // index.ts
    import { v4 } from 'uuid'
    v4()
    // ...code...
    
    • 1
    • 2
    • 3
    • 4

    这时候 uuid 在函数package.json这一层的 node_modules 文件夹里,测试运行后,运转良好。

    让我们在当前目录下,创建一个 layers 文件夹,然后再在里面创建一个 uuid(名称可自定义)文件夹,创建 package.json 并写入:

    {
      "dependencies": {
        "uuid": "^9.0.0"
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    layers/uuid/package.json#dependencies 字段中的 uuid 即为你主目录下安装的版本。

    然后执行 npm i/yarn 安装依赖 (不要使用pnpm),安装完成后会出现 layers/uuid/node_modules 文件夹,接下来就可以 layer 的上传和绑定了。

    与函数同时创建和绑定

    我们可以复用原先部署函数的那个 serverless.yml 文件,让它不但部署函数,也同时创建 layer 并进行绑定。具体配置如下:

    layers:
      # layer 配置
      uuid:
        # 路径
        path: layers/uuid
        # aws lambda layer 里的名称
        name: ${sls:stage}-uuid
    
    functions:
      api:
        # ...
        package:
          individually: true
          # 既然我们把所有运行时都打成 layer 了自然不用上传 node_modules 了
          patterns:
            - "!node_modules/**"
        # layer 绑定配置
        layers:
          # 绑定 UuidLambdaLayer
          - !Ref UuidLambdaLayer
        environment:
          # 环境变量,告诉函数应该从哪里找依赖
          NODE_PATH: "./:/opt/node_modules"
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    其中最重要的就是2layers的配置了,其中 !Ref UuidLambdaLayer 这个其实就是引用了layers.uuid,只不过它的命名是一种约定,即 uuidu 大写加上 LambdaLayer,于是就变成了 UuidLambdaLayer 了,命名规则为 layer 名称的 Title_CaseLambdaLayer

    注意!NODE_PATH: "./:/opt/node_modules" 这个环境变量是必不可少的,不然会导致无法从 layer 中加载 node_modules

    相关配置的参考链接

    此时使用 sls package 会在 .serverless 目录下生成 2zip:

    • api.zip 函数代码包
    • uuid.zip layer包

    2个压缩包的名字,就是我们在 serverless.yml注册的名字。

    sls deploy 会把它们依次上传,先layerfunction,并把layer上传部署后的结果,注册进我们的function,从而达成绑定的效果。

    单独上传 layer 再绑定函数(推荐)

    除了与函数同时创建和绑定的形式,我们还可以分步骤单独上传 layer 再绑定函数,这也是我推荐的方式。

    这个就顾名思义,我们可以先上传layer拿到结果,在把结果写到函数的 serverless.yml 中去。这样步骤方面分为了 2 步,但是好处却是显而易见的。

    它适用于这样的场景,我们的 layer 包很大,且不经常更新,这种情况是没有必要每次都去打包上传 layer 的。

    我们部署只需要部署我们自己的函数代码,然后告诉 AWS 应该去绑定哪个 layer 的哪个版本就行。

    按照这样的思路,我们就可以把刚刚那个 serverless.yml 拆成 2yml 文件:

    1. 单独上传 layer 配置文件
    2. 函数的 deploy 配置文件,加一行绑定 layer 的配置即可

    单独部署 layer 的配置,内容如下:

    # layers/serverless.yml
    layers:
      uuid:
        path: uuid
        name: ${sls:stage}-uuid
    
    • 1
    • 2
    • 3
    • 4
    • 5

    执行 sls deploy 后上传部署成功会显示:

    layers:
      uuid: arn:aws-cn:lambda:cn-northwest-1:000000000000:layer:dev-uuid:2
    
    • 1
    • 2

    这一个字符串就是接下来我们需要写入函数 yml 配置进行绑定的关键,当然当时没保存刷了terminal也没关系,这个信息通过在当前目录执行 sls info 还会显示出来。

    接下来我们去函数的 yml 配置去绑定 layers:

    # serverless.yml
    functions:
      api:
        # ....
        layers:
          # - !Ref UuidLambdaLayer 改为
          - arn:aws-cn:lambda:cn-northwest-1:000000000000:layer:dev-uuid:2
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    这样再在函数这一层执行 sls deploy 这层绑定关系就完成了,同时上传速度也要比 与函数同时创建和绑定 快不少,因为这种方法只要上传函数的代码包,再通过调用 AWS API 告诉它们这层绑定关系即可。

    真正的运行时依赖

    之前有一个细节没有讲,为什么我们上传 layer 是单独建一个 layers 的目录,在里面单个单个上传,而不是把外面函数的 node_modules 整个打包上传上去呢?

    其实那样也可以,但是不够完美,因为这样做会导致layer代码包中的文件,过于冗余

    举个简单的例子,默认情况下 npm 安装包时,会把 devDependenciesdependencies 都安装在 node_modules

    比如 dependencies 里就是一些 express/lodash 啥的,devDependencies 里面就是 eslint/sass/webpack 这些开发时用的包,运行代码的时候用不着。

    这时候,你直接打包上传 node_modules 显然是可以的,因为你所有的运行时依赖已经在里面了,不幸的是 eslint 等等许多无关紧要的包也进去了,这些加起来有可能会占用你 layer 包整体体积的一般以上甚至更多,显然这是没有必要的。

    所以,你必须找到真正的运行时依赖!

    注册包的约定

    首先你必须在安装包时正确的注册 devDependenciesdependencies

    我们把运行时用的到的放在 dependencies 里,到时候也从这里面抽出包名和版本,做成 layer。至于devDependencies 里我们只会把那些开发时用得到的包放在里面,它们就没有必要上传了,本地运行即可。

    与平台强关联的运行时

    其次你必须很清楚你的运行时是 nodejs 而不是什么浏览器。

    虽然说 nodejs 是跨平台的,但是我们使用的第三方库里,有时候会存在着很多和平台强关联的二进制文件。

    比如有些包会在下载下来之后,调用 postinstall 脚本获取我们的系统信息(平台,cpu架构),然后根据这些信息去使用不同的二进制文件。

    又或者有些包基于 C++ addons 的,安装好之后,才在我们机器上实时去编译,生成适配我们系统平台的二进制文件。

    这就导致一个问题,假如你直接用 win/macos 开发,你安装的二进制包,大概率无法在 aws lambda 环境下运行,因为它的环境是 Amazon Linux 2

    所以,我们应该在挑选与线上 Lambda 运行时,相似的容器中,进行开发,上传代码和层 (layer)

    具体怎么做呢?这里给出三种解决方案:

    1. 云端安装依赖

    顾名思义,直接在云上的目标容器环境中进行安装依赖这个行为,这也是最简单的解决方案。

    2. 本地构建 Amazon Linux 2 容器环境

    这要求我们在 docker 容器中开发,拉下 Amazon Linux 2 的镜像,在里面开发并上传层函数和代码包。

    3. 利用 CI 构建并进行上传和部署

    这个思路便是,由环境相同或者相似的 CI 容器,进行上传和部署。

    比如我们可以利用 Github Action 去构建和上传,大致的配置如下:

    name: Layer
    
    on:
      workflow_dispatch:
    
    jobs:
      upload:
        name: upload
        timeout-minutes: 15
        strategy:
          fail-fast: false
          matrix:
            # 找一个相似的镜像 os
            os: [ubuntu-latest]
            node-version: [18]
        runs-on: ${{ matrix.os }}
    
        steps:
          - name: Check out code
            uses: actions/checkout@v3
            with:
              fetch-depth: 2
    
          - name: Configure AWS credentials from Test account
            uses: aws-actions/configure-aws-credentials@v3
            with:
              aws-region: cn-northwest-1
              aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
              aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
    
          - name: Setup Node.js environment
            uses: actions/setup-node@v3
            with:
              node-version: ${{ matrix.node-version }}
    
          - name: Install Serverless
            run: yarn global add serverless
    
          - name: Install puppeteer Layer
            run: yarn
            working-directory: apps/aws-express-api-ts/layers/puppeteer
    
          - name: sls deploy
            run: yarn deploy
            working-directory: apps/aws-express-api-ts/layers
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45

    镜像部署

    当然,假如你使用镜像去部署 lambda,那以上这些问题都可以避免,但是代价便是冷启动时间比较长。

    不过很多技术上的问题,都是可以通过付出更多金钱的方式去解决的,这点就综合考虑利弊吧。

    Next Chapter

    现在你已经学会了 layer 的用法和一些稀奇古怪的场景。

    下一篇,《lambda nodejs 函数降低冷启动时间的最佳实践》中,将会详细介绍如何优化我们自身的代码,欢迎阅读。

    完整示例及文章仓库地址

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

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

  • 相关阅读:
    5.go语言函数提纲
    [深度学习论文笔记]医学图像分割U型网络大合集
    【Django笔记】 登录功能
    verilog REG 寄存器、向量、整数、实数、时间寄存器
    【Java】JDBC基础使用教程
    Linux权限——“Linux”
    Springboot+Axios双token解决token过期续签问题
    SVM 支持向量机算法(Support Vector Machine )【Python机器学习系列(十四)】
    dubbo命令行
    聊聊springboot自动装配出现的TypeNotPresentExceptionProxy异常排查
  • 原文地址:https://blog.csdn.net/qq_33020601/article/details/132719638