标签:
1.1 Node简介
Node的异步I/O, 在Node中,可以从语言层面很自然的进行并行I/O操作,每个操作直接无需等待之前的I/O调用结束;
事件与回调函数;
Node保持了JavaScript在浏览器中单线程的特点,而且在Node中,JavaScript与其余线程是无法共享热河状态的,单线程的最大好处是不用像多线程编程那样处处在意状态的同步问题,没有死锁的存在,也没有线程上下文交换所带来的性能上的开销;
1.2 模块
JavaScript先天缺乏一项功能: 模块; javascript通过<script> 标签引入代码的方式显得杂乱,不得不使用命名空间等方式人为的约束代码,已达到安全和易用的目的;
CommonJS 规范为JavaScript制订了一个美好的愿景------希望javascript能够在任何地方运行;
CommonJS 构建的这套模块导出和引入机制(require export)使得用户不必考虑变量污染,命名空间等;
在Node中引入模块需要经历3个步骤: 路径分析、文件定位、编译执行; Node中的模块分为两类:核心模块(Node提供的)、文件模块(用户编写的);
1.3
在看node相关,觉得node主要应用于前端比较简单的逻辑,如果业务逻辑过于复杂,回调模式的异步特性,会造成层层回调。然而如果是简单的逻辑(渲染页面),一个页面include多个子页面,Node可以异步,读文件可以并行,,异步的优势就很明显了,真正做到哪个文件先渲染完就先输出显示。
1.4 负载均衡 node中间件
在传统的架构中,负载均衡调度系统每隔一秒钟会对每台服务器 80 端口的特定 URL 发起一次 get 请求,根据返回的 HTTP Status Code 是否为 200 来判断该服务器是否正常工作。如果请求 1s 后超时或者 HTTP Status Code 不为 200,则不将任何流量引入该服务器,避免线上问题。引入 Node.js 之后,相关代码如下:
var http = require(‘http‘);
app.get(‘/status.alexgaoyh‘, function(req, res) {
http.get({
host: ‘127.0.0.1‘,
port: 9999,
path: ‘/status.alexgaoyh‘
}, function(res) {
res.send(res.statusCode);
}).on(‘error‘, function(err) {
logger.error(err);
res.send(404);
});
});
测试过程中,发现 Node.js 在转发这类请求的时候,每六七次就有一次会耗时几秒甚至十几秒才能得到 Java 端的返回。这样会导致负载均衡调度系统认为该服务器发生异常,随即切断流量,但实际上这台服务器是能够正常工作的。
排查一番发现,默认情况下, Node.js 会使用 HTTP Agent 这个类来创建 HTTP 连接,这个类实现了 socket 连接池,每个主机+端口对的连接数默认上限是 5。同时 HTTP Agent 类发起的请求中默认带上了 Connection: Keep-Alive,导致已返回的连接没有及时释放,后面发起的请求只能排队。
最后的解决办法有三种:
1:禁用 HTTP Agent,即在在调用 get 方法时额外添加参数 agent: false ;(可选第一种)
2;设置 http 对象的全局 socket 数量上限: http.globalAgent.maxSockets = 1000;
3:在请求返回的时候及时主动断开连接:
http.get(options, function(res) {
}).on("socket", function (socket) {
socket.emit("agentRemove"); // 监听 socket 事件,在回调中派发 agentRemove 事件
});
http://ued.taobao.org/blog/2014/05/midway-deploy/
1.5 异步I/O
在浏览器中javascript在单线程上执行,而且它还与UI渲染共用一个线程,这就意味着在javascript执行的时候,UI渲染和响应是处于停滞状态的,如果脚本执行时间超过100毫秒,用户就会感受到页面的卡顿,在B/S模型中,网络速度的限制给网页的实时体验造成麻烦,如果网页临时需要获取一个网络资源,通过同步的方式获取,那么javascript则需要等待资源完全从服务器获取后才能继续执行,这期间UI将停顿,不响应用户的交互行为。体检变差,而采取一步的行为,在下载资源的期间,javascript和UI的执行都不会处于等待状态,可以继续响应用户的交互行为,给用户一个鲜明的页面。
在前端获取资源的过程中,如果第一个资源需要T1耗时,第二个资源需要T2耗时……,那么同步的方式需要 T1+T2+……, 异步方式需要MAX(T1,T2,……)。
异步方式,第一个资源的获取并不会堵塞第二个资源,也即第二个资源的请求不依赖于第一个资源的结束,可以享受到并发的优势。
假设因为场景中有一组互不相关的任务需要完成,主流的方式有两种:1、单线程串行依次执行;2、多线程并行执行;,如果创建多线程的开销小于并行执行,则多线程方式首选,但是多线程在创建线程和执行过程中上下文切换的代价较大,同时面临锁,状态同步等问题。单线程在执行任务过程中比较符合编程人员的思维方式,但是任意一个略慢的任务都会导致执行代码堵塞。在计算机资源中,I/O和CPU计算是可以并行执行的,但是同步的编程模式导致问题---I/O的进行会让后续任务等待,造成资源不能更好利用。单线程同步编程模型会因堵塞I/O导致硬件资源不能更好的使用。 在这种情况下,Node给出了他的方案:单线程(远离多线程死锁、状态同步等问题);异步I/O(远离堵塞,更好的利用CPU)。
异步I/O的提出,是期望I/O的调用不再堵塞后续计算,将原有等待I/O完成的这段时间分配给其余需要的业务执行。同时提供类似Web Workers的子进程,搞笑利用多核CPU的特点。
1.6 javascript单线程
JavaScript语言的一大特点就是单线程,也就是说,同一个时间只能做一件事。那么,为什么JavaScript不能有多个线程呢?这样能提高效率啊。 JavaScript的单线程,与它的用途有关。作为浏览器脚本语言,JavaScript的主要用途是与用户互动,以及操作DOM。这决定了它只能是单线程,否则会带来很复杂的同步问题。比如,假定JavaScript同时有两个线程,一个线程在某个DOM节点上添加内容,另一个线程删除了这个节点,这时浏览器应该以哪个线程为准? 所以,为了避免复杂性,从一诞生,JavaScript就是单线程,这已经成了这门语言的核心特征,将来也不会改变。
任务队列
单线程就意味着,所有任务需要排队,前一个任务结束,才会执行后一个任务。如果前一个任务耗时很长,后一个任务就不得不一直等着。如果排队是因为计算量大,CPU忙不过来,倒也算了,但是很多时候CPU是闲着的,因为IO设备(输入输出设备)很慢(比如Ajax操作从网络读取数据),不得不等着结果出来,再往下执行。
JavaScript语言的设计者意识到,这时主线程完全可以不管IO设备,挂起处于等待中的任务,先运行排在后面的任务。等到IO设备返回了结果,再回过头,把挂起的任务继续执行下去。于是,所有任务可以分成两种,一种是同步任务(synchronous),另一种是异步任务(asynchronous)。同步任务指的是,在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务;异步任务指的是,不进入主线程、而进入"任务队列"(task queue)的任务,只有"任务队列"通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行。
具体来说,异步执行的运行机制如下。(同步执行也是如此,因为它可以被视为没有异步任务的异步执行。)
(1)所有同步任务都在主线程上执行,形成一个执行栈(execution context stack)。
(2)主线程之外,还存在一个"任务队列"(task queue)。只要异步任务有了运行结果,就在"任务队列"之中放置一个事件。
(3)一旦"执行栈"中的所有同步任务执行完毕,系统就会读取"任务队列",看看里面有哪些事件。那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行。
(4)主线程不断重复上面的第三步。
1.7 异步编程
1.7.1 高阶编程:
后续传递风格的程序编写将函数的业务重点从返回值转移到了回调函数中:
function foo(x, bar) {
return bar(x);
};
对于相同的foo函数,传入的bar参数不同,则可以得到不同的结果,一个经典的例子便是数组的sort() 方法,可以接受一个方法作为参数参与运算排序:
var points = [40, 100, 1, 5, 25];
points.sort( function(a, b) {
return a-b;
} );
通过改动sort方法的参数,可以决定不同的排序方式。
结合Node提供的最基本的事件模块可以看到,事件的处理方式正是基于这种方式完成的,在自定义事件实例中,通过为相同事件注册不同的回调函数,可以很灵活的处理业务逻辑:
var emitter = new events.EventEmitter();
emitter.on(‘event_foo‘, function() {
// TODO
} );
1.7.2 偏函数
通过指定部分参数来产生一个新的定制函数的形式就是偏函数:
原函数:
var toString = Object.prototype.toString;
var isString = function(obj) {
return toString.call(obj) == ‘[object String]‘;
};
var isFunction = function(obj) {
return toString.call(obj) == ‘[object Function]‘;
};
如果需要定义一系列类似的 isXXX() 的函数,出现很多重复代码,解决重复问题,如下:
新函数:
var isType = function (type) {
return function(obj) {
return toString.call(obj) == ‘[object ‘+type+‘]‘;
};
};
var isString = isType(‘String‘);
vat isFunction = isType(‘Function‘);
1.7.3 Node 基于事件驱动的非堵塞I/O模型
利用事件驱动的方式,javascript线程像一个分配任务和处理结果的大管家,I/O线程池里的各个I/O线程都是小二,兢兢业业的完成分配过来的任务,小二与管家之间互不依赖,可以保持整体的高效率。(利用事件循环的经典调度方式)。模型的缺点在于管家无法承担过多的细节性任务,如果承担过多,会影响到任务的调度,管家忙个不停,小二却得不到活干,造成整体效率降低。Node是为了解决编程模型中堵塞I/O的性能问题的,采用单线程模型,导致更像是一个处理I/O密集的能手。
1.7.4 Promise/Deferred 模式
Deferred 提供了一个抽象的非阻塞的解决方案(如 Ajax 请求的响应),它创建一个 “promise” 对象,其目的是在未来某个时间点返回一个响应。
举一个例子会有助于理解,假设你正在建设一个web应用程序, 它很大程度上依赖第三方api的数据。那么就会面临一个共同的问题:我们无法获悉一个API响应的延迟时间,应用程序的其他部分可能会被阻塞,直到它返回 结果。Deferreds 对这个问题提供了一个更好的解决方案,它是非阻塞的,并且与代码完全解耦 。
Promise/A提议’定义了一个’then‘方法来注册回调,当处理函数返回结果时回调会执行。它返回一个promise的伪代码看起来是这样的:
promise = callToAPI( arg1, arg2, ...);
promise.then(function( futureValue ) {
/* handle futureValue */
});
promise.then(function( futureValue ) {
/* do something else */
});
1.7.5 流程控制库
async 解决‘恶魔金字塔(回调函数多层嵌套)
流程控制: series()方法提供了一组任务的串行执行;parallel()方法提供了并行执行一些异步操作;
Step 接受任意数量的任务,所有的任务都将会串行依次执行;
同时支持parallel()方法执行多个异步任务的并行执行;
1.8 内存控制
Node使用javascript引擎V8, 服务端使用内存时会发现只能使用部分内存(64位系统下1.4G,32位系统下0.7G),这样的限制下,导致Node无法直接操作大内存对象,即使物理内存32GB,也无法将一个2GB的文件读入内存中进行字符串分析处理。
Node提供V8中内存使用量的查看方式 process.memoryUsage();
当我们在代码中声明变量并赋值时,所使用对象的内存就分配在堆中。如果已申请的堆空间内存不够分配新的对象,将继续申请堆内存,直到堆的大小超过V8的限制为止。
V8的垃圾回收策略主要基于分代式垃圾回收机制。在自动垃圾回收的演变过程中,发现没有一种垃圾回收算法能够胜任所有场景,因为在实际的应用中,对象的生存周期长短不一,不同算法只能针对特定情况具有最好的效果,所以,统计学在垃圾回收算法的发展中产生了较大的作用,按对象的存活时间将内存的垃圾回收进行不同的分代,分别针对不同分代的内存选择更高效的算法。V8使用的内存没有办法根据情况自动扩充,当内存分配过程中超过极限值时,就会引起进程出错。
V8采用内存分代处理垃圾回收:
对于新生代中的对象,通过Scavenge算法进行垃圾回收,Scavenge的具体实现中,主要采用了Cheney算法: Cheney算法是一种采用复制的方式实现的垃圾回收算法,将堆内存一分为二,每一部分空间称为semispace,这两个semispace空间中,只有一个处于使用中(From空间),另一个处于闲置状态(To空间),分配对象时,现在From空间中分配,当开始进行垃圾回收时,会检查From空间中的存活对象,存活对象会被复制到To空间中,非存活对象占用的空间会被释放,复制完成后,两个空间角色进行互换。他的缺点是只使用堆空间一般,典型的空间换取时间的算法。
对于老生代的对象,采用Mark-Sweep Mark-Compace相结合的方式进行垃圾回收,Mark-Sweep是标记清除的意思,分为标记和清除两个阶段,标记阶段遍历所以堆中的对象,标记活着的对象,清除阶段则清除没有标记的对象(Mark-Sweep 只清理死亡对象),Mark-Sweep 最大的问题在于进行标记清除回收后,内存空间会出现不连续的状态,这种内存碎片会对后续的内存分配造成问题,为了解决内存碎片的问题,Mark-Compact被提出来,Mark-Compact是标记整理的意思,是在Mark-Sweep的基础上演变的,差别在对象标记为死亡后,在整理的过程中,将活着的对象往一端移动,移动完成后,直接清理掉边界外的内存。由于存在移动对象的情况,V8主要使用Mark-Sweep,在空间不足对新生代晋升过来的对象进行分配时,才使用Mark-Compact。
1.9 闭包
javascript中,实现外部作用域访问内部作用域中变量的方法叫做闭包(closure)。 闭包会引起相关作用域释放的问题。
1.10 Node 内存泄露
1.10.1 慎将内存当做缓存
对象不会被释放,造成内存泄露,加入策略限制缓存无限增长,或者采用进程外缓存(redis, memcached),进程自身不存储状态,不影响自身Node进程的性能。
1.10.2 关注队列状态
生产组-消费者模式中,队列充当中间产物,如果某一个阶段出现问题,会造成堆积,内存溢出,可以健康队列的长度,或者异步调用的超时机制。对于Bigpipe来说,提供超时模式和拒绝模式,防止队列堵塞导致的内存泄露问题。
1.11 大内存应用
Node中,会存在操作大文件的场景,Node提供了stream模块用于处理大文件,fs.createReadStream() fs.createWriteStream() 通过流的方式,不会受到V8内存限制的影响,如果不需要进行字符串层面的操作,则不需要借助V8来处理,可以尝试进行纯粹的Buffer操作,同时也要注意物理内存的限制。
1.12 Buffer对象
Node中,应用需要处理网络协议,操作数据库,处理图片,接收上传文件……,在网络流和文件的操作中,需要处理大量二进制数据,javascript自有的字符串不能满足这些需要,于是Buffer对象应运而生。
1.12.1 Buffer 拼接
正确的拼接方式是用一个数组来存储接收到的所有Buffer片段并记录下所有片段的总长度。
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‘);
console.log(str);
});
Buffer有很多不支持的编码类型,Buffer.isEncoding(encoding); iconv和 iconv-lite两个模块可以支持更多的编码类型转换。
1.12.2 Buffer性能
Buffer在文件I/O和网络I/O中运用广泛,通过预先转换静态内容为Buffer对象(helloworld = new Buffer(helloworld)) ,转换为Buffre对象转换在进行数据传输,可以有效地减少CPU重复使用,节省服务器资源。
文件读取的过程中,highWaterMark的设置对性能影响很大,读取一个相同的大文件时,highWaterMark值的大小与读取速度成正比。
字符串与Buffer之间有实质上的差异,即Buffer是二进制数据,字符串与Buffer之间存在编码关系。
1.13 HTTP服务
Node中,HTTP服务继承自TCP服务器,能够与多个客户端保持连接,由于采用事件驱动的形式,并不为每个连接创建额外的线程和进程,保持很低的内存占用,能实现高并发。
1.13.1 WebSocket服务
客户端与服务器端只建立一个TCP连接,可以使用更少的连接;
WebSocket服务器端可以推送数据到客户端, 灵活高效;
在WebSocket之前,网页客户端与服务器端能够通信最高效的是Comet技术,采用长轮询(long-polling)或 iframe流。使用WebSocket的话,网页客户端只需一个TCP连接即可完成双向通信,在服务器端和客户端频繁通信时,无需频繁断开连接和重发请求,连接可以高效应用。
1.14 构建Web 应用
1.14.1 cookie
由于cookie的实现机制,一旦服务器端向客户端发送了设置cookie的意图,除非cookie过期,否则客户端每次请求都会发送cookie到服务器端,一旦cookie设置过多,导致报头较大,浪费带宽。
1.14.1.1 减少cookie大小
静态文件的业务定位几乎不关心状态,也就不需要cookie,所以在相关设计方面可以限定相关的域,只有域名相同时才会发送。
1.14.1.2 为静态组件使用不同的域
为不需要cookie的组件换个域名减少无效cookie的传输,所以很多网站的静态文件会有特殊的域名,使业务相关的cookie不再影响静态资源,同时可以突破浏览器下载线程数量的限制。
目前,广告和在线统计领域最依赖的就是cookie,通过嵌入第三方广告或者统计脚本,将cookie和当前页面绑定,就可以标识用户,得到用户的浏览行为,广告商就可以定向投放广告,如果担心自己站点的用户被记录下行为,那就不要挂任何第三方脚本。
1.14.2 session
session存储在服务器端,数据的安全性就可以得到保障,但是如何将每个客户和服务器中的数据一一对应起来:
1.14.2.1 基于cookie实现用户和数据的映射
虽然将所有数据防止cookie不可取,但是将口令放在cookie还是可以的,口令一旦被篡改,就丢失了映射关系,一旦服务器启用了session,将约定一个键值作为session的口令(可以随意约定),比如Connect默认采用connect_uid,Tomcat采用jsessionid,一旦服务器检查到用户请求Cookie没有携带这个值,就会为之生存一个值,唯一不重复的值,并设定超时时间。
1.14.3 缓存
为了提高性能,USlow中提到几条关于缓存的规则 添加Expires、Cache-Control到报文头中;ETags;Ajax可缓存。通常来说,POST DELETE PUT这类带行为性的请求操作一般不做任何缓存,大多数缓存只应用在GET请求中。
简单来讲,本地没有文件时,浏览器必然会请求服务器端的内容,并将这部分内容放置在本地的某个缓存目录中,第二次请求时,对本地文件进行检查,如果不能确定这份本地文件是否可以直接使用,则发起一次条件请求(在普通的GET请求报文中,附带If-Modified-Since字段),询问服务器端是否有更新的版本,本地文件的最后修改时间,如果没有新版本,相应304状态码,如果有新版本,将新版本的内容发送给客户端。
尽管条件请求可以在文件内容没有修改的情况下节省带宽,但是仍然会发起一个HTTP请求,客户端仍然需要花一定时间等待相应。最好的情况是客户端连条件请求都不发起,解决方案是服务器端在响应内容时,让浏览器明确的将内容缓存起来,设置Expires/Cache-Control头。
大体来说,根据文件内容的hash值进行缓存淘汰会更高效,因为文件内容不一定随着版本的更新而更新。
1.14.4 数据上传与安全
内存限制: 限制上传内容的大小,超出返回400响应码; 流式解析,数据量导入磁盘,Node只保存文件路径等小数据。
CSRF 攻击,Cross-Site Request Forgery 跨站请求伪造。解决方案可以添加相应的token,添加随机值的方案,每个请求,session中赋予一个随机值,并告知前端,验证时进行匹配操作。
1.15 页面渲染
模板技术,与MVC中的数据、逻辑、视图分离如出一辙,更与前端HTML、CSS、javascript分离的设计理念一致。随着Node的出现,模板能在前后端共用是一件寻常不过的事情,最知名的EJS(APS PHP JSP风格的模板引擎)、Jade(Python ruby风格的模板引擎)等。
Bigpipe是Facebook的前端加载技术,为了解决重数据页面的加载速度问题,解决思路是将页面分割成多个部分,先向用户输出没有数据的布局(框架),将每个部分逐步输出到前端,最终渲染天成框架,完成页面渲染,提高用户体验。Bigpipe将网页布局和数据渲染分离,使得用户在视觉上觉得网页提前渲染好了,随着数据输出的过程逐步渲染页面,使得用户能够感知页面是活的,比一开始给出空白页面,然后在某个时候突然渲染好带给用户的体验更好。
前端成熟的Web框架 Connect Express……。
var express = require(‘express‘);
var app = express();
app.use(express.bodyParser());
app.all(‘/‘, function(req, res){
res.send(req.body.title + req.body.text);
}); // all 表示get,post等任何一种请求方式,当然也可以指定为某种特定的请求方式。
app.listen(3000);
app.use 就是引入一个所谓的中间件,其实就是用来再实际请求发生之前hack req和res对象来实现一些功能,比如果最简单的logger就是在res的end事件上添加监听写入一条日志记录
var logger = require(‘morgan‘);
app.use(logger());
app.use(express.static(__dirname + ‘/public‘));
app.use(function(req, res){
res.send(‘Hello‘);
});
1.16 进程
Apache采用多线程/多进程 模型实现的,当并发增长到上万时,内存耗用的问题将会暴露出来,著名的C10K问题,为了解决高并发的问题,基于事件驱动的服务模型出现了,像Node和Nginx均是基于事件驱动的方式实现的,采用单线程避免了不必要的内存开销和上下文切换开销。由于所有处理都在单线程上进行,影响事件驱动服务模型性能的点在于CPU的计算能力,他的上限决定这类服务模型的性能上限,不受多进程或多线程模式中资源上限的影响,伸缩性比较高,解决多喝CPU的利用问题的话,带来的性能上的提升是客观的。
面对单进程多线程对多核使用不足的问题,可以启动多进程处理,每个进程各自利用一个CPU,实现多核CPU的利用,Node提供了child_process模块,提供child_process.fork()函数实现进程复制。进程分为两种,主进程和工作进程,典型的分布式架构中用于处理并行业务的模式,具备较好的可伸缩性和稳定性,主进程不负责处理具体的业务,而是负责调度和管理工作进程,趋向于稳定的,工作进程负责具体的业务处理。通过fork()复制的进程都是一个独立的进程,这个进程中有着独立全新的V8实例,需要至少30毫秒的启动时间和10MB+的内存,使每个CPU内核都使用上,注意这里启动多个进程只是为了充分将CPU利用起来,不是为了解决并发问题。
针对多核CPU环境下,多个服务(进程)都监听同一个端口的情况,可以传递句柄,在启动独立进程期间,并不知道文件描述符的存在,所以监听相同端口时就会失败,但对于send()发送的句柄还原出来的服务而言,文件描述符是想同的,所以监听相同的端口不会引起异常,当网络请求向服务端发送时,只有一个幸运的进程能够抢到连接,只有他能够为这个请求进行服务,这些进程间的服务室抢占式的。
child_process模块中,通过此模块构建强大的单机集群,针对这个问题,Node v.08之后,就引入了Cluster模块,用以解决多核CPU利用率的问题,并且提供了较为完善的API,处理进程的健壮性问题。
Node 基础 备注
标签:
原文地址:http://my.oschina.net/alexgaoyh/blog/419955