2

TL;DR

「一」不要看,主要想看踩坑的建议直接跳到「六、七、八」

面向「函数计算」的开发是一种新的心智模式,要关心「Event」标准

上面这句话真的是对的吗?有没有好心人封装一层,让我们继续用原有的心智模式开发,接上去直接能用呢?像 sidecar 那样?

一、概念梳理

  1. egg 是对 koa 的易用性上的封装,甚至可以理解为 koa@3,它的目标应该是做基建,克制
    1.1. 核心概念是「约定优于配置」,这个需要有被实战经验折磨过才能体会
    1.2. GitHub 上搜 koa 问题,基本都是 死马 在回答,所以完全可以脑补领养和孵化的过程
  2. midway 是对 egg工程化,或者我更想说的是 spring 化,一个坚挺了这么多年的工程级、企业级的框架必然是有它的道理的
    2.1. 真实世界的开发其实是很脏的,并且脏是对的,按照流行的说法,脏说明熵低,更容易活下来并进化
    2.2. 代码的复制粘贴要优于提前抽象,或者说只要代码的形状看上去相似,那就是好的抽象,没有必要去做一个基类,然后给很多配置开关,并不停重载的
    2.3. 打个比方,midway 对标的是 spring,不是 spring-boot,目前这个生态里缺一个有社区背书的最佳实践,应该有人站出来把版本给锁了,并提供长达 5-10 年的后续维护,我一直认为鼓励用户使用 ^ 管理版本是一个很不负责任的做法
  3. @midway/faas 和上述两者毫无关系,这个结论确实很惊讶,但目前确实如此
    3.1. 其实仔细看它的真实名字就是 faas,否则应该是 @midway/midway-faas
    3.2. 接下来的文章将详细讲解,这里就不赘述

二、项目目标

  1. 基于 egg/midway,准确的说是基于 midway 的约定与写法,以及能引入各种社区的 egg-plugin
  2. 传统的服务端渲染基础 html,前端做 js 富应用,不涉及 router 和 spa,不涉及同构的 ssr(后续文章再讲)
  3. 只考虑 Http 类事件(后续文章再讲其它)
  4. 生产能用,Getting Real

三、锁版本

# dep
@midwayjs/faas@1.2.15
    @midwayjs/core@2.3.18
    @midwayjs/decorator@2.3.18
@midwayjs/egg@1.2.0
    @midwayjs/decorator@2.3.18
    @midwayjs/egg-base-module@1.1.0
@midwayjs/serverless-invoke@1.2.12
    @midwayjs/fcli-plugin-invoke@1.2.20
        @midwayjs/serverless-http-parser@1.1.8

egg@2.26.1
egg-errors@2.1.1
egg-view-nunjucks@2.2.0

# devDep
@midwayjs/fcli-plugin-fc@1.2.20
    @alicloud/fun@3.6.18
    @midwayjs/serverless-fc-starter@1.2.9
        @midwayjs/serverless-http-parser@1.1.8

# global
@midwayjs/faas-cli@1.2.14
    @midwayjs/fcli-plugin-invoke@1.2.20
        @midwayjs/serverless-fc-starter@1.2.9
            @midwayjs/serverless-http-parser@1.1.8

四、鸟瞰 FaaS 架构

image

  1. 「函数计算」将所有的「行为」都视作一次函数「调用」,所以函数的网关和普通的网关不一样,它会把 Http「Req」封装成「标准」的「入参」,然后把「标准」的「出参」封装成 Http「Resp」
  2. 「Midway」是一个 Http Web 框架,所以需要在前面挡一层「FaaS」把「Event」再反转成「Http」,业界应该还有其它类似的实现,比如「Serverless」
  3. 实践上「@Midway/FaaS」将「Event」直接转成了我们熟悉的「ctx」

五、基础请求

  1. GET / 返回 home.html
    1.1. 首先我们按照 midway 的约定,把函数文件放在了 ./src/app/controller/view.ts,实践上好像并没有这个限制,大概是全局扫描 decorator 然后注册的
    1.2. 目前 faas 会去扫 @Funcmidway 会去扫 @Get, @Post, ...,期望未来可以统一用后者,并且自动声场 f.yml
# ./src/app/controller/view.ts

// @Get('/')
@Func('view.home')
async home() {
    await this.ctx.render('home.html', { slogan: 'Httpbin powered by Midway' });
}

# ./f.yml

functions:
  home:
    handler: view.home
    events:
      - http:
          method: get
          path: /
  1. GET /ping,返回 pong.json
    2.1. 基本同上,以及框架自己会处理 json 类型的返回
# ./src/app/controller/api.ts

// @Get('/ping')
@Func('api.ping')
async ping() {
    return { code: 0, message: 'pong' };
}

# ./f.yml

functions:
  ping:
    handler: api.ping
    events:
      - http:
          method: get
          path: /ping

六、静态资源

  1. 首先正常引入 egg-static,看得出来确试图友好的支持 egg 系,尽管后续深入使用会发现有半成品的感觉
  2. 然后会发现 faasappInfo.baseDir 和普通 egg 的并不一致,所以这里的配置要主动覆盖
  3. 「函数计算」需要有入口,所以要放一个空的 controller/public
  4. 翻了 faas 的源码发现不支持 stream 模式,所以要开启 buffer 模式;以及翻相关 issue 发现,「函数计算」似乎不喜欢 stream 模式
  5. 「函数网关」是个黑盒,反正不可以类比为 nginx,目前测下来的效果是既不支持配置 gzip,即使在代码里主动 gzip 了再返回也会被拆掉;这一点似乎可以和 4 印证,「函数网关」实际上是等到整个函数执行完成后,拿到了完整的「Resp」再封装的,并不是流操作
# ./src/config/plugin.ts
module.exports.static = true;

# ./src/config/config.default.ts
config.static = {
    dir: path.join(appInfo.baseDir, 'app/public'),
    buffer: true,
    // gzip: true,
};

# ./src/app/controller/view.ts
// @Get('/public/*')
@Func('view.public')
async public() {
}

# ./f.yml
functions:
  public:
    handler: view.public
    events:
      - http:
          path: /public/*
          method: get  

七、那么怎么做 gzip

  1. 考虑到一些其它因素,目前决定简单的做法就是放到自有的 nginx 后面
  2. 官方文档:配置函数访问VPC内资源,这个标题可能有些迷惑,它实际想说的是“如果你勾中了这个 VPC 开关,那么,有且只有 vpc 可以访问函数网关,公网不能访问到函数网关”
    2.1. 其实没想通这个功能是怎么实现的,因为并没有给出 vpc 内访问函数网关的内网域名
    2.2. 而且实际上没有实现,我是用外网域名去访问的,是通的。开工单和客服交流过之后,客服表示上述功能在境外是有的,境内只有北京区域有,并且要人工申请开通;我急着测主流程,所以没有去尝试
  3. 「函数」能够访问「vpc」是通的,首先需要给足 弹性网卡 ENI 相关的授权
    3.1. 看上去是每次发起调用的时候,会自动生成一个临时的「ENI」,「函数」会通过这个「ENI」去访问 vpc 内的资源,我的例子用的是 http 接口
    3.2. 不太理解为什么要每次生成临时的,而不是事先配好一个长效的,授权使用,也许是我文档还没吃透
    3.3. 以及生成临时的,性能问题很大吧
# ./f.yml
provider:
  name: aliyun
  runtime: nodejs12
  vpcConfig:
    vpcId: vpc-xxxx
    vSwitchIds:
      - vsw-xxxx
    securityGroupId: sg-xxxx
  policies:
    - AliyunECSNetworkInterfaceManagementAccess
    - AliyunVPCReadOnlyAccess

八、我自己来校验一下只允许 vpc 访问「函数网关」

  1. 目前简单的方案是限 IP,长远来看其实可以按零信任去做更丰富的校验
  2. 在我过去的常识里,各级代理会使用 x-forwarded-for 来传递 ip,所以代码里拿到的是路径上依次 ip 的数组,即 [chromeIp, haproxyIp, nginxIp],实践上发现 ctx.request.ip 拿到的是 nginxIp 即最后一跳的 ip
    2.1. 反复翻源码,虽然没有找到直接证据,但应该可以认为 ctx.request.ip 实际上是 event.ip,是从 socket / connection 上直接取的,也就是和「函数网关」直接发生 connection 的那个硬件的 ip
    2.2. 本文的重点一来了,在使用「函数计算」的过程中,必须时刻牢记「Event」的拆和装这个过程,这是和以往开发的心智模型非常不同的一个点
  3. 重点二koa 如果开启了 proxy,则自带 ctx.request.ips 这个属性,并且 ctx.request.ip 会取前者数组的第一个值,具体使用的是 x-forwarded-for 约定;egg 似乎是通过把相应代码复制一份到 app/extend/request 来实现了这个约定
    3.1. 然而根据 2.1 的实测结果,faas 并没有遵守这个约定,实际上我到 egg 里去打日志,显示并没有执行进去
    3.2. 所以我心智中的模型是,ctx => faas extends midway extends egg extends koa,然而实际的模型是 ctx 只是在「Event」的拆和封中长得像 ctx,但物理上完全不是同一个东西
    3.3. 不知道作者未来作何打算,有两种可能:1)目前还是个半成品,未来会考虑真的去 extends 或者封得更漂亮以至于和 extends 完全一样,duck type;2)「函数计算」就是一种新的模型,我们应该转变心智模型,按新的思路开发
  4. 以上,在本地开发的时候「Event」的拆与封是由安装在「全局」的那个 @mideway/faas-cli 下的 @midwayjs/serverless-http-parser 等包完成的
    4.1. 打了无数日志无果,突然灵光一现去看了装在全局的那个 cli,当然也主要是反复翻源码,开始注意到「Event」拆与封这个事情才想到的
    4.2. 这个本地开发工具显然是想要模拟「函数网关」的效果,但不知道深入使用是否能做到透明体验,例如不支持 gzip 就没能模拟出来,不过不管怎么说,最近几年想提供「开发沙盒」的云服务越来越少了,几乎都是要求在线调试的,点个赞

相关代码在:https://github.com/lisitede/f...

随时可能修改,并导致与本文不符,如果发现有关键的 example 可以提醒我复制出来。

系列文章:
阿里云函数计算(Midway FaaS)一探
阿里云函数计算(@Midway/Egg-Layer)二探


理斯特
18 声望9 粉丝

web/mobile/iot, front/back, js/java