计算机里的时间 —— 延迟、时钟、表示、感知

计算机时间不是一个东西。一个工程师每天都在跟 4 种不同的「时间」 打交道:

  1. 延迟(Latency) —— 一个操作等多久。
  2. 时钟(Clock) —— 现在几点 / 已经过了多久。
  3. 表示(Representation) —— 怎么存、怎么在网络上传、怎么跨时区表达。
  4. 感知(Perception) —— 用户感觉到这个东西有多慢。

把它们混在一起谈是大部分时间相关 bug 的根因 —— 用 wall clock 测耗时、用字符串排序时间戳、目标 latency 设成”越快越好”而不是”用户感觉不到”。本文一篇文章把这 4 层串起来,从纳秒到秒、从 Jeff Dean 延迟数字 到 leap second、从 wall clock 到 Lamport 时间戳。

1. 延迟尺度:从 1ns 到 1s,跨 9 个数量级

第一件事是建立尺度感。下面是 2025 年前后典型现代硬件的数字(数量级估算,不同硬件 2~5 倍浮动正常):

操作延迟备注
L1 cache 访问1 ns~3 个 CPU cycle
L2 cache 访问4 ns
L3 cache 访问12 ns跨核共享
Main memory (DRAM) 随机读100 ns一次 cache miss 的代价
同核 mutex lock + unlock(无争用)20 ns
一次 syscall(用户态↔内核态)~100 nsspectre/meltdown 后涨到 ~500 ns
Context switch1~3 μs进程间切换
NVMe SSD 4KB 随机读10 μs
同机房 RTT(10Gb 网络)100~250 μs
HDD 随机寻道5~10 ms机械臂物理移动
跨大陆 RTT(US ↔ EU)80~100 ms光速 + 路由
跨地球 RTT(US ↔ 亚洲)150~200 ms海底电缆
用户感知到”明显卡”~1000 ms见 §5

横跨 9 个数量级。这些数字之间的相对关系比绝对值更重要 —— 一次 DRAM 访问比 L1 慢 100 倍、一次跨洲 RTT 比 DRAM 访问慢 100 万倍。

把 ns 放大成 s,直觉立刻不一样

最有用的视觉化是 Colin Scott 那个交互式页面 用的方法 —— 把 1 ns 放大成 1 s(× 10⁹),整个谱重新呈现:

真实操作放大后(1ns = 1s)
L1 cache1 秒
Main memory1 分 40 秒
NVMe SSD 读2.8 小时
同机房 RTT3 天
HDD 寻道4 个月
跨洲 RTT3.2 年
1 秒31.7 年

这个表的意义:一次跨洲 HTTP 请求等于”取一份 L1 cache 数据 3.2 年”。这不是文学比喻,是真实的 CPU 浪费 —— 同步阻塞那 100ms 内本来可以做 1 亿次 cache 访问。理解这一点之后,“为什么异步 IO 重要”、“为什么 CDN 重要”、“为什么 cache 是优化的核心”全部变成废话级别的常识。

2. 三种”时钟”,别混用

很多人没意识到操作系统暴露了至少 3 种不同的时钟,各自适合不同场景。

Wall clock(墙钟,CLOCK_REALTIME

跟着 UTC 走,跟着 NTP 跳。

  • 含义:“现在是公历几年几月几日几点几分几秒”
  • 行为:可前可后 —— NTP 同步可能把它向前跳几秒、向后退几秒,DST 切换会人为加减 1 小时
  • 适合:给用户显示当前时间、写入日志的时间戳、跨机器 / 跨进程比较”什么时候发生的”
  • 不适合:测函数耗时、做 timeout
  • API:System.currentTimeMillis() / Date.now() / time.time() / clock_gettime(CLOCK_REALTIME)

Monotonic clock(单调钟,CLOCK_MONOTONIC

自系统启动以来单调递增。

  • 含义:没有”几点”的语义,只有”经过了多少”
  • 行为:永远不回退,不被 NTP 影响、不被 DST 影响
  • 适合:测耗时、超时控制、计算 elapsed time、性能 benchmark
  • 不适合:跨机器比较(每台机器从自己的 boot 开始算)、显示给用户
  • API:System.nanoTime() / time.monotonic() / process.hrtime.bigint() / clock_gettime(CLOCK_MONOTONIC)

Logical clock(逻辑钟,Lamport / 向量时钟)

完全不试图描述真实时间,只描述事件之间的因果关系

  • 含义:A 事件的逻辑时间 < B 事件的逻辑时间 ⟹ A 可能影响 B;反之不一定
  • 适合:分布式系统的事件排序、CRDT 冲突解决、Raft / Paxos 内部
  • 不适合:任何”真实经过多少时间”的问题
  • 它治的是”分布式系统中,物理时钟根本不能可靠地告诉你 A 和 B 谁先发生”这个问题,是分布式时间的基础

经典错用:用 wall clock 做 timeout

// 错:用 Date.now() 做 timeout
const start = Date.now();
while (Date.now() - start < 5000) { doWork(); }

// 如果中途 NTP 把系统时间向后调 10 秒,循环会立刻退出 (差值变成负的或巨大值)。
// 反过来如果向前调 1 小时,循环立刻"超时"也退出。

// 对:用 monotonic
const start = performance.now();        // 浏览器
while (performance.now() - start < 5000) { doWork(); }

简单记忆:测耗时用 monotonic,显示用 wall。两个不能换。

3. 闰秒:小数字,大灾难

地球自转不是匀速的(潮汐摩擦让自转变慢),所以 UTC 偶尔要加一个 leap second(闰秒)—— 在 6 月 30 日或 12 月 31 日的 23:59

后塞进一个 23:59

听起来无害的一秒,制造了 IT 史上几次知名 outage:

  • 2012-06-30 —— Linux kernel 在加闰秒后触发 hrtimer 死循环,Reddit / LinkedIn / Mozilla 大面积宕机;Java 应用 CPU 100% 卡死
  • 2017-01-01 —— Cloudflare 因为 Go runtime 在闰秒时返回了负的 elapsed time,触发 DNS 解析路径的 panic,1% 的请求在 30 分钟内失败

应对方案分裂成两派:

方案实现用户
直接塞系统真的播放 23:59
,应用要能处理”重复的一秒”或”60 秒分钟”
Linux 默认(实际把 23:59
重复一次)
Smear(平摊)把这 1 秒平摊到 20 小时,每秒慢一点点Google / AWS / Facebook
Step(跳过)闰秒发生时直接跳,应用看到时间”少了一秒”部分系统

2022 年 Meta / Google 等联名呼吁停止 leap secondIERS 在 2022 通过决议 2035 年前后停止 leap second。在那之前,做基础设施的工程师要假定 leap second 仍然存在,并且默认 NTP 行为会让你的系统在某一天 23:59

那刻突然崩溃。

4. 时间表示的陷阱

Unix epoch & 2038 问题

Unix 时间从 1970-01-01 00:00

UTC 起算秒数。time_t 在历史上是 32-bit signed int,能表示的最大值是 2147483647 —— 对应 2038-01-19 03:14
UTC
。再加 1 秒会溢出成负数,回到 1901 年。

2038 还有 12 年。绝大多数 64-bit 系统已经把 time_t 升到 64-bit,但 32-bit 嵌入式设备 / 旧 32-bit Linux 系统 / 某些数据库列 / 一些金融老系统 还在用 32-bit。NTP 协议本身也有 2036 问题(NTP timestamp 是从 1900 起的 32-bit)。Y2K 在 2000 年没炸是因为有 5 年准备时间,Y2K38 倒数中。

Unix epoch 也不是唯一的:

系统epoch
Unix / POSIX1970-01-01
NSDate (Apple)2001-01-01
Windows FILETIME1601-01-01
OLE Automation Date1899-12-30
Excel1900-01-01(但 错把 1900 当闰年

跨系统传时间戳要小心。

Timezone 不只是 UTC offset

很多工程师以为 "timezone = UTC offset",比如”上海 = UTC+8”。这是错的:

  • DST 切换:同一个时区,夏天和冬天的 offset 不一样。America/New_York 冬天 UTC-5、夏天 UTC-4
  • 历史变化:时区规则随政府决定改。2007 年美国把 DST 从 4 月第一周改成 3 月第二周;俄罗斯 2011 年取消 DST、2014 年又改了;中国 1949~1991 之间有过 DST
  • 政治变化:朝鲜 2015 年从 UTC+9 改到 UTC+8
    、2018 年改回 UTC+9
  • 半小时 / 45 分钟 offset:印度 UTC+5
    、尼泊尔 UTC+5

正确的 timezone 是一个 IANA 名字Asia/ShanghaiAmerica/New_York),不是一个数字 offset。运行时根据日期查 tzdb 计算 offset。永远存 IANA 名 + UTC timestamp,不要存”东八区时间”或”local time string”

ISO 8601 vs RFC 3339

写时间字符串只需要知道一件事:用 RFC 3339,带 timezone offset

2026-06-09T14:30:00+08:00   ✓ RFC 3339, 带偏移
2026-06-09T06:30:00Z        ✓ RFC 3339, UTC ('Z' = +00:00)
2026-06-09T14:30:00         ✗ 没 offset, 不知道哪个时区
20260609T143000             ✗ ISO 8601 允许但 RFC 3339 不允许, 各种 parser 不一致
2026/06/09 14:30:00         ✗ 自定义格式, 跨语言解析地狱

ISO 8601 是个 metaspec、允许大量变体;RFC 3339 是它的严格子集、网络传输标准。写代码就用 RFC 3339

各语言 time API 的常见坑

语言 / 库
JavaScript Datenew Date('2026-01-01') 解析成 UTC,new Date('2026/01/01') 解析成本地时间。getMonth() 0-indexed(11 = 12 月)。Date 本身没有 timezone 信息,只是 epoch ms
Python datetimenaive datetime(无 timezone)vs aware datetime。datetime.now() 返回 naive 是常见坑 —— 跟其他 aware 的算术会抛 TypeError。永远用 datetime.now(tz=timezone.utc)
Go time.Time内部同时有 wall + monotonic 两部分。.Round() / .Truncate() 和某些序列化会剥离 monotonic,之后做 t2.Sub(t1) 又退化成 wall 差值
Java Date / Calendar已经废弃,新代码用 java.time.Instant / ZonedDateTimeCalendar.MONTH 也是 0-indexed
PostgreSQLtimestamp 不带时区(=naive,不要用),timestamp with time zone(timestamptz,实际存 UTC、显示按 session timezone 转)才是默认
MySQLDATETIME 不带时区(naive),TIMESTAMP 带(但只能到 2038-01-19,因为内部是 32-bit)

5. 人类感知阈值

来自 Jakob Nielsen 1993 《Response Times: The 3 Important Limits》 的经典三档:

阈值用户感受
< 100 ms感觉是”立即响应”,操作和结果在脑子里是同一个动作
100 ms ~ 1 s感觉到延迟,但思维不被打断,仍在”操作流”里
1 s ~ 10 s注意力开始转移,需要 loading 提示
> 10 s用户会切到别的事情,需要进度条 + 可取消

再叠加屏幕刷新:

帧率帧预算场景
60 fps16.67 ms主流屏幕;浏览器 JS 主线程要在这之内完成一帧的工作
120 fps8.33 ms高刷屏 / Apple ProMotion
144 fps6.94 ms电竞屏

反推 latency 预算

  • HTTP API:目标 200ms 内(用户感觉流畅,包含网络 + 解析 + 渲染)
  • DB 查询:目标 50ms 内(给 API 处理留 150ms)
  • 单个 syscall:目标 10μs 内(一次 HTTP 路径有几百到几千个 syscall)
  • 一帧 JS 工作:目标 < 10ms(给浏览器 layout/paint 留 6ms)

这些数字不是”越快越好”,是”用户感知不到的临界点”。HTTP API 优化到 200ms 和 50ms 对用户没有可感差异,把同样的工程时间花到”让 1500ms 的接口降到 500ms”上 ROI 高得多。

6. 测量时间的正确姿势

要么用对工具,要么数据都是噪音。

测函数耗时

永远用 monotonic clockDate.now() / System.currentTimeMillis() / time.time() 全部不行 —— 它们可能在 NTP 调时刻把你的”已用 50ms”变成”已用 -3 秒”或者”已用 1 小时”。

语言推荐 API
C/C++clock_gettime(CLOCK_MONOTONIC, &ts)
JavaSystem.nanoTime()
Pythontime.monotonic()time.perf_counter()(高精度)
Gotime.Since(start)(内部用 monotonic)
Node.jsprocess.hrtime.bigint()
浏览器 JSperformance.now()

微基准(< 1μs 级别)

在 x86 上可以用 RDTSC 周期计数器直接读 CPU cycle counter。但要注意:

  1. 跨核可能不同步(现代 invariant TSC 大多 OK)
  2. 频率随 P-state 变(要么固定频率,要么用 rdtscp 算实际频率)
  3. 多次测量取最小值,不要取平均(GC / 中断会把单次拉高)

更稳的做法:用专门的 benchmark 框架(Go testing.B / Rust criterion / JMH for Java),自动处理 warmup、统计、稳态判定。

应用 profiling

不要靠 printf 加时间戳推断瓶颈。用专门工具:

  • Linux: perf record + perf report / FlameGraph
  • macOS: Instruments / samply
  • Java: async-profiler / JFR
  • Node.js: --prof + 0x flamegraph / clinic.js
  • Python: py-spy / austin
  • Go: pprof(内置)

浏览器特殊:故意降低精度

performance.now() 在浏览器里默认 100μs ~ 1ms 颗粒度(不是 ns)—— Spectre 时间侧信道防护。想要更高精度要 crossOriginIsolated 上下文(COOP + COEP header),开启后大约 5μs。

7. Anti-pattern 清单

按工程师日常踩到的顺序排:

  1. 用 wall clock 测耗时 / 做 timeout —— NTP 调时一秒,你的 5 秒 timeout 立刻触发。改 monotonic
  2. 字符串排序时间戳 —— "9:00" > "12:00" 因为 '9' > '1'。要么 zero-pad("09:00"),要么解析成数值再比
  3. 假设网络 RTT 稳定 —— p50 30ms 不代表 p99 不会到 500ms。timeout 永远参考 p99,不是平均
  4. 跨时区存 local time —— 数据库永远存 UTC,显示时按 IANA timezone 转
  5. 假设 1 秒永远 = 1 秒 —— leap second / NTP smear / VM clock drift 都会让”1 秒”实际是 0.99~1.5 秒
  6. 假设 timezone = UTC offset —— 不是,DST + 历史 + 政治变化让 offset 随日期变
  7. 假设所有日期都是公历 —— 还有阴历、日本年号、民国纪年、波斯历、希伯来历,国际化产品会撞上
  8. leap second 假装不存在 —— 默认 NTP 处理可能让你的系统在 23:59
    那秒崩。要么 smear(推荐),要么明确测试过 step 行为
  9. 假设 epoch = 1970-01-01 —— 跨系统传时间戳前确认 epoch(macOS / Windows / Excel 全是不同的)
  10. 测试里写死”现在” —— assert today == "2026-06-09" 这种测试在 23
    跑会失败、跨 DST 跑会失败、跨时区跑会失败。Inject clock,不要硬编码
  11. Date.now() 当随机数种子 —— 同一毫秒内两个进程会拿到同样的种子,导致碰撞
  12. 32-bit time_t 还活在你的数据库里 —— 检查所有 INT UNSIGNED 存 epoch 的列,2038 之前要升 64-bit

收尾:3 条时间直觉

文章很长,记得这 3 条就够:

  1. 尺度感 —— 看一个 op 立刻知道它在 ns / μs / ms / s 哪个量级。这决定哪里需要优化、哪里不必。一次跨洲 RTT 等于 1 亿次 cache 访问 —— 同步等它就是浪费
  2. 时钟分清 —— 测耗时用 monotonic,显示给用户用 wall,分布式排序用 logical。三条规则,不能换
  3. 人类感知锚点 —— 200ms(API 流畅)、1s(注意力临界)、16.67ms(一帧)刻在心里。这些反向告诉你哪里值得花工程时间

进一步阅读

这篇怎么样?