原文已发布在玩物得志技术公众号,地址:https://mp.weixin.qq.com/s/ra4F659RoY8eQcfMUvMPgw
从一个房间走到另一个房间,要经过一扇门。从一个网络向另一个网络发送消息,也需要经过一道“关口”,这道关口就是网关。
1.前言
公司在快速发展,为了更好的服务业务,前端团队在性能体验、工程化建设等方面也有了更高的追求。
NodeJs提供的服务端能力,不仅拓展了前端的能力边界,也为性能体验优化和工程化提供了更丰富的方案。
2.项目介绍
2.1 技术选型
| 框架 | 描述 |
| express | 早期的node框架,功能齐全,开箱即用。但基本停止更新,与koa最大的区别是中间件机制,虽然能实现类似koa的洋葱形调用,但是实际上依旧存在差别。 |
| koa | 没有内置任何多余的中间件,按需引入,简洁。洋葱形中间件机制,灵活。 |
| egg | 基于koa, 高度封装,功能齐全。 |
Java的SpringCloud Gateway、 和基于nginx+lua的Kong都是成熟的网关项目,但是和前端当前的技术栈不匹配。
自研一套小型网关,并根据实际需求去慢慢完善,是我们选择的方案。
总体上,我们希望网关的底层框架尽可能简洁、灵活,同时拥有高扩展性,所以最终选择了koa。
2.2 架构
2.3 执行流程
上图是网关执行时内部的大致流转过程,请求经过多层中间件处理,
可以看到一个中间件的处理逻辑可以在请求和响应阶段发挥作用,这得益于Koa中间件的洋葱形机制 如下图:
下方演示了Koa中间件的写法,只需几行代码,实现一个记录访问信息和请求耗时的中间件:
const accessLog = async (ctx, next) => {
const start = Date.now();
await next();
const rt = Date.now() - start;
logger.info(`${ctx.method} ${ctx.host} ${ctx.url} - ${rt}ms`);
};
module.exports = accessLog;
请求经过中间件处理后到达服务层,服务层处理完成后返回结果,再经过中间件处理后响应。
2.4 性能&稳定
如上图所示,Node是**单进程 **+ **异步I/O **+ 事件循环模型,这种模型比较适合处理高并发和密集I/O的场景,不过也有一些缺陷,比如不能充分使用cpu,出现异常时整个应用崩溃等。针对这些情况,需要做一些优化:
多进程:Node本身提供了多进程方案,使用Cluster模块,只需要少量修改就能实现实现多进程,示例:
const cluster = require('cluster');
const http = require('http');
const numCPUs = require('os').cpus().length;
// 主进程逻辑
if (cluster.isMaster) {
// 创建工作进程
for (let i = 0; i < numCPUs; i++) {
cluster.fork();
}
cluster.on('exit', (worker, code, signal) => {
// 工作进程退出后的逻辑
});
} else {
http.createServer((req, res) => {
res.writeHead(200);
res.end('hello world!');
}).listen(8000);
}
有一个问题:示例里多个子进程都监听了8000端口,程序却不会报错,是因为listen方法内部判断了进程是主进程还是子进程,最终只有主进程监听了端口。最后通过进程间通信和任务调度算法来实现多进程的工作模式。
如果使用PM2来启动Node服务,无需修改任何代码也能实现多进程。 示例:
$ pm2 start app.js -i max
(max表示CPU数,如果服务器的资源可以都用来跑对应的Node服务,那么让进程数=CPU核心数算是最佳实践)
数据缓存:对外部获取的数据进行缓存,减少不必要的请求能明显提升性能。Redis是一个高性能的缓存服务,将数据缓存到Redis,不仅可以减少同进程的请求次数,也可以在多个进程间共享数据。
进程守护:默认情况下,node是没有进程守护功能的。为了防止应用崩溃导致整个服务宕机,需要增加进程守护机制。
常用的两种方案:
1. 常驻进程作为守护进程
** 2. ****crontab + shell 定时检测**
进程守护的基本逻辑,都是运行程序检测应用是否正常运行,检测出异常后进行日志记录,发送警报,重启应用等等操作。
如果使用PM2来管理Node服务,无需额外操作,因为PM2自带了守护进程。
错误日志:记录运行中产生的错误日志并分析原因,能帮助我们进一步优化应用的稳定性。
3.应用场景
3.1 文档请求接管&动态化
区别于常见的API网关,接管来自用户的文档请求也是前端网关的应用场景。
当前前端一些项目已经改造成了微前端的形式,多个项目可以独立开发和部署,同时跨项目访问是在一个SPA应用内的路由切换。但是也带来了一定的性能损失,比如项目初始化时需要多2次串行的请求。
这种情况下,在请求经过网关时,通过微前端服务获取内容,并预注入到文档内,在初始化过程中就不需要额外发起请求,节省了2次网络请求的时间。
如下图所示:
通过模板服务、数据预取服务、微前端服务,注入接口数据、骨架、脚本等内容,使输出的html动态化,可以有效提升首屏的访问体验。
这种方式看上去与 SSR有一些相似。相比而言,服务端预注入数据,渲染由客户端完成的方式具有更强的通用性和更低的接入成本。
主要体现在:
** 1. 业务代码无侵入** 无需大规模修改代码做适配,也不会影响原有的开发习惯。
** 2. 无框架兼容问题**无论开发框架是Vue、React, 或是自研的H5 SPA应用均可使用同一套后端逻辑。
3.2 离线文件实时更新
在App内缓存H5项目的文件,目前常见的方式有:
** 1. App打包时包含h5项目文件 **离线文件更新受发版限制,不够灵活。
** 2. 文件通过接口获取 **接口通常由后端同学提供,难以和前端工程体系打通,导致文件需要定期手动更新。
将前端发布系统与网关打通后,通过离线缓存服务实时生成最新的离线文件列表并返回给客户端,实现客户端缓存文件的实时更新。
如下图所示:
4.总结&展望
当前,玩物得志h5商城的流量已全部由前端网关处理。随着业务的不断发展,后续会新增更多的Node服务和网关功能。目前网关的功能还比较简单,内集的一些服务也未做拆分,未引入RPC,我们也将持续完善网关,建设更完整健全的Node服务化体系。
本文主要分享了基于node的网关设计思路和一些实践经验。
因篇幅有限,对文中出现的一些概念未做进一步解释(如异步I/O,事件循环),对于一些值得探讨的细节(如Cluster原理、多进程任务调度、父子进程通信等等)也未做深入,有兴趣的同学请自行查阅相关资料🙈