Author:
bugall
Wechat:bugallF
Email:769088641@qq.com
Github: https://github.com/bugall
一. 说好的单线程异步呢?
众所周知Node是单线程异步,其实这个是相对于Node这层来说是没有问题的。但是如果整体来看其实还是有个thread_pool的概念,我更喜欢把Node看做一个胶水层,把libuv与v8粘合在一起。v8作为js执行的引擎,libuv封装了一些c++代码来实现一些内核调用,同时把同类功能接口做抽象,满足跨平台的需求。我觉得写Node有两条主线,一个是对js语法的使用和理解比如[1]==[1] //false
, 另外一条主线就是对libuv的理解,因为libuv中的event_loop实现的机制,也是Node在某些场景性能出色的原因。
二. 什么是IO多路复用(max-Multiplexing)
通常我们如果在创建一个socket请求的时候,内核会为这个socket创建一个标示,我们通常称为文件描述符(file descriptor),文件描述符其实是一个索引值,索引值对应的实际存储是一个关于文件的一些元数据的数据结构,比如这个文件的操作权限,创建时间,文件是否可读可写等,总而言之这个结构里存储了一个文件的所有源信息(metadata)。假如我们现在有10个socket请求进来,首先我们内核要分配10个文件描述符,那这些被创建的文件描述符一定要有个地方存储才行,在linux有一个文件描述符表是用来管理这些文件描述符信息的,为了方便理解我们就把这个文件描述符表想象成一个数组(这里需要补充说明下,通常情况硬件相对于内核来说是异步的),这时候我们想知道哪些socket有数据到达?比较笨的方法就是枚举每个文件描述符对应的源数据,然后查看他们对应状态,如果可读我们就可以从对应的buffer中读取数据,但是这有一个问题,假如我们的socket请求线性增长,那么遍历一次数组的时间也会跟着线性增长,这种就是poll
,select
的实现。后来为了解决这个问题有了epoll
三. 为什么epoll的效率高
这里补充一个点,epoll的IO多路复用与iocp的IO多路复用虽然功能看似相同,但是底层的实现原理截然不同,epoll之所以高效要归功于linux内核的基于事件的实现机制,比如当网卡在加载数据的时候是不会占用cpu的,因为硬件对于内核实现来说是异步的,当网卡在加载完数据后会发送一个中断
给内核,假如你代表一个线程,你在看电视的时候,你妈妈在厨房做饭,当饭做好的时候妈妈会说:“饭好了吃饭了”。那你现在可以选择是过去吃饭还是继续看电视,至少饭做好了这个事件你是知道的,等你看完电视的时候你会想到还有吃饭这个事情没完成。对于我们的应用来说可能会有很多个事件会在将来的某个时间发生,我们需要把这些事件存储起来,一个一个处理。而windows的内核实现并不是基于事件的所以iocp采用的是多线程轮训的方式实现的IO多路复用,所以这也是为什么不建议用windows跑Node服务。
epoll中主要有三个方法epoll_create
,epoll_ctl
,epoll_wait
. create就是创建一个epoll实例,ctl就是准备让epoll监听哪个文件描述符上的哪些事件,wait获取被激活的事件。按照上面的例子来理解:create就相当于我现在要准备看电视了,准备好本子记录将来要发生时的事情和未处理的事情,ctl就是在本子上记录:当妈妈在做饭的时候,如果妈妈做好了这个事件发生请告诉我,wait就是看下哪些事件被激活,看看妈妈饭有没有做好。epoll_wait中存储的那个被激活的事件列表就是我们常说的event_pool
四. 既然有了IO多路复用为什么还需要thread_pool
上面说了,在libuv中文件操作和DNS解析都是用线程池实现的,接下来我们看下原因
1. 为什么文件操作要用thread_pool
刚开始我也很郁闷这个问题,也在stackoverflow上发了帖子,本身是想刨根问底的从技术实现找到答案的,但是这个没有明确的答案和文档。我自己的想法就是: 其实磁盘IO大多数时间是花在磁盘寻道上,一旦开始读取数据读写量会很大,不像socket网络延迟高,每次收到的IP分段后的数据包小,当buffer被写满后才会通知内核处理,所以socket是可以用epoll来实现的。但是文件一旦开始读取,那么buffer瞬间会被写满,epoll中的被激活的事件列表中一直会有这个文件可读这个事件。如果是一个非常大的文件的话会造成epoll的event_pool阻塞。
2. 为什么DNS解析也要用thread_pool
通常我们在发送http请求的时候首先需要对host进行解析获取域名对应的ip的地址,这样我们才能真正的发送请求。所以说这个过程一定是个同步的过程,因为在DNS解析没有成功的时候发送http请求是没有意义的。在http中默认调用的是dns.lookup方法。这个方法会去读取/etc/hosts
这个文件,我想然后查本地的DNS缓存,如果都没有然后再发请求向DNS解析服务器查询。这个查询有个超时时间,如果dns解析失败那么这个请求也就失败了。
因为我负责的项目每天有3000w的外网http请求和3亿次的文件读写(用Node写我也是醉了)。每天凌晨docker重启容器的时候会造成本地缓存的DNS失效,然后瞬间大量的外网http请求需要走DNS解析,Node默认设置libuv的thread_pool是3,结果在重启完成的几分钟内会有很多报错。后来我把线程调整到128明显好很多。
五. Node使用多进程的意义大不大
这个问题我现在也没有明确的认知和测试结果。后期我会尽快完成。
可以肯定的是,多进程对epoll没有影响。
对thread_poll更没有多大影响,因为使用thread_poll的场景是文件操作与DNS解析这两个
因为文件操作的性能取决于磁盘性能,就算你有16核16个进程,当磁盘到达性能阈值值的时候再多核也没有意义。
唯一能提升性能的地方就是主线程。也就是v8执行这里,因为我们从event_loop中得到的被激活的事件是为了做一些事件对应的逻辑也就是我们所说的callback,我们会把这些被激活的事件对应的callback放到一个叫做任务队列的地方中去,主线程处理的就是任务队列的东西,假如我们把一个任务队列需要处理的code分散到4个进程和4个任务队列中去性能是有提高的。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。