侧边栏壁纸
博主头像
一笑痕

人生若只如初见,
是可喜亦或者是可悲?

  • 累计撰写 104 篇文章
  • 累计收到 7 条评论

2147483647 之后的毫秒数,Node 直接当成 1 毫秒

2026-9-23 / 0 评论 / 6 阅读

2147483647 之后的毫秒数,Node 直接当成 1 毫秒

2147483647 毫秒是 Node 定时器能接受的最大延时,折合 24.855 天。给它 2147483648 毫秒,也就是比上限多 1 毫秒,行为会反转:不再等到上限,当场按 1 毫秒执行。下面这张表是本机实测,第二列是被截掉的那个值,第五列是它实际等了多久。

延时(ms)            说明            传入值        Node 存的 _idleTimeout   60ms 内触发?   实际耗时
----------------------------------------------------------------------------------------------------------------
0                   零               0            1                        是              1.4 ms
-1                  负数              -1           1                        是              1.5 ms
-0.5                负小数             -0.5         1                        是              1.6 ms
0.4                 不足 1 毫秒         0.4          1                        是              0.1 ms
1                   一毫秒             1            1                        是              1.4 ms
1000                一秒              1000         1000                     否
2147483646          上限减一            2147483646   2147483646               否
2147483647          上限本身            2147483647   2147483647               否
2147483648          上限加一            2147483648   1                        是              1.4 ms
2147483648          2^31            2147483648   1                        是              1.5 ms
2147483647.5        上限加半毫秒          2147483647.5 1                        是              1.5 ms
4294967296          2^32            4294967296   1                        是              1.6 ms
604800000           一周              604800000    604800000                否
2592000000          三十天             2592000000   1                        是              1.6 ms
31536000000         一年              31536000000  1                        是              1.5 ms
10000000000         一百亿毫秒           10000000000  1                        是              0.4 ms

最该看的两行

上限本身和一毫秒之后,行为完全相反:2147483647 会真的等 24.8 天,2147483648 被落到 1 毫秒当场触发。_idleTimeout 在这两个值之间从 2147483647 掉到 1。

一周是 604800000 毫秒,还在范围内,所以周任务不会出事。三十天是 2592000000,超了,被当成 1 毫秒。一年更不用说。任何想跨月跑一次的定时器,都会在这里变成一个高速循环。

为什么是 1,不是 0

Node 把延时存进 32 位有符号整数,这是从浏览器时代带下来的实现细节,上限就是 int32 能表示的最大正整数。超出去的值不会饱和到上限,而是被换成 1,同时在 stderr 留一条警告:

(node:4546) TimeoutNegativeWarning: -1 is a negative number.
Timeout duration was set to 1.
(Use `node --trace-warnings ...` to show where the warning was created)
(node:4546) TimeoutOverflowWarning: 2147483648 does not fit into a 32-bit signed integer.
Timeout duration was set to 1.
(node:4546) TimeoutOverflowWarning: 2147483648 does not fit into a 32-bit signed integer.
Timeout duration was set to 1.
(node:4546) TimeoutOverflowWarning: 2147483647.5 does not fit into a 32-bit signed integer.
Timeout duration was set to 1.
(node:4546) TimeoutOverflowWarning: 4294967296 does not fit into a 32-bit signed integer.

警告里那句 does not fit into a 32-bit signed integer 就是全部原因。0 和负数也被归到 1 毫秒,用的是另一条 TimeoutNegativeWarning,文本不一样,好区分。上限加半毫秒这种小数,判的是整个值放不放得下,所以 2147483647.5 也照样溢出。

警告只写进 stderr,不影响退出码。日志收集只抓 stdout 的部署里,这个循环会跑起来却没有任何记录。

三个入口都一样

setIntervaltimers/promises 的 setTimeout、AbortSignal.timeout 全都走同一套底层计时器。

--- setInterval 同样受这个上限约束吗 ---
setInterval(…, 1000        )  _idleTimeout = 1000
setInterval(…, 2147483647  )  _idleTimeout = 2147483647
setInterval(…, 2592000000  )  _idleTimeout = 1

--- timers/promises 的 setTimeout ---
sleep(1           )  -> 已到点       耗时 1.7 ms
sleep(2147483647  )  -> 还在等       耗时 60.7 ms
sleep(2592000000  )  -> 已到点       耗时 1.6 ms
sleep(2147483648  )  -> 已到点       耗时 1.5 ms

AbortSignal.timeout 这一组四个值都没抛错,也没提前 abort,所以它挡不住这个坑:

--- AbortSignal.timeout 的大数值 ---
AbortSignal.timeout(1000        )  没有抛错,aborted = false
AbortSignal.timeout(2147483647  )  没有抛错,aborted = false
AbortSignal.timeout(2147483648  )  没有抛错,aborted = false
AbortSignal.timeout(2592000000  )  没有抛错,aborted = false

长周期该怎么写

最省事的改法是分段续期:每次只安排不超过上限的一段,到点之后再排下一段。段长取一天,既远低于上限,也不会因为进程被反复唤醒而浪费。

const MAX = 2 ** 31 - 1; // 2147483647
const SEGMENT = 24 * 3600 * 1000; // 一天一段,必须 ≤ MAX

function sleepLong(ms) {
  return new Promise((resolve) => {
    let left = ms;
    const step = () => {
      const d = Math.min(left, SEGMENT);
      setTimeout(() => {
        left -= d;
        if (left > 0) step();
        else resolve();
      }, d);
    };
    step();
  });
}

把段长缩到 1200 毫秒、总时长 3000 毫秒跑一遍,能看清它是怎么拆的:

--- 分段续期实测(把一天一段缩成 1200 毫秒一段,跑 3000 毫秒)---
  每段毫秒数: 1200 + 1200 + 600 ,共 3 段
  实际耗时: 3006.2 ms

另一种是记账式:定时器固定短周期跑,比如每分钟醒一次,读记录里的 next_run 时间戳,到了才执行。多实例部署时这条路更合适,每次醒来都要重新抢一次执行权,进程重启也不会把计划丢掉。

分段续期我一般用在单机进程里的长延时任务,多实例的定时任务走记账式。系统调度最省心,代价是部署时多一份 cron 配置,本地开发环境里跑不起来。

真按天或按月跑的任务,交给系统调度更稳。setTimeout 在这里只是一个计时原语,不用它当调度器。

抖动的量级

同一个 1000 毫秒的定时器连做五次,实测每次都在 1 秒出头,误差是毫秒级:

--- 定时器触发时的随机抖动(同一个 1 秒定时器连做 5 次)---
  第 1 次: 1001.3 ms
  第 2 次: 1002.2 ms
  第 3 次: 1002.3 ms
  第 4 次: 1002.2 ms
  第 5 次: 1001.8 ms

这个误差和前面那个从 24.8 天变成 1 毫秒的差别不在一个量级。触发得早或晚几十毫秒可以接受,被换成 1 毫秒不行。这类故障的表现是任务被高频重复触发,查日志时要先想到去看 stderr。

最后一点容易踩:上面那个 sleep(2147483647) 还挂着的时候,进程不会自己退出,事件循环里有一个 24.8 天后才到点的定时器撑着。写长周期脚本时如果发现命令跑完不返回,先看有没有这种没清掉的定时器。

    🤞 分享