serverless踩坑指南

641 阅读6分钟

本文主要描述用serverless搭建RESTful API踩坑记录。

所谓Serverless,简单理解就是指FaaS+BaaS。放在本示例中科院理解为,我们的编写的业务代码被放在FaaS中,其替我们管理执行;而我们依赖的MySQL,由BaaS提供,我们不需要自己到虚拟机管理MySQL服务,取而代之的由云厂商来提供。

Serverless最吸引我的也莫过于此,对于个人开发者和小团队来说,你不需要太多关心诸如监控、日志、扩容等运维问题,它们全都被当做服务外包给了云厂商,取而代之的是,你可以专心开发真正自己要实现的产品逻辑。

FaaS是如何运行代码的

对于FaaS来说,我们应用的基本单位是函数,一个个函数在一起组成了一个应用服务。而每一个函数,都会注册一个事件,我们可以根据自己的需要去「监听」相应的事件例如http请求、OSS变更,当事件来临时,便会去触发我们的函数执行。 也就是说,与传统模式不同的是,我们的代码并不是常驻的,而是仅当事件触发时,才执行我们的函数。在没有触发的时候,我们的程序是不在运行。因此,整个FaaS就是一个事件驱动模型。

构建开发环境

传统Nodejs项目开发环境一般可能会通过nodemon来配置,nodemon监测代码变动,然后执行相应的操作。由于我们本例使用Webpack来build代码,因此开发环境也用webpack来构建。 对于熟悉Web开发的人都知道,webpack有个插件「webpack-dev-server」,其会监听代码变更,将变更重新build;然而「webpack-dev-server」默认是将build后的代码放进内存,而在这里我们需要build代码带磁盘,好在dev-server插件支持该功能,我们可以直接通过 writeToDisk 配置。通过该配置,加上 funcraft 的指令 fun local start,就可以实现NodeJS Server 开发环境的实时更新。

大致配置如下

const path = require('path')
const Dotenv = require('dotenv-webpack');
const TerserPlugin = require('terser-webpack-plugin');
module.exports = {
  entry: './src/index.ts',
  output: {
    filename: 'bundle.js',
    path: path.resolve(__dirname, 'dist'),
    libraryTarget: 'umd' // 输出umd包供 Serverless调用
  },
  target: 'node',
  devServer:{
    writeToDisk: true,
    hot: true,
  },
  resolve:{
    extensions: ['.ts', '.js']
  },
  optimization: {
    minimizer: [
      // https://github.com/sidorares/node-mysql2/issues/1016
      new TerserPlugin({
        terserOptions: {
          keep_fnames: true
        }
      })
    ]
  },
  module: {
    rules: [
      {
        test: /\.(ts|js)$/,
        exclude: /node_modules/,
        use: {
          loader: 'babel-loader',
          options: {
            presets: [
              [
                '@babel/preset-env',
                {
                  targets: {
                    node: '10',
                  },
                },
              ],
              '@babel/preset-typescript',
            ],
          },
        },
      },
    ],
  },
  plugins:[
    new Dotenv() // read .env file
  ]
}

创建入口文件

至此配置相关已经开发完毕,现在来创建我们所需的入口文件

// src/index.ts

import { Server } from '@webserverless/fc-express'
import express from 'express'
import { Response, Request } from 'express'

const app = express()

app.get('/', (req: Request, res: Response)=> res.send('hello world'))

const server = new Server(app)

export const initializer = async (context: any, cb: any) => {
  console.log('initializer done')
  cb()
}

export const handler = function(req: any, res: any, context: any) {
  server.httpProxy(req, res, context)
}

通过该方式暴露了 initializer函数与handler函数,至此就可以在里面开发业务代码了。我们不需要去起一个NodeJS服务,剩下的都会有Serverless代我们管理。

Serverless config

除了一般的业务代码之外,我们还需要额外的编写一份config,用来告诉 云厂商我们的代码入口函数、触发器类型等操作。 大致如下

ROSTemplateFormatVersion: '2015-09-01'
Transform: 'Aliyun::Serverless-2018-04-03'
Resources:
  service-name: # service name
    Type: 'Aliyun::Serverless::Service'
    Properties:
      Description: foodie serverless service
      Role: 'acs:ram::1480466305752483:role/new-service1585108066159-role'
      VpcConfig:
        VpcId: vpc-uf6uv9bt270rqm83t2ezi
        VSwitchIds:
          - vsw-uf65ml7vbt4jqrby1jgeg
        SecurityGroupId: sg-uf6ijcs0cia6azmm6k75
      InternetAccess: true
    fun-name: # function name
      Type: 'Aliyun::Serverless::Function'
      Properties:
        Handler: bundle.handler
        Runtime: nodejs10
        CodeUri: './dist'
        Timeout: 60
        Initializer: bundle.initializer
      Events:
        httpTrigger:
          Type: HTTP
          Properties:
            AuthType: ANONYMOUS
            Methods: ['POST', 'GET']

如上大致就是一份可以Serverless配置清单,不同的云厂商配置可能有些许不同,但是大体思路是相当的。 这里大部分配置都顾名思义,值得一提的是VpcConfig,我们都知道,Serverless包含了FaaS和BaaS,那么对于一个后端服务来说,我们的数据库就是由BaaS提供的。而由于FaaS在运行时,IP是不固定的,无法通过白名单的控制数据库的访问权限,因此我们需要在函数计算中进行相关的VPC配置。

冷启动优化

并不是说将所有东西都交给了云厂商之后,我们就完全不用关注性能优化了。基于Serverless的特点,我们仍然有些需要做的事情。

由于Serverless的一大特点是按量收费,也就意味这我们的程序实例是根据实际调用情况运行的。当函数被调用时其实例被运行,而一段时间无访问时,实例便会被释放,具体管理由云厂商管理。

那也就意味着我们的接口在第一次访问的时候会比平时更慢,第一次访问也叫做冷启动。冷启动具体做了哪些操作呢。以阿里云为例,大致包括了代码下载、启动函数实例容器、运行时初始化、用户代码初始化。 那么针对这些特点,我们可以做的常规优化是: 使用webpack等打包工具,减少我们的代码体积,并且对于非dependence,不要打包进去 将共享的操作例如数据库连接等放在 initializer 中处理,一来它的执行是异步的,二来当冷启动结束后,后续的执行都会共享该执行操作,直到实例释放为止 如上是每个项目都非常建议做的常规优化,如果说依旧比较慢的话,可以继续做一些优化: 定时唤醒函数,可以用定时触发器定时访问我们的函数,个人认为定时的长短是一个经验判断,间隔过长起不到优化效果,定时太短会使serverless的费用陡增; 使用预留实例,当使用预留实例时,我们的函数计费方式会变成按时计费,费用的变高换来的是,我们的冷启动问题得到解决。同样的,可以采用预留实例与按量实例共存,根据业务情况分配预留实例

更多内容可以参考该repo sunOpar/foodie-server