从Node Request ESOCKETTIMEDOUT错误说开来

最近制作了一款脚本AlipaySupervisor,其主要目的和行为就是实时请求我的支付宝交易列表并推送通知到指定服务器,以模拟支付宝即时到账接口的自动通知功能。脚本使用NodeJS开发,使用request模块异步网络请求,每隔一分钟请求一次列表,解析符合条件的订单信息推送,但是使用过程中脚本的邮件通知经常发送邮件提示error,大部分错误发生在推送的过程,错误代码为The error info is: ESOCKETTIMEDOUT。可以确认的是我使用阿里云主机没有网络的问题,那么需要从request模块的一些配置上入手解决问题。

ESOCKETTIMEDOUT错误原因

在stackoverflow上查找解决方案有一条这样的回答:

By default, Node has 4 workers to resolve DNS queries. If your DNS query takes long-ish time, requests will block on the DNS phase, and the symptom is exactly ESOCKETTIMEDOUT or ETIMEDOUT  

具体意思就是默认情况下,Node开辟四个workers执行用户tasks包括解析DNS查询,如果DNS查询较慢,就会阻塞请求在DNS阶段,最终超时返回ESOCKETTIMEDOUT或者ETIMEDOUT错误码,而由于workers数量有限,又被占用时其他任务只能排队,最终导致越来越多的超时错误。

NodeJS Libuv与线程池

Libuv架构

Libuv是NodeJS的底层模块,其维护线程池来实现各种用户代码任务的执行,关于Libuv线程池的介绍 查看此文

而worker thread默认数量4也是由Libuv定义的,但是它可以通过定义UV_THREADPOOL_SIZE环境变量来修改默认值,最大值为128。对于我们的问题,为了应对频繁请求(包含可能较慢的DNS解析任务),需要增大线程池的容量,比如将默认4 worker threads提高至10。

延伸: NodeJS线程

不考虑通过cluster和webworker-threads带来的多进程/多线程能力,仅仅考虑典型的非线程的Node。

Node运行单个事件循环,它是单线程的,所有你写的js代码都是在此循环内执行,如果代码中发生了阻塞操作的情况,会导致整个循环阻塞,其他任务无法继续执行除非该操作完成。以上是典型的单线程自然特性,但是Node不完全是这样。

很多使用C/C++编写的确定的函数和模块,它们是支持异步I/O的。当你调用这些函数和方法时,调用会被传递到worker thread。例如,当你使用fs模块请求文件,fs模块传递调用到一个worker thread,worker执行完代码并响应之后将传递回到事件循环,而事件循环中的队列已经不是传递调用之前的状态,可能已经执行到了更多的任务之后了。通过libuv这些工作都被抽象而不需要用户或开发者直接去关心的。

Libuv实现异步I/O并不是完全依赖线程池的。特别是http模块目前是使用了不同的策略实现异步。但是明确的是Libuv维护的线程池是众多实现异步特性策略的一种。

关于Node如何实现异步,可以阅读这篇分析文章 - On problems with threads in node.js