最近制作了一个爬虫脚本,基于 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