TL;DR
「一」不要看,主要想看踩坑的建议直接跳到「六、七、八」
面向「函数计算」的开发是一种新的心智模式,要关心「Event」标准
上面这句话真的是对的吗?有没有好心人封装一层,让我们继续用原有的心智模式开发,接上去直接能用呢?像 sidecar
那样?
一、概念梳理
egg
是对koa
的易用性上的封装,甚至可以理解为koa@3
,它的目标应该是做基建,克制
1.1. 核心概念是「约定优于配置」,这个需要有被实战经验折磨过才能体会
1.2. GitHub 上搜koa
问题,基本都是死马
在回答,所以完全可以脑补领养和孵化的过程midway
是对egg
的工程化,或者我更想说的是spring
化,一个坚挺了这么多年的工程级、企业级的框架必然是有它的道理的
2.1. 真实世界的开发其实是很脏的,并且脏是对的,按照流行的说法,脏说明熵低,更容易活下来并进化
2.2. 代码的复制粘贴要优于提前抽象,或者说只要代码的形状看上去相似,那就是好的抽象,没有必要去做一个基类,然后给很多配置开关,并不停重载的
2.3. 打个比方,midway
对标的是spring
,不是spring-boot
,目前这个生态里缺一个有社区背书的最佳实践,应该有人站出来把版本给锁了,并提供长达 5-10 年的后续维护,我一直认为鼓励用户使用^
管理版本是一个很不负责任的做法@midway/faas
和上述两者毫无关系,这个结论确实很惊讶,但目前确实如此
3.1. 其实仔细看它的真实名字就是faas
,否则应该是@midway/midway-faas
3.2. 接下来的文章将详细讲解,这里就不赘述
二、项目目标
- 基于
egg/midway
,准确的说是基于midway
的约定与写法,以及能引入各种社区的egg-plugin
- 传统的服务端渲染基础 html,前端做 js 富应用,不涉及 router 和 spa,不涉及同构的 ssr(后续文章再讲)
- 只考虑 Http 类事件(后续文章再讲其它)
- 生产能用,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 架构
- 「函数计算」将所有的「行为」都视作一次函数「调用」,所以函数的网关和普通的网关不一样,它会把 Http「Req」封装成「标准」的「入参」,然后把「标准」的「出参」封装成 Http「Resp」
- 「Midway」是一个 Http Web 框架,所以需要在前面挡一层「FaaS」把「Event」再反转成「Http」,业界应该还有其它类似的实现,比如「Serverless」
- 实践上「@Midway/FaaS」将「Event」直接转成了我们熟悉的「ctx」
五、基础请求
GET /
返回home.html
1.1. 首先我们按照midway
的约定,把函数文件放在了./src/app/controller/view.ts
,实践上好像并没有这个限制,大概是全局扫描decorator
然后注册的
1.2. 目前faas
会去扫@Func
,midway
会去扫@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: /
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
六、静态资源
- 首先正常引入
egg-static
,看得出来确试图友好的支持egg
系,尽管后续深入使用会发现有半成品的感觉 - 然后会发现
faas
的appInfo.baseDir
和普通egg
的并不一致,所以这里的配置要主动覆盖 - 「函数计算」需要有入口,所以要放一个空的
controller/public
- 翻了
faas
的源码发现不支持stream
模式,所以要开启buffer
模式;以及翻相关 issue 发现,「函数计算」似乎不喜欢stream
模式 - 「函数网关」是个黑盒,反正不可以类比为
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
?
- 考虑到一些其它因素,目前决定简单的做法就是放到自有的
nginx
后面 - 官方文档:配置函数访问VPC内资源,这个标题可能有些迷惑,它实际想说的是“如果你勾中了这个 VPC 开关,那么,有且只有 vpc 可以访问函数网关,公网不能访问到函数网关”
2.1. 其实没想通这个功能是怎么实现的,因为并没有给出 vpc 内访问函数网关的内网域名
2.2. 而且实际上没有实现,我是用外网域名去访问的,是通的。开工单和客服交流过之后,客服表示上述功能在境外是有的,境内只有北京区域有,并且要人工申请开通;我急着测主流程,所以没有去尝试 - 「函数」能够访问「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 访问「函数网关」
- 目前简单的方案是限 IP,长远来看其实可以按零信任去做更丰富的校验
- 在我过去的常识里,各级代理会使用
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」的拆和装这个过程,这是和以往开发的心智模型非常不同的一个点 - 重点二,
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)「函数计算」就是一种新的模型,我们应该转变心智模型,按新的思路开发 - 以上,在本地开发的时候「Event」的拆与封是由安装在「全局」的那个
@mideway/faas-cli
下的@midwayjs/serverless-http-parser
等包完成的
4.1. 打了无数日志无果,突然灵光一现去看了装在全局的那个cli
,当然也主要是反复翻源码,开始注意到「Event」拆与封这个事情才想到的
4.2. 这个本地开发工具显然是想要模拟「函数网关」的效果,但不知道深入使用是否能做到透明体验,例如不支持gzip
就没能模拟出来,不过不管怎么说,最近几年想提供「开发沙盒」的云服务越来越少了,几乎都是要求在线调试的,点个赞
相关代码在:https://github.com/lisitede/f...
随时可能修改,并导致与本文不符,如果发现有关键的 example 可以提醒我复制出来。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。