libuv 选型

linux native aio

Linux native aio 有两种API,一种是libaio提供的API,一种是利用系统调用封装成的API,后者使用的较多,因为不需要额外的库且简单。

  • io_setup : 是用来设置一个异步请求的上下文,第一个参数是请求事件的个数,第二个参数唯一标识一个异步请求。
  • io_commit: 是用来提交一个异步io请求的,在提交之前,需要设置一下结构体iocb
  • io_getevents: 用来获取完成的io事件,参数min_nr是事件个数的的最小值,nr是事件个数的最大值,如果没有足够的事件发生,该函数会阻塞。
  • io_destroy:在所有时间处理完之后,调用此函数销毁异步io请求。

限制

aio只能使用于常规的文件IO,不能使用于socket,管道等IO,但对于 libuv 的 fs 模块使用需求已经足够了。

io_getevents在调用之后会阻塞直到有足够的事件发生,因此要实现真正的异步IO,需要借助eventfd和epoll达到目的。

libuv native aio 实现

笔者实现过一个基于 libuv 的 native aio,https://github.com/yjhjstz/libuv/commit/2748728635c4f74d6f27524fd36e680a88e4f04a

从理论上看,在libuv中实现AIO,

  • 其一:比原来的libuv实现少了一次write系统调用,无需在用户态实现线程池和工作队列.
  • 其二:native aio实现可以实现批量回调。

我们看下性能对比数据, 测试脚本是简单的文件读取:

  • Threadpool 模型

    [email protected]:~/workspace/libuv$ wrk -t4 -c100 -d30s http://127.0.0.1:30003/
    Running 30s test @ http://127.0.0.1:30003/
    4 threads and 100 connections
    Thread Stats Avg Stdev Max +/- Stdev
    Latency 16.77ms 1.14ms 31.68ms 86.68%
    Req/Sec 1.51k 162.66 2.08k 81.34%
    178925 requests in 30.00s, 104.26MB read
    Requests/sec: 5963.45
    Transfer/sec: 3.47MB
    
  • Native AIO 模型

    [email protected]:~/workspace/libuv$ wrk -t4 -c100 -d30s http://127.0.0.1:30003/
    Running 30s test @ http://127.0.0.1:30003/
    4 threads and 100 connections
    Thread Stats Avg Stdev Max +/- Stdev
    Latency 16.22ms 0.95ms 26.39ms 88.12%
    Req/Sec 1.57k 191.14 2.08k 68.50%
    185084 requests in 30.00s, 107.85MB read
    Requests/sec: 6169.28
    Transfer/sec: 3.59MB
    

Max Latency减小16%,tps提升3%。

Threadpool 模型

我们先看下一次 node.js read 的调用示意图:

代码的运行经历了以下步骤:

  • 1 node, libuv 初始化;

  • 2 node_file.cc中的Read方法调用libuv(fs.c)的 uv_fs_read , 封装请求;

  • 3 libuv 将请求封装成 uv_work, 提交到任务队列尾部,触发信号;

  • 4 此时主线程的read调用返回。

  • 5 线程池从uv_work队列中取出一个请求,开始执行read IO;

  • 6 向主线程发送信号表明任务完成,等待执行read调用后的其它操作。

  • 7 主线程 epoll,从响应队列取已经完成的请求;

  • 8 主线程响应 epoll事件;

  • 9 主线程执行请求的callback函数。

node.js 异步 IO 的脉络已经清晰,我们清楚的看到这样的一个 Threadpool 模型是全平台适用的。

Linux 上的 AIO AIO 在 2.5 版本的内核中首次出现,现在已经是 2.6 版本的产品内核的一个标准特性了。

并且由于 Native AIO 是在 linux 2.6之后引入,并且并不稳定。 社区也有过激烈的讨论:

权衡再三,笔者也非常支持社区采用的模型,赋予用户更多的选择性和可靠性。

总结

用户态的线程池实现给了用户更大的灵活性和选择性。比如:

  • 1.线程池的个数,默认是4个,用户可以通过设置环境变量 UV_THREADPOOL_SIZE指定。
  • 2.和耗时的 GETADDRINFO 复用线程池。

需要指出的是,线程池模型还有改进的空间:

  • static uv_mutex_t mutex;全局锁的优化;
  • 支持任务优先级。

参考

results matching ""

    No results matching ""