阿里云 FaaS 云函数应该是基于 Kubernates 和容器
的 Serverless 架构。今天以阿里云 http 的云函数为例子,记录探索的过程。
Runtime
此前没有使用 FaaS 的经验, 所以好奇云函数是运行在 Nodejs 环境中
还是受限的 VM 环境
中。
确认 require 函数 是否可以用
exports.handler = (event, context, callback) => {
const os = require('os');
const cpus = os.cups();
callback(null, `hello
${JSON.stringify(cpus, null, 2)}
`);
}
复制代码
打印
[
{
"model": "Intel(R) Xeon(R) Platinum 8163 CPU @ 2.50GHz",
"speed": 2499,
"times": {
"user": 220400,
"nice": 0,
"sys": 106800,
"idle": 22482700,
"irq": 0
}
},
{
"model": "Intel(R) Xeon(R) Platinum 8163 CPU @ 2.50GHz",
"speed": 2499,
"times": {
"user": 248000,
"nice": 0,
"sys": 102900,
"idle": 22490600,
"irq": 0
}
}
]
复制代码
可见:
- require 模块是可以使用的
- 运行实例的服务器CPU配置还是挺高的(给阿里点赞)
确认用户代码路径
Module模块有两个参数: filename, paths, 分别代表了当前模块路径和模块查找的路径。
exports.handler = (event, context, callback) => {
callback(null, `hello
${require.main.filename} // 主模块
${require.main.paths}
${module.filename} // 当前模块
${module.paths} //
`);
}
复制代码
打印
- 主模块
- 主模块路径 –
/var/fc/runtime/nodejs10/src/server.js
- 主模块寻址路径 –
/var/fc/runtime/nodejs10/src/node_modules,/var/fc/runtime/nodejs10/node_modules,/var/fc/runtime/node_modules,/var/fc/node_modules,/var/node_modules,/node_modules
- 主模块路径 –
- 用户模块
- 用户模块路径 –
/code/index.js
- 用户模块寻址路径 –
/code/node_modules,/node_modules
- 用户模块路径 –
由此可见:
- 用户模块挂载到
/code
目录下 - 程序注入口在
/var/fc/runtime/nodejs10
目录下
通过以上信息,我们基本上可以判断程序是运行在完整的 Nodejs 环境
中
Containerd
确定了应用是运行在完整 Nodejs 环境后, 那么Nodejs又是运行在什么环境中呢?基于目前对国内Faas的一点点认知,猜测是运行在 Containerd 中。由于Nodejs核心包都可以使用,所以我们直接使用child_process.spawn
来查看容器内的信息。
exports.handler = (event, context, callback) => {
const { spawnSync} = require('child_process');
const {stdout } = spawnSync('cat', ['/proc/self/cgroup'])
callback(null, `hello
${stdout.toString()}
`);
}
复制代码
第一次打印:
11:hugetlb:/docker/f8d493c517bfb01120053b23882eb33cc326b748d91d347bf15696e13ddcd43d
第二次打印(修改代码,再次执行):
11:hugetlb:/docker/141a4a8f9cabf953c4c0510874ffc0023c62276b055dac33914b28a436e3190a
可见:
Nodejs 运行在容器环境
中(能看出来阿里 K8S 调度的是 Docker 还是 Containerd 吗)- 每次代码修改执行都会
启动新的容器
serverless framework
通过上面的步骤我们可知:
- 代码的主入口在
/var/fc/runtime/nodejs10/src/server.js
- 可以使用任意的 Nodejs 模块 – 代表了获取了
整个容器的权限
所以我们可以获取所有的代码, 下面是获取代码的方式:
const fs = require('fs');
const path = require('path');
let length = 1024*1024;
let byteOffset = 0;
let buffer = Buffer.alloc(length);
function addToBuffer(b) {
if (!Buffer.isBuffer(b)) b = Buffer.from(b)
const len = b.length;
if (len + byteOffset > length) {
length = len < length/2 ? Math.floor(length*1.5) : length*2;
const bigBuf = Buffer.alloc(length)
buffer.copy(bigBuf)
buffer = bigBuf;
}
b.copy(buffer, byteOffset)
byteOffset = byteOffset + len;
return buffer;
}
// 遍历目录, 把文件内容放到buffer中
function getFileContent(dir) {
const dirs = fs.readdirSync(dir);
for (const item of dirs) {
const itemPath = path.join(dir, item);
const stat = fs.statSync(itemPath);
if (stat.isFile()) {
addToBuffer(Buffer.from('-----------> '+itemPath+'\n\n'))
const buffer = fs.readFileSync(itemPath);
addToBuffer(buffer);
addToBuffer(Buffer.from('\n\n\n\n'));
} else {
getFileContent(itemPath);
}
}
}
getFileContent('/var/fc/runtime/nodejs10/src');
console.log(buffer.toString())
复制代码
整个代码结构如下:
serverless
├── agent.sh
├── package.json
└── src
├── buffer_builder.js
├── callback.js
├── config.js
├── console.js
├── constant.js
├── context.js
├── environment.js
├── even_loop.js
├── httpparam.js
├── invoke.js
├── logger.js
├── npp.js
├── pandora // pardora 目录, 没去打印
├── prepare_code.js
├── response_parser.js
├── sanitizer.js
├── server.js
├── setup.js
├── throttle.js
└── validator.js
复制代码
通过 package.json 管中窥豹
{
"author": "aliyun-fc",
"name": "aliyun-fc-nodejs",
"version": "1.0.0",
"description": "This package contains nodejs container agent.",
"main": "src/server.js",
"license": "UNLICENSED",
"readme": "README",
"repository": "http://gitlab.alibaba-inc.com/serverless/lambda",
"directories": {
"test": "test"
},
"dependencies": {
"@pandorajs/component-actuator-server": "^3.0.1",
"@pandorajs/component-decorator": "^3.0.1",
"@pandorajs/component-process-info": "^3.0.1",
"@pandorajs/core-sdk": "^3.0.1",
"@pandorajs/hub": "^3.0.1",
"express": "^4.14.0",
"winston": "^2.2.0",
"winston-daily-rotate-file": "^1.2.0",
"body-parser": "^1.15.2",
"mkdirp": "^0.5.1"
},
"devDependencies": {
"co": "^4.6.0",
"body": "^5.1.0",
"istanbul": "^0.4.4",
"js-beautify": "^1.6.12",
"raw-body": "^2.3.2",
"mocha": "^3.0.2"
},
"engines": {
"node": "*",
"npm": ">=2.15.8"
},
"scripts": {
"lint": "find src test -name '*.js' | xargs -I F js-beautify -r F",
"start": "./agent.sh start",
"test": "istanbul cover _mocha -- -t 10000"
}
}
复制代码
通过 server.js 窥探运行的生命周期
// Set up server.
var app = express();
app.post('/invoke', [setup(config), responseParser, validator.validateReqHeader, throttle, caRequest]);
app.post('/initialize', [setup(config), responseParser, validator.validateReqHeader, throttle, caRequest]);
app.get('/pre-stop', [setup(config), responseParser, validator.validateReqHeader, throttle, caRequest]);
app.get('/pre-freeze', [setup(config), responseParser, validator.validateReqHeader, throttle, caRequest]);
// add a new api to load code from disk into memory
app.post('/prepare_code', [setup(config), responseParser, validator.validatePrepareCode, throttle, prepare_code.prepareFunction]);
复制代码
总结
通过上面的信息,可以看到阿里的http FaaS 云函数是通过 express 承载业务逻辑,通过K8S和容器做负载扩容的, 整个思路比较简单。除了基本的业务流程, FaaS 还要求冷启动,毫秒响应,应用监控等等我不知道的东西。
在探索的过程中,有两个问题还没来得及实践:
- 能否获取到反弹shell
- 能够绕过 FaaS 的收费策略
关注我的微信公众号,欢迎留言讨论,我会尽可能回复,感谢您的阅读。