写点什么

苏宁 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:265839

评论

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

35岁大厂程序员被劝退!老板说:没年轻人有冲劲!真有内味了吗?

程序员生活志

程序员 职场

jQuery笔记

一个坚强的小怪兽

jquery

深化产教融合,共育数字人才

InfoQ_967a83c6d0d7

linux入门系列8--shell编程入门

黑马腾云

Linux centos Shell linux命令 linux编程

威联通(NAS)应用篇:搭建个人音乐中心

BigYoung

NAS QNAP 音乐 搭建 无损

为什么Mysql索引非得是B+树

知方可达

MySQL

SSH免密登录

Radix10

Linux Shell 加密 openssh SSH

《精益产品开发》随笔

研发管理Jojo

敏捷开发 精益思想 敏捷教练

两分钟给你讲清楚JavaScript中的闭包与this

在沉默中

Java 闭包

要老婆吗? AR一键生成的那种

程序员生活志

英伟达收购ARM:双赢还是灾难?

脑极体

JVM原理与实战

东哥

学习python(嵩天老师的课)

Geek_2a27b0

区块链的想象,解决贫富差距

CECBC

区块链 货币 股市

没有一个冬天不会过去!疫情当下,企业“逆势而上”必选“上云”跑道

华为云开发者联盟

云计算 新基建 华为云 企业上云 云服务器

SkyWalking为超大规模而生

热心的朝阳群众

Skywalking 开源社区

2020年运维行业学啥技术比较值钱?

EUSCE

DevOps 运维 运维自动化 系统运维 linux运维

如何查看Django ORM执行的SQL语句

BigYoung

sql django ORM 查询

没想到,Git居然有3种“后悔药”!

Geek Tech

git git reset

MySQL-技术专题-分区表和合并表详解

洛神灬殇

你的面向接口编程一定对吗?

架构师修行之路

linux入门系列10--firewalld防火墙管理

黑马腾云

Linux centos 防火墙 linux运维 linux防火墙

内容审核平台助力猫爪构建健康安全的社交环境

百度大脑

人工智能 百度 百度大脑 内容审核

《八佰》,电影的价值已在真实之外

zhoo299

随笔杂谈 电影

学习笔记

Qx

学习

B站抽奖

・ 懒ヾ

linux入门系列9--用户管理及文件权限控制

黑马腾云

Linux centos centos7 linux运维 linux用户权限

推荐几个实用的前端编辑工具VSCode插件,让你开发事半功倍,告别加班烦恼

web前端程序猿

vscode 大前端 工具软件

你用对锁了吗?浅谈 Java “锁” 事

yes

Java 多线程与高并发

深度学习框架“国货”正当时,但要警惕无差别投入的“产业陷阱”

脑极体

图解JavaScript——进阶篇(执行上下文、变量对象、作用域、作用域链、闭包、this、原型及原型链、事件循环等一把梭)

执鸢者

Java 大前端 函数执行 事件循环

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