NTP 原理与报文 — 48 字节头、四时间戳算法与时钟纪律
拆解 NTP 报文字段、offset/delay 推导、stratum 层级与选择-聚类-合并算法 / 应用层 / UDP 123 / RFC 5905
先建立直觉
NTP 干的事,本质上就是"打电话对表":你记一下自己发出请求的时刻,对方回话时也报出他收到和发出的时刻,你再记一下自己收到的时刻。四个时刻一凑,只要默认"请求去程和响应回程在路上花的时间差不多",就能算出你比对方快还是慢了多少、这条路来回要多久——然后不开玩笑地、一点点地把你的表频率拧准,而不是猛地一跳。
它解决什么问题
NTP 的核心任务是回答两个问题:我的时钟和参考时钟差多少(offset)?这条链路的往返延迟是多少(delay)? 这两个数字不解决,日志顺序、认证票据、证书有效期、分布式事务这些全都会乱。下面看它具体怎么算。
工作流程(简化版)
- 客户端发请求,记下发送时刻
t1,请求里带上自己的发送时间戳。 - 服务端收到(
t2)再发出响应(t3),把t1/t2/t3都填进响应包。 - 客户端收到响应记下
t4,用四时间戳公式算出 offset 与 delay。 - 每隔一段时间(poll)重复采样,每个源保留最近 8 次结果,挑 delay 最小的那次。
- 多个源的结果先剔除"说谎源",再聚类、加权合并出"系统偏移"。
- 用反馈环路(PLL/FLL)调本地时钟频率缓慢逼近正确值;只有偏差极大或刚启动时才会直接跳变。
报文 / 头部长什么样
看这张表前先记住:NTP 报文头固定 48 字节(可选追加认证扩展与 MAC),是一段紧凑的二进制头,没有"一行行可读文本",所有字段都靠位宽精确摆放。
NTP 报文头固定 48 字节(可选追加认证扩展与 MAC)。
| |
2.1 字段详解
| 字段 | 位宽 | 含义与取值 |
|---|---|---|
| LI (Leap Indicator) | 2 bit | 0=无警告;1=当天最后一分钟有 61 秒(插入闰秒);2=最后一分钟有 59 秒(删除闰秒);3=时钟未同步(告警状态) |
| VN (Version Number) | 3 bit | 协议版本,当前为 4(NTPv4);3 为 NTPv3 |
| Mode | 3 bit | 0=保留;1=对称主动;2=对称被动;3=客户端;4=服务端;5=广播;6=控制(ntpq);7=私有保留(monlist 即在此模式,反射放大源头) |
| Stratum | 8 bit | 0=Kiss-o-Death(未指定/无效,refid 载 KoD 码);1=主服务器(直连参考钟);2~15=次级服务器;16=未同步;17~255=保留 |
| Poll | 8 bit | 有符号,最大轮询间隔的 log₂ 秒。6=64 s、10=1024 s;chrony 常见 6~10 |
| Precision | 8 bit | 有符号,本地时钟精度的 log₂ 秒。-20 ≈ 1 μs,-24 ≈ 60 ns |
| Root Delay | 32 bit | 到 Stratum 1 参考源的总往返延迟,16.16 定点秒 |
| Root Dispersion | 32 bit | 到参考源的最大累积误差,16.16 定点秒。二者共同构成"根离散",用于评估可信度 |
| Reference ID | 32 bit | Stratum 1 时为 4 字符 ASCII 源码(GPS、PPS、DCF、ATOM、GOES、PTP);Stratum ≥ 2 时为上游服务器 IPv4 地址(IPv6 为地址哈希前 4 字节);Stratum 0 时为 KoD 码(RATE、DENY、RSTR) |
| Reference Timestamp | 64 bit | 本地时钟最近一次被校准的时刻 |
| Origin Timestamp | 64 bit | t1:服务端原样回填客户端的发送时间戳(用于匹配请求,也是防伪造的弱校验) |
| Receive Timestamp | 64 bit | t2:服务端收到请求的时刻 |
| Transmit Timestamp | 64 bit | t3:服务端发出响应的时刻(在请求包中则是 t1) |
| Extension / MAC | 变长 | 可选:NTS 扩展字段;或 Key ID(4B) + Message Digest(16/20B) 对称密钥认证 |
2.2 NTP 时间戳格式(64 位)
看这张表前先记住:NTP 的时间戳纪元是 1900 年,不是大家更熟悉的 Unix 1970 年;这一字之差是换算时最容易写错的地方。
| 部分 | 位宽 | 含义 |
|---|---|---|
| Seconds | 32 bit | 自 1900-01-01 00:00:00 UTC 起的秒数(注意不是 Unix 纪元 1970) |
| Fraction | 32 bit | 秒的小数部分,分辨率 = 2⁻³² s ≈ 233 皮秒 |
换算关系:
| |
Era 问题:32 位秒数在 2036-02-07 06:28:16 UTC 溢出(约 136 年一个 Era)。NTPv4 通过 Era Number 概念处理,实现上要求节点初始时间误差在 68 年内即可正确归属 Era。
2.3 Kiss-o-Death(KoD)码
看这张表前先记住:当对端回给你的包
Stratum = 0,它不是"第 0 层",而是在用 refid 字段向你发"别再打了"的拒绝信号(Kiss-o-Death)。
Stratum = 0 时,refid 携带 4 字符 ASCII 告知客户端"别再打了":
| KoD 码 | 含义 | 客户端应对 |
|---|---|---|
RATE | 轮询过于频繁,超出限速 | 立即增大 poll 间隔 |
DENY | 服务器拒绝服务 | 停止访问该服务器 |
RSTR | 访问被策略限制 | 停止访问 |
ACST | 任播服务器(信息性) | — |
AUTH | 认证失败 | 检查密钥配置 |
INIT / STEP | 服务端正在初始化 / 步进调整中 | 稍后重试 |
交互时序
一句话看懂这张图:从 GPS 经 Stratum 1 → 2 逐级下发,客户端发请求、收响应算 offset/delay,再多次采样滤波、多源统计合并,最后用反馈环路调频率(小偏差只 slew、大偏差才 step),被限速时收到 KoD 就调大 poll。
| |
关键机制 / 变体
4.1 四时间戳模型与推导
这是干什么的:用一次往返的四个时刻,在"默认来回路径对称"的前提下,把"钟差"和"网络延迟"两个未知量同时解出来。
一次客户端-服务端交换产生 4 个时间戳:
| 时间戳 | 记录方 | 时机 | 报文字段 |
|---|---|---|---|
t1 | 客户端 | 发出请求的瞬间 | Transmit Timestamp(请求包) |
t2 | 服务端 | 收到请求的瞬间 | Receive Timestamp(响应包) |
t3 | 服务端 | 发出响应的瞬间 | Transmit Timestamp(响应包) |
t4 | 客户端 | 收到响应的瞬间 | 本地记录,不在报文中 |
推导(关键假设:网络往返路径对称):
直觉解释:(t2-t1) 是"去程观测差"(含钟差 + 去程延迟),(t3-t4) 是"回程观测差"(含钟差 - 回程延迟),两者取平均恰好抵消掉延迟,剩下钟差。
注意:(t3 - t2) 是服务端处理耗时,从往返总时间中扣除它才是真正的网络时延。
4.2 路径不对称是最大误差源
这是干什么的:提醒你 NTP 的精度天花板在哪——它默认来回对称,而现实链路往往不对称。
若去程 10 ms、回程 30 ms,NTP 会误认为各 20 ms,产生 10 ms 的系统性偏差。这就是:
- 广域网 NTP 精度通常只有几毫秒到几十毫秒;
- 非对称链路(ADSL 上下行不同、跨运营商绕行)会引入固定偏差;
- PTP 通过硬件时戳 + 路径测量把这一误差降到亚微秒。
4.3 从"测量"到"调时"
这是干什么的:单次测量含噪声,NTP 不会照单全收,而是"多采样 + 多源统计 + 反馈调频"三步把误差磨平。
单次测量含噪声,NTP 不会立即照搬 offset,而是:
- 多次采样(poll 间隔 64~1024 秒,动态调整),维护每个源的 8 次采样滤波器(clock filter,按 delay 最小优先选取);
- 多源统计:Selection → Clustering → Combining(见 §4.2);
- 时钟纪律(Clock Discipline):用 PLL/FLL 混合环路调整本地时钟频率,使其缓慢逼近正确时间,同时把频率误差写入 drift 文件,下次启动直接补偿。
4.4 Stratum 层级与"根"度量
这是干什么的:解释"Stratum 这个层级数字到底代表什么",以及为什么不能只看它大小判断准不准。
| Stratum | 含义 | 示例 |
|---|---|---|
| 0 | 参考时钟本体(不直接联网) | 铯原子钟、GPS/北斗接收机、DCF77 长波、PPS 秒脉冲 |
| 1 | 直连 Stratum 0 的主服务器 | time.nist.gov、ntp.aliyun.com、国家授时中心 |
| 2 | 同步自 Stratum 1 | 企业内网时间服务器 |
| 3~15 | 逐级下降 | 业务服务器、交换机、终端 |
| 16 | 未同步(不可用) | 刚启动或失联的节点 |
每经过一级,Root Delay 与 Root Dispersion 累加,可信度下降。Stratum 数字小 ≠ 一定更准——如果 Stratum 1 在地球另一端、RTT 300 ms,还不如同机房的 Stratum 3 靠谱。选源应综合 stratum、delay、jitter、dispersion。
4.5 选择-聚类-合并三步算法(NTP 精髓)
这是干什么的:告诉你 NTP 靠什么识别出"说谎的时间源"并综合出可信时间——也是"为什么至少要配 4 个源"。
| 步骤 | 目的 | 做法 |
|---|---|---|
| Selection(选择) | 剔除falseticker(说谎的时间源) | 用 Marzullo/交集算法:每个源给出置信区间 [offset-δ, offset+δ],找被最多区间覆盖的交集;不在交集内的源被判为 falseticker 淘汰 |
| Clustering(聚类) | 从幸存者中筛出最一致的一组 | 反复剔除离散度(jitter/dispersion)最大的外围源,直到剩余源足够一致 |
| Combining(合并) | 得出单一系统偏移 | 按各源权重(与 root distance 成反比)加权平均 |
推论:配置的服务器数量很关键。
- 1 个源:无法检测其是否说谎;
- 2 个源:分歧时无法判断谁对;
- 3 个源:可识别 1 个 falseticker(但幸存者只剩 2 个,勉强);
- ≥4 个源:能可靠剔除 1 个错误源,是生产推荐最小配置。
4.6 时钟纪律:slew vs step
这是干什么的:解释"调时间"的两种手法,以及为什么默认宁可慢调也不直接跳——因为跳变会把依赖时间的程序搞崩。
| 方式 | 触发条件 | 行为 | 风险 |
|——|———|——|
| slew(渐进) | 偏差较小(chrony 默认 <1 s 用 slew;ntpd <128 ms) | 通过 adjtimex() 修改时钟频率(最大 500 ppm,即每秒最多追 0.5 ms),时间单调不倒退 | 追大偏差很慢:128 ms 需约 256 秒 |
| step(步进) | 偏差大或启动时 | 直接 settimeofday() 跳变 | 时间可能倒退,破坏依赖单调时间的程序(数据库、定时器、日志顺序) |
| panic | ntpd 默认偏差 > 1000 s | 拒绝调整并退出,防止误配置造成灾难 | 需 ntpd -g 或 tinker panic 0 放行 |
工程实践:启动时允许一次 step,运行中只 slew。chrony 的 makestep 1.0 3 即"前 3 次更新中若偏差 >1 s 则 step,之后只 slew"。
4.7 频率补偿(drift)
这是干什么的:说明 NTP 为什么断网也不崩——它把"我的表天生走快/走慢多少"记进文件,下次直接补偿。
晶振偏差是系统性的(如恒定快 12 ppm)。NTP 测出该频率误差后写入 drift 文件(/var/lib/ntp/ntp.drift 或 /var/lib/chrony/drift),下次启动直接加载,即使断网也能维持较好精度——这叫自由运行(free-running)保持能力。
4.8 工作模式
这是干什么的:列清 NTP 那些 Mode 编号分别干什么,尤其要记住 mode 7 是安全隐患。
| Mode | 名称 | 场景 |
|---|---|---|
| 1 / 2 | 对称主动 / 被动 | 同级服务器互为对等体(peer),互相校验,常用于 Stratum 1 之间 |
| 3 / 4 | 客户端 / 服务端 | 最常见的单播模式 |
| 5 / 6 | 广播 / 组播 | 服务端周期性广播(224.0.1.1 / ff05::101),客户端被动接收;无法测量延迟,精度差且易被伪造,不推荐 |
| 6 | 控制 | ntpq 查询(NTPv2 控制报文) |
| 7 | 私有 | ntpdc 的 monlist 等,必须禁用 |
4.9 闰秒(Leap Second)
这是干什么的:解释那个会让程序宕机的"23:59:60"是怎么回事,以及现代为什么改用"抹平"。
- UTC 为跟上地球自转不均匀,会在 6/30 或 12/31 末尾插入一秒(
23:59:60)。 - NTP 通过 LI 字段在闰秒当天提前告知客户端。
- 历史上闰秒引发过大规模故障(2012 年 Linux 内核 hrtimer 死锁导致 Reddit、LinkedIn 等宕机)。
- 现代主流方案:闰秒抹平(Leap Smear)——Google/AWS/阿里云的 NTP 服务把这 1 秒分散到前后 12~24 小时内微调(每秒慢约 11.6 μs),客户端全程无感、无 23:59:60。
- 注意:抹平源与非抹平源不能混用,否则两组源会互判为 falseticker。
- 国际计量大会(2022 CGPM)已决议最晚 2035 年前取消闰秒。
4.10 安全问题与防护
这是干什么的:点明 NTP 历史上的两大安全坑——反射放大 DDoS 和时间欺骗,以及对应的现代防护(NTS、对称密钥)。
| 威胁 | 说明 | 防护 |
|---|---|---|
| monlist 反射放大 DDoS | mode 7 的 monlist 请求 234 字节可返回最多 600 条记录(约 48 KB),放大约 556 倍(CVE-2013-5211) | 升级 ntpd ≥ 4.2.7p26;disable monitor;restrict ... noquery;改用 chrony(不实现 mode 7) |
| 时间欺骗(MITM) | 伪造 NTP 响应把时间调远,可使过期证书重新"有效"、令 TOTP 失效、绕过 Kerberos 票据有效期 | 多源配置、maxdistance 限制、NTS 或对称密钥认证 |
| 抢答/伪造 | 攻击者猜测 Origin Timestamp 伪造响应 | t1 是 64 位高精度随机化时间戳,猜中概率极低;NTS 提供密码学保证 |
| NTS(RFC 8915) | 基于 TLS 1.3 在 TCP 4460 完成密钥交换(NTS-KE),再用 AEAD 保护 UDP 123 上的 NTP 报文;含 Unique Identifier 与 cookie 机制防重放与追踪 | 推荐:server time.cloudflare.com iburst nts |
| 对称密钥认证 | 共享密钥 + MD5/AES-CMAC,密钥分发困难,仅适合可控内网 | keys / trustedkey 配置 |
常见误区
- “时区差 8 小时就是时间不对。” 错。时区只是显示口径;NTP 管的是 UTC 本身准不准。显示差 8 小时用
timedatectl set-timezone,与 NTP 无关。 - “Stratum 越小一定越准。” 错。一个 RTT 300 ms 的远端 Stratum 1,精度可能远不如同机房 RTT 0.2 ms 的 Stratum 3。选源要综合 stratum、delay、jitter、dispersion。
- “NTP 时间戳和 Unix 一样从 1970 年算。” 错。NTP 纪元是 1900-01-01,换算要减 2208988800;这点写错会导致时间整体偏移 70 年。
- “NTP 调时间都是一下子跳过去。” 错。默认小偏差只 slew(改频率缓慢逼近),避免时间倒退;只有偏差极大或刚启动才 step。
- “配 1~2 个时间源够了。” 错。少于 4 个源无法可靠识别"说谎源"(falseticker),生产建议 ≥4 个不同机构的源。
- “容器里跑个 NTP 客户端同步就行。” 错。容器与宿主共享内核时钟,容器内改时间要么无效要么拖累宿主;正确做法是在宿主机同步、容器只挂时区。
速记口诀
- 四时刻算两差:
t1发、t2收、t3发回、t4收;offset=((t2-t1)+(t3-t4))/2,delay=(t4-t1)-(t3-t2)。 - 对称假设是地基:公式成立前提"去回一样快";链路不对称就引入约一半偏差的误差。
- 四源起步防说谎:1 源无校验、2 源难裁决、3 源勉强、≥4 才能稳剔 1 个 falseticker。
- 小偏差缓调大偏差跳:默认 slew 改频率(不倒退),step 才跳变;断网靠 driftfile 续命。
知识框架
| |