Unix 时间戳常被当作一个简单的整数:自 1970 年 1 月 1 日 00:00:00 UTC 起经过的秒数。真把它放进程序里,麻烦才出现:两个事件可能落在同一个时间戳上,系统时钟会倒退,两台服务器对“现在几点”各执一词,而一个能报告纳秒的时钟仍可能差上数毫秒。

这就是 Linux 的时间管理:一个看似枯燥的整数,背后是一串并不轻松的工程问题。

墙钟与单调时钟:先分清用途

设想程序开始下载一个文件,记录开始时间,下载完成后记录结束时间。

但下载进行到一半时,系统时钟被校正了。于是程序认为下载在开始之前就已经结束。

文件当然没有穿越时间。问题在于:你用了一个可以被调整的时钟去测量耗时。

Linux 为不同用途提供了不同的时钟,其中两个最有用的时钟是:

  • CLOCK_REALTIME 表示当前的墙钟时间。
  • CLOCK_MONOTONIC 提供一个持续前进的时钟,用于测量经过的时间。

系统调整时间时,墙钟可能向前或向后跳变。单调时钟不会倒退,不过在 Linux 上它的速率仍然可以被调整。

在 Python 中也能看到同样的区分:

import time
print("Wall clock:", time.time())
print("Monotonic clock:", time.monotonic())

这些值的含义不同。time.time() 返回基于 Unix 纪元的时间;time.monotonic() 使用某个与时钟相关的参考点,因此它的原始值并不是日历时间戳。

要点: 回答“这件事发生在什么时候”,用墙钟;回答“这件事花了多久”,用单调时钟。

时钟漂移与 NTP 同步

计算机有时钟,并不意味着它总知道正确的时间。

硬件时钟会漂移:有的走得稍快,有的走得稍慢。两台机器运行足够久,它们的时钟就可能不一致。

这正是 NTP(Network Time Protocol,网络时间协议)的用武之地:它帮助计算机与外部时间源同步时钟。

但同步不是魔法。网络延迟、出错的时间源以及时钟校正都会影响结果。取决于配置,时间同步软件可能逐步调整时钟速率,也可能把时钟向前或向后跳变。

在典型的基于 systemd 的 Linux 发行版上,可以用下面的命令查看时间配置:

timedatectl status

输出会因发行版和配置而异,重点看系统时钟与时间同步状态。

要点: 时间戳只告诉你某个时钟当时报告了什么,并不自动告诉你这个读数离正确时间有多近。

本地日历时间不是固定秒数

假设你要让一个脚本每天 9 点运行。

最直观的做法似乎是每次运行后等待 86,400 秒(24 hours)。

但本地日历日并不总是包含 86,400 个流逝秒。夏令时切换会让某个本地日变成 23 或 25 hours 小时长。历史上,闰秒也让 UTC 与简单的秒数计数之间的关系变得复杂。

而且时区并不只是相对 UTC 的固定偏移。政府可以修改规则,软件需要最新的信息才能应对这些变化。

在 Linux 系统上试试这两条命令:

TZ=UTC date
TZ=Asia/Kolkata date

它们按指定时区显示当前的日期与时间。

对于一次性事件,Unix 时间戳可能正好够用。但考虑这样的需求:

每周一纽约时间 9 点运行这个脚本。

单靠一个时间戳无法表达这个意图。你需要一条重复规则、一个本地时间,以及一个时区标识符。

关于这些规则,可以查阅 IANA 时区数据库。

要点: “某个时间点”和“关于本地日历时间的规则”是两回事。

跨机器排序不能只看时间戳

事情到这里才真正变得有意思。

服务器 A 在 12:00:00.100 记录了一个事件。

服务器 B 在 12:00:00.080 记录了另一个事件。

哪个事件先发生?

你可能会因为服务器 B 的时间戳更小而认为它的事件更早。

但如果服务器 B 的时钟比服务器 A 的走得快呢?

这就触及了分布式系统的一个基本问题:仅凭时间戳无法可靠地确定跨机器事件的先后顺序。

时钟同步有帮助,但无法保证每台服务器都报告完全相同的时间。

分布式系统会用其他机制来推断事件顺序,视问题而定,可能包括序列号、逻辑时钟、数据库日志位置或协调协议。

还要记住:即使两个时钟都工作正常,两个事件也可能拥有同一个时间戳——它们可能只是落在同一个时钟分辨率之内。

要点: 不要假定时间戳最小的事件就一定最早发生。

用时间戳当主键的冲突

设想你在开发一个支付服务,决定用当前 Unix 时间戳作为每笔支付的主键。

起初一切正常。

然后两位客户在同一秒内完成支付。两个请求生成了相同的时间戳,都试图插入主键相同的行。

其中一次插入失败。

你把精度从秒换成毫秒,冲突概率降低,但仍可能发生。换成纳秒也不能保证唯一性。

问题未必在于精度不够,而在于你要求时钟提供一项它本就不是为此设计的保证。

更好的设计把“身份”与“时间”分开:

CREATE TABLE payments (
id UUID PRIMARY KEY,
created_at TIMESTAMPTZ NOT NULL,
amount NUMERIC NOT NULL
);

这个 PostgreSQL 风格的例子用 UUID 标识每笔支付,用时间戳记录创建时间。UUID 仍需由应用或数据库生成。

每个字段职责清晰。

要点: 时间戳描述某事何时发生,标识符则把它与其他事物区分开。

终端实验:date、timedatectl 与 Python

理论说够了,打开终端运行这些命令:

# What time does your system report?
date
# What time is it in UTC?
date -u
# What is the current Unix timestamp?
date +%s
# What does the system report about time synchronization?
timedatectl status

最后一条命令面向基于 systemd 的 Linux 系统。其余命令在类 Unix 系统上普遍可用,但选项可能因实现而异。

如果你安装了 Python 3,再运行:

import time
print("Wall clock:", time.time())
print("Monotonic clock:", time.monotonic())
print("Performance counter:", time.perf_counter())

这三个调用用途不同:

  • time.time() 给出基于纪元(epoch)的墙钟时间。
  • time.monotonic() 适合测量经过的时间。
  • time.perf_counter() 用于测量短时性能。

示例输出:

Wall clock: 1792679694.0232277
Monotonic clock: 445.774622884
Performance counter: 445.774647309

单调时钟和性能计数器不提供普通的日历时间戳,不要拿它们的原始值与 Unix 时间戳比较。

底层 Linux 细节可参阅 clock_gettime() 手册。

真正复杂的地方

你从一个自 1970 起计数的整数出发,如今已经遇到不准确的时钟、时间同步、夏令时切换、分布式事件排序和数据库身份问题。

出人意料的不是时间管理本身有多复杂,而是如此多的软件依赖它,却把它当成已经解决的问题。

还有更多内容值得探索:闰秒、2038 年问题、时间戳精度,以及编程语言处理 Unix 纪元之前日期的微妙方式。