写点什么

苏宁 Nodejs 性能优化实战

  • 2018-05-22
  • 本文字数:3755 字

    阅读完需:约 12 分钟

Nodejs 项目背景介绍

自 2016 年以来,苏宁大规模的使用了基于 Nodejs 渲染的项目,架构使用 Nginx+Nodejs+PM2 组合,其中 Nodejs 版本从最初的 6.0+ 升级到如今的 8.0+,Nodejs 框架从 Express 过度到 Koa2,而 Nodejs 的性能优化作为其中的核心,苏宁在其性能提升上,也从 0 到 1,开始摸索。

初步优化—css、js 注册与合并

ejs 模板相关优化

在苏宁的 nodejs 项目中,刚开始使用 express 框架,后来随着 node.js 8.0 LTS 版本的发布,又开始使用 kos 框架。无论是 express 还是 koa 框架,苏宁在项目开发中都使用 ejs 模板语言(关于 ejs 模板语言这里就不多做介绍,有兴趣的同学可以自行搜索)。

合并 css 和 js 带来的性能损失

在使用 ejs 模板过程中,苏宁把公共部分抽出来为 layout.ejs 文件,页面模板通过 ejs include 方法在 layout.ejs 引入,例如:

复制代码
//layout.ejs
<link type="text/css" rel="stylesheet" href="public.css" />
<script src="public.js"></script>
...
include(page1);
...
//page1.ejs
<link type="text/css" rel="stylesheet" href="page1.css" />
<script src="page1.js"></script>
<h1>hello</h1>

这样做解决了公共部分与页面业务逻辑的分离,但是也带来另一个问题 --layout 模板和 page1 模板中静态资源标签位置的问题,以下是渲染过后返回给客户端的 html 页面:

复制代码
...
<link type="text/css" rel="stylesheet" href="public.css" />
<script src="public.js"></script>
</header>
<body>
<div class="header"></div>
<link type="text/css" rel="stylesheet" href="page1.css" />
<script src="page1.js"></script>
<h1>hello</h1>
</body>
...

我们可以看到 page1 的静态资源引用标签都在 body 内,复杂的页面可能还会有 page2、page3、pageN… 这样会有大量的静态资源引用标签出现在 body 内,这显然不符合我们的预期,我们需要控制静态资源标签在页面中的调用位置,为了解决上面的问题,苏宁引入了 ejs 模板静态资源 register 机制,其注册步骤如下:

a. 使用 getResource() 方法输出占位符。

b. 使用 register() 注册方法注册资源,例如:register(‘a.css’, ‘b.js’)。

c. 将注册的静态资源处理合并后进行字符串 replace 操作。

使用 register 方法后 ejs 模板渲染过后的 html 页面如下:

复制代码
...
{{{CSS_PLACEHOLDER}}}
</header>
<body>
<div class="header"></div>
<h1>hello</h1>
</body>
{{{JS_PLACEHOLDER}}}
...

“{{{CSS_PLACEHOLDER}}}”和“{{{JS_PLACEHOLDER}}} ”就是 getResource() 输出的占位符,在服务器 response 之前进行字符串 replace 操作,将占位符替换成 register() 方法中注册的路径:

复制代码
...
<link type="text/css" rel="stylesheet" href="public.css" />
<link type="text/css" rel="stylesheet" href="page1.css" />
</header>
<body>
<div class="header"></div>
<h1>hello</h1>
</body>
<script src="public.js"></script>
<script src="page1.js"></script>
...

这样就符合了正常的页面静态资源引入位置,同时苏宁在 register() 方法做路径合并的功能,合并后的地址路径如下:

复制代码
<link type="text/css" rel="stylesheet" href="public.css,page1.css " /></header>
<body>
<div class="header"></div>
<h1>hello</h1>
</body>
<script src="public.js, page1.js "></script>
...

这样浏览器中发起的请求就会少很多,减少页面请求也是性能优化的一个点。

缓存机制

使用 register 机制后我们又发现了一个问题,当客户端每一个 request 请求发起,nodejs 服务在响应之前都会进行字符串查找替换, 如果页面够复杂,最终渲染生成的字符串足够大,每一次进行字符串查找替换的过程中也造成了一定的性能损耗。正常在实际的使用中我们多次访问一个路由地址,其页面引用的静态资源并不会发生变化。利用这个特性苏宁引入了静态资源缓存机制。

当一个新的页面请求进来之后,在执行 register 方法之前,会根据页面请求地址的 pathname 进行缓存查找,如果命中缓存,则 getResource() 直接返回缓存内容,相应的 regsiter 方法也不会去执行。否则执行 register() 流程。引入缓存机制后,非第一次访问代码逻辑中少了注册、替换流程,相应的页面响应时间也缩短了,经过多次测试,页面响应时间大概缩短 4-8ms。

进阶优化—大量路由的优化匹配

在开发苏宁易购香港站过程当中,由于整站页面较多、参数开发人员众多及基于项目安全性的考虑,项目开发中配置了多达 173 条静态路由以及 11 条动态路由,所以路由匹配效率明显下降。究其原因,得从 express 源码入手,express 框架在处理路由配置的方法是,将每一条配置信息转换成一条正则表达式,在请求进入的时候,逐条进行匹配,直到匹配成功为止。

对于动态路由——路由中含有模糊匹配,则必须使用正则表达式来进行匹配,无法优化。而对于静态路由,就是固定的字符串的路由表达式,则可以通过键值对映射进行匹配,复杂度从 O(n) 变成了 O(1) 大大缩短匹配时间,且不会随着路由增加而耗时加长。在实际代码中,由于架构采用了集中路由配置,所以很方便的从配置文件里面就筛选出了静态路由,然后存放在一个 Object 中(HashMap)。然后形成一个中间件形式,相当于把多条路由中间件变成了一条路由中间件。

缺陷:和原来的逻辑相比,优化后的方案缺少了路由匹配的顺序,所以在开发的时候需要额外注意,不过总体来说影响甚微,因为静态路由优先匹配,也是应该优先响应的。

高阶优化—TPS 的提升

在苏宁易购大聚惠系统的前后端分离中,初次提交压力测试结果非常差。怀疑有什么配置没有配好,当时的数据是这样的(16 台 4C4G):

TPS 低的不能忍,而且当时已经配备了 Node.js 8.9.1 这个版本,理论上绝不可能那么差,在观察代码,也没有发现特别消耗性能的地方。最后我们找到了原因,在 ejs 模版配置的时候没有开启模版缓存导致。如果不开启模版缓存那么每次请求渲染的时候,都会从磁盘中读取本地模版文件进行操作,这个磁盘读取的动作消耗了很多 CPU。平时使用不会察觉,只有当压力测试的时候才会体现出来。设置好了参数后,我们得到了 10 倍的性能提升。

但我们的优化并没有止步于此,我们定的目标是 3000TPS,也就是还需要再提高 50% 的渲染性能。这时候我们就必须找到影响 nodejs 性能的点。Nodejs 的特点是单线程异步编程,意味着异步操作对性能的影响不大,而同步操作则会严重影响性能。

所以第一步,是先检查代码中同步操作的逻辑,是否有消耗 CPU 的代码。经过检查,排除了代码部分的嫌疑。只好借助 chrome 提供的 devtools 来进行分析,启动 node 参数—inspect,打开 chrome 的 devtools 插件就可以通过 CPU profile 进行分析了。排除掉不可避免的 CPU 消耗,问题浮出水面,原来还有一部分的 CPU 消耗来自于 ejs 模版引擎的内部。

从图中可以看出来有两部分消耗,一部分是来自 ejs 模版引擎内部的浅拷贝,一部分是来自查找文件是否存在的系统命令。由于大聚会系统的 ejs 里面大量使用 include,导致了这部分消耗凸显了出来。打开 ejs 引擎源码查看,发现虽然缓存了模版,但每次 include 函数依然会去执行 fs.exsitSync 函数。找到罪魁祸首以后,修改起来其实很简单,在执行改函数的判断条件里面加上先判断缓存中是否存在。修改后这部分消耗减少了不少。

浅拷贝的问题,通过 js 的原型链解决,将传入的数据对象作为原型对象,通过 Object.create 函数构造一个派生对象,实现原来浅拷贝达到的目的(模版内部修改对象属性不会影响原始对象,防止污染原始对象传入到其他模版中去)。派生对象修改属性,并不会修改原型中对象的属性,只会在派生对象中新建一个同名的属性,所以不会污染原始对象。新增属性也只会在派生对象中。这一步优化减少了很多赋值操作。

经过以上的优化,再进行 CPU profile 分析,发现在 ejs 引擎内部依然有一个函数在消耗 CPU,那就是 getIncludePath。这个函数的目的是在执行 include 的时候讲传入的相对路径转成绝对路径,目的是防止嵌套的 include 中传入相同的相对路径字符串,却是代表不同的模版文件。但是在转换成绝对路径这一步里面会调用文件系统函数造成 CPU 消耗。

解决的思路很快就出来了,就是需要讲相对路径映射成绝对路径,然后缓存起来,这样就不必每次去计算绝对路径了。当然这个缓存不能是全局的,必须每一个 include 创建一个缓存,这样才能避免相同的相对路径有歧义的问题。

原始逻辑:

优化的逻辑:

说明:路径映射 Map 是一个定义在模版函数所在作用域上的,只有该模版函数内部能访问到,每次执行模版函数的时候都会拥有一个独立的 Map。

经过上述优化后,本地进行压测有 50% 的性能提升,故提交测试组对大聚会进行线上压测。

压测结果非常好,从 2000tps 到了 3500 多,提升了 75% 之多。单台机器大约 220tps 左右,而原 java 系统单台大概 150tps 左右。

总 结

Nodejs 系统的性能优势主要体现在异步 IO 上面,所以性能瓶颈基本都是出在同步操作上面,那么优化也是主要尽量减少同步操作,适当使用一些 js 的技巧,另外 npm 包的开源特点也给优化工作带来了便利。

作者介绍

李浩,苏宁易购高级前端技术经理,主要负责苏宁前后端分离 nodejs 项目开发。具有多年的 web 前端从业经历,曾任途牛金服前端负责人。热爱前端,对新技术有学习热情,在 nodejs 前后端分离、koa 等框架方面有独特的见解和丰富的项目实践。

感谢覃云对本文的审校。

2018-05-22 18:266020

评论

发布
暂无评论
发现更多内容

挖掘文本的奇妙力量:传统与深度方法探索匹配之道

汀丶人工智能

推荐系统 语义搜索 向量搜索

了解容器运行时安全:保护你的容器应用

统信软件

容器 安全 运行时

IPQ9554 and QCN9274 - The key to high-speed connectivity - Go beyond tradition and lead the future:

wifi6-yiyi

DeFi/DApp矿机算力质押挖矿系统开发

l8l259l3365

谁在“操控”虚拟人?

自象限

虚拟人

软件测试|Pytest必会技巧(三)

霍格沃兹测试开发学社

premiere pro 2024 新功能介绍 pr2024中文激活版下载mac/win

iMac小白

Premiere Pro 2024 Pr2024下载 pr2024破解版

Chiplet封装技术的应用现状

IC男奋斗史

封装 芯片 半导体 chiplet

NFTScan | 10.09~10.15 NFT 市场热点汇总

NFT Research

NFT\ NFTScan

CSS魔法!如何将任意CSS类型转换为数值?

高端章鱼哥

CSS

每个开发人员都想使用的编程语言

互联网工科生

rust

Atlassian午餐会直播回顾:如何在Jira中进行项目时间与成本管理?

龙智—DevSecOps解决方案

工时管理 jira工时 Jira工时管理插件

如何管理嵌入式开发中产生的数字资产?ACT汽车电子与软件技术周演讲回顾

龙智—DevSecOps解决方案

嵌入式开发 汽车嵌入式开发

10个最佳区块链分析工具 区块链系统开发

区块链软件开发推广运营

交易所开发 dapp开发 区块链开发 链游开发 NFT开发

再玩玩B端搭建

得物技术

架构 规则引擎 B端搭建

探索工作流应用场景下解决重复审批的方法

inBuilder低代码平台

工作流 低代码

Linux爆发好时机!Windows这次换代为何这么难!

树上有只程序猿

windows 11

进来“抄作业”!示例代码、操作手册,尽在华为云Codelabs!

华为云PaaS服务小智

云计算 软件开发 华为云

亲测可用 Illustrator 2024更新,最新AI2024下载mac/win

iMac小白

Illustrator 2024 ai2024下载 AI2024破解版

软件测试|教你用skip灵活跳过用例

霍格沃兹测试开发学社

在 Windows 平台下安装与配置 MySQL 5.7.36

小齐写代码

OpenHarmony创新赛丨报名倒计时,超强秘籍带你直通大奖!

OpenHarmony开发者

软件测试|Pytest的必会技巧(一)

霍格沃兹测试开发学社

软件测试|简单易学的性能监控体系prometheus+grafana搭建教程

霍格沃兹测试开发学社

锁定云栖大会!共同见证阿里云大数据+AI产品年度重磅发布及创新

阿里云大数据AI技术

大数据 AI

业内首个基于Iceberg的“云端仓转湖”生产实践探索

腾讯云大数据

湖仓一体

深度学习应用开发示例之目标识别

矩视智能

深度学习 机器视觉

浅谈 33 台 iPad 发展史;OpenAI“悄悄”修改了企业核心价值观丨 RTE 开发者日报 Vol.67

声网

中文永久激活版:IBM SPSS Statistics 26 for Mac破解资源 支持M1

iMac小白

IBM SPSS Statistics 26 SPSS26破解版

体育赛事技术演进历程,开发技术发展趋势

软件开发-梦幻运营部

OpenHarmony创新赛|赋能直播第四期

OpenHarmony开发者

苏宁Nodejs性能优化实战_后端_李浩_InfoQ精选文章