如何将传统 Node.js 项目部署到 Serverless
大厂技术 高级前端 Node进阶
点击上方 程序员成长指北,关注公众号
回复1,加入高级Node交流群
背景
因为 Serverless 的“无服务器架构”应用相比于传统应用有很多优点,比如:无需关心服务器、免运维、弹性伸缩、按需付费、开发可以更加关注业务逻辑等等,所以现在 Serverless 应用已经逐渐广泛起来。
但是目前原生的 Serverless 开发框架还比较少,也没有那么成熟,另外主流的 Web 框架还不支持直接 Serverless 部署,但好在是现在国内各大云厂商比如阿里云、腾讯云已经提供能力能够将我们的传统框架以简单、快速、科学的方式部署到 Serverless 上,下面让我们一起研究看看它们是怎么做的吧。
我们以 Node.js 的 Express 应用为例,看看如何通过阿里云函数计算,实现不用按照传统部署方式购买云主机去部署,不用自己运维,快速部署到 Serverless 平台上。
传统应用与函数计算的入口差异
传统应用的入口文件
首先看下传统 Express 应用的入口文件:
const express = require('express')
const app = express()
const port = 3000// 监听 / 路由,处理请求
app.get('/', (req, res) => {res.send('Hello World!')
})// 监听 3000 端口,启动 HTTP 服务
app.listen(port, () => {console.log(`Example app listening on port ${port}`)
})
可以看到传统 Express 应用是:
1.通过 app.listen() 启动了 HTTP 服务,其本质上是调用的 Node.js http 模块的 createServer() 方法创建了一个 HTTP Server
2.监听了 /
路由,由回调函数 function(request, response)
处理请求
函数计算的入口函数
Serverless 应用中, FaaS 是基于事件触发的,触发器是触发函数执行的方式, 其中 API 网关触发器与 HTTP 触发器与均可应用于 Web应用的创建。函数计算会从指定的入口函数开始执行,其中 API 网关触发器对应的入口函数叫事件函数,HTTP 触发器对应的入口函数叫 HTTP 函数,它们的入口函数形式不同。
API 网关触发器的入口函数形式
API 网关触发器的入口函数形式如下,函数入参包括 event、context、callback,以 Node.js 为例,如下:
/*
* handler: 函数名 handler 需要与创建函数时的 handler 字段相对应。例如创建函数时指定的 handler 为 index.handler,那么函数计算会去加载 index.js 文件中定义的 handler 函数
* event: 您调用函数时传入的数据,其类型是 Buffer,是函数的输入参数。您在函数中可以根据实际情况对 event 进行转换。如果输入数据是一个 JSON 字符串 ,您可以把它转换成一个 Object。
* context: 包含一些函数的运行信息,例如 request Id、 临时 AK 等。您在代码中可以使用这些信息
* callback: 由系统定义的函数,作为入口函数的入参用于返回调用函数的结果,标识函数执行结束。与 Node.js 中使用的 callback 一样,它的第一个参数是 error,第二个参数 data。
*/
module.exports.handler = (event, context, callback) => {// 处理业务逻辑callback(null, data);};
HTTP 触发器的入口函数形式
一个简单的 Node.js HTTP 函数示例如下所示:
module.exports.handler = function(request, response, context) {response.send("hello world");
}
差异对比
对比可以看出,在传统应用中,是启动一个服务监听端口号去处理 HTTP 请求,服务处理的是 HTTP 的请求和响应参数;而在 Serverless 应用中, Faas 是基于事件触发的,触发器类型不同,参数映射和处理不同:
若是 API 网关触发器
当有请求到达后端服务设置为函数计算的 API 网关时,API 网关会触发函数的执行,触发器会将事件信息生成 event 参数,然后 FaaS 以 event 为参数执行入口函数,最后将执行结果返回给 API 网关。所以传统应用和 Serverless 应用在请求响应方式和参数的数据结构上都有很大差异,要想办法让函数计算的入口方法适配 express。
若是 HTTP 触发器
相对 API 网关触发器参数处理会简单些。因为 HTTP 触发器通过发送 HTTP 请求触发函数执行,会把真实的 HTTP 请求直接传递给 FaaS 平台,不需要编码或解码成 JSON 格式,不用增加转换逻辑,性能也更优。
适配层
下面我们通过解读阿里云 FC 提供的将函数计算的请求转发给 express 应用的 npm 包 @webserverless/fc-express 源码,看看函数计算的入口方法是如何适配 express 的,如何适配 API 网关 和 HTTP 触发器这两种类型。
根据上述分析,Web 应用若想 Serverless 化需要开发一个适配层,将函数计算接收到的请求转发给 express 应用处理,最后再返回给函数计算。
API 网关触发的适配层
实现原理
API 网关触发的情况下,通过适配层将 FaaS 函数接收到的 API 网关事件参数 event 先转化为标准的 HTTP 请求,再去让传统 Web 服务去处理请求和响应,最后再将 HTTP 响应转换为函数返回值。整体工作原理如下图所示:
适配层核心就是:把 event 映射到 express 的 request 对象上, 再把 express 的 response 对象映射到 callback 的数据参数上。
API 网关调用函数计算的事件函数时,会将 API 的相关数据转换为 Map 形式传给函数计算服务。函数计算服务处理后,按照下图中 Output Format 的格式返回 statusCode、headers、body 等相关数据。API 网关再将函数计算返回的内容映射到 statusCode、header、body等位置返回给客户端。
(此图来源于阿里云)
核心过程
通过分析 @webserverless/fc-express 源码,我们可以抽取核心过程实现一个简易版的适配层。
1.创建一个自定义 HTTP Server,通过监听 Unix Domain Socket,启动服务
(友情链接:不清楚 Unix Domain Socket 的小伙伴可以先看下这篇文章: Unix domain socket 简介 (https://www.cnblogs.com/sparkdev/p/8359028.html))
第一步我们若想把函数计算接收的 event 参数映射到 Express.js 的 request 对象上,就需要创建并启动一个自定义的 HTTP 服务来代替 Express.js 的 app.listen,然后接下来就可以将函数的事件参数 event 转换为 Express.js 的 request 请求参数。
首先创建一个 server.js 文件如下:
// server.js
const http = require('http');
const ApiGatewayProxy = require('./api-gateway-proxy');// api-gateway-proxy.js 文件下一步会说明其内容/*
* requestListener:被代理的 express 应用
* serverListenCallback:http 代理服务开始监听的回调函数
* binaryTypes: 当 express 应用的响应头 content-type 符合 binaryTypes 中定义的任意规则,则返回给 API 网关的 isBase64Encoded 属性为 true
*/
function Server(requestListener,serverListenCallback,binaryTypes) { this.apiGatewayProxy = new ApiGatewayProxy(this); // ApiGatewayProxy 核心过程 2 会介绍this.server = http.createServer(requestListener);// 1.1 创建一个自定义 HTTP Serverthis.socketPathSuffix = getRandomString(); // 随机生成一个字符串,作为 Unix Domain Socket 使用this.binaryTypes = binaryTypes ? binaryTypes.slice() : [];// 当 express 应用响应的 content-type 符合 Server 构造函数参数 binaryTypes 中定义的任意规则时,则函数的返回值的 isBase64Encoded 为 true,从而告诉 API 网关如何解析函数返回值的 body 参数this.server.on("listening", () => {this.isListening = true;if (serverListenCallback) serverListenCallback();});this.server.on("close", () => {this.isListening = false;}).on("error", (error) => {// 异常处理});}// 暴露给函数计算入口函数 handler 调用的方法
Server.prototype.proxy = function (event, context, callback) {const e = JSON.parse(event);this.apiGatewayProxy.handle({event: e,context,callback});
}// 1.2 启动服务
Server.prototype.startServer = function () {return this.server.listen(this.getSocketPath()); // 采用监听 Unix Domain Socket 方式启动服务,减少函数执行时间,节约成本
}Server.prototype.getSocketPath = function () {/* istanbul ignore if *//* only running tests on Linux; Window support is for local dev only */if (/^win/.test(process.platform)) {const path = require('path');return path.join('\\\\?\\pipe', process.cwd(), `server-${this.socketPathSuffix}`);} else {return `/tmp/server-${this.socketPathSuffix}.sock`;}
}function getRandomString() {return Math.random().toString(36).substring(2, 15);
}module.exports = Server;
在 server.js 中,我们定义了一个构造函数 Server 并导出。在 Server 中,我们创建了一个自定义的 HTTP 服务,然后随机生成了一个 Unix Domain Socket,采用监听该 Socket 方式启动服务来代替 Express.js 的 app.listen
。
2.将函数计算参数 event 转换为 Express.js 的 HTTP request
下面开始第 2 步,创建一个 api-gateway-proxy.js 文件,将函数计算参数 event 转换为 Express.js 的 HTTP request。
//api-gateway-proxy.js
const http = require('http');
const isType = require('type-is');function ApiGatewayProxy(server) {this.server = server;
}ApiGatewayProxy.prototype.handle = function ({event,context,callback
}) {this.server.startServer().on('listening', () => {this.forwardRequestToNodeServer({event,context,callback});});
}ApiGatewayProxy.prototype.forwardRequestToNodeServer = function ({event,context,callback
}) {const resolver = data => callback(null, data);try {// 2.1将 API 网关事件转换为 HTTP requestconst requestOptions = this.mapContextToHttpRequest({event,context,callback});// 2.2 通过 http.request() 将 HTTP request 转发给 Node.js Server 处理,发起 HTTP 请求const req = http.request(requestOptions, response => this.forwardResponse(response, resolver));req.on('error', error => {//...});req.end();} catch (error) {// ...}
}ApiGatewayProxy.prototype.mapContextToHttpRequest = function ({event,context,callback
}) {const headers = Object.assign({}, event.headers);return {method: event.httpMethod,path: event.path,headers,socketPath: this.server.getSocketPath()// protocol: `${headers['X-Forwarded-Proto']}:`,// host: headers.Host,// hostname: headers.Host, // Alias for host// port: headers['X-Forwarded-Port']};
}// 核心过程 3 会介绍
ApiGatewayProxy.prototype.forwardResponse = function (response, resolver) {const buf = [];response.on('data', chunk => buf.push(chunk)).on('end', () => {const bodyBuffer = Buffer.concat(buf);const statusCode = response.statusCode;const headers = response.headers;const contentType = headers['content-type'] ? headers['content-type'].split(';')[0] : '';const isBase64Encoded = this.server.binaryTypes && this.server.binaryTypes.length > 0 && !!isType.is(contentType, this.server.binaryTypes);const body = bodyBuffer.toString(isBase64Encoded ? 'base64' : 'utf8');const successResponse = {statusCode,body,headers,isBase64Encoded};resolver(successResponse);});
}module.exports = ApiGatewayProxy;
在 api-gateway-proxy.js 中,我们定义了一个构造函数 ApiGatewayProxy 并导出。在这里我们会将 event 转换为 HTTP request,然后向 Node.js Server 发起请求,由 Node.js Server 再进行处理做出响应。
3.将 HTTP response 转换为 API 网关标准数据结构,作为 callback 的参数返回给 API 网关
接着继续对 api-gateway-proxy.js 文件中的http.request(requestOptions, response => this.forwardResponse(response, resolver))
分析发出 HTTP 请求后的响应处理部分。
//api-gateway-proxy.jsApiGatewayProxy.prototype.forwardRequestToNodeServer = function ({event,context,callback
}) {const resolver = data => callback(null, data); // 封装 callback 为 resolver//...// 请求、响应const req = http.request(requestOptions, response => this.forwardResponse(response, resolver));//...
}//3.Node.js Server 对 HTTP 响应进行处理,将 HTTP response 转换为 API 网关标准数据结构,作为函数计算返回值
ApiGatewayProxy.prototype.forwardResponse = function (response, resolver) {const buf = [];response.on('data', chunk => buf.push(chunk)).on('end', () => {const bodyBuffer = Buffer.concat(buf);const statusCode = response.statusCode;const headers = response.headers;const contentType = headers['content-type'] ? headers['content-type'].split(';')[0] : '';const isBase64Encoded = this.server.binaryTypes && this.server.binaryTypes.length > 0 && !!isType.is(contentType, this.server.binaryTypes);const body = bodyBuffer.toString(isBase64Encoded ? 'base64' : 'utf8');// 函数返回值const successResponse = {statusCode,body,headers,isBase64Encoded //当函数的 event.isBase64Encoded 是 true 时,会按照 base64 编码来解析 event.body,并透传给 express 应用,否则就按照默认的编码方式来解析,默认是 utf8};// 将 API 网关标准数据结构作为回调 callback 参数,执行 callback,返回给 API 网关resolver(successResponse);});
}
接着第 2 步,Node.js Server 对 http.request() 发出的 HTTP 请求做出响应处理,将 HTTP response 转换为 API 网关标准数据结构,把它作为回调 callback 的参数,调用 callback 返回给 API 网关。
4.在入口函数中引入适配层代码并调用
以上 3 步就将适配层核心代码完成了,整个过程就是:将 API 网关事件转换成 HTTP 请求,通过本地 socket 和函数起 Node.js Server 进行通信。
最后我们在入口函数所在文件 index.js 中引入 server.js,先用 Server 构建一个 HTTP 代理服务,然后在入口函数 handler 中调用 server.proxy(event, context, callback);
即可将函数计算的请求转发给 express 应用处理。
// index.js
const express = require('express');const Server = require('./server.js'); const app = express();
app.all('*', (req, res) => {res.send('express-app hello world!');
});const server = new Server(app); // 创建一个自定义 HTTP Servermodule.exports.handler = function(event, context, callback) {server.proxy(event, context, callback); // server.proxy 将函数计算的请求转发到 express 应用
};
我们将以上代码在 FC 上部署、调用,执行成功结果如下:
HTTP 触发的适配层
实现原理
HTTP 触发的情况下,不用对请求参数做转换,其它原理与 API 网关触发器一致:通过适配层将 FaaS 函数接收到的请求参数直接转发到自定义的 Web 服务内,最后再将 HTTP 响应包装返回即可,整体工作原理如下图所示:
核心过程
同样我们抽取核心过程简单实现一个适配层,与 API 网关触发器原理相同的部分将不再赘述 。
1.创建一个自定义 HTTP Server,通过监听 Unix Domain Socket,启动服务
server.js 代码如下:
// server.js
const http = require('http');
const HttpTriggerProxy = require('./http-trigger-proxy');function Server(requestListener,serverListenCallback) {this.httpTriggerProxy = new HttpTriggerProxy(this);this.server = http.createServer(requestListener); // 1.1 创建一个自定义 HTTP Serverthis.socketPathSuffix = getRandomString();this.server.on("listening", () => {this.isListening = true;if (serverListenCallback) serverListenCallback();});this.server.on("close", () => {this.isListening = false;}).on("error", (error) => {// 异常处理,例如判读 socket 是否已被监听});}// 暴露给函数计算入口函数 handler 调用的方法
Server.prototype.httpProxy = function (request, response, context) {this.httpTriggerProxy.handle({ request, response, context });
}// 1.2 启动服务
Server.prototype.startServer = function () {return this.server.listen(this.getSocketPath());
}Server.prototype.getSocketPath = function () {/* istanbul ignore if *//* only running tests on Linux; Window support is for local dev only */if (/^win/.test(process.platform)) {const path = require('path');return path.join('\\\\?\\pipe', process.cwd(), `server-${this.socketPathSuffix}`);} else {return `/tmp/server-${this.socketPathSuffix}.sock`;}
}function getRandomString() {return Math.random().toString(36).substring(2, 15);
}module.exports = Server;
2.将 HTTP request 直接转发给 Web Server,再将 HTTP response 包装返回
创建一个 api-trigger-proxy.js 文件如下:
// api-trigger-proxy.js
const http = require('http');
const isType = require('type-is');
const url = require('url');
const getRawBody = require('raw-body');function HttpTriggerProxy(server) {this.server = server;
}HttpTriggerProxy.prototype.handle = function ({request,response,context
}) {this.server.startServer().on('listening', () => {this.forwardRequestToNodeServer({request,response,context});});
}HttpTriggerProxy.prototype.forwardRequestToNodeServer = function ({request,response,context
}) {// 封装 resolverconst resolver = data => {response.setStatusCode(data.statusCode);for (const key in data.headers) {if (data.headers.hasOwnProperty(key)) {const value = data.headers[key];response.setHeader(key, value);}}response.send(data.body); // 返回 response body};try {// 透传 requestconst requestOptions = this.mapContextToHttpRequest({request,context});// 2.将 HTTP request 直接转发给 Web Server,再将 HTTP response 包装返回const req = http.request(requestOptions, response => this.forwardResponse(response, resolver));req.on('error', error => {// ...});// http 触发器类型支持自定义 body:可以获取自定义 bodyif (request.body) {req.write(request.body);req.end();} else {// 若没有自定义 body:http 触发器触发函数,会通过流的方式传输 body 信息,可以通过 npm 包 raw-body 来获取getRawBody(request, (err, body) => {req.write(body);req.end();});}} catch (error) {// ...}
}HttpTriggerProxy.prototype.mapContextToHttpRequest = function ({request,context
}) {const headers = Object.assign({}, request.headers); headers['x-fc-express-context'] = encodeURIComponent(JSON.stringify(context));return {method: request.method,path: url.format({ pathname: request.path, query: request.queries }),headers,socketPath: this.server.getSocketPath()// protocol: `${headers['X-Forwarded-Proto']}:`,// host: headers.Host,// hostname: headers.Host, // Alias for host// port: headers['X-Forwarded-Port']};
}HttpTriggerProxy.prototype.forwardResponse = function (response, resolver) {const buf = [];response.on('data', chunk => buf.push(chunk)).on('end', () => {const bodyBuffer = Buffer.concat(buf);const statusCode = response.statusCode;const headers = response.headers;const contentType = headers['content-type'] ? headers['content-type'].split(';')[0] : '';const isBase64Encoded = this.server.binaryTypes && this.server.binaryTypes.length > 0 && !!isType.is(contentType, this.server.binaryTypes);const body = bodyBuffer.toString(isBase64Encoded ? 'base64' : 'utf8');const successResponse = {statusCode,body,headers,isBase64Encoded};resolver(successResponse);});
}module.exports = HttpTriggerProxy;
3.入口函数引入适配层代码
// index.js
const express = require('express');
const Server = require('./server.js');const app = express();
app.all('*', (req, res) => {res.send('express-app-httpTrigger hello world!');
});const server = new Server(app);module.exports.handler = function (req, res, context) { server.httpProxy(req, res, context);
};
同样地,我们将以上代码在 FC 上部署、调用,执行成功结果如下:
看到最后,大家会发现 API 网关触发器和 HTTP 触发器很多代码逻辑是可以复用的,大家可以自行阅读优秀的源码是如何实现的~
其他部署到 Serverless 平台的方案
将传统 Web 框架部署到 Serverless 除了通过适配层转换实现,还可以通过 Custom Runtime 或者 Custom Container Runtime (https://juejin.cn/post/6981921291980767269#heading-5) ,3 种方案总结如下:
通过引入适配层,将函数计算接收的事件参数转换为 HTTP 请求交给自定义的 Web Server 处理
通过 Custom Runtime
本质上也是一个 HTTP Server,接管了函数计算平台的所有请求,包括事件调用或者 HTTP 函数调用等
开发者需要创建一个启动目标 Server 的可执行文件 bootstrap
通过 Custom Container Runtime
工作原理与 Custom Runtime 基本相同
开发者需要把应用代码和运行环境打包为 Docker 镜像
小结
本文介绍了传统 Web 框架如何部署到 Serverless 平台的方案:可以通过适配层和自定义(容器)运行时。其中主要以 Express.js 和阿里云函数计算为例讲解了通过适配层实现的原理和核心过程,其它 Web 框架 Serverless 化的原理也基本一致,腾讯云也提供了原理一样的 tencent-serverless-http (https://github.com/serverless-plus/tencent-serverless-http) 方便大家直接使用(但腾讯云不支持 HTTP 触发器),大家可以将自己所使用的 Web 框架对照云厂商函数计算的使用方法亲自开发一个适配层实践一下~
Node 社群
我组建了一个氛围特别好的 Node.js 社群,里面有很多 Node.js小伙伴,如果你对Node.js学习感兴趣的话(后续有计划也可以),我们可以一起进行Node.js相关的交流、学习、共建。下方加 考拉 好友回复「Node」即可。
如果你觉得这篇内容对你有帮助,我想请你帮我2个小忙:
1. 点个「在看」,让更多人也能看到这篇文章
2. 订阅官方博客 www.inode.club 让我们一起成长
点赞和在看就是最大的支持
参考资料
Webserverless - FC Express extension (https://github.com/awesome-fc/webserverless/tree/master/packages/fc-express)
如何将 Web 框架迁移到 Serverless (https://zhuanlan.zhihu.com/p/152391799)
Serverless 工程实践 | 传统 Web 框架迁移 (https://developer.aliyun.com/article/790302)
阿里云-触发器简介 (https://help.aliyun.com/document_detail/53102.html)
前端学 serverless 系列—— WebApplication 迁移实践 (https://zhuanlan.zhihu.com/p/72076708)
如何将传统 Node.js 项目部署到 Serverless相关推荐
- vue+mysql+node.js项目部署到腾讯云-云服务器/轻量应用服务器
node 14.16.1 nginx 1.18.0 mysql 8.0.26 nginx 静态高性能的HTTP和反向代理web服务器 nginx主配置文件 /usr/local/lighthouse/ ...
- node.js PM2部署项目
文章更新 2023-05-21 更新NSSM安装服务的方式 pm2 是什么 pm2 是一个守护进程管理工具,它能帮你守护和管理你的应用程序.通常一般会在服务上线的时候使用 pm2 进行管理.本文围绕以 ...
- 部署Node.js项目(CentOS)
原文链接 部署Node.js项目(CentOS) 驻云科技 2017-05-11 17:46:43 浏览334 评论1 发表于: 阿里云服务 >> 最佳实践 云栖社区 linux ...
- Node.js项目实践:构建可扩展的Web应用
2019独角兽企业重金招聘Python工程师标准>>> Node.js项目实践:构建可扩展的Web应用 <Node.js项目实践:构建可扩展的Web应用>用专业的讲解方式 ...
- SAP UI5 应用开发教程之五十五 - 如何将本地 SAP UI5 应用通过 Node.js Express 部署到公网上试读版
一套适合 SAP UI5 初学者循序渐进的学习教程 教程目录 SAP UI5 本地开发环境的搭建 SAP UI5 应用开发教程之一:Hello World SAP UI5 应用开发教程之二:SAP U ...
- WebStorm中Node.js项目配置教程(1)——创建项目
Node.js绝对是一个web开发的热点话题,作为web神器的WebStorm也是开发Node.js的佼佼者. 接下来就Node.js项目在WebStorm的配置操作就行详细的讲解,首先是创建项目.两 ...
- 有没有办法为Node.js项目自动构建package.json文件
本文翻译自:Is there a way to automatically build the package.json file for Node.js projects Is package.js ...
- 带你开发一个完整的 node.js 项目
「他们根本不知道,现在的电商大促有多么依赖 Node.js」任职阿里的架构师朋友这么说. 说真的,我倒并不意外.作为一个定位明确的高性能 Web 服务器,Node.js 非常火热,几乎霸占了前端生态. ...
- Node.js项目中动态加载环境变量配置
NODE_MODULES:项目中动态加载环境变量配置 开始 在平时的 Node.js 项目开发中,我们需要在项目中添加各种各样的配置:服务端口.服务地址.图片上传.数据库.Redis 等等. 通常情况 ...
最新文章
- Oracle学习之段区块初步概念
- sql server转mysql工具下载_SQL Server转换为MySQL工具推荐(Mss2sql)
- 生产订单结算时候的几个差异
- g++ linux 编译开栈_Linux下编写C++服务器(配置C++编译调试环境)
- 串口通信与编程:串口基础知识
- CV学习笔记-特征提取
- mysql 主主_MySQL双主(主主)架构
- location匹配
- VBA玩转系统剪贴板
- 【AI模型部署】maskrcnn在tfserver部署以及调用时遇到问题:Servable not found for request “xx”、‘incompatible_shape_error‘
- android呼叫转移代码,动态Android呼叫转移
- 十五年学不会英语的原因
- 硬件工程师成长之路(1)——元件基础
- 一战北邮计专考研经验分享
- Echarts 3d地球toolstips实现
- Hive中的left semi join和left anti join
- 马丁富勒微服务论文连接
- 企业微信社群该如何引流
- ECharts如何制作省份地图并在地图上显示自定义图标/散点图
- 上海市“专精特新”中小企业认定条件、流程、奖励政策