超时配置:为什么不同库用不同时间单位
超时(timeout)是分布式系统稳定性的第一道防线。但一个看似简单的”等多久放弃”问题,在不同库里却用的是不同时间单位:fetch 没有原生 timeout 参数,axios 用毫秒,Node.js http.request 用毫秒,XMLHttpRequest 用毫秒,gRPC 用 Duration(纳秒级),数据库连接池用毫秒或秒,HTTP 服务器配置混用秒与毫秒。
这种不统一源于历史原因与 API 设计哲学:浏览器早期没有 timeout 概念(依赖 XMLHttpRequest 后加的属性),Node.js 沿用 setTimeout 的毫秒约定,gRPC 受 Google 内部 Duration proto 影响,HTTP 服务器则因配置项演进保留多种单位。结果是——开发者每天在不同单位间换算,稍有不慎就会写出 1000 倍偏差的超时配置,要么过早放弃导致请求雪崩,要么永不超时让线程池耗尽。
配套工具:超时时间单位换算工具 —— 支持毫秒/秒/分/时/天/周/月/年八种单位双向换算,可直接解析
300ms/5s/1m这类超时字面量并输出标准化表示
一、主流网络库的超时单位约定
1.1 fetch:无原生 timeout,需 AbortController + setTimeout
浏览器 fetch API 设计上没有 timeout 参数,这是 Web 标准的有意取舍(避免隐式定时器造成语义混乱)。开发者必须用 AbortController + setTimeout 显式组合:
// fetch 超时标准模式:AbortController + setTimeout(单位:毫秒)
async function fetchWithTimeout(url, timeoutMs = 5000) {
const controller = new AbortController();
// setTimeout 接收毫秒,5000 = 5 秒
const timer = setTimeout(() => controller.abort(), timeoutMs);
try {
const res = await fetch(url, { signal: controller.signal });
return await res.json();
} finally {
clearTimeout(timer);
}
}
陷阱:setTimeout 的第二参数是毫秒。若习惯写 setTimeout(fn, 5) 期望 5 秒,实际只等 5 毫秒,几乎等于”立即超时”。
1.2 axios:毫秒
axios 的 timeout 选项用毫秒:
// axios 超时(毫秒)
axios.get('/api/data', { timeout: 5000 }); // 5 秒
axios.defaults.timeout = 10000; // 全局 10 秒
陷阱:从 Node.js 后端(常以秒配置)迁移到前端 axios(毫秒)时,容易直接搬数字。后端 timeout: 5(5 秒)写成前端 timeout: 5(5 毫秒),结果是所有请求瞬间失败。
1.3 Node.js http / https:毫秒
Node.js 内置 http.request 的 timeout 选项与 socket.setTimeout 都用毫秒:
// Node.js http 超时(毫秒)
const req = http.request({ hostname: 'api.example.com', timeout: 5000 }, (res) => {
// ...
});
req.on('timeout', () => req.destroy(new Error('request timeout')));
// socket 层超时(毫秒)
socket.setTimeout(30000); // 30 秒空闲超时
socket.on('timeout', () => socket.destroy());
陷阱:req.timeout 与 socket.setTimeout 是两层超时。前者控制”请求发出后多久没收到响应头”,后者控制”socket 多久没数据”。两者必须协同,否则一层失效另一层兜底。
1.4 XMLHttpRequest:毫秒
老牌的 XMLHttpRequest 的 timeout 属性也是毫秒:
const xhr = new XMLHttpRequest();
xhr.open('GET', '/api/data');
xhr.timeout = 5000; // 5 秒(毫秒)
xhr.ontimeout = () => console.error('请求超时');
xhr.send();
1.5 got / ky / request:毫秒
主流第三方 HTTP 客户端普遍用毫秒:
// got(Node.js)
import got from 'got';
await got('https://api.example.com/data', { timeout: { request: 5000 } });
// ky(浏览器 + Node.js)
import ky from 'ky';
await ky('https://api.example.com/data', { timeout: 5000 });
// request(已废弃但仍有项目使用)
request({ url: '/api/data', timeout: 5000 });
注意:got 的 timeout 是对象,可分别设置 request / connect / socket / response / send / lookup 等多阶段超时,全部用毫秒。需要先用 毫秒与秒双向换算工具 把每个阶段换算清楚,避免混淆。
二、服务端超时配置
2.1 数据库连接池
数据库连接池通常有多个超时参数,单位不统一:
| 参数 | 含义 | 常见单位 | 典型值 |
|---|---|---|---|
idleTimeout | 空闲连接多久后回收 | 毫秒(node-postgres)/ 秒(HikariCP) | 30 秒 |
connectionTimeout | 获取连接的等待上限 | 毫秒 | 5-30 秒 |
statementTimeout | 单条 SQL 执行上限 | 毫秒(PostgreSQL)/ 秒(MySQL wait_timeout) | 30-60 秒 |
loginTimeout | 登录握手上限 | 秒 | 5-10 秒 |
// node-postgres(毫秒)
const pool = new Pool({
idleTimeoutMillis: 30000, // 30 秒
connectionTimeoutMillis: 5000, // 5 秒
});
// HikariCP(Java,毫秒)
HikariConfig config = new HikariConfig();
config.setIdleTimeout(30000); // 30 秒
config.setConnectionTimeout(5000); // 5 秒
config.setMaxLifetime(1800000); // 30 分钟
陷阱:Java HikariCP 的 maxLifetime 推荐比数据库 wait_timeout 短 30-60 秒。如果数据库 wait_timeout 是 28800 秒(8 小时,MySQL 默认),maxLifetime 应设为 28200-28740 秒,单位都是秒。但若配置文件混用毫秒(28800000)与秒(28800),极易写错。
2.2 HTTP 服务器
主流 HTTP 服务器的超时配置混用单位:
| 服务器 | 配置项 | 单位 | 默认值 |
|---|---|---|---|
| Nginx | proxy_read_timeout | 秒 | 60s |
| Nginx | keepalive_timeout | 秒 | 75s |
| Nginx | client_body_timeout | 秒 | 60s |
| Node.js | server.keepAliveTimeout | 毫秒 | 5000 |
| Node.js | server.headersTimeout | 毫秒 | 60000 |
| Node.js | server.requestTimeout | 毫秒 | 300000 |
| Apache | Timeout | 秒 | 60 |
| Tomcat | connectionTimeout | 毫秒 | 20000 |
# Nginx 配置(秒)
location /api/ {
proxy_read_timeout 60s; # 60 秒
proxy_send_timeout 60s; # 60 秒
keepalive_timeout 75s; # 75 秒
}
// Node.js 服务器(毫秒)
const server = http.createServer(handler);
server.keepAliveTimeout = 5000; // 5 秒
server.headersTimeout = 60000; // 60 秒
server.requestTimeout = 300000; // 5 分钟
陷阱:Nginx(秒)与 Node.js(毫秒)混部时,若两者都设”60”,Nginx 是 60 秒,Node.js 是 60 毫秒(几乎瞬间超时)。反向代理链路中,后端超时必须长于前端超时,否则前端还在等,后端已断开,返回 502。
2.3 gRPC deadline:Duration
gRPC 用 google.protobuf.Duration proto 表示 deadline,本质是纳秒级整数对(seconds + nanos),但各语言 SDK 暴露的 API 不同:
// Go gRPC(time.Duration,底层纳秒)
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()
resp, err := client.Call(ctx, req)
// Java gRPC(Duration)
ctx = ctx.withDeadlineAfter(5, TimeUnit.SECONDS);
// Python gRPC(datetime.timedelta 或秒)
ctx = ctx.withDeadline(time.time() + 5) # 5 秒后
// Node.js gRPC(毫秒)
const deadline = Date.now() + 5000; // 5 秒(毫秒)
client.Call(req, { deadline }, (err, resp) => {});
陷阱:跨语言 gRPC 调用时,Go 用 time.Second,Python 用 datetime.timedelta,Node.js 用毫秒。若客户端 deadline 短于服务端处理时间,会触发 DEADLINE_EXCEEDED;若客户端不设 deadline,服务端慢调用会拖垮整个调用链。
三、客户端超时实战
3.1 AbortController + setTimeout 模式
现代浏览器与 Node.js 18+ 推荐用 AbortController 统一管理超时:
// 单一超时
async function fetchJson(url, opts = {}, timeoutMs = 5000) {
const ctrl = new AbortController();
const t = setTimeout(() => ctrl.abort(), timeoutMs);
try {
const res = await fetch(url, { ...opts, signal: ctrl.signal });
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return await res.json();
} catch (err) {
if (err.name === 'AbortError') throw new Error(`请求超过 ${timeoutMs}ms 超时`);
throw err;
} finally {
clearTimeout(t);
}
}
// 多阶段超时(连接 / 响应头 / 响应体)
async function fetchWithStages(url, { connectMs = 2000, headerMs = 5000, bodyMs = 30000 }) {
const ctrl = new AbortController();
const timers = [];
const race = (ms, msg) => new Promise((_, reject) => {
timers.push(setTimeout(() => {
ctrl.abort();
reject(new Error(msg));
}, ms));
});
try {
const fetchPromise = fetch(url, { signal: ctrl.signal });
await Promise.race([fetchPromise, race(connectMs, '连接超时')]);
const res = await Promise.race([fetchPromise, race(headerMs, '响应头超时')]);
const bodyPromise = res.json();
return await Promise.race([bodyPromise, race(bodyMs, '响应体超时')]);
} finally {
timers.forEach(clearTimeout);
}
}
3.2 Promise.race 模式(兼容旧环境)
不支持 AbortController 的环境可用 Promise.race:
// Promise.race:超时后无法真正中断请求,仅丢弃结果
function withTimeout(promise, timeoutMs) {
const timeout = new Promise((_, reject) =>
setTimeout(() => reject(new Error('timeout')), timeoutMs)
);
return Promise.race([promise, timeout]);
}
// 注意:即使 race 返回 timeout,原始 promise 仍会继续执行到底
// 资源真正释放仍需 AbortController
3.3 用户感知超时
UI 层的超时往往比网络层更短——用户对”卡住”的容忍远低于对”失败”的容忍:
// 用户感知超时:先显示 loading,超时后降级展示
const PERCEIVED_SLOW = 1500; // 1.5 秒后显示骨架屏
const PERCEIVED_FAIL = 8000; // 8 秒后提示"网络较慢"
const t1 = setTimeout(() => showSkeleton(), PERCEIVED_SLOW);
const t2 = setTimeout(() => showSlowHint(), PERCEIVED_FAIL);
try {
const data = await fetchJson('/api/data', {}, 10000);
clearTimeout(t1); clearTimeout(t2);
renderData(data);
} catch (err) {
clearTimeout(t1); clearTimeout(t2);
renderError(err);
}
UI 阈值用毫秒(1500ms = 1.5s),但网络层超时常用 10 秒。两层超时各自独立,用 网络超时单位换算工具 把所有阈值换算成统一单位(推荐毫秒)做对照表,避免 1.5 秒写成 15(毫秒则等于 15ms)这类笔误。跨时区团队还需注意「本地 1.5 秒」对应的 UTC 时刻不同,可用 跨时区超时时间换算工具 把同一截止时刻并列展示为各时区的本地时间,避免跨时区同事对超时点理解错位。
四、超时与重试的协同
4.1 指数退避 + 抖动
重试退避算法涉及大量时长换算,单位错误会让重试变成雪崩:
// 指数退避(毫秒)
function backoff(attempt, baseMs = 1000, capMs = 30000) {
const raw = Math.min(baseMs * 2 ** attempt, capMs);
// 抖动:避免重试同步化(full jitter 策略)
return Math.random() * raw;
}
// 重试循环
async function fetchRetry(url, { retries = 3, baseMs = 1000, timeoutMs = 5000 }) {
for (let i = 0; i <= retries; i++) {
try {
return await fetchWithTimeout(url, {}, timeoutMs);
} catch (err) {
if (i === retries) throw err;
const delay = backoff(i, baseMs);
await new Promise(r => setTimeout(r, delay));
}
}
}
陷阱:baseMs 与 timeoutMs 必须同单位。若 baseMs = 1000(1 秒)但误写成 baseMs = 1(1 毫秒),重试间隔几乎为 0,重试风暴直接压垮下游。
4.2 重试上限与超时上限的边界
总耗时上限 = 单次超时 × (重试次数 + 1) + 累计退避时间。若单次超时 5 秒、重试 3 次、退避 1+2+4=7 秒,总耗时可达 27 秒,远超用户感知阈值。设计时需用 超时配置时间单位换算器 算清总耗时上限,与上层 deadline 对齐。
五、超时单位的历史演变
为什么 fetch 没有 timeout?Web 标准设计者认为”超时是应用语义”而非”传输语义”——fetch 只负责发请求,超时由调用方用 AbortController 自行组合。这避免了 API 表面膨胀,但增加了样板代码。
为什么 axios 用毫秒?前端需要精细控制(动画帧 16ms、用户感知 100ms),毫秒是最小可控粒度。秒级粒度对 UI 太粗。
为什么 gRPC 用 Duration?Google 内部 proto 长期用 (seconds, nanos) 二元组表示时长,跨语言序列化稳定。各语言 SDK 把它包装成原生类型(Go time.Duration、Java Duration、Python timedelta),但底层 wire format 一致。
为什么 HTTP 服务器混用秒与毫秒?Nginx 沿用 C 时代 time_t(秒)传统,配置可读性高;Node.js 沿用 setTimeout(毫秒)约定;Tomcat 早期用秒,后改为毫秒以支持精细控制。迁移时务必逐项核对。
六、常见超时值速查表
| 场景 | 推荐值 | 单位 | 说明 |
|---|---|---|---|
| 浏览器 fetch 主请求 | 5-10 秒 | 毫秒 | 用户感知阈值 8 秒 |
| 浏览器 fetch 后台同步 | 30-60 秒 | 毫秒 | 不阻塞 UI |
| axios 全局默认 | 10 秒 | 毫秒 | 平衡可用性与体验 |
| Node.js http 请求 | 5-30 秒 | 毫秒 | 视下游 SLA |
| 数据库连接获取 | 5-10 秒 | 毫秒 | HikariCP 推荐 |
| 数据库空闲连接回收 | 30 秒 | 毫秒/秒 | 视负载调整 |
| 数据库 SQL 执行 | 30-60 秒 | 毫秒/秒 | 长查询单独放宽 |
| Nginx proxy_read | 60 秒 | 秒 | 长于后端超时 |
| Nginx keepalive | 60-75 秒 | 秒 | 配合后端 |
| gRPC 客户端 deadline | 5-30 秒 | Duration | 跨调用链传递 |
| UI 骨架屏触发 | 1-1.5 秒 | 毫秒 | 用户感知”慢” |
| UI 慢网络提示 | 8-10 秒 | 毫秒 | 用户感知”失败” |
| 重试退避基准 | 1 秒 | 毫秒 | 指数退避 base |
| 重试退避上限 | 30 秒 | 毫秒 | cap 上限 |
换算要点:所有”秒级”值在 JavaScript 代码里都要 × 1000 转毫秒,在 Nginx 配置里直接写秒。混用时先用 请求超时单位换算工具 把全链路超时画一张表,避免单点错配。
七、超时配置最佳实践
7.1 全链路超时预算
分布式系统中,每个调用链都有总 deadline。从入口到最底层,超时预算逐层递减:
用户请求总 deadline: 30s
├─ 网关层: 28s(预留 2s 给响应回传)
│ ├─ 服务 A: 25s(预留 3s 给内部处理)
│ │ ├─ 服务 B: 20s(预留 5s 给本地逻辑)
│ │ └─ 数据库: 15s(预留 5s 给服务 B 其他操作)
│ └─ 服务 C: 22s
└─ 缓存: 5s(快速失败,不阻塞主链路)
原则:上层超时 > 下层超时 + 本层处理时间。若违反,下层未完成就被上层断开,造成”幽灵请求”(上层已返回错误,下层仍在跑)。
7.2 命名常量与单位显式化
// 反例:裸数字,单位不明
axios.get(url, { timeout: 5000 }); // 5 秒?5 毫秒?
// 正例:常量 + 单位后缀
const TIMEOUT = {
FAST: 1_000, // 1 秒(毫秒)
NORMAL: 5_000, // 5 秒
SLOW: 30_000, // 30 秒
};
axios.get(url, { timeout: TIMEOUT.NORMAL });
7.3 配置集中化与可观测
# timeout-config.yaml —— 全链路超时集中管理(单位:毫秒,除非显式标注)
gateway:
request: 28000
proxy_read: 28s # Nginx 配置项保留秒单位后缀
service_a:
downstream_b: 20000
downstream_c: 22000
db:
acquire: 5000
query: 15000
idle: 30000
ui:
skeleton: 1500
slow_hint: 8000
迁移单位时,配合 超时时间单位换算工具 批量验证每个值,避免遗漏。
八、常见误区
误区 1:超时设得越长越安全。超时过长会让慢请求堆积,线程池/连接池耗尽,引发雪崩。正确做法是基于 SLA 设定合理上限,让快速失败触发降级。
误区 2:重试就是无条件再发一次。无退避的重试会让下游压力倍增。必须配合指数退避 + 抖动,并设重试上限。
误区 3:fetch 设了 timeout 选项。fetch 没有 timeout 选项,传了也会被忽略。必须用 AbortController。
误区 4:socket.setTimeout 等于请求超时。socket.setTimeout 是”socket 空闲多久后断开”,与”请求发出后多久没响应”是不同维度。两者必须协同。
误区 5:超时单位在前后端一致。后端常以秒(Nginx、MySQL wait_timeout),前端以毫秒(axios、setTimeout)。跨端配置时务必换算。
误区 6:客户端 deadline 可以短于服务端处理时间。客户端 deadline 短于服务端实际处理时,会触发 DEADLINE_EXCEEDED,但服务端仍会跑完(资源浪费)。应在服务端配置 cancellation 传播。
九、相关工具与延伸阅读
超时配置往往不是孤立存在的,它会和 HTTP 头部、状态码、时间戳、定时任务交织在一起:
- 想检查
Keep-Alive/Timeout/Retry-After等响应头的实际值?用 HTTP 响应头查看工具 抓取线上返回头,核对超时相关头部是否生效 - 调试
408 Request Timeout/504 Gateway Timeout/425 Too Early等状态码?用 HTTP 状态码查询工具 查看完整语义与触发条件 - 超时计算基于 Unix 时间戳(如 JWT
exp、gRPC deadline)?配合 Unix 时间戳转换工具 在秒级时间戳与可读时间之间切换 - 定时任务需要超时上限?用 CRON 表达式解析器 验证调度表达式,并配合
timeout命令限制单次执行时长
掌握时间单位换算后,你会发现超时配置的很多”玄学问题”本质上都是单位陷阱。养成”看到数字先问单位”的习惯,配合工具快速验证,能避免绝大多数线上事故。
配套工具:本文所有换算均可通过 时间单位换算器 完成——支持毫秒/秒/分/时/天/周/月/年八种单位双向换算、复合时长解析(1h 30min)、毫秒转人类可读表示,全本地处理,零上传零追踪。