我们平时在开发部署 Node.js 应用的过程中,对于应用进程启动的耗时很少有人会关注,大多数的应用 5 分钟左右就可以启动完成,这个过程中会涉及到和集团很多系统的交互,这个耗时看起来也没有什么问题。

目前,集团 Serverless 大潮已至,Node.js serverless-runtime 作为前端新研发模式的基石,也发展的如火如荼。Serverless 的优势在于弹性、高效、经济,如果我们的 Node.js FaaS 还像应用一样,一次部署耗时在分钟级,无法快速、有效地响应请求,甚至在脉冲请求时引发资源雪崩,那么一切的优势都将变成灾难。

所有提供 Node.js FaaS 能力的平台,都在绞尽脑汁的把冷/热启动的时间缩短,这里面除了在流程、资源分配等底层基建的优化外,作为其中提供服务的关键一环 —— Node.js 函数,本身也应该参与到这场时间攻坚战中。

Faas平台从接到请求到启动业务容器并能够响应请求的这个时间必须足够短,当前的总目标是 500ms,那么分解到函数运行时的目标是 100ms。这 100ms 包括了 Node.js 运行时、函数运行时、函数框架启动到能够响应请求的时间。巧的是,人类反应速度的极限目前科学界公认为 100ms。

Node.js 有多快

在我们印象中 Node.js 是比较快的,敲一段代码,马上就可以执行出结果。那么到底有多快呢?

以最简单的 console.log 为例(例一),代码如下:

// console.js
console.log(process.uptime() * 1000);

在 Node.js 最新 LTS 版本 v10.16.0 上,在我们个人工作电脑上:

node console.js
// 平均时间为 86ms
time node console.js
// node console.js  0.08s user 0.03s system 92% cpu 0.114 total

看起来,在 100ms 的目标下,留给后面代码加载的时间不多了。。。

在来看看目前函数平台提供的容器里的执行情况:

node console.js
// 平均时间在 170ms
time node console.js
// real    0m0.177s
// user    0m0.051s
// sys     0m0.009s

Emmm… 情况看起来更糟了。

我们在引入一个模块看看,以 serverless-runtime 为例(例二):

// require.js
console.time('load');
require('serverless-runtime');
console.timeEnd('load');

本地环境:

node reuqire.js
// 平均耗时 329ms

服务器环境:

node require.js
// 平均耗时 1433ms

我枯了。。。
这样看来,从 Node.js 本身加载完,然后加载一个函数运行时,就要耗时 1700ms。
看来 Node.js 本身并没有那么快,我们 100ms 的目标看起来很困难啊!

为什么这么慢

为什么会运行的这么慢?而且两个环境差异这么大?我们需要对整个运行过程进行分析,找到耗时比较高的点,这里我们使用 Node.js 本身自带的 profile 工具。

node --prof require.js
node --prof-process isolate-xxx-v8.log > result
[Summary]:
ticks  total  nonlib   name60   13.7%   13.8%  JavaScript371   84.7%   85.5%  C++10    2.3%    2.3%  GC4    0.9%          Shared libraries3    0.7%          Unaccounted
[C++]:
ticks  total  nonlib   name198   45.2%   45.6%  node::contextify::ContextifyScript::New(v8::FunctionCallbackInfo<v8::Value> const&)13    3.0%    3.0%  node::fs::InternalModuleStat(v8::FunctionCallbackInfo<v8::Value> const&)8    1.8%    1.8%  void node::Buffer::(anonymous namespace)::StringSlice<(node::encoding)1>(v8::FunctionCallbackInfo<v8::V
alue> const&)5    1.1%    1.2%  node::GetBinding(v8::FunctionCallbackInfo<v8::Value> const&)4    0.9%    0.9%  __memmove_ssse3_back4    0.9%    0.9%  __GI_mprotect3    0.7%    0.7%  v8::internal::StringTable::LookupStringIfExists_NoAllocate(v8::internal::String*)3    0.7%    0.7%  v8::internal::Scavenger::ScavengeObject(v8::internal::HeapObjectReference**, v8::internal::HeapObject*)3    0.7%    0.7%  node::fs::Open(v8::FunctionCallbackInfo<v8::Value> const&)

对运行时启动做同样的操作

[Summary]:
ticks  total  nonlib   name236   11.7%   12.0%  JavaScript1701   84.5%   86.6%  C++35    1.7%    1.8%  GC47    2.3%          Shared libraries28    1.4%          Unaccounted
[C++]:
ticks  total  nonlib   name453   22.5%   23.1%  t node::fs::Open(v8::FunctionCallbackInfo<v8::Value> const&)319   15.9%   16.2%  T node::contextify::ContextifyContext::CompileFunction(v8::FunctionCallbackInfo<v8::Value> const&)93    4.6%    4.7%  t node::fs::InternalModuleReadJSON(v8::FunctionCallbackInfo<v8::Value> const&)84    4.2%    4.3%  t node::fs::Read(v8::FunctionCallbackInfo<v8::Value> const&)74    3.7%    3.8%  T node::contextify::ContextifyScript::New(v8::FunctionCallbackInfo<v8::Value> const&)45    2.2%    2.3%  t node::fs::InternalModuleStat(v8::FunctionCallbackInfo<v8::Value> const&)...

可以看到,整个过程主要耗时是在 C++ 层面,相应的操作主要为 Open、ContextifyContext、CompileFunction。这些调用通常是出现在 require 操作中,主要覆盖的内容是模块查找,加载文件,编译内容到 context 等。

看来,require 是我们可以优化的第一个点。

如何更快

从上面得知,主要影响我们启动速度的是两个点,文件 I/O 和代码编译。我们分别来看如何优化。

▐ 文件 I/O

整个加载过程中,能够产生文件 I/O 的有两个操作:

一、查找模块

因为 Node.js 的模块查找其实是一个嗅探文件在指定目录列表里是否存在的过程,这其中会因为判断文件存不存在,产生大量的 Open 操作,在模块依赖比较复杂的场景,这个开销会比较大。

二、读取模块内容

找到模块后,需要读取其中的内容,然后进入之后的编译过程,如果文件内容比较多,这个过程也会比较慢。

那么,如何能够减少这些操作呢?既然模块依赖会产生很多 I/O 操作,那把模块扁平化,像前端代码一样,变成一个文件,是否可以加快速度呢?

说干就干,我们找到了社区中一个比较好的工具 ncc,我们把 serverless-runtime 这个模块打包一次,看看效果。

服务器环境:

ncc build node_modules/serverless-runtime/src/index.ts
node require.js
// 平均加载时间 934ms

看起来效果不错,大概提升了 34% 左右的速度。

但是,ncc 就没有问题嘛?我们写了如下的函数:

import * as _ from 'lodash';
import * as Sequelize from 'sequelize';
import * as Pandorajs from 'pandora';
console.log('lodash: ', _);
console.log('Sequelize: ', Sequelize);
console.log('Pandorajs: ', Pandorajs);

测试了启用 ncc 前后的差异:

可以看到,ncc 之后启动时间反而变大了。这种情况,是因为太多的模块打包到一个文件中,导致文件体积变大,整体加载时间延长。可见,在使用 ncc 时,我们还需要考虑 tree-shaking 的问题。

▐ 代码编译

我们可以看到,除了文件 I/O 外,另一个耗时的操作就是把 Javascript 代码编译成 v8 的字节码用来执行。我们的很多模块,是公用的,并不是动态变化的,那么为什么每次都要编译呢?能不能编译好了之后,以后直接使用呢?

这个问题,V8 在 2015 年已经替我们想到了,在 Node.js v5.7.0 版本中,这个能力通过 VM.Script 的 cachedData暴露了出来。而且,这些 cache 是跟 V8 版本相关的,所以一次编译,可以在多次分发。

我们先来看下效果:

//使用 v8-compile-cache 在本地获得 cache,然后部署到服务器上
node require.js
// 平均耗时 868ms

大概有 40% 的速度提升,看起来是一个不错的工具。

但它也不够完美,在加载 code cache 后,所有的模块加载不需要编译,但是还是会有模块查找所产生的文件 I/O 操作。

▐ 黑科技

如果我们把 require 函数做下修改,因为我们在函数加载过程中,所有的模块都是已知已经 cache 过的,那么我们可以直接通过 cache 文件加载模块,不用在查找模块是否存在,就可以通过一次文件 I/O 完成所有的模块加载,看起来是很理想的。

不过,可能对远程调试等场景不够优化,源码索引上会有问题。这个,之后会做进一步尝试。

近期计划

有了上面的一些理论验证,我们准备在生产环境中将上述优化点,如:ncc、code cache,甚至 require 的黑科技,付诸实践,探索在加载速度,用户体验上的平衡点,以取得速度上的提升。

其次,会 review 整个函数运行时的设计及业务逻辑,减少因为逻辑不合理导致的耗时,合理的业务逻辑,才能保证业务的高效运行。

最后,Node.js 12 版本对内部的模块默认做了 code cache,对 Node.js 默认进程的启动速度提升比较明显,在服务器环境中,可以控制在 120ms 左右,也可以考虑引用尝试下。

未来思考

其实,V8 本身还提供了像 Snapshot 这样的能力,来加快本身的加载速度,这个方案在 Node.js 桌面开发中已经有所实践,比如 NW.js、Electron 等,一方面能够保护源码不泄露,一方面还能加快进程启动速度。Node.js 12.6 的版本,也开启了 Node.js 进程本身的在 user code 加载前的 Snapshot 能力,但目前看起来启动速度提升不是很理想,在 10% ~ 15% 左右。我们可以尝试将函数运行时以 Snapshot 的形式打包到 Node.js 中交付,不过效果我们暂时还没有定论,现阶段先着手于比较容易取得成果的方案,硬骨头后面在啃。

另外,Java 的函数计算在考虑使用 GraalVM 这样方案,来加快启动速度,可以做到 10ms 级,不过会失去一些语言上的特性。这个也是我们后续的一个研究方向,将函数运行时整体编译成 LLVM IR,最终转换成 native 代码运行。不过又是另一块难啃的骨头。

原文链接
本文为云栖社区原创内容,未经允许不得转载。

如何加快 Node.js 应用的启动速度相关推荐

  1. 使用FortJs使用现代JavaScript开发Node.js

    介绍 (Introduction) Nodejs gives you the power to write server side code using JavaScript. In fact, it ...

  2. 【JSConf EU 2018】Ryan Dahl: Node.js 的设计错误

    在稍早前的 JS Conf Berlin 上,被称为 Nodejs 之父的 Ryan Dahl 发表了<10 Things I Regret About Node.js>演讲,并且发布了新 ...

  3. Node.js 框架

    Node.js的是一个JavaScript平台,它允许你建立大型的Web应用程序.  Node.js的框架平台使用JavaScript作为它的脚本语言来构建可伸缩的应用. 当涉及到Web应用程序的开发 ...

  4. Deno 并不是下一代 Node.js

    2019独角兽企业重金招聘Python工程师标准>>> 这几天前端圈最火的事件莫过于 ry(Ryan Dahl) 的新项目 deno 了,很多 IT 新闻和媒体都用了标题:" ...

  5. 10个最佳Node.js企业应用案例:从Uber到LinkedIn

    译者按: Node.js 8已经发布了,NPM模块每周下载量早已超过10亿,从Uber到LinkedIn都在使用Node.js,谁说JavaScript不能写后台? 原文: 10 best Node. ...

  6. Node.js环境搭建npm安装

    Node.js环境搭建 什么使Node.js呢?我们知道JavaScript开始作为客户端语言,但早已在浏览器端一统江湖,这时,野心越来越大,它就想向服务器端拓展了,于是Node.js就是这样的,我们 ...

  7. 使用Node.JS监听文件夹变化

    使用Node.JS监听文件夹改变有许多应用场合,比如: 构建自动编绎工具 当源文件改变时,自动运行build过程,比如当你写CoffeeScript文件或SASS CSS文件时,保存之后可即时生成对应 ...

  8. YodaOS: 一个属于 Node.js 社区的操作系统

    开发四年只会写业务代码,分布式高并发都不会还做程序员? >>>   大家好,很开心在这里宣布 YodaOS开源了.他将承载 Rokid 4年以来对于人工智能和语音交互领域的沉淀,并选 ...

  9. Node.js 框架设计及企业 Node.js 基础建设相关讨论

    大家好,我是若川.19年我写的 lodash源码 文章投稿到海镜大神知乎专栏竟然通过了,后来20年海镜大神还star了我的博客,同时还转发了我的微博.时间真快啊.今天分享这篇Node.js的讨论. 2 ...

最新文章

  1. 数位DP 回文序列 POJ-3280 Cheapest Palindrome
  2. Datawhale组队学习周报(第040周)
  3. 信息树和XML文件的遍历及XML文件的应用
  4. BZOJ.4821.[SDOI2017]相关分析(线段树)
  5. [转] 外企面试官最爱提的问题 TOP10
  6. 中国慕课java_回收的吸油毡通常应放置一边以备再次使用。
  7. 如何在vue项目中使用md5加密
  8. 理解与学习linux 文件系统的目录结构
  9. 如何处理苹果Mac冻结和无响应的应用程序?
  10. 免费复制百度文库的VIP文章(非常简单!)
  11. 自动化测试平台化[v1.0.0][模块化设计方法]
  12. centos6.6_vsftpd 虚拟账户FTP服务搭建
  13. USACO-Fractions to Decimals
  14. Python代码爬取下载应用宝所有APP软件
  15. 怎么设置织梦栏目html结尾,dedecms网站栏目地址url优化成.html结尾的而不是文件夹形式结尾的。请大家来帮忙。...
  16. linux xxx命令,linux命令ps aux|grep xxx详解
  17. 编程初学者必备的基础知识
  18. 【硬件在环HIL环境配置】
  19. matlab 模拟滤波器设计与实现,转一个讲matlab设计模拟滤波器的文章2(转)
  20. JSP网上鞋子商城网站

热门文章

  1. 增量式pid调节方式有何优点_增量式pid和位置式pid相比各有什么优缺点
  2. 计算机网络与通信思维导图,用思维导图描述5G场景
  3. dax 筛选 包含某个字_DAX分享9:DAX中用变量来计算动态filter context中数值
  4. python算法题排序_python-数据结构与算法- 面试常考排序算法题-快排-冒泡-堆排-二分-选择等...
  5. python写前端和js_Python之路【第十二篇】前端之jsdomejQuery
  6. geoserver发布瓦片_Geoserver2.15.1配置自带GeoWebCache 插件发布ArcGIS Server瓦片
  7. 深度linux缺点,原来国产深度系统有这些“缺陷”,难怪只有少数人在使用!
  8. system流怎么判断为空_并行流ParallelStream中隐藏的陷阱
  9. nginx文件服务器密码登录,风的方向
  10. java面试加分_不只是给面试加分 -- Java WeakReference的理解与使用