第1页
探究 Node.js 的服务端之路
ElemeFe - 黄鼎恒
第2页
大纲
1. 前端 -> Node.js -> 后端 2. V8 内存的简介 3. Node.js 服务端开发的常见问题
第3页
2016 Developer Occupations
Full-Stack Web Developer Back-End Web Developer
Student Mobile Developer (Android, IOS, etc)
Desktop Developer Front-End Web Developer
12.2% 11.4% 8.4% 6.9% 5.8%
28.0%
第4页
Node.js in Eleme
实时推送
中间件
快速建站
第5页
Front to Back
业务差异 环境差异 思想差异
业务
环境
思想
第6页
展示
状态管理
需求频繁变动 推送
样式
面向DB
面向DOM操作
通信
业务差异
效果
面向View
数据量大多对多
交互
数据一致性
数据量小一对一
第7页
基于操作系统提供的接口
基于浏览器提供的接口/对象
Polyfill
webview 兼容
灾难备份
环境差异
浏览器兼容
响应式
Scalable
分布式
第8页
Frontend 快速开发 快速渲染 视觉效果
思想差异
Backend 稳定 性能 负载
第9页
Backend’s Perspective
性能 负载 稳定性
—— n% 流量丢失 —— m% 流量丢失 —— 100% 流量丢失
第10页
Backend’s Perspective
稳定性因素
• CPU • 内存容量 • 网络 I/O • 存储设备 I/O • 存储容量 • 存储控制器 • 网络控制器 • CPU 互联 • 内存互联 • I/O 互联
I/O 互联
存储设备 I/O
内存容量
网络 I/O
CPU
第11页
V8 内存结构
Node.js - 后端
V8 JavaScript Engine
Chrome 浏览器 - 前端
第12页
#include <stdio.h>
void init() {
int arr[5] = {}; for (int i = 0; i < 5; ++i) {
arr[i] = i; } }
void test() {
int arr[5]; for (int i = 0; i < 5; ++i) {
printf("%d\n", arr[i]); } }
初始化 int arr[5]
输出结果:
主函数
0 1
输出未初始化的 a32rr
调用 init 4 调用 test
int main(int argc, char const *argv[]) {
init(); test(); return 0; }
第13页
#include <stdio.h>
void init() {
int arr[5] = {}; for (int i = 0; i < 5; ++i) {
arr[i] = i; } }
void test() {
int arr[5]; for (int i = 0; i < 5; ++i) {
printf("%d\n", arr[i]); } }
主函数
init test
int main(int argc, char const *argv[]) {
init(); test(); return 0; }
栈顶 栈顶
打印 打印 打印 打印 栈顶 打印
栈
umnkainnow unitnkesniotw unk0now unk1now unk2now unk3now unk4now unknow
…
第14页
function test() { let number = 123; return number;
}
function init() { let arr = [1,2,3,4,5]; return { get: (i) => arr[i] };
}
let {get} = init(); let n = test();
栈 …
第15页
V8 New Space
1. 准备两个 Stack
2. 函数执行完不释放内存
3. 当正在使用的 Stack 快满, 将不打 算释放的内存到另外一个 Stack
4. 释放整个旧的 stack, 使用新 Stack
Stack Stack 12
第16页
V8 Old Space
New Space
将幸存2次以上 对象挪动到 Old space
Old Space
第17页
内存结构
新生代 老生代
New Space Old Space Large Object Space Map Space Code Space
新数据存储区 老数据存储区 大对象存储区 对象结构信息 机器码存储区
第18页
Garbage Collection
Scavenge
准备两份内存轮番使用, 当某分内存即将用完时切换
Mark-Sweep/Compact
根据引用标记对象后视情况选择方法清理
第19页
Garbage Collection
root
V8 的 GC 的触发由 v8 自身决定(内含默认4个 线程)。如果要需要主动触发 GC,可以在 node 启动时加上 -expose-gc 参数(不推荐)。
第20页
Memory Leak
当变量挂靠在 root 以及 module.exports 等全局变量 上时,对象的内存始终不会释放
异常产生之后没有正确 恢复状态可能导致内存泄漏
全局变 量
闭包
闭包因为写法的问题 容易使得闭包引用的作用域内存泄漏
引用被持有
错异误常处 理
事件监 听
对同一个事件重复监听 或者忘记移除
容易导致内存泄漏
第21页
Memory Leak
Node.js 的事件监听也可能出现的内存泄漏。例如对同一个事件重复监听,忘记移除( removeListener),将造成内存泄漏。这种情况很容易在复用对象上添加事件时出现, 所以事件重复监听可能收到如下警告:
(node:2752) Warning: Possible EventEmitter memory leak detected。 11 myTest listeners added。Use emitter。setMaxListeners() to increase limit
例如,Node.js 中 Http.Agent可能造成的内存泄漏。当 Agent keepAlive 为 true 或者短时间有大量情况的时候,都会复用之前使用过的 socket,如果此时在 socket 上添加事件监听,忘记清除的话,因为 socket 的复用,将导致事件重复监听 从而产生内存泄漏。
第22页
Common Problem
异步
部署
弱计算
Dev
内存泄漏
Ops 分布式 维护
第23页
异步
1. 流程处理曲折 2. 闭包持有引用的释放问题 3. 异步队列的问题 4. 不适合处理复杂的状态机
第24页
异步
1. 使用 Promise, Async/await 等现代方式 2. 学习 V8 内存结构了解闭包释放问题 3. 遵循 immutable 等思想减少操作副作用 4. 使用队列结构进行可控的异步并发
第25页
异步队列
第26页
Common Problem
异步
部署
弱计算
Dev
内存泄漏
Ops 分布式 维护
第27页
弱计算
Node.js 擅长 IO 密集型应用 不擅长 CPU 密集型
第28页
弱计算
什么是 IO 密集型? 什么是 CPU 密集型?
第29页
弱计算
输入设备
存储器
输出设备
运算器
控制器
CPU
数据流
指令流
控制流
第30页
弱计算
1. 确认业务类型
{ CPU密集型 IO密集型
2.了解 CPU 在干什么
{ 死循环 序列化对象
{3.常规监控
利用率
饱和度
GC
Crypt...
第31页
弱计算
CPU Profilling
火焰图
webstorm 工具 https://www.jetbrains.com/help/webstorm/2016.2/v8-cpu-and-memory-profiling.html
第32页
Common Problem
异步
部署
弱计算
Dev
内存泄漏
Ops 分布式 维护
第33页
内存泄漏
• 1. 引用问题: 不论是闭包编写,还是事件监听没注意释放。你需要知道什么情况下, 你的引
用是被持有的,什么情况下又不是。所有引用问题归结到最后都是 v8 内存释放原理了解程度的问题 。
• 2. 队列问题: 一个流程调用的过程中可能经过了非常多个过程,包括通信层、业务层、数据
层等,其中每一个层级对于事务的处理都存在的队列的问题,你需要避免整个流程中某个环节的负载 超过其能处理的上限。
• 3. CPU问题: 引用的释放(GC)、队列设置的不合理(超过负载)、业务逻辑的编写问题(
死循环)都可能导致 CPU 资源紧张,而 CPU 资源紧张同样会导致内存泄漏(没有足够的 CPU 执 行 GC 操作,释放速度赶不上生产速度)。
• 4. 错误处理: 错误出现之后,如果没有对当前流程进行正确的状态恢复,可能导致错误状态
下的内存始终得不到释放从而导致内存泄漏。
第34页
Common Problem
异步
部署
弱计算
Dev
内存泄漏
分布式 Ops
维护
第35页
部署
Node.js 的 child_process.fork 与
POSIX 的 fork
第36页
部署
• 1. exec: 启动一个子进程来执行命令,调用 bash 来解释命令, 所以如果有命令有外部参数,
则需要注意被注入的情况。
• 2. spawn: 更安全的启动一个子进程来执行命令,使用 option 传入各种参数来设置子进程的
stdin、stdout 等。通过内置的命名管道来与子进程建立 IPC 通信。
• 3. fork: spawn 的特殊情况,专门用来产生 worker 或者 worker 池。 返回值是
ChildProcess 对象可以方便的与子进程交互。
Node.js 的 child_process.fork() 不像 POSIX fork(2) 系统调用, 不会克隆当前父进程的空间,也不需要 手动的等待子进程结束回收。
第37页
部署
1. IPC 通信问题
{ RPC通过 Node.js 的 fork 创建的进程使用的是其内置的 IPC (进程间通信)功能,该功能基于管道实现。 架构调整目前在实践使用过程中发现其自带的 IPC 通信功能较弱,在传 输较大数据(1MB以上)是存在性能问题,所以不推荐使用。
2. swap 内存异常
{ master 健康检查 在线上部署的过程中出现机器的 swap 内存爆满情况,排查
发现多进程模式中存在 master 死亡后没有通知到
worke节r 终点止进注程册,使(得 zwoorkoekr e成e为p孤e儿r进程被等系)统
init 领养,在长时间无请求的情况下将 worker 的内存
折叠进入 swap 内存。
第38页
Common Problem
异步
部署
弱计算
Dev
内存泄漏
Ops 分布式 维护
第39页
分布式
1. cluster 的负载均衡
句柄共享 (win)
由主进程创建 socket 监听端口后,将 socket 句 柄直接分发给相应的 worker,然后当连接进来时, 就直接由相应的 worker 来接收连接并处理。 多个 worker 之间会存在竞争关系,产生“惊群效应”
round-robin (*nix)
通过时间片轮转法(round-robin)分发连接. 主进程 监听端口, 接收到新连接之后, 通过时间片轮转法来决 定将接收到的客户端的 socket 句柄传递给指定的 worker 处理. 至于每个连接由哪个 worker 来处理, 完全由内置的循环算法决定.
第40页
分布式
2. 数据一致性
Mongodb 弱项
MonogoDB 的事务能力分常弱,如果你的 业务对数据一致性有要求,请更换事务能力 更强的数据库,比如 mysql 的 innodb
异步的先天弱势
异步的写法以及异步的流程,使得失 败操作的回滚存在先天的困难。
第41页
Common Problem
异步
部署
弱计算
Dev
内存泄漏
Ops 分布式 维护
第42页
维护
• 1.日志思想: 与传统语言不同,Node.js 操作系统资源的形式几乎都是异步,不像 PHP 等传
统的情况可以使用 Dtrace 等动态分析工具。并且异步的情况下,错误栈存在曲折、截断的情况。
• 2.异常处理: 上文中有提到过,异常出现之后,如果没有进行正确的恢复操作可能导致内存泄
漏。其根本原因在于一些异常产生之后 Node.js 的行为是未定义的。官方有明确表示 process 的 uncaughtException 事件是为了让你准备之后再 exit 进程。
• 3.依赖管理: 2016年的 left-pad 事件让大家意识到了使用第三方依赖存在的安全隐患,同时
如果版本未做明确限制的话依赖的模块如果出现 breaking change 可能导致项目构建失败。
维护并不是一个孤立的问题,包括前面提到的异步、CPU、内存、部署、分布式,某一个环节如果架构 的不好,必然导致后期维护困难。
第43页
Common Problem
异步
部署
弱计算
Dev
内存泄漏
Ops 分布式 维护
第44页
大型应用的短板
第45页
microservice arch微i服te务cture
第46页
Step by Step
Node.js 基金会成立
ECMA 2017
第47页
JavaScript 称霸 Github
官方 Security 平台
npms.io nodesecurity.io
NPM 数目破 40万
第48页
Node.js v7.6 发 布
加入 Linux 基金会
第49页
*.js pattern
第50页
Node.js 面试github.com/ElemeFE/node-interview
微博@Lellansin