{"action":"created","issue":{"url":"https://api.github.com/repos/gf-rd/blog/issues/31","repository_url":"https://api.github.com/repos/gf-rd/blog","labels_url":"https://api.github.com/repos/gf-rd/blog/issues/31/labels{/name}","comments_url":"https://api.github.com/repos/gf-rd/blog/issues/31/comments","events_url":"https://api.github.com/repos/gf-rd/blog/issues/31/events","html_url":"https://github.com/gf-rd/blog/issues/31","id":135663247,"number":31,"title":"[Node] - Node异步那些事","user":{"login":"lizhonghui","id":2040608,"avatar_url":"https://avatars.githubusercontent.com/u/2040608?v=3","gravatar_id":"","url":"https://api.github.com/users/lizhonghui","html_url":"https://github.com/lizhonghui","followers_url":"https://api.github.com/users/lizhonghui/followers","following_url":"https://api.github.com/users/lizhonghui/following{/other_user}","gists_url":"https://api.github.com/users/lizhonghui/gists{/gist_id}","starred_url":"https://api.github.com/users/lizhonghui/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/lizhonghui/subscriptions","organizations_url":"https://api.github.com/users/lizhonghui/orgs","repos_url":"https://api.github.com/users/lizhonghui/repos","events_url":"https://api.github.com/users/lizhonghui/events{/privacy}","received_events_url":"https://api.github.com/users/lizhonghui/received_events","type":"User","site_admin":false},"labels":[],"state":"open","locked":false,"assignee":null,"milestone":null,"comments":1,"created_at":"2016-02-23T07:50:22Z","updated_at":"2016-03-03T23:13:58Z","closed_at":null,"body":"在离开前公司之前产品的技术架构陆续地迁移到Node，在这段时间里有机会使用Node投入到实际的生产项目中，来到新公司之后跟团队的高手们学习了半年的Angular和Ionic，这半年时间也没有太多Node的实际开发经验，不过近段时间团队开始折腾Node，所以把之前的Node的经验沉淀下来，希望能为团队的Node技术积淀点滴。\r\n\r\n记得当时使用Node版本为0.10.x，平滑更新、负载均衡、异常重启等都得自己实现，不过这些问题随着Node的发展已经有很多强大的工具帮忙解决。不管Node怎么更新迭代，事件驱动、异步IO始终是Node的基本，这也是本文想跟大家一起探讨的东西。\r\n\r\n## Node层次结构\r\n要探讨Node的异步流程，先简要描述一下Node的层次结构。\r\n\r\n![Node structure](https://raw.githubusercontent.com/lizhonghui/blog/master/assets/nodejs/node-structure.png)\r\n\r\n可以看出Node主要分为3层结构：\r\n1. JS应用层。我们编写的应用主要在这一层，包含Node提供的标准库的JS部分（如`http`，`net`，`fs`等）。\r\n2. V8引擎。Node精简了原来运行在Chrome里的V8，移除了与浏览器相关的模块（如DOM操作辑等），并对其进行优化，基于Node API层提供的API，新增了一些新模块，如IO和网络处理，Buffer类等，使其能独立于浏览器环境，并适用于后台服务。\r\n3. Node API层。包含`libuv`异步IO库，`c-ares`异步DNS库等。\r\n\r\n大致的执行流程是：\r\n1. V8引擎解释执行JS脚本；\r\n2. V8在执行过程中异步调用Node API完成复杂任务（如IO操作，加密解密操作等）；\r\n3. Node API将执行结果返回给V8引擎。\r\n\r\n其中在Node API层里面，[`libuv`](https://github.com/libuv/libuv)是实现异步的一个核心组件。\r\n\r\n## libuv\r\n`libuv`是Node的一个关键组成部分，内部封装了网络操作库，进程管理以及IO组件，是一个跨平台的工具集，为上层提供了统一的API调用，使其不用考虑平台差异，隐藏了底层实现。\r\n\r\n<img src=\"http://docs.libuv.org/en/v1.x/_images/architecture.png\" style=\"width:100%\">\r\n\r\nJs是单线程执行的，运行过程产生的所有异步任务，通过层层调用，最终传送到libuv，libuv是统筹这些异步任务的大管家，借用一个大致流程图来说明。\r\n\r\n<img src=\"http://image.beekka.com/blog/2014/bg2014100803.png\" style=\"width:100%\">\r\n\r\n从流程图可知，阻塞任务都会分配至其他线程或其他模块里执行，而并不影响JS执行线程的运作，其他线程或模块执行完毕后会标记对象状态，然后通过`Event Loop`（类似于一个`while true`的无限循环）遍历来执行对象里的回调函数。上图的这个流程比较粗糙，实际上libuv处理不同的异步任务并不尽相同，我们结合下面的例子详说。\r\n\r\n```Javascript\r\nvar http = require('http');\r\nvar fs = require('fs');\r\n\r\nhttp.createServer((res) => {\r\n    console.log('network request occur')\r\n}).listen(8080);\r\nfs.readFile('path/to/file', (err, data) => {\r\n    if(err) throw err;\r\n    console.log('file data is ready!');\r\n});\r\nsetTimeout(() => {\r\n    console.log('delay operation')\r\n}, 1000);\r\n\r\nconsole.log('other operation.')\r\n```\r\n\r\nV8执行上面的代码，代码里面产生了3种不同类型的异步任务，分别是网络IO，文件IO以及延时任务。我们都知道这三种异步类型的接口调用完毕会立即返回，复杂的异步操作都交给了libuv线程处理，所以运行结果里首先出现的是『other operation』。\r\n\r\n1. `http.createServer`执行完后，libuv会监听来自8080端口的网络请求，当有新请求接入时候，会创建对应的FD（File descriptor），接着libuv着手处理这个FD的数据状态。实际上libuv在处理这些网络IO时候并没有另起单独线程，这些操作都是在Event Loop线程里完成，处理过程是使用`epoll`技术（一种高性能的IO复用技术，OSX和BSD是`kqueue`）实现。首先调用`epoll_create`监听这个FD（file descriptor）状态，调用完毕后立即返回，不会阻塞主线程继续执行，数据处理完毕后即会标记这个FD为就绪状态，在Event  Loop的循环里会调用`epoll_wait`获取这些已经就绪的FD并执行其回调函数。`epoll`内部也是基于事件回调机制，同时还有很多提升性能的特性，得益于`epoll`Node在处理网络IO方面具有极高的并发能力。\r\n2. `fs.readFile`的执行会涉及到IO线程池的调用，跟网络IO处理方案不尽相同，libuv处理文件IO是在其他单独的线程里完成的。当调用`fs.readFile`操作时，libuv会生成一个请求对象（包含文件路径，读写参数，回调函数等），这个请求对象会被发送至IO线程池，此时`readFile`调用完毕立即返回，主线程继续执行后面的代码。当线程池处理完毕后会把数据写入到请求对象里，同时把回调函数插入到一个回调队列等待执行，Event Loop每一次循环都会适时执行这个队列里头的回调函数，实现类似异步的效果。\r\n\r\n3. `setTimeout`执行完后，这个延时任务会被插入至一颗红黑树，Event Loop的每次循环都会检查这颗红黑树，若时间到期了即执行回调函数。\r\n\r\n### 为何处理文件IO用线程池实现？\r\n实际上早在2.6.22的内核版本已经提供了内核级别的异步IO接口，不过其设计并不完美，不能有效利用系统缓存，而且还有系统版本兼容问题，libuv选用了一种相对稳定且兼容的方案，就是利用线程池模拟异步IO操作。线程内采用同步IO读写文件，Node启动时线程池默认开启4条线程，不过可以在启动时设置`UV_THREADPOOL_SIZE`来改变，不过，最大也只能是 128，因为这个是涉及到内存占用，原因[戳这里](https://github.com/libuv/libuv/blob/v1.x/docs/src/threadpool.rst)。线程池是全局的，每个Node进程共用。除了文件IO使用线程池，DNS函数（`getaddrinfo`和`getnameinfo`）也会用到线程池。\r\n\r\n### 关于Windows的异步接口处理\r\n上述的流程是linux平台选用的方案，在Window平台libuv处理异步的方式并不完全一样，Windows系统提供了另一套异步IO方案`IOCP`，从调用者的角度来说`IOCP`提供了相对完美的系统级异步IO接口，实际上内里也是通过多线程方式模拟，只是线程是由系统管理。\r\n\r\n> libuv在编译安装阶段已经对平台进行选择，并不是在运行阶段。\r\n\r\n### Event Loop\r\nEvent Loop的每次循环都会检查各异步任务的状态，但其中不同类型的异步是具有不同的执行优先级的，弄清楚这个对于我们写Node逻辑代码会有很大的帮助，我们继续往下看。\r\n\r\n![event loop tick](https://raw.githubusercontent.com/lizhonghui/blog/master/assets/nodejs/event-loop-tick.png)\r\n\r\n> 更详细流程可查看`libuv`源码`src/unix/core.c`的`uv_run`函数，[戳这里](https://github.com/libuv/libuv/blob/c0fa2e7518a3a0e364c56f8223bdd0f549ddac66/src/unix/core.c)。\r\n\r\nEvent Loop的每一次循环称为一次Tick，从上图可以看出每一Tick都会依照优先级执行不同的异步任务。Timer任务的优先级最高，不过前提是Timer里的任务时间已到期。到`IO polling`阶段会检查IO事件是否就绪，若就绪则按照一定的调度规则执行其回调函数，有兴趣请[戳这里](http://docs.libuv.org/en/v1.x/design.html)了解更多。\r\n\r\n说到Event Loop Tick的任务优先级，不得不提`process.nextTick`和`setImmediate`这两个重要API，这两个API跟`setTimeout`功能相似，都是延迟任务处理，但内里的实现完全不一样，性能和执行时机都有很大的差异。\r\n\r\n1. 首先，`process.nextTick`绑定的回调函数会存放在一个队列里，并且会在当前的Tick里适时执行完毕，而不会延后到下一次Tick里（跟他的名字并不一致），因此`process.nextTick`的任务会阻塞下一个Tick的任务，从某种程度来说优先级是最高的，正是由于此，`process.nextTick`不能出现嵌套，否则会阻塞掉整个Event Loop，不过实际情况Node会预判这种嵌套并抛出错误。\r\n2. `setImmediate`的回调函数也会存放到一个队列里面，不过执行时机是在下一次Tick里的`#7 run check handles`阶段，后于IO处理，同时也是一次性执行完毕，并不会再延后到下一个Tick。\r\n3. `setTimeout(cb, 0)`的回调函数也并非在本次Tick里完成，但与`setImmediate`相比二者谁先谁后不太好确定，IO事件的介入会影响二者的执行顺序，github上也有讨论[戳这里](https://github.com/nodejs/node-v0.x-archive/issues/25788)。\r\n\r\n`setTimeout`需要创建定时器对象和迭代等操作，任务的执行需要操作红黑树，时间复杂度为o(lg(n))，而`process.nextTick`和`setImmediate`时间复杂度为o(1)，因此性能更高。\r\n\r\n\r\n## 总结\r\nNode是一个庞大的系统，本文从异步实现的角度去剖析Node的内部组件，在写书之际抽空沉淀下来，时间有限写得粗糙，难免有错漏，欢迎大家留言讨论。\r\n\r\n\r\n## Reference\r\n* http://www.infoq.com/cn/articles/nodejs-asynchronous-io/\r\n* http://blog.csdn.net/ajian005/article/details/18183761\r\n* http://docs.libuv.org/en/v1.x/design.html\r\n* http://taobaofed.org/blog/2015/10/29/deep-into-node-1/\r\n* http://baudehlo.com/2013/02/14/how-an-event-loop-works/\r\n* http://www.ruanyifeng.com/blog/2014/10/event-loop.html"},"comment":{"url":"https://api.github.com/repos/gf-rd/blog/issues/comments/192012874","html_url":"https://github.com/gf-rd/blog/issues/31#issuecomment-192012874","issue_url":"https://api.github.com/repos/gf-rd/blog/issues/31","id":192012874,"user":{"login":"renaesop","id":5894477,"avatar_url":"https://avatars.githubusercontent.com/u/5894477?v=3","gravatar_id":"","url":"https://api.github.com/users/renaesop","html_url":"https://github.com/renaesop","followers_url":"https://api.github.com/users/renaesop/followers","following_url":"https://api.github.com/users/renaesop/following{/other_user}","gists_url":"https://api.github.com/users/renaesop/gists{/gist_id}","starred_url":"https://api.github.com/users/renaesop/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/renaesop/subscriptions","organizations_url":"https://api.github.com/users/renaesop/orgs","repos_url":"https://api.github.com/users/renaesop/repos","events_url":"https://api.github.com/users/renaesop/events{/privacy}","received_events_url":"https://api.github.com/users/renaesop/received_events","type":"User","site_admin":false},"created_at":"2016-03-03T23:13:58Z","updated_at":"2016-03-03T23:13:58Z","body":"话说timer部分，看libuv用的好像是小根堆，不是红黑树啊"}}