深入浅出node读书笔记

1,309 阅读18分钟

github地址:戳这里

简介

目标:写一个基于事件驱动非阻塞i/o 的web服务器,以达到更高的性能。构建快速,可伸缩的网络应用平台

js开发性能低,事件驱动应用

node强制不共享任何资源的 单线程 ,单进程系统,包含十分适宜网络的库

应用:

  1. 访问本地文件
  2. 搭建websocket服务端
  3. 连接数据库
  4. web workers多进程(不处理ui)

特点:

  1. 依旧基于作用域和原型链
  2. 异步i/o

两个readFile的操作最终时间为最慢的那一个

  1. 事件和回调函数

事件编程方式:轻量级,轻耦合,只关注事务点等优势

  1. 单线程

    特点:
    1. js与其他线程是无法共享任何状态
    2. 不用像多线程一样处处在意状态的同步
    3. 没有死锁
    4. 没有线程上下文交换带来的性能上的开销
    弱点:
    1. 无法利用多核cpu
    2. 错误会引起整个应用退出,应用的健壮性值得考研
    3. 大量计算占用cpu导致无法调用异步i/o
    4. js与ui共用一个线程,长时间执行会导致ui的渲染和响应被中断
    解决:
    1. web workers能够创建工作线程来进行计算,以解决js大计算阻塞ui渲染的问题
    2. child_process子进程,将计算分发到各个子进程,可以将大量计算分解掉

应用场景:

  1. i/o密集型,利用事件循环的处理能力
  2. cpu非密集型,i/o阻塞造成的性能浪费远比cpu的影响小
  3. 分布式应用,利用高效并行i/o,可以高效使用数据库

模块机制

前言:

  • web 1.0 : JavaScript用于表单校验和网页特效,只有对bom,dom的支持

  • web 2.0 : 提升了网页的用户体验,bs应用展现出了比cs(需要装客户端)应用优越的地方。h5崭露头角

此过程经历了工具-组件-框架-应用的变迁

js的规范缺陷:

  1. 没有模块系统
  2. 标准库较少
  3. 没有标准接口
  4. 缺乏包管理系统

commonjs模块规范

  1. 模块引入 require()
  2. 提供exports对象用于导出当前模块的方法或者变量
  3. 模块标识,就是require的参数,必须驼峰命名,相对路径或者绝对路径,可以没有后缀

同步,为后端js指定的规范,并不完全适合前端的应用场景

模块实现

模块分为两类:

  1. node提供的 核心模块

已被编译进了二进制执行文件,node启动时就被加载进内存,所以1.2步骤可以省略。且加载速度最快

  1. 用户编写的 文件模块

动态加载,速度比核心模块慢

优先从缓存加载

  • node缓存的是 编译执行后的对象
  • 不论核心模块还是用户模块,对应相同模块的二次加载都是缓存优先

在node中引入模块要经过下面三个步骤

  1. 路径分析

    1. 标识符分析:
      1. 核心模块
      2. .. 或者 . 相对路劲模块
      3. / 开头的绝对路径模块
      4. 非路径形式的模块,如自定义的 connect 模块
    • 如果想加载与核心模块标识符相同的模块,必须选择 不同的标识符 或者 换用路径 的方法

    • .../ 开头的标识符,会将路径转换成真实路径

    • 自定义模块是最费时的

      module.paths模仿搜索路径

      规则如下:

      1. 当前文件目录下的node_modules
      2. 父目录下的node_modules
      3. 沿路径向上逐级递归直到根目录下的node_modules
  2. 文件定位

    1. 文件扩展名

      • .js .node .json顺序补齐

      • fs模块同步阻塞式的判断文件是否存在,如果是.node.json 文件,带上扩展名再配合缓存可以加快速度

    2. 目录和包的处理

      • 如果得到的是一个目录,则会被当做包来处理。这时先进入包目录,查找 package.json ,取出 main 属性指定的文件名定位。
      • 如果找不到这个文件或者没有 package.json , 会将 index 作为默认文件名
  3. 编译执行

node会新建一个模块对象,然后根据路径载入并编译,对应不同扩展名,载入方法不同:

  • .js 通过 fs 同步读取
  • .node 通过 dlopen()加载
  • .json 通过fs读取,再 JSON.parse
  • 其余扩展名都被当做 .js

每一个编译成功的模块都会被绑定在 Module._cache

编译过程对文件内容进行头尾包装

// 通过vm原生模块runInThisContext方法执行,不污染全局
(function (exports, require, module, __filename, __dirname) {
    
})

另外,这样会出错

exports = function () {
    // My class
}

原因在于,exports对象是通过形参的方式传入的,直接赋值会改变形参的作用,但并不能改变作用域外的值。

js核心模块的编译过程

  1. 转存为c/c++代码
  2. 编译js核心模块

c/c++核心模块编译过程

  1. 内建模块的组织方式

c++模块主内完成核心,js主外实现封装

性能优于脚本语言

被编译成二进制文件,一旦node开始执行,就直接加载进缓存

  1. 内建模块导出

依赖关系:文件模块 <-- 核心模 块<-- 内建模块

包与npm

cnpm搭建私有的npm服务

包结构

  • package.json 包描述文件

    • name:包名,不允许出现空格
    • description:包简介
    • version:版本号
    • keywords:关键词数组
    • maintainers:包维护者列表,每个维护者有name,email,web
    • dependencies:所需要的依赖包列表
    • devDependencies:只在开发时需要的依赖
    • scripts:脚本说明对象
    • main:模块引入方法require在引入包时,会优先检查这个字段,并将其作为包中其余模块的入口
    • bin:一些包作者希望包可以作为命令行工具,配置好bin后,通过npm install package_name -g将脚本添加到执行路径中,之后可以再命令行直接执行
  • bin 存放可执行二进制文件的目录

  • lib 存放js的代码目录

  • doc 存放文档

  • test 存放单元测试用例

常用功能

  1. 查看帮助npm help

  2. 安装依赖包npm install --save/--save-dev express

    1. 全局安装

    -g是讲一个包安装到全局可用的可执行命令。它根据包描述文件中的bin字段配置,将实际脚本连接到与node可执行文件相同的路径下

    如果node可执行文件的位置是/usr/local/bin/node ,那么模块目录就是/usr/local/lib/node_modules 。最后通过软链接方式将bin字段配置的可执行文件链接到node的可执行目录下

    1. 本地安装

      换源:

      1. npm install underscore --registry=http:registry.url
      2. npm config set registry http:registry.url
  3. npm钩子

  4. 发布包

    1. 编写模块
    2. 初始化包描述文件
    3. 注册包仓库账号 npm adduser
    4. 上传包 npm publish<folder>
    5. 管理包权限

    npm owner ls <package_name>

    npm owner add <user> <package_name>

    npm owner rm <user> <package_name> 6. 分析包 npm ls

模块考察点

  1. 良好的测试
  2. 良好的文档
  3. 良好的测试覆盖率
  4. 良好的编码规范
  5. 更多条件

前后端共用模块

node模块引入几乎都是同步的,但如果前端模块也采用同步的方式来引入,用户体验会造成问题

AMD规范

需要用define来明确定义一个模块,而在node实现中是隐式包装的。

所有的依赖,通过形参传递到依赖模块内容中

define(['dep1', 'dep2'], function (dep1, dep2) {
    return function () {}   
})

目的是作用域隔离

内容需要返回的方式实现导出

define(function () {
    var exports = {};
    exports.sayHello = function () {
        ...
    }
    return exports
    
})

CMD规范

更接近commonjs规范

define(function (require, exports, module) {
    // ...
})

require,exports, module通过形参传递给模块。

兼容多种模块规范

;(function (name, definition) {
    var hasDefine = typeof define === 'function';
    var hasExports = typeof module !== 'undefined' && module.exports;
    if (hasDefine) { // AMD或者CMD
        define(definition);  
    } else if(hasExports) { // 定义为普通模块
        module.exports = definition()
    } else {
        this[name] = definition()
    }
})('hello', function () {
  var hello = function () {}  
  return hello
})

异步i/o

  • node面向网络而设计

  • 利用单线程,原理多线程死锁,状态同步问题

  • 利用异步i/o,让单线程原理阻塞,更好的利用cpu

  • 内核在进行文件i/o的操作时,通过文件描述符进行管理,文件描述符类似于应用程序与系统内核之间的凭证。

  • 阻塞i/o造成cpu等待浪费,非阻塞却要 轮询 去确认是否完全完成数据获取

  • 理想非阻塞异步i/o:发起非阻塞调用后,可以直接处理下一个任务,只需i/o完成后通过信号或回调将数据传递给应用程序

  • 显示的异步i/o:通过让部分线程进行阻塞i/p或者非阻塞i/o加轮询技术来完成数据获取,让一个线程进行计算处理,通过线程之间的通信将i/o得到的数据进行传递

为什么要异步i/o

  1. 用户体验

    如果是同步,js执行ui渲染和响应将处于停滞状态

    采用异步,在下载资源期间,js和ui的执行都不会处于等待状态

    采用异步方式所花时间为max(m, n)

  2. 资源分配

    • 单线程串行依次执行

    缺点:

    单线程同步编程模型会因为阻塞i/o导致性能差,

    • 多线程并行完成

    缺点:

    代价在于创建线程和执行期线程上下文切换的开销较大

    多线程常面临锁,状态同步问题

    优点:

    但是能有效提升cpu利用率

node的异步i/o

模型基本要素:事件循环,观察者,请求对象,i/o线程池

node自身其实是多线程的,只是i/o线程使用的cpu较少

  1. 事件循环
  2. 观察者

每个事件循环中有一个或者多个观察者

  1. 请求对象

异步i/o过程中的重要中间产物,所有的状态都保存在这个对象中,包括送入线程池等待执行以及i/o操作完毕后的回调处理

  1. 执行回调

非i/o得异步api

  1. 定时器,setTimeout和setInterval

创建的定时器会被插入到定时器观察者内部的一个红黑树中

每次Tick执行时,会从红黑树中迭代取出定时器对象,检查是否超过定时时间。如果超过,就形成一个时间,它的回调函数将立即执行

时间复杂度O(lg(n)) 2. process.nextTick

将回调函数放入队列,在下一轮Tick时取出执行

时间复杂度 0(1)

事件驱动与高性能服务器

服务器模型:

  • 同步式。一次只能处理一个请求,其他请求都在等待
  • 每进程/每请求。为每个请求启动一个进程,这样可以处理多个请求,但是系统资源只有那么多,所以不具备扩展性
  • 每线程/每请求。为每个请求启动一个线程来处理。当大并发请你去到来时,内存将用光。

node高性能:

  • node通过实践驱动的方式处理请求,无须为每一个请求创建额外的对应线程
  • 省掉创建和销毁线程的开销。
  • 线程少,上线文切换的代价少

异步编程

函数式编程

  1. 高阶函数,将函数作为输入或返回值
  2. 偏函数,创建一个调用另外一部分--参数或变量已预置的函数---的函数的用法。
var toString = Object.prototype.toString;
var isType = function (type) {
    return function (obj) {
        return toString.call(obj) == '[object' + type + ']'
    }
}
var isFunction = isType('Function')

优势

  1. 基于事件驱动的非阻塞i/o模型
  2. 使cpu与i/o并不相互依赖等待
  3. 并行带来的想象空间更大,延展开来是分布式和云

难点

  1. 异常处理

异步i/o提交请求和处理结果两个阶段中间,有事件循环的调度。异步方法则通常在提交请求后立即返回,因为一场并不一定发生在这个阶段,所以try/catch在这里无效

try/catch对于callback执行时抛出的异常无能为力

  1. 回调炼狱
  2. 阻塞代码,由于没有sleep,用setTimeout代替
  3. 多线程编程:web workers和child_process
  4. 异步转同步

异步编程解决方案

  1. 事件发布/订阅模式

    1. 继承events模块
    var events = require('events');
    function Stream () {
        events.EventEmitter.call(this)
    }
    util.inherits(Stream, events.EventEmitter)
    
    1. 利用事件队列解决雪崩问题,once方法

    2. 多异步之间的写作方案

      1. 利用哨兵变量
      2. EventProxy
  2. Promise/Deferred

    1. Promise/A

      • 只有三种状态:rejected,fullfiled, rejected

      • 只能未完成到完成,或者失败,不能逆反

      • 状态不能更改

  3. 流程控制库

    1. 尾触发和next
    2. async的parallel,waterful等方法
    3. step
    4. wind

内存控制

  • js在浏览器的应用场景,由于运行时间短,随着进程的推出,内存会释放,几乎没有内存管理的额必要

  • 内存控制正式在海量请求和长时间运行的前提下进行探讨的。

  • 在服务器端,资源寸土寸金

  • 对于性能敏感的服务器端程序,内存管理的好坏,垃圾回收状况的优良,影响很大

js引擎V8(虚拟机)

内存限制

在node中通过js使用内存时,只能使用部分,无法直接操作大内存对象

64位系统下约为1.4GB,32位系统下约为0.7GB

node中使用js对象,都是通过V8来进行分配和管理的

对象分配

js对象通过堆来分配

当在代码中生命变量并赋值时,所使用对象的内存就分配在堆中。如果已申请的堆空闲内存不够分配新的对象,将继续申请堆内存,直到堆得大小超过V8的限制为止

V8为何限制堆得大小:表层原因是起初为浏览器而设计,限制值已经绰绰有余。深层原因是V8的垃圾回收机制的限制,做一次非增量式的垃圾回收时间花销大

垃圾回收机制

V8垃圾回收策略主要基 分代式垃圾回收机制

垃圾回收算法:

  1. V8的内存分带

将内存分为 新生代老生代

新生代中的对象为存活时间较短的对象,老生代的对象为存活时间较长或常驻内存的对象

  1. Scavenge算法

    • 具体实现主要采用Cheney算法

    • 采用复制的方式实现垃圾回收算法。

    • 将堆内存一分为二。每一份空间成为semispace。处于闲置状态的称为To空间,处于使用状态的称为From空间。

    • 当开始进行垃圾回收时,会检查From空间的存活对象,这些存活对象会被复制到To空间。非存活对象占用空间会被释放

    • 缺点:用空间换时间

    • 当一个对象经过多次复制依然存活时,被认为是生命周期较长的对象。被移到老生代中。称为晋升

    • 对象晋升的条件:

      1. 一个对象经历过Scavenge回收

      通过检查它的内存地址来判断。如果经历过了,从From复制到老生代

      1. To空间的内存占用比超过限制25%

缺点:1. 存活对象较多时,复制存活对象的效率低。 2. 浪费一般空间

  1. Mark-Sweep(标记清除)

    • 遍历堆中的所有对象,标记存活对象。在清除阶段只清除没有被标记的对象。
    • 标记清除后 内存空间出现不连续 的状态,如果需要分配一个大对象,就无法完成
  2. Mark-Compat(标记整理)

    • 对象在标记为死亡后,整理过程中,将活着的对象往一端移动。完成后,直接清理掉边界外的内存

    • 在空间不足以对从新生代晋升过来的对象进行分配时才使用

  3. Incremental Marking

    • 上述基本算法都需要将应用逻辑暂停下来,执行完垃圾回收后再恢复,这种行为成为 全停顿
    • 全堆垃圾回收的标记,清理,整理等动作造成停顿
    • 将一口气完成的标记改为增量标记,拆分成许多小“步进”
  4. 延迟清理和增量清理

  5. 并行标记和并行清理

小结:

  • web服务器的会话实现,一般通过内存来存储,但在访问了大的到时候会导致老生代中的存活对象骤增,不尽造成清理/整理过程费时,还会造成内存紧张,甚至溢出

查看垃圾回收日志

node --trace_gc -e "..."

可以了解垃圾回收的运行状况,找出哪些阶段比较费时

node --prof xx.js

会在该目录下生成v8.log文件,得到性能分析数据

node --prof-process isolate-0x103001200-v8.log

由于日志文件不具备可读性,故这样可以统计日志信息

高效使用内存

  1. 作用域

    • 函数调用,被调用时创建对应作用域,执行结束后作用域摧毁。
    var foo = function () {
        var local = {};
    }
    foo();
    

    内存回收过程:只被局部变量引用的对象存活周期较短,会被分配在新生代的From空间,在作用域释放后,局部变量local失效,引用的对象会在下次垃圾回收时被释放

    • with
    • 全局作用域

标识符查找:

js在执行时回去找该变量在哪里定义,在当前作用域没有查到,将会向上级的作用域里查找,直到查到为止

作用域链:

根据在内部函数可以访问外部函数变量的这种机制,用链式查找决定哪些数据能被内部函数访问。

执行环境:

js为每一个执行环境关联了一个变量对象。环境中定义的所有变量和函数都保存在这个对象中。

变量的主动释放:

全局变量,直到进程退出才释放。引用的对象常驻内存(老生代)。

可以用delete操作和重新赋值(null或者undefined)

  1. 闭包

实现外部作用域访问内部作用域中变量的方法

作用域中产生的内存占用不会得到释放。除非不再有引用,才会逐步释放

内存指标

进程的内存一部分是rss,其余部分在交换区或者文件系统中

$ node
> process.memoryUsage()
{
    rss:  // 常驻内存
    heapTotal: // 总申请的内存量
    heapUsed:  // 使用中的内存量
}
 
> os.totalmem()  // 总内存
> os.freemem()  // 闲置内存

Buffer对象并非通过V8分配,没有堆内存的大小闲置

小结:受V8的垃圾回收限制的主要是V8堆内存

内存泄漏

哪怕一字节的内存泄漏也会造成堆积,垃圾回收过程中将会耗费更多时间进行对象描述,应用响应缓慢,直到进程内存溢出,应用奔溃

原因:

  1. 缓存

缓存中存储的键越多,长期存活对象也就越多,常驻在老生代

普通对象无过期策略

var cached = {};
function get (key) {
    if (cached[key]) {
        return cached[key]
    } else {
        
    }
}
function set (key, value) {
    cached[key] = value;
}

解决:

  • 缓存限制策略

    超过数量,先进先出的方式进行淘汰

    设计模块时,应添加清空队列的相应接口

  • 缓存的解决方案

    进程间无法共享内存

    1. 将缓存转移到外部,减少常驻内存的对象的数量,让垃圾回收更高效
    2. 进程之间可以共享缓存
  1. 队列消费不及时

队列消费速度低于生产速度,将会形成堆积。而js相关作用域也不会得到释放,内存占用不会回落,从而出现内存泄漏

解决方案:

  • 表层:换用消费速度更高的技术
  • 深度:监控队列的长度
  • 任意异步调用都应该包含超时机制
  1. 作用域未释放

大内存应用

node中大多数模块都有stream应用。由于V8内存限制,采用流实现对大文件的操作

如果不需要进行字符串层面的操作,则不需要V8来处理,尝试进行纯粹的Buffer操作

Buffer

特点

  1. Buffer 类的实例类似于 整数数组 ,但 Buffer 的大小是固定的、且在 V8 堆外分配物理内存
  2. Buffer 的大小在被创建时确定,且无法调整。
  3. 性能相关部分由c++实现,非性能相关由js实现

内存分配

  • 在node的c++层面实现内存的申请,在js中分配内存
  • 使用slab分配机制
    • 预先申请,事后分配
    • slab状态:
      1. full,完全分配状态
      2. partial,没有分配诶状态
      3. empty,没有被分配状态
    • 同一个slab可能分配给多个buffer对象
    • 分配大Buffer对象,直接由c++层面提供的内存,而无需细腻的分配操作

乱码

  1. 缓冲器的大小取决于传递给流构造函数的 highWaterMark 选项
const fs = require('fs');
var reader = fs.createReadStream('./text.md', {highWaterMark: 11});
var data = ''
reader.on('data', function (chunk) {
	data += chunk
})
reader.on('end', function () {
	console.log(data)
})
  1. buffer对象的长度为11,可读流要读取很多次才能完成完整的读取
  2. 宽字节字符串可能存在被截断的情况。

解决乱码

  1. 设置编码
var reader = fs.createReadStream('./text.md', {highWaterMark: 11});
render.setEncoding('utf8')

setEncoding的时候,可读流对象在内部设置了一个decoder对象。每次data事件都通过该decoder对象进行Buffer到字符串的解码。

decoder的对象会暂时存储,buffer读取的剩余字节

  1. 将小buffer对象合并
const fs = require('fs');
var reader = fs.createReadStream('./text.md', {highWaterMark: 11});

var chunks = [];
var size = 0;
reader.on('data', function (chunk) {
	chunks.push(chunk);
	size += chunk.length;
})
reader.on('end', function () {
	var buf = Buffer.concat(chunks, size);
	console.log(buf.toString())
})

Buffer与性能

  • 通过预先转换静态内容为Buffer对象,可以有效地减少cpu的重复使用,节省服务器资源
  • highWaterMark值的大小与读取速度的关系:该值越大,读取速度越快

网络编程

前言

在web领域,大多数的编程语言需要专门的web服务器作为容器,如ASP、ASP.NET需要IIS作为服务器,PHP需要打在Apache或Nginx环境等,JSP需要Tomcat服务器等。但对于Node而言,只需要几行代码即可构建服务器,无需额外的容器。

构建TCP服务

  • TCP

    • 面向连接的协议
    • 创建会话的过程,服务端和客户端分别提供一个套接字,共同形成连接。
    • 如果客户端要与另一个TCP服务通信,需要另创建一个套接字来完成连接
  • 创建TCP服务器端

const net = require('net');
let server = net.createServer();
server.on('connection', function (socket) {
    console.log('connection')
}) 
server.listen(8000)
  • TCP服务的事件
    • 服务器事件
      1. listening,在调用server.listen绑定端口或者Domain Socket后出发
      2. connection,每个客户端套接字连接到服务器端时触发,简洁写法为通过net.createServer,最后一个参数传递
      3. close,当服务器关闭时触发。server.close后,服务器将停止接受新的套接字连接
      4. error,当服务器发生异常时触发
    • 连接事件
      1. data,当一端调用write发送数据时,另一端会触发data事件
      2. end,当任意一端发送FIN数据时触发
      3. connect,用于客户端,当套接字与服务的连接成功时触发
      4. drain,当任意一端调用write发送数据时,当前这段会触发者事件
      5. error
      6. close,当套接字完全关闭时,触发
      7. timeout,当连接被闲置时触发

构建UDP服务

UDP不是面向连接的。

一个套接字可以与多个UDP服务通信,它虽然提供面向事务的简单不可靠信息传输服务,在网络差的情况下存在丢包严重的问题

优点:无连接,资源消耗低,处理快速且灵活

应用:音频,视频,dns服务

  • 创建UDP
const dgram = require('dgram');
const server = dgram.createSocket('udp4')
server.on('error', (err) => {
  console.log(`服务器异常:\n${err.stack}`);
  server.close();
});

server.on('message', (msg, rinfo) => {
  console.log(`服务器收到:${msg} 来自 ${rinfo.address}:${rinfo.port}`);
});

server.on('listening', () => {
  const address = server.address();
  console.log(`服务器监听 ${address.address}:${address.port}`);
});
server.bind(1000)
  • UDP套接字事件
    • message,当UDP套接字侦听网卡端口后,接收到消息时触发该事件
    • listening
    • close
    • error

HTTP

特点:

  1. 基于请求响应式,以一问一答的方式实现服务,虽然基于TCP会话,但是本身却并无会话的特点
  2. 浏览器,其实是一个HTTP的代理,用户的行为将会通过它转化为HTTP请求报文发送给服务端,服务端处理请求后,发送响应报文给代理,代理在解析报文后,将用户需要的内容呈现在界面上。
  3. TCP服务以connection为单位进行服务,HTTP服务以request为单位进行服务。http是将connection到request进行了封装
  4. 一旦开始了数据发送,writeHead和setHeader将不再生效。
res.writeHead(()
res.write() // 发送数据
res.end()
  • http服务端事件

    • connection,在http请求前,建立tcp时触发
    • request,当请求数据发送到服务端,在解析出http请求头后触发
    • close,当tcp连接断开
    • checkContinue,和request事件互斥。当客户端在发送较大数据的时候,并不会将数据直接发送,而是先发送一个头部带Expect:100-continue的请求到服务器,这是服务器会触发checkContinue
    • connect, 当客户端发起CONNECT请求时触发,而发起CONNECT请求通常在http代理时出现。
    • upgrade,当客户端要求升级连接的协议时,需要和服务端协商
    • clientError,连接的客户端触发error事件,传递到服务端
  • http客户端

示例:

var req = http.request(options, function (res) {
    res.setEncoding('utf8');
    res.on('data', function (chunk) {
        console.log(chunk)
    })
})
  • http代理

在keepalive的情况下,一个底层会话连接可以多次用于请求。为了重用tcp连接,可以用http.globalAgent客户端代理对象

默认情况下,通过ClientRequest对象对同一个服务器发起的http请求最多可以创建五个连接

如需改变,可在options中传递agent选项

var agent = new http.Agent({
    maxSockets: 10
})
var options = {
    hostname: '127.0.0.1',
    port: 1334,
    path: '/',
    method: 'GET',
    agent: agent
}
  • http客户端事件
    • response:客户端在请求后得到服务端响应时触发
    • socket:当底层连接池中建立的连接分配给当前请求对象时触发
    • connect: 当客户端向浏览器发起CONNECT请求时,如果服务器端响应了200状态码,客户端会触发该事件
    • upgrade,客户端向服务器发起upgrade请求时,如果服务端响应了101 switching protocol状态
    • continue,客户端向服务端发起Expect:100-continue以试图发送大数据量

websocket服务

特点:

  1. 基于事件编程模型(事件驱动)
  2. 长连接
  3. 更接近于传输层协议,分为握手(由http完成)和数据传输两部分

好处:

  1. 客户端与服务端只建立一个TCP连接,可以使用更少的连接
  2. websocket服务端可以推送数据到客户端,比http请求响应模式更灵活,更高效
  3. 更轻量级的协议头,减少数据传送量

构建过程

  1. 握手
  2. 数据传输

握手完成后,不再进行http交互,客户端的onopen将会触发执行

当客户端调用send发送数据时,服务端触发onmessage事件;当服务端调用send发送数据时,客户端触发message事件。

当send发送一条数据时,协议可能将这个数据封装为一帧或多帧数据,然后逐帧发送

网络安全

  1. tls/ssl

交换公钥过程中,可能遇到中间人攻击,所以应引入数字证书来认证。

创建私钥:

openssl genrsa -out ryans-key.pem 2048

生成csr

openssl req -new -sha256 -key ryans-key.pem -out ryans-csr.pem

生成自签名证书

openssl x509 -req -in ryans-csr.pem -signkey ryans-key.pem -out ryans-cert.pem

验证:

const https = require('https');
const fs = require('fs');
const options = {
	key: fs.readFileSync('./ryans-key.pem'),
	cert: fs.readFileSync('./ryans-cert.pem')
}
https.createServer(options, function (req, res) {
	res.writeHead(200);
	res.end('hello world')
}).listen(2000)

-k忽略掉证书的验证

curl -k https://localhost:2000

构建web应用

基础功能

请求方法

HTTP_Parser在解析请求报文的时候,将报文头抽取出来,设置为req.method。有诸如:GET, POST, HEAD, PUT, DELETE, OPTIONS, TRACE, CONNECT

路径解析

路径部分存在于报文的第一行的第二部分,如:

GET /path?foo=bar HTTP/1.1

HTTP_Parser将其解析为req.url, 一般而言,完整的url地址如下

http://user:pass@host.com:8080/p/a/t/h?query=string#hash

这里hash部分会被丢弃,不会存在于报文的任何地方, 下列的url对象不是报文中的,故有hash

解析出来的url对象

Url {
  protocol: 'https:',
  slashes: true,
  auth: 'user:pass',
  host: 'sub.host.com:8080',
  port: '8080',
  hostname: 'sub.host.com',
  hash: '#hash',
  search: '?query=string',
  query: 'query=string',
  pathname: '/p/a/t/h',
  path: '/p/a/t/h?query=string',
  href: 'https://user:pass@sub.host.com:8080/p/a/t/h?query=string#hash' }

查询字符串

查询字符串,如果键出现多次,那么它的值会是一个数组

foo=bar&foo=baz
var query = url.parse(req.url, true).query;
{
    foo: ['bar', 'baz']
}

cookie

cookie处理:

  1. 服务器向客户端发送cookie
  2. 浏览器将cookie保存
  3. 之后每次浏览器都会将cookie发向服务器端

Set-Cookie: name=vale; Path=/;Expires=Sun, 23-Apr-23 09:01:35 GMT; Domain=.domain.com;

path表示cookie影响路径,表示服务器目录下的子html都能访问

expires和max-age表示过期时间,一个是绝对时间,一个是相对时间

httpOnly告知浏览器不能通过document.cookie获取

secure为true表示在https才有效

domain:子域名访问父域名

**性能影响:**大多数cookie并不需要每次都用上,因为这会造成带宽的部分浪费

解决:

  1. 减少cookie体积,设置path和domain
  2. 为不需要cookie的组件换个域名
  3. 减少dns查询

session

session的数据只保留在服务器端,客户端无法修改。

应用:

  1. 基于cookie来实现用户和数据的映射

将口令放在cookie中,口令一旦被褚昂爱,就丢失映射关系。通常session的有效期通常短,过期就将数据删除

一旦服务器检查到用户请求cookie中没有携带session_id,它会为之生成一个值,这个值是唯一且不重复的值,并设定超时时间。如果过期就重新生成,如果没有过期,就更新超时时间

var sessions = {};
var key = 'session_id';
var EXPIRES = 20*60*1000;
var generate  = function () {
	var session = {};
	session.id = (new Date().getTime()) + Math.random();
	session.cookie = {
		expire: (new Date()).getTime() + EXPIRES
	}
	sessions[session.id] = session
}

function (req, res) {
	var id = req.cookies[key];
	if (!id) {
		req.session = generate();
	} else {
		var session = sessions[id];
		if (session) {
			if (session.cookie.expire > new Date().getTime()) {
				session.cookie.expire = new Date().getTime() + EXPIRES;
				req.session = session;
			} else {
				delete sessions[id];
				req.session = generate();
			}
		} else {
			req.session = generate();
		}
	}
}
  1. 通过检查字符串来实现浏览器端和服务器端数据的对应

原理:检查查询字符串,如果没有值,会生成新的带值的url


var getURL = function (_url, key, value) {
	var obj = url.parse(_url, true);
	obj.query[key] = value;
	return url.format(obj);
}

function (req, res) {
	var redirect = function (url) {
		res.setHeader('Location', url);
		res.writeHead(302);
		res.end();
	}
	var id = req.query[key];
	if (!id) {
		var session = generate();
		redirect(getURL(req.url), key, session.id);
	} else {
		var session = sessions[id];
		if (session) {
			if (session.cookie.expire > new Date().getTime()) {
				session.cookie.expire = new Date().getTime() + EXPIRES;
				req.session = session;
				handle(req, res);
			} else {
				delete sessions[id];
				var session = generate();
				redirect(getURL(req.url), key, session.id)
			}
		} else {
			var session = generate();
			redirect(getURL(req.url), key, session.id)
		}
	}
}

隐患

由于session存储在sessions对象中,故在内存中,若数据量加大,会引起垃圾回收的频繁扫描,引起性能问题。

为了利用多核cpu而启动多个进程,用户请求的连接将可能随意分配到各个进程中,node的进程与进程之间不能直接共享内存,用户的session可能会引起错乱

解决方案

将session集中化,将可能分散在多个进程里的数据,统一转移到集中数据存储中。目前常用工具是redis,memcached。node无需在内部维护数据对象。

问题: 会引起网络访问

session与安全

  1. 将口令通过私钥加密,使得伪造的成本较高

缓存

  1. 添加expires或者cache-control到报文头中
  2. 配置etags
  3. 让ajax可缓存

设置last-modified

var handle = function (req, res) {
	fs.stat(filename, function (err, stat) {
		var lastModified = stat.mtime.toUTCString();
		if (lastModified === req.headers['if-modified-since']) {
			res.writeHead(304, 'Not Modified');
			res.end()
		} else {
			fs.readFile(filename, function (err, file) {
				var lastModified = stat.mtime.toUTCString();
				res.setHeader('Last-modified', lastModified);
				res.writeHead(200, 'ok');
				res.end(file);
			})
		}
	})
}

缺陷:

  1. 文件的时间戳改动但内容不一定改动
  2. 时间戳只能精确到秒级别

设置etag


var getHash = function (str) {
	var shasum = crypto.createHash('sha1');
	return shasum.update(str).digest('base64');
}

var handle = function (req, res) {
	fs.readFile(filename, function (err, file) {
		var hash = getHash(file);
		var noneMatch = req['if-none-match'];
		if (hash === noneMath) {
			res.writeHead(304, "Not Modified");
			res.end()
		} else {	
			res.setHeader("ETag", hash);
			res.writeHead(200, "ok");
			res.end(file);
		}
	})
}

强制缓存

var handle = function (req, res) {
	fs.readFile(filename, function (err, file) {
		res.setHeader("Cache-Control", "max-age=" + 10*365*24*60*60*1000);
		res.writeHead(200, "ok");
		res.end(file);
	})
}

用expires可能导致浏览器端与服务器端时间不同步带来的不一致性问题

清除缓存

浏览器是根据url进行缓存,那么一旦内容有所更新时,我们就让浏览器发起新的url请求,使得新内容能够被客户端更新。

数据上传

var hasBody = function (req) {
	return 'transfer-encoding' in req.headers || 'content-length' in req.headers;
}

function (req, res) {
	if (hasBody(req)) {
		var buffers = [];
		req.on('data', functino (chunk) {
			buffers.push(chunk);
		})
		req.on('end', function () {
			req.rawBody = Buffer.concat(buffers).toString(); // 拼接buffer
			handle(req, res);
		})
	} else {
		handle(req, res);
	}
}

处理json格式

// application/json;charset=utf-8;
var mime = function (req) {
	var str = req.headers['content-type'] || '';
	return str.split(';')[0]
}

var handle = function (req, res) {
	if (mime(req) === 'application/json') {
		try {
			req.body = JSON.parse(req.rawBody);
		} catch(e) {
			res.writeHead(400);
			res.end("Invalid JSON");
			return 
		}
	}
	todo(req, res)
}

处理xml文件

var xml2js = require('xml2.js');
var handle = function (req, res) {
	if (mime(req) === 'appliction/xml') {
		xml2js.parseString(req.rawBody, function (err, xml) {
			if (err) {
				res.writeHead(400);
				res.end('Invalid XML');
				return;
			}
			req.body = xml;
			todo(req, res);
		})
	}
}

图片上传

var formidable = require('formidable'),
    http = require('http'),
    util = require('util'),
    fs = require('fs');

http.createServer(function(req, res) {
  if (req.url == '/upload' && req.method.toLowerCase() == 'post') {
    // parse a file upload
    var form = new formidable.IncomingForm();
    form.parse(req, function(err, fields, files) {
    	fs.renameSync(files.upload.path,"./tmp/text.jpeg"); // 另存图片
		res.writeHead(200, {'content-type': 'text/plain'});
		res.write('received upload:\n\n');
		res.end(util.inspect({fields: fields, files: files}));
    });

    return;
  }

  if (req.url == '/')

  // show a file upload form
  res.writeHead(200, {'content-type': 'text/html'});
  res.end(
    '<form action="/upload" enctype="multipart/form-data" method="post">'+
    '<input type="text" name="title"><br>'+
    '<input type="file" name="upload" multiple="multiple"><br>'+
    '<input type="submit" value="Upload">'+
    '</form>'
  );
}).listen(8080);

数据上传与安全

  1. 内存限制

在解析表单,json和xml部分,我们采取的策略是先保存用户提交的所有数据,然后再解析处理,最后才传递给业务逻辑。

弊端:数据量大,占内存

解决方案:

  1. 限制上传内容的大小,一旦超过限制停止接收数据,并相应400状态码
  2. 通过流式解析,将数据导向到磁盘中,node只保存文件路径等小数据

限制大小方案代码:

var bytes = 1024;
function (req, res) {
	var received = 0;
	var len = req.headers['content-length'] ? parseInt(req.headers['content-length'], 10) : null;
	if (len && len > bytes) {
		res.writeHead(413);
		res.end();
		return;
	}

	req.on('data', function (chunk) {
		received += chunk.length;
		if (received > bytes) {
			req.destroy();
		}
	})
	handle(req, res);
}
  1. csrf

var generateRandom = function (len) {
	return crypto.randomBytes(Math.ceil(len*3/4)).toString('base64').slice(0, len);
}

var token = req.session._csrf || (req.session._crsf = generateRandom(24));

// 做页面渲染的时候服务器端渲染这个_csrf
function (req, res) {
    var token = req.session._csrf || (req.session._csrf = generateRandom(24));
    var _csrf = req.body._csrf;
    if (token !== _csrf) {
        res.writeHead(413);
        res.end("禁止访问");
    } else {
        handle(req, res);
    }
    
}

路由解析

文件路径型

  1. 静态文件,其url的路径与网站目录的路径一致,无需转换。
  2. 动态文件,根据路径执行动态脚本,原理: web服务器根据url路径找到对应的文件,如index.asp或者index.php。根据后缀寻找脚本的解析器,并传入http请求的上下文。然而node中无需按这种方式

mvc工作模式

  1. 路由解析,根据url寻找到对应的控制器和行为
  2. 行为调用相关的模型,进行数据操作
  3. 数据操作结束后,调用视图和相关数据进行页面渲染,输出到客户端

手工映射

自由映射,从入口程序中判断url,然后执行对应的逻辑。

匹配的时候,能够正则匹配

自然映射

/controller/action/param1/param2/param3

按约定去找controllers目录下的user文件,将其require出来,调用这个文件模块的setting方法,其余的参数直接传递到这个方法中

RESTful(representational state transfer)

需要区分请求方法

一个地址代表了一个资源,对这个资源的操作,主要体现在http请求方法上,不是体现在url上

设计:

POST,GET,PUT,DELETE

POST /user/add?username=jack
GET /user/remove?username=jack

中间件

含义:指底层封装细节,为上层提供更方便服务的意义,为我们封装所有http请求细节处理的中间件

中间件性能

  1. 编写高效的中间件

缓存需要重复计算的结果,避免不必要的计算。

  1. 合理使用路由,是的不必要的中间件不参与请求处理过程

页面渲染

内容响应

响应头中的content-*字段十分重要。

示例

Content-Encoding:gzip
Content-Length:21170
Content-Type:text/javascript;charfset=utf-8

客户端在接收到后,通过gzip来解码报文体重的内容,用长度校验报文体内容是否正确,然后在以字符集utf-8将解码后的脚本插入到文档节点中

  1. MIME

application/json, application/xml, application/pdf

  1. 附件下载

背景:无论响应的内容是什么MIME,只需要弹出并下载它

Content-Disposition

判断是应该将报文数据当做及时浏览的内容,还是可下载的附件。

inline // 内容只需查看
attachment // 数据可以存为附件

还能指定保存时使用的文件名

Content-Disposition:attachment;filename="filename.txt"

响应附件api

res.sendfile = (filepath) => {
	fs.stat(filepath, (err, stat) => {
		let stream = fs.createReadStream(filepath);
		res.setHeader("Content-Type", mime.lookup(filepath));
		res.setHeader("Content-length", stat.size);
		res.setHeader("Content-Disposition", 'attachment;filename="'+ path.basename(filepath) +'"')
		res.writeHead(200);
		stream.pipe(res);
	})
}
  1. 响应json
res.json = function (json) {
    res.setHeader("Content-Type", "application/json");
    res.writeHead(200);
    res.end(JSON.stringify(json))
}
  1. 响应跳转
res.redirect = function (url) {
    res.setHeader('Location', url);
    res.writeHead(200);
    res.end('redirect to' + url)
}

视图渲染

res.render = function (view, data) {
    res.setHeader("Content-Type", "text/html");
    res.writeHead(200);
    var html = render(view, data);
    res.end(html)
}

模板要素:

  1. 模板语言
  2. 包含模板语言的模板文件
  3. 拥有动态数据的数据对象
  4. 模板引擎
    1. 语法分解
    2. 处理表达式
    3. 生成待执行的语句
    4. 与数据一起执行,生成最终字符串
  5. 模板安全,防止xss,就是转译
function render (str, data) {
    var tpl = str.replace(/<%=([\s\S]+?)%>/g, function (match, code) {
        return "' + obj." + code + "+ '";
    })
    tpl = "var tpl = '" + tpl + "'\nreturn tpl;";
    var compiled = new Function('obj', tpl);
    return compiled(data);
}

集成文件系统

fs.readFile('file/path', 'utf8', function (err, txt) {
    if(err) {
        res.writeHead(500, {'Content-Type': 'text/html'});
        res.end('模板文件错误');
        return;
    }
    res.writeHead(200, {"Content-Type": "text/html"});
    var html = render(compile(text), data);
    res.end(html);
})

这样做每次都需要读取模板文件,因此可设置cache={}

模板性能

  1. 缓存模板文件
  2. 缓存文件编译后的函数

进程

一个进程只能利用一个核,如何充分利用多核cpu服务器

单线程上抛出的异常没有被捕获,如何保证进程的健壮性和稳定性

石器时代:同步

一次只为一个请求服务

青铜时代:复制进程

通过进程的赋值同时服务更多的请求和用户。进程赋值会导致内存浪费

白银时代:多线程

一个线程服务一个请求,线程相对于进程的开销要小,线程之间可以共享数据,内存浪费问题得到解决

但是线程上线文切换会产生时间消耗

黄金时代:事件驱动

解决高并发问题

单线程避免不必要的内存开销和上下文切换

php为每个请求都简历独立的上下文

多线程架构

master.js实现进程的复制

let fork = require('child_process').fork;

let cpus = require('os').cpus();

for (let i = 0; i < cpus.length; i++) {
	fork('./worker.js');
}

worker.js

const http = require('http');
http.createServer((req, res) => {
	res.writeHead(200, {"Content-Type": "text/plain"});
	res.end('hello')
}).listen(parseInt(Math.random()*10000), '127.0.0.1')

ps aux | grep worker.js查看进程的数量

lejunjie          3306   0.0  0.0  4267752    868 s001  S+   11:18上午   0:00.00 grep worker.js
lejunjie          3171   0.0  0.3  4893888  21656 s000  S+   11:18上午   0:00.13 /Users/lejunjie/.nvm/versions/node/v8.11.1/bin/node ./worker.js
lejunjie          3170   0.0  0.3  4893888  21632 s000  S+   11:18上午   0:00.13 /Users/lejunjie/.nvm/versions/node/v8.11.1/bin/node ./worker.js
lejunjie          3169   0.0  0.3  4893888  21708 s000  S+   11:18上午   0:00.13 /Users/lejunjie/.nvm/versions/node/v8.11.1/bin/node ./worker.js
lejunjie          3168   0.0  0.3  4893888  21664 s000  S+   11:18上午   0:00.13 /Users/lejunjie/.nvm/versions/node/v8.11.1/bin/node ./worker.js

通过fork复制的进程都是一个独立的进程,启动多个进程只是为了充分将cpu资源利用起来,而不是为了解决并发问题

创建子进程

  1. spawn,启动一个子进程来执行命令

cp.spawn('node', ['worker.js']);

  1. exec,情动一个子进程来执行命令

sp.exec('node worker.js', () => {})

  1. execFile

启动一个子进程来执行可执行文件

  1. fork

创建node子进程只需要指定要执行的javascript文件模块

进程间通信

主线程与工作线程之间通过onmessage和postMessage进行通信,子进程对象则由send方法实现主进程向子进程发送数据

parent.js

var cp = require('child_process');

var n = cp.fork('./child.js');
n.on('message', function (data) {
	console.log('parent data: ' + data.name);
})
n.send({name: 'parent'})

child.js

process.on('message', function (data) {
	console.log('child: ' + data.name);
})
process.send({name: 'child'})

结果

child: parent
parent data: child

ipc进程间通信(inter-process communication)

node中实现ipc通道的是管道技术,具体由libuv提供

父进程在实际创建子进程之前,会创建ipc通道并监听它,然后才真正创建子进程,并通过环境变量告诉子进程这个ipc通道的文件描述符。

双向通信,在系统内核中完成通信,不用经过实际的网络层

句柄传送

多个进程监听通过端口会抛出EADDRINUSE异常,这是端口被占用的情况。可以通过代理,在代理进程上做适当的负载均衡,使得每个子进程可以较为均衡地执行任务。但是代理进程连接到工作进程的过程需要用掉两个文件描述符

句柄是一种可以用来标识资源的应用,他的内部包含了只想对象的文件描述符。比如句柄可以用来表示一个服务器端socket对象,一个客户端socket对象,一个udp套接字,一个管道等。

发送句柄使得主进程接收到socket请求后,将这个socket直接发给工作进程,而不是重新与工作进程之间建立新的socket连接来转发数据。解决文件描述符的浪费问题

parent.js

const cp = require('child_process');
var child1 = cp.fork('./child.js');
var child2 = cp.fork('./child.js');

var server = require('net').createServer();
server.on('connection', (socket) => {
	socket.end('handled by parent');
})
server.listen(1338, () => {
	child1.send('server', server);
	child2.send('server', server);
})

child.js

process.on('message', (m, server) => {
	if (m === 'server') {
		server.on('connection', function (socket) {
			socket.end('handled by child , pid is' + process.pid);
		})
	}
})

让请求都由子进程处理

parent

const cp = require('child_process');
var child1 = cp.fork('./child.js');
var child2 = cp.fork('./child.js');

var server = require('net').createServer();
server.on('connection', (socket) => {
	socket.end('handled by parent');
})
server.listen(1338, () => {
	child1.send('server', server);
	child2.send('server', server);
	server.close();
})

child

var http = require('http');
var server = http.createServer((req, res) => {
	res.writeHead(200, {"Content-Type": "text/plain"});
	res.end("handled by child, pid is" + process.pid);
})
process.on('message', (m, tcp) => {
	if (m === 'server') {
		tcp.on('connection', function (socket) {
			server.emit('connection', socket);
		})
	}
})

多个子进程可以同时监听相同端口,再没有EADDRINUSE异常发生

总结:

  1. 发送到ipc管道的实际是要发送的句柄文件描述符
  2. 连接了ipc通道的子进程可以读取到父进程发来的消息,将字符串还原成对象,才出发message时间将消息体传递给应用层使用
  3. 并非任意类型的句柄都能在进程之间传递,除非有完整的发送和还原的过程
  4. 多个进程监听同个端口不引起EADDRINUSE异常的原因

独立启动的进程中,tcp服务器端socket套接字的文件描述符并不相同,导致监听到相同的端口时会抛出异常

多个应用监听相同端口时,文件描述符同一时间只能被某一个进程所用,所以是抢占式的

进程事件

  1. error,当子进程无法被复制创建,无法被杀死,无法发送消息时触发
  2. exit,子进程退出时触发
  3. close,在子进程的标准输入输出终止时触发该事件
  4. disconnect,在父进程或子进程中调用disconnect方法时触发

自动重启

进程退出时,让所有工作进程退出。子进程退出时重新create

const cp = require('child_process');

var server = require('net').createServer();

var cpus = require('os').cpus();
var workers = {};
function create () {
	var worker = cp.fork('./child.js');
	worker.on('exit', function () {
		console.log('worker: ' + worker.pid + 'exited');
	})
	worker.send('server', server);
	workers[worker.pid] = worker;
	console.log('create worker pid: ' + worker.pid);
}
for (var i = 0; i < cpus.length; i++) {
	create();
}

process.on('exit', function () {
	for (var pid in workers) {
		workers[pid].kill();
	}
})

在极端情况下,所有工作进程都停止接受新的连接,全出在等待退出的状态。但在等进程完全退出才重启的过程中,所有新来的请求可能存在没有工作进程为新用户服务的情景,这会丢掉大部分请求

因此可在子进程中监听uncaughtException,然后发送自杀信号

process.on('uncaughtException', function (err) {
    process.send({act: 'suicide'});
    worker.close(function () {
        process.exit(1);
    })
})

负载均衡

node默认提供的机制是采用操作系统的抢占式策略。

新的策略是轮叫调度。工作方式是由主进程接受连接,将其一次分发给工作进程。

状态共享

在多个进程之间共享数据

  1. 第三方数据存储

实现同步:子进程向第三方进行定时轮训

  1. 主动通知

主动通知子进程,轮训。

cluster模块

要创建单机node集群,由于有许多细节需要处理,于是引入cluster,解决多核cpu的利用率问题

const cluster = require('cluster');
const http = require('http');
const numCPUs = require('os').cpus().length;

if (cluster.isMaster) {
  console.log(`主进程 ${process.pid} 正在运行`);

  // 衍生工作进程。
  for (let i = 0; i < numCPUs; i++) {
    cluster.fork();
  }
  cluster.on('listening', () => {
    console.log('listening')
  })
  cluster.on('exit', (worker, code, signal) => {
    console.log(`工作进程 ${worker.process.pid} 已退出`);
  });
} else {
  // 工作进程可以共享任何 TCP 连接。
  // 在本例子中,共享的是一个 HTTP 服务器。
  http.createServer((req, res) => {
    res.writeHead(200);
    res.end('你好世界\n');
  }).listen(8000);
  console.log(`工作进程 ${process.pid} 已启动`);
}
process.on('exit', () => {
  console.log('exit')
})

原理:cluster模块就是child_process和net模块的组合应用。在fork子进程时,将socket的文件描述符发送给工作进程。通过so_reuseaddr端口重用,从而实现多个子进程共享端口。

产品化

项目工程化

项目的组织能力

  1. 目录结构
  2. 构建工具
  3. 编码规范
  4. 代码审查

部署流程

代码流程--》stage普通测试环境--》pre-release预发布环境--》product实际生产环境

部署操作

node file.js以启动应用,会站住一个命令行窗口,窗口退出进程也退出

nohup node app.js & 不挂断进程的方式

bash脚本, 解决进程id不容易查找的问题。重启,中断,启动

性能

动静分离:

让node只处理动态请求,将静态文件引导到专业的静态文件服务器。用nginx或者专业的cdn来处理

cdn缓存,将文件放在离用户尽可能近的服务器

对静态请求使用不同的域名或者多个域名还能消除掉不必要的cookie传输和浏览器对下载线程数的限制

启用缓存

提升服务速度,避免不必要的计算

多进程架构

读写分离

对数据库进行主从设计,这样读取数据操作不再受到写入的影响,降低了性能的影响。

日志

写到磁盘上

数据库写入要经历锁表,日志等操作,如果大量访问会排队,进而内存泄露。

  1. 访问日志
  2. 异常日志

监控报警

监控

  1. 日志监控

通过监控异常日志文件的变动,将新增的异常按异常类型和数量反应出来。

监控访问日志,体现业务qps值,pv/uv,预知访问高峰

  1. 响应时间

在nginx类的反向代理上监控

通过应用自行产生的访问日志来监控

  1. 进程监控

检查操作系统中运行的应用进程数,对于采用多进程架构的web应用,就需要检查工作进程的数量,如果低于预估值,就应当发出报警

  1. 磁盘监控

监控磁盘的用量,设置警戒值

  1. 内存监控

健康的内存是有升有降的

  1. cpu占用监控

cpu分为内核态,用户态,iowait等。

用户态占用高: 服务器上应用大量cpu开销

内核态占用高:服务器花费大量时间进程调度或者系统调用。

  1. cpu load监控(cpu平均负载)

描述操作系统当前的繁忙程度

指标过高,在node中可能体现在用子进程模块反复启动新的进程

  1. i/o负载

反应磁盘读写情况

  1. 网络监控

流入流量和流出流量

  1. 应用状态监控

  2. dns监控

报警的实现

  1. 邮件报警
  2. 短信报警