如何加快 Node.js 应用的启动速度,实现分钟到毫秒的转化

2024-03-17 04:32

本文主要是介绍如何加快 Node.js 应用的启动速度,实现分钟到毫秒的转化,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

640?wx_fmt=jpeg

作者|杜佳昆(凌恒) 

出品|阿里巴巴新零售淘系技术部

我们平时在开发部署 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   name	60   13.7%   13.8%  JavaScript	371   84.7%   85.5%  C++	10    2.3%    2.3%  GC	4    0.9%          Shared libraries	3    0.7%          Unaccounted	
[C++]:	
ticks  total  nonlib   name	198   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_back	4    0.9%    0.9%  __GI_mprotect	3    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   name	236   11.7%   12.0%  JavaScript	1701   84.5%   86.6%  C++	35    1.7%    1.8%  GC	47    2.3%          Shared libraries	28    1.4%          Unaccounted	
[C++]:	
ticks  total  nonlib   name	453   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 前后的差异:

640?wx_fmt=png

可以看到,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 代码运行。不过又是另一块难啃的骨头。

加入

我们

函数运行时启动 100ms,一个很刺激的目标,很有挑战,欢迎有想法的各位来一起交流。Make Node.js Great Again!简历投递:点击下方阅读原文

▐  团队介绍

  • 负责高性能 Node.js C++ Addon 开发

  • 负责 AliNode 的设计、研发和维护,支撑阿里集团旗下公司的 Node.js 生态

  • 负责 Serverless 场景 Node.js 函数运行时的设计和优化

▐  职位要求

  • 有强烈的技术热情,工作责任感,具备迅速掌握解决问题所需技术的方法和能力。

  • 熟练掌握 C++/Node.js 作为开发语言,具备优秀的编程素养。

  • 熟练掌握调试工具和调试方法,具备调试复杂软件的能力(比如虚拟机或编译器)者优先;

  • 具备下列一项或多项领域知识或设计和开发经验甚佳:V8/JSCore/SpiderMonkey/Chakra等任一脚本引擎、系统性能分析工具和方法、编译器设计和开发。有良好的表达能力,善于运营开源项目和开源社区,持有具备影响力和 Javascript 语言技术相关的开源项目者优先。

END

你可能还喜欢

点击下方图片即可阅读

640?wx_fmt=png

阅读原文进行简历投递

这篇关于如何加快 Node.js 应用的启动速度,实现分钟到毫秒的转化的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/817802

相关文章

Oracle查询优化之高效实现仅查询前10条记录的方法与实践

《Oracle查询优化之高效实现仅查询前10条记录的方法与实践》:本文主要介绍Oracle查询优化之高效实现仅查询前10条记录的相关资料,包括使用ROWNUM、ROW_NUMBER()函数、FET... 目录1. 使用 ROWNUM 查询2. 使用 ROW_NUMBER() 函数3. 使用 FETCH FI

Python脚本实现自动删除C盘临时文件夹

《Python脚本实现自动删除C盘临时文件夹》在日常使用电脑的过程中,临时文件夹往往会积累大量的无用数据,占用宝贵的磁盘空间,下面我们就来看看Python如何通过脚本实现自动删除C盘临时文件夹吧... 目录一、准备工作二、python脚本编写三、脚本解析四、运行脚本五、案例演示六、注意事项七、总结在日常使用

Java实现Excel与HTML互转

《Java实现Excel与HTML互转》Excel是一种电子表格格式,而HTM则是一种用于创建网页的标记语言,虽然两者在用途上存在差异,但有时我们需要将数据从一种格式转换为另一种格式,下面我们就来看看... Excel是一种电子表格格式,广泛用于数据处理和分析,而HTM则是一种用于创建网页的标记语言。虽然两

Java中Springboot集成Kafka实现消息发送和接收功能

《Java中Springboot集成Kafka实现消息发送和接收功能》Kafka是一个高吞吐量的分布式发布-订阅消息系统,主要用于处理大规模数据流,它由生产者、消费者、主题、分区和代理等组件构成,Ka... 目录一、Kafka 简介二、Kafka 功能三、POM依赖四、配置文件五、生产者六、消费者一、Kaf

使用Python实现在Word中添加或删除超链接

《使用Python实现在Word中添加或删除超链接》在Word文档中,超链接是一种将文本或图像连接到其他文档、网页或同一文档中不同部分的功能,本文将为大家介绍一下Python如何实现在Word中添加或... 在Word文档中,超链接是一种将文本或图像连接到其他文档、网页或同一文档中不同部分的功能。通过添加超

windos server2022里的DFS配置的实现

《windosserver2022里的DFS配置的实现》DFS是WindowsServer操作系统提供的一种功能,用于在多台服务器上集中管理共享文件夹和文件的分布式存储解决方案,本文就来介绍一下wi... 目录什么是DFS?优势:应用场景:DFS配置步骤什么是DFS?DFS指的是分布式文件系统(Distr

NFS实现多服务器文件的共享的方法步骤

《NFS实现多服务器文件的共享的方法步骤》NFS允许网络中的计算机之间共享资源,客户端可以透明地读写远端NFS服务器上的文件,本文就来介绍一下NFS实现多服务器文件的共享的方法步骤,感兴趣的可以了解一... 目录一、简介二、部署1、准备1、服务端和客户端:安装nfs-utils2、服务端:创建共享目录3、服

C#使用yield关键字实现提升迭代性能与效率

《C#使用yield关键字实现提升迭代性能与效率》yield关键字在C#中简化了数据迭代的方式,实现了按需生成数据,自动维护迭代状态,本文主要来聊聊如何使用yield关键字实现提升迭代性能与效率,感兴... 目录前言传统迭代和yield迭代方式对比yield延迟加载按需获取数据yield break显式示迭

Python实现高效地读写大型文件

《Python实现高效地读写大型文件》Python如何读写的是大型文件,有没有什么方法来提高效率呢,这篇文章就来和大家聊聊如何在Python中高效地读写大型文件,需要的可以了解下... 目录一、逐行读取大型文件二、分块读取大型文件三、使用 mmap 模块进行内存映射文件操作(适用于大文件)四、使用 pand

python实现pdf转word和excel的示例代码

《python实现pdf转word和excel的示例代码》本文主要介绍了python实现pdf转word和excel的示例代码,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价... 目录一、引言二、python编程1,PDF转Word2,PDF转Excel三、前端页面效果展示总结一