Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

深入浅出Node.js读书笔记 #30

Open
ChenJiaH opened this issue Aug 5, 2019 · 0 comments
Open

深入浅出Node.js读书笔记 #30

ChenJiaH opened this issue Aug 5, 2019 · 0 comments
Labels
node.js notes 读书笔记

Comments

@ChenJiaH
Copy link
Owner

ChenJiaH commented Aug 5, 2019

首发于微信公众号《前端成长记》,写于 2019.05.06

Node简介

Node应用场景

  • I/O密集型,因为可以利用事件循环的处理能力,并行I/O,所以擅长I/O密集型
  • CPU密集型 建议适当分解大型运算任务,不阻塞I/O,如果计算耗时超过普通阻塞I/O的好使,那么就需要重新评估

模块机制

Node模块实现

  1. 路径分析
  2. 文件定位
  3. 编译执行

Node缓存编辑和执行后的对象

文件定位

文件拓展名分析,会调用fs模块同步阻塞式判断文件是否存在,依次.js、.node、.json。

如果是.node和.json,在传递给require时带上拓展名,会加快一点速度。同步配合缓存,可以大幅度缓解阻塞式调用的缺陷

编译过程

javascript核心模块通过 process.binding('natives') 取出,编译成功的模块缓存到NativeModule._cache对象,文件模块缓存到Module._cache

兼容多种模块规范

;(function (name, definition) {
// 检测上下文环境是否为AMD或CMD
var hasDefine = typeof define === 'function'
// 检测上下文环境是否为Node
var hasExports = typeof module !== 'undefined' && module.exports
if (hasDefine) {
// 如果是AMD或CMD环境
define(definition)
} else if (hasExports) {
// 如果是Node环境,定义为Node模块
module.exports = definition()
} else {
// 执行结果挂载window上
this[name] = definition()
}
})('hello', function () {
var hello = function () {}
return hello
})

异步I/O

Node异步I/O模型基本要素

事件循环,观察者,请求对象,I/O线程池构成了Node异步I/O模型的基本要素

观察者模式

idle观察者先于I/O观察者,I/O观察者先于check观察者。
idle观察者

  • process.nextTick 保存在数组中,每轮循环中会把全部回调执行完

check观察者

  • setInmediate 保存在链表中

Node高性能原因之一

Node通过事件驱动的方式处理请求,无须为每一个请求创建额外的对应线程,可以省掉创建线程和销毁线程的开销,同时操作系统在调度任务时因为线程较少,上下文切换的代价很低。这使得服务器能够有条不紊地处理请求,即使在大量连接的情况下,也不受线程上下文切换开销的影响。

异步编程

EventEmitter

多异步之间协作方案

使用事件发布/订阅

var emitter = new events.Emitter()
emitter.on('data', function (msg) {
console.log(msg)
})
emitter.emit('data', 'I'm a message')

也可再次封装,可参考 EventProxy 模块

异步并发控制

通过队列去判断,是否达到阀值,达到就从队列中去取出执行

目前ES6 ES7 已经支持 Promise Async/Await 来实现,不需要再自行编写

内存控制

V8内存分类

  • rss 常驻内存
  • heapTotal 申请的堆内存
  • heapUsed 当前使用的内存

V8的垃圾回收算法

分为新生代(存活时间较短)和老生代。
新生代采用 Scavenge 算法,内存一分为二,From - To。通过复制存活对象再进行交换,空间换时间。
老生代采用 Mark-Sweep & Mark-Compact ,Sweep 标记清除,Compact 对象移动(无内存碎片)

Buffer 内存不通过V8分配,而是从Node自行分配(突破大小限制)。所以V8回收的主要适合V8的堆内存。

内存泄漏常见原因

  • 缓存,将内存当缓存,常见的hashObj,建议使用Redis
  • 队列消费不及时,队列消费速度小于生产速度,比如说日志写入数据库
  • 作用域未释放

内存泄漏排查

  • node-heapdump
  • node-memwatch

大文件读取使用 createReadStream/createWriteStream 代替 readFile/writeFile ,pipe来通过文件流进行操作。

Buffer

Buffer内存分配

采用slab动态内存管理机制,固定大小的内存区域。

  • 小Buffer对象,主要使用一个局部变量pool作为中间处理对象,处于分配状态的slab单元都指向它。

正确拼接buffer

+= 内隐藏了 toString() 操作,在宽字节中文可能会出现问题。

var fs = require('fs')
var iconv = require('iconv-lite')
var res = fs.createReadStream('test.md')
var chunks = []
var size = 0
res.on('data', function (chunk) {
  chunks.push(chunk)
  size += chunk.length
})
res.on('end', function () {
  var buf = Buffer.concat(chunks, size)
  var str = iconv.decode(buf, 'utf8')
})

设置 highWaterMark 值可以提高二进制文件的读取速度

网络编程

TCP 传输控制协议

在OSI模型上属于传输层协议,由物理层(网络物理硬件)、数据链结层(网络特有的链路接口)、网络层(IP)、传输层(TCP/UDP)、会话层(通信连接/维持会话)、表示层(加密/解密等)、应用层(HTTP/SMTP/IMAP等)组成。

TCP是面向连接的,典型的3次握手行程会话, 客户端请求连接 -> 服务端响应 -> 客户端开始传输,通过套接字连接。

UDP 用户数据包协议

TCP中会话基于连接完成,如果客户端需要与另一个TCP服务通信需要另创建一个套接字(socket)。UDP一个套接字可以与多个UDP服务通信。目前广泛应用,DNS服务基于此实现。

socket.send(buf, offset, length, port, address, [callback])

HTTP 超文本传输协议

HTTP请求报文和响应报文都包括报文头和报问题。HTTP服务继承自TCP服务器,可以与多个客户端保持连接,由于采用事件驱动,不会为每个连接创建额外的线程或进程,所以可以保持很低的内存占用,进而满足高并发。

WebSocket

WebSocket客户端基于事件的编程模型与Node中自定义事件相差无几;WebSocket实现了客户端与服务器端之间的长连接,而Node事件驱动的方式十分擅长与大量的客户端保持高并发连接。

优势:

  • 客户端与服务器端只建立一个TCP链接,可以使用更少链接。
  • WebSocket服务器端可以推送数据到客户端,这远比HTTP请求响应模式更灵活、更高效。
  • 有更轻量级的协议头,减少数据传送量。

进程

Node中父进程在实际创建子进程之前,会创建IPC通道并监听它,然后才真正创建出子进程,并通过环境变量(NODE_CHANNEL_FD)告诉子进程这个IPC通道的文件描述符。子进程在启动的过程中,根据文件描述符去连接这个已存在的IPC通道,从而完成父子进程之间的链接。

只有启动的子进程是Node进程时,子进程才会根据环境变量去连接IPC通道,对于其他类型的子进程则无法实现进程间通信,除非其他进程也按约定去连接这个已经创建好的IPC通道

集群稳定,利用上CPU多核,多工作进程存活状态管理,平滑重启,重启限量阀值

// 主进程代码
var fork = require('child_process').fork
var cpus = require('os').cpus()

var server = require('net').createServer()
server.listen(1337)

var limit = 10
var during = 60000
var restart = []
var isTooFrequently = function(){
    var time = Date.now()
    var length = restart.push(time)
    if (length > limit) {
        restart = restart.slice(limit * -1)
    }
    return restart.length >= limit && restart[restartl.length - 1] - restart[0] < during
}

var workers = {}

var createWorker = function () {
    if (isTooFrequently()) {
        process.emit('giveup', length, during)
        return;
    }
    var worker = fork(__dirname + '/worker.js')
    worker.on('message', function (message) {
        if (message.act === 'suicide') {
            createWorker()
        }
    })
    worker.on('exit', function () {
        console.log('Worker ' + worker.pid + ' exited.')
        delete workers[worker.pid]
    })
    worker.send('server', server)
    workers[worker.pid] = worker
    console.log('Create Worker. pid: ' + worker.pid)
}

for(var i = 0; i < cpus.length; i++) {
    createWorker()
}

process.on('exit', function (){
    console.log('process exiting...')
    for(var pid in workers) {
        workers[pid].kill()
    }
})
// 业务逻辑 worker.js
var http = require('http')
var server = http.createServer(function(req, res){
    res.writeHead(200, {'Content-Type': 'text/plain'})
    res.end('handled by child, pid is ' + process.pid + '\n')
    throw new Error('throw exception')
})

var worker
process.on('message', function (m, tcp) {
    if (m === 'server') {
        worker = tcp
        worker.on('connection', function (socket) {
            server.emit('connection', socket)
        })
    }
})
process.on('uncaughtException', function () {
    process.send({act: 'suicide'})
    worker.close(function () {
        process.exit(1)
    })
    setTimeout(() => {
        process.exit(1)
    }, 5000);
})

测试

单元测试

  • 单一职责
  • 接口抽象
  • 层次分类

通过 assert 模块做断言

测试用例、测试报告、测试覆盖率、mock、工程化和自动化(travis-ci)

性能测试

  • 基准测试
  • 压力测试

RT:响应时间
TPS:吞吐量,在单位时间内处理请求的数量
QPS:每秒查询率
并发数:并发用户数是指系统可以同时承载的正常使用系统功能的用户的数量

产品化

项目工程化

  1. 目录结构

|—— History.md // 项目改动历史
|—— INSTALL.md // 安装说明
|—— Makefile // Makefile文件
|—— benchmark // 基准测试
|—— controllers // 控制器
|—— lib // 没有模块化的文件牡蛎
|—— middlewares // 中间件
|—— package.json // 包描述文件,项目依赖配置等
|—— proxy // 数据代理目录,类似MVC中的M
|—— test // 测试目录
|—— tools // 工具目录
|—— views // 视图目录
|—— routes.js // 路由注册表
|—— dispatch.js // 多进程管理
|—— README.md // 项目说明文件
|—— assets // 静态文件目录
|—— assets.json // 静态文件与CDN路径的映射文件
|—— bin // 可执行脚本
|—— config // 配置目录
|—— logs // 日志目录
|—— app.js // 工作进程

  1. 构建工具
  • makefile
  • grunt
  1. 编码规范
  • jslint
  • jshint
  1. 代码审查

部署流程

  1. 部署环境
  • stage环境,普通测试环境
  • pre-release环境,预发布环境
  • product环境,生产环境
  1. 部署操作

可以在主进程启动时将进程ID写到pid文件中,在脚本停止或者重启应用时通过kill给进程发SIGTERM信号,收到信号删除该pid文件并退出进程。

# 完整应用启动、停止和重启脚本
#!/bin/sh
DIR=`PWD`
NODE=`which node`
# get action
ACTION = $1

# help
usage() {
echo "Usage: ./appct1.sh {start|stop|restart}"
exit 1;
}

get_pid() {
if [ -f ./run/app.pid]; then
echo `cat ./run/app.pid`
fi
}

# start app
start() {
pid = `get_pid`

if [ l -z $pid]; then
echo 'server is already running'
else
$NODE $DIR/app.js 2>&1 &
echo 'server is running'
fi
}

# stop app
stop() {
pid=`get_pid`
if [ -z $pid ]; then
echo 'server not running'
else
echo 'server is stopping...'
kill -1$ $pid
echo "server stopped !"
fi
}
# restart app
restart() {
stop
sleep 0.5
echo ====
start
}

case "$ACTION" in
start)
start
;;
stop)
stop
;;
restart)
restart
;;
*(
usage
;;
esac

性能

拆分原则:做专一的事、让擅长的工具做擅长的事情、将模型简化、将风险分离、缓存

  1. 动静分离

可以将静态资源和请求让nginx或者cdn处理,node只处理动态内容

  1. 启动缓存

redis或memcached

  1. 多进程架构

cluster/pm/forever

  1. 读写分离

因为读取数据库速度远远高于写入的速度,通常会进行数据库的读写分离,将数据库进行主从设计,这样读数据操作不再受到写入的影响

日志

  1. 访问日志

  2. 异常日志

日志格式化记录

日志写入可以在线写,但建议进行离线数据分析,分析完成后同步到数据库。因为数据库写入会有一系列处理,消费速度低于写入速度,导致内存泄漏

  1. 日志分割

监控报警

  1. 监控
  • 日志监控
  • 响应时间,异常或性能瓶颈
  • 进程监控,多进程需要监控进程数量
  • 磁盘监控,监控日志频繁写导致磁盘空间被用光带来的问题
  • 内存监控,监控服务器内存使用情况,如果只升不降,那一定存在内存泄漏问题
  • CPU占用监控,用户态较高说明需要较高的CPU开销(一般低于70%),内核态较高说明花时间进行进程调度或系统调用(小于35%)
  • CPU load监控,CPU平均负载,过高说明进程数量过多,可能是重复启动新进程导致
  • I/O负载,反应磁盘读写情况
  • 网络监控,流入流量和流出流量
  • 应用状态监控,监控应用状态,最简单直接响应时间戳校验
  • DNS监控,DNSPod。

报警实现

  1. 邮件报警

nodemailer可实现

  1. 短信或电话报警

通过短信服务平台

稳定性

  • 多机器部署,利用更多的硬件资源,需要考虑负载均衡,状态共享和数据一致性。
  • 多机房部署,解决地理位置带来的延迟,并且可以互为灾备
  • 容灾备份,备份进行灾备

目前尽量部署在虚拟机上,避免实体机上多服务器同时停止服务

异构共存

Node能够通过协议与已有的系统很好地异构共存。

(完)


本文为原创文章,可能会更新知识点及修正错误,因此转载请保留原出处,方便溯源,避免陈旧错误知识的误导,同时有更好的阅读体验
如果能给您带去些许帮助,欢迎 ⭐️star 或 ✏️ fork
(转载请注明出处:https://chenjiahao.xyz)

@ChenJiaH ChenJiaH added node.js notes 读书笔记 labels Aug 5, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
node.js notes 读书笔记
Projects
None yet
Development

No branches or pull requests

1 participant