概述
一次线上故障让我彻底重视起时间同步这件事。某天凌晨,业务反馈 HTTPS 请求大面积失败,报错 SSL_ERROR_BAD_CERTIFICATE。排查了两个小时,翻遍网络拓扑、防火墙规则、证书配置,都没问题。最后有人无意间看了眼系统时间——比真实时间快了 12 秒。就这 12 秒,让服务器在验证客户端证书的 notBefore 时间时判定"证书尚未生效",拒绝了所有握手。
这不是孤例。时间不同步在分布式系统里是一颗隐形地雷:Kerberos 认证对时间偏差容忍只有 5 分钟,超了直接拒登;Kubernetes 节点时间偏差过大,kubelet 心跳过期,Pod 被驱逐;MySQL 主从复制依赖时间戳,偏差大会导致 binlog 顺序错乱;分布式事务的二阶段提交,节点间时钟不同步会让超时判断失效。
时间同步听起来简单——装个 NTP 客户端,指向时间服务器就完事了。但生产环境的坑远不止于此:虚拟机时钟漂移、容器里没有 NTP 守护进程、多机房时钟源选型、chrony 和 ntpd 冲突、证书续期任务因时间跳变失败。这篇文章把这些问题一次性讲清楚,给出生产可用的配置和排障思路。
从 ntpd 到 chrony:为什么换了
ntpd 是老牌的时间同步服务,跑了二十多年。但从 RHEL 8、CentOS 8、Ubuntu 20.04 开始,主流发行版默认都换成了 chrony。原因不是 ntpd 不好,而是 chrony 在几个关键场景下表现更好:
| 对比维度 | ntpd | chrony |
|---|---|---|
| 首次同步速度 | 慢,需要逐步逼近 | 快,iburst 几秒内拉到毫秒级 |
| 网络中断后恢复 | 慢,重新同步耗时长 | 快,历史漂移率记录加速收敛 |
| 虚拟机时钟漂移 | 处理差,容易抖动 | 处理好,driftfile 自动补偿 |
| 资源占用 | 较高 | 更低 |
| 离线保持 | 漂移大 | driftfile 让离线也能维持 |
| 是否支持时间服务器功能 | 是 | 是(allow 指令) |
关键区别在首次同步速度。ntpd 出于"不能让时间跳变"的设计哲学,启动后是缓慢逼近目标时间,可能要十几分钟才稳定。chrony 的 iburst(initial burst)选项,启动时一次性发 4 个探测包,几秒内就能完成首次同步。这对虚拟机、容器这种频繁启停的场景至关重要——你不可能等 10 分钟才让一个容器的时间正确。
什么时候还用 ntpd:老系统兼容、需要 broadcast/multicast 模式、对硬件时间戳(PTP)有特殊需求。新部署一律推荐 chrony。
chrony 客户端配置
基础配置
# 安装(大多数发行版已预装)
# RHEL/CentOS/Fedora
sudo dnf install chrony -y
# Debian/Ubuntu
sudo apt install chrony -y
# 启动并设置开机自启
sudo systemctl enable --now chronyd
核心配置文件 /etc/chrony.conf,生产环境推荐配置:
# /etc/chrony.conf
# === 时间源 ===
# 国内推荐用阿里云 NTP(延迟低、稳定),多源冗余
server ntp.aliyun.com iburst minpoll 4 maxpoll 6
server ntp1.aliyun.com iburst minpoll 4 maxpoll 6
server time.google.com iburst minpoll 4 maxpoll 6
# pool 作为兜底(NTP Pool Project 全球节点)
pool cn.pool.ntp.org iburst
# === 时钟调整策略 ===
# 时钟偏差 >1 秒时直接跳变(而非缓慢调整),启动时最多跳 3 次
# 对虚拟机特别重要——快照恢复后时间可能差很多
makestep 1.0 3
# 允许负向 makestep(时间往回调)
# 默认只允许往前调,加上 -1 后可往回调,防止"假快"
makestep 1.0 -1
# 定期把系统时间写入硬件时钟(RTC),避免重启后时间回退
rtcsync
# === 漂移记录 ===
# 记录本地时钟的漂移率,离线时也能根据历史漂移率推算时间
driftfile /var/lib/chrony/drift
# === 日志 ===
logdir /var/log/chrony
# 时钟偏移超过 0.5 秒时记日志,便于追溯
logchange 0.5
# 关闭客户端日志(做服务端时才需要打开)
noclientlog
# === 安全限制 ===
# 只允许本地查询 chronyc
bindcmdaddress 127.0.0.1
bindcmdaddress ::1
几个容易踩的参数:
makestep 1.0 3:第一行很多人漏掉。默认 chrony 启动后是"缓慢调整"(slew),每秒只调一点点。如果时钟差了 30 秒,要半小时才能修正。加上makestep 1.0 3,偏差超过 1 秒就立即跳变,启动时最多跳 3 次。虚拟机快照恢复后这个选项是救命的。makestep 1.0 -1:负向跳变。默认只允许时间往前调(变快)。但如果你的时钟比真实时间快了,需要往回调(变慢),就得加-1。不加的话,快了的时钟只能"原地等真实时间追上来",可能要等很久。rtcsync:让系统时间定期写回硬件时钟。不加这行,重启后时间可能回退到旧值。iburst:首次同步加速。不加的话首次同步慢。
验证配置生效
配置改完重载服务(注意用 reload 而非 restart):
sudo systemctl reload chronyd
然后验证:
# 1. 看服务状态
systemctl status chronyd
# 2. 看时间源状态(关键)
chronyc sources -v
chronyc sources -v 的输出长这样:
.-- Mode( ^ = server, = = peer, # = local clock)
/ .- Source state (* = current synced, + = combined, - = not combined)
| / .- Source
| | | .- Stratum(a)
| | | | .- Poll(pp)
| | | | | .- Reach(oct)
| | | | | | .- LastRx
| | | | | | | ==> .- Last sample
| | | | | | | / .- Sample milliseconds
MS Name/IP address Stratum Poll Reach LastRx LastSample
=======================================================================
^* 203.107.6.88 (ntp.aliyun) 2 6 377 16 <arg_value>+1235us[+1235us] +/- 10ms
^+ time.google.com 2 6 377 17 狩+2356us[+2356us] +/- 20ms
^- 192.168.1.100 2 6 377 18 狩-3456us[-3456us] +/- 15ms
看什么:每个源前面有个标记,只有带 * 的才是真正用于校时的源。如果全是 - 或 +,说明 chrony 认为所有源都不可靠——常见于防火墙拦了 UDP 123、DNS 解析失败、或池域名返回了私有 IP。
# 3. 看同步质量
chronyc tracking
输出:
Reference ID : CB6B0658 (203.107.6.88:123)
Stratum : 3
Ref time (UTC) : Fri Jul 18 16:30:00 2026
System time : 0.000123456 seconds slow of NTP time
Last offset : +0.000098765 seconds
RMS offset : 0.000156789 seconds
Frequency : 5.678 ppm slow
Residual freq : 0.001 ppm
Skew : 0.234 ppm
Root delay : 0.012345 seconds
Root dispersion : 0.000567 seconds
Update interval : 64.2 seconds
Leap status : Normal
看什么:
System time:当前系统时间与 NTP 标准时间的偏差。小于 ±10ms 算正常,金融场景要求 ±1ms。Last offset:上次校时后的偏移量。Leap status: Normal:时间正常(不是闰秒状态)。Stratum:你距离一级时间源有几跳。3 表示经过 2 层中继,已经很接近一级源了。
别只看 Leap status。很多人只看 Leap status: Normal 就以为万事大吉,但这个值只要 chrony 能连上任何一个源就显示 Normal。真正要看的是 System time 偏差是否持续收敛到可接受范围。
chrony 作为内网时间服务器
大集群里每台机器都连外网 NTP 不现实——出口带宽浪费、延迟高、防火墙规则复杂。标准做法是内网搭 1-2 台时间服务器,对外连权威源,对内给所有机器提供校时服务。
# /etc/chrony.conf(内网时间服务器配置)
# 上游时间源(连外网)
server ntp.aliyun.com iburst
server time.google.com iburst
pool cn.pool.ntp.org iburst
# 允许内网网段来同步
allow 192.168.0.0/16
allow 10.0.0.0/8
# 本地时钟作为兜底(当所有上游源都不可达时)
# stratum 10 表示这个本地时钟可信度低,只在万不得已时用
local stratum 10
# 服务端需要记录客户端访问(排障用)
# noclientlog # 客户端多时关掉省日志
clientloglimit 52428800 # 单客户端日志上限 50MB
# 其他参数同客户端配置
makestep 1.0 -1
rtcsync
driftfile /var/lib/chrony/drift
bindcmdaddress 127.0.0.1
关键点 local stratum 10:这行让时间服务器在外网源全挂时,仍能用本地时钟给内网提供时间——虽然不准,但至少内网所有机器时间一致。一致性比绝对准确更重要,这是分布式系统的铁律。
内网客户端配置只需指向内网时间服务器:
# /etc/chrony.conf(内网客户端)
server 192.168.1.100 iburst prefer # 主时间服务器
server 192.168.1.101 iburst # 备时间服务器
makestep 1.0 -1
rtcsync
driftfile /var/lib/chrony/drift
防火墙放行:时间服务器需要开放 UDP 123 端口:
# firewalld
sudo firewall-cmd --permanent --add-service=ntp
sudo firewall-cmd --reload
# iptables
sudo iptables -A INPUT -p udp --dport 123 -s 192.168.0.0/16 -j ACCEPT
虚拟机与容器场景的时间同步
虚拟机时钟漂移
虚拟机的时间同步是重灾区。虚拟机的"时钟"是宿主机虚拟出来的,宿主机负载高时,虚拟机感知到的时间会抖动。VMware/QEMU/KVM 各家的虚拟时钟精度不同,但都有这个问题。
解决方案:
- 宿主机装时间同步(VMware Tools、QEMU guest agent 或 chrony)
- 虚拟机也装 chrony,双保险
- 虚拟机配置里启用时间同步选项(VMware Tools 的 time sync)
虚拟机里 chrony 的特殊配置:
# /etc/chrony.conf(虚拟机)
# 轮询间隔缩短,更快发现漂移
server ntp.aliyun.com iburst minpoll 2 maxpoll 8
# 虚拟机时钟抖动大,允许更大范围的 makestep
makestep 0.1 -1
# 更频繁地记录漂移率
driftfile /var/lib/chrony/drift
# rtcsync 对虚拟机意义不大(虚拟 RTC),但加上无害
rtcsync
# 关闭硬件时间戳(虚拟机没有)
# hwtimestamp * ← 注释掉这行
容器场景
容器的时间同步是个特殊问题。容器默认共享宿主机的内核时钟——宿主机时间对了,容器时间就对。但有两个坑:
坑一:容器里装 chrony 没用。容器没有独立的内核时钟,容器内的 chrony 调整时间实际上在调宿主机的时间。而且容器默认没有 CAP_SYS_TIME 权限,连 settimeofday 系统调用都执行不了,chrony 会报权限错误。正确做法是宿主机装 chrony,容器不用管时间。
坑二:容器里看到的时间是对的,但应用行为异常。常见于 Java 应用——JVM 启动时读了一次系统时间,后续依赖这个缓存的基准时间做定时任务。如果宿主机时间在 JVM 启动后发生了跳变,JVM 内部的时间感知和系统时间就对不上。解决办法:JVM 启动参数加 -XX:+UseSystemTimeSource。
容器必须单独管时间的场景:特权容器、用独立 namespace 的容器、某些安全合规要求。这种情况下给容器加 CAP_SYS_TIME 能力:
# docker-compose.yml
services:
chrony:
image: chrony:latest
cap_add:
- SYS_TIME
network_mode: host # NTP 用 UDP 123,host 网络最省事
restart: unless-stopped
# Kubernetes Pod
apiVersion: v1
kind: Pod
metadata:
name: chrony-sidecar
spec:
containers:
- name: chrony
image: chrony:latest
securityContext:
capabilities:
add: ["SYS_TIME"]
volumeMounts:
- name: etc-chrony
mountPath: /etc/chrony.conf
subPath: chrony.conf
volumes:
- name: etc-chrony
configMap:
name: chrony-config
警告:这种方式只在必须时用。正常情况下容器时间靠宿主机,容器内跑 chrony 是反模式——多个容器同时调宿主机时钟会互相打架。
监控时间同步状态
时间同步状态必须纳入监控。光配好不够,得能持续确认"时间真的同步了"。
用 node_exporter 采集
node_exporter 本身不采集时间同步状态,但可以用 textfile collector 配合脚本采集。写个脚本读 chronyc tracking 输出,转成 Prometheus 指标:
#!/usr/bin/env python3
"""chrony_status_exporter.py
解析 chronyc tracking 输出,暴露时间同步指标给 Prometheus。
"""
import re
import subprocess
from prometheus_client import CollectorRegistry, Gauge, write_to_textfile
registry = CollectorRegistry()
# 系统时间与 NTP 标准时间的偏差(秒)
clock_offset = Gauge(
'chrony_clock_offset_seconds',
'System clock offset from NTP time in seconds',
registry=registry
)
# 同步状态(0=未同步, 1=正常, 2=闰秒警告)
leap_status = Gauge(
'chrony_leap_status',
'Leap status: 0=not synced, 1=normal, 2=leap warning',
registry=registry
)
# 与参考源的根延迟
root_delay = Gauge(
'chrony_root_delay_seconds',
'Root delay to reference source in seconds',
registry=registry
)
# Stratum 层级
stratum = Gauge(
'chrony_stratum',
'Stratum of this clock relative to reference',
registry=registry
)
try:
output = subprocess.check_output(['chronyc', 'tracking'], text=True)
# 解析 System time
m = re.search(r'System time\s+:\s+([\d.]+)\s+(\w+)', output)
if m:
value = float(m.group(1))
if m.group(2) == 'slow':
value = -value
clock_offset.set(value)
# 解析 Leap status
m = re.search(r'Leap status\s+:\s+(\w+)', output)
if m:
status_map = {'Normal': 1, 'Insert second': 2, 'Delete second': 2, 'Not synchronised': 0}
leap_status.set(status_map.get(m.group(1), 0))
# 解析 Root delay
m = re.search(r'Root delay\s+:\s+([\d.]+)\s+seconds', output)
if m:
root_delay.set(float(m.group(1)))
# 解析 Stratum
m = re.search(r'Strum\s+:\s+(\d+)', output)
if m:
stratum.set(int(m.group(1)))
except (subprocess.CalledProcessError, FileNotFoundError):
# chrony 不可用,指标全部置 0
leap_status.set(0)
write_to_textfile('/var/lib/node_exporter/textfile/chrony_status.prom', registry)
丢进 crontab 每分钟跑一次:
# /etc/cron.d/chrony-exporter
* * * * * root /usr/local/bin/chrony_status_exporter.py
Prometheus 告警规则
groups:
- name: time_sync
interval: 30s
rules:
# P1:时间偏差超过 100ms
- alert: ClockOffsetHigh
expr: abs(chrony_clock_offset_seconds) > 0.1
for: 2m
labels:
severity: P1
annotations:
summary: "{{ $labels.instance }} 时钟偏差过大"
description: "系统时间与 NTP 标准时间偏差 {{ $value }} 秒,超过 100ms 阈值"
# P0:时间偏差超过 1 秒(可能导致证书失效、认证失败)
- alert: ClockOffsetCritical
expr: abs(chrony_clock_offset_seconds) > 1
for: 1m
labels:
severity: P0
annotations:
summary: "{{ $labels.instance }} 时钟严重偏差"
description: "系统时间偏差 {{ $value }} 秒,可能导致 TLS 证书校验失败、Kerberos 认证失败"
# P1:chrony 未同步
- alert: ChronyNotSynced
expr: chrony_leap_status == 0
for: 5m
labels:
severity: P1
annotations:
summary: "{{ $labels.instance }} chrony 未同步"
description: "chrony 连续 5 分钟未与任何时间源同步"
# P2:Stratum 过高(离时间源太远)
- alert: ChronyStratumHigh
expr: chrony_stratum > 10
for: 10m
labels:
severity: P2
annotations:
summary: "{{ $labels.instance }} Stratum 层级过高"
description: "当前 Stratum {{ $value }},距离一级时间源过远,精度差"
常见故障排查
故障一:chrony 启动但无法同步
# 看时间源状态
chronyc sources -v
# 如果全是 ^-(横线),说明源不可用
排查步骤:
# 1. 测试网络连通性(NTP 用 UDP 123)
nc -uvz ntp.aliyun.com 123
# 或用 ntpdate 测试(注意:ntpdate 已废弃,仅测试用)
ntpdate -q ntp.aliyun.com
# 2. 检查防火墙
sudo firewall-cmd --list-services
sudo iptables -L -n | grep 123
# 3. 检查 DNS 解析
nslookup ntp.aliyun.com
dig ntp.aliyun.com
# 4. 看 chrony 日志
journalctl -u chronyd --since "10 minutes ago"
常见原因:云厂商安全组没放行 UDP 123、DNS 解析失败、NTP 源服务器宕机。换国内源通常能解决网络问题。
故障二:时间偏差过大无法修正
chrony 默认不修正超过 1000 秒的偏差。这是保护机制——防止配置错误的时间源把系统时间带偏。
# 手动强制同步(跳变)
sudo chronyc makestep
# 如果偏差还是很大,先手动设置大致时间,再让 chrony 微调
sudo date -s "2026-07-19 00:30:00"
sudo chronyc makestep
前提是配置里有 makestep 1.0 -1,否则手动 makestep 也会被拒绝。
故障三:系统重启后时间回退
典型表现:重启前时间是对的,重启后时间回到了几天前。
原因:硬件时钟(RTC)没同步。系统时间在运行时是靠内核维护的,重启后从 RTC 读取初始时间。如果 RTC 没写回正确时间,重启后时间就回退了。
# 检查 RTC 时间
sudo hwclock --show
# 如果 RTC 时间不对,手动同步
sudo hwclock --systohc # 把系统时间写入 RTC
# 确认 chrony 配置里有 rtcsync
grep rtcsync /etc/chrony.conf
# 没有就加上,重启 chronyd
故障四:双系统(Windows + Linux)时间冲突
装双系统的机器常见问题:Windows 显示时间正确,重启进 Linux 后时间快了 8 小时。
原因:Windows 默认把硬件时钟当作本地时间,Linux 默认把硬件时钟当作 UTC。两边对 RTC 的理解不一致。
# 让 Linux 把 RTC 当作本地时间
sudo timedatectl set-local-rtc 1
# 但这违反 Linux 惯例,推荐反过来改 Windows
更推荐的做法是在 Windows 里改注册表,让 Windows 把 RTC 当 UTC:
# Windows 注册表
[HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\TimeZoneInformation]
"RealTimeIsUniversal"=dword:00000001
故障五:ntpd 和 chronyd 冲突
同一台机器不能同时跑 ntpd 和 chronyd,它们会抢 UDP 123 端口,互相干扰。
# 检查是否有 ntpd 在跑
systemctl status ntpd 2>/dev/null
systemctl status ntp 2>/dev/null
# 如果有,停掉并禁用
sudo systemctl disable --now ntpd
sudo systemctl disable --now ntp
# 还要检查 systemd-timesyncd
systemctl status systemd-timesyncd
# 如果 chronyd 在跑,timesyncd 应该是 inactive
规则:一台机器只用一个时间同步服务。chronyd、ntpd、systemd-timesyncd 三选一,不能并存。
时间同步与 TLS 证书的隐患
前面提到的故障案例——12 秒偏差导致 HTTPS 大面积失败——根源在于 TLS 证书的有效期校验。
X.509 证书有两个时间字段:notBefore(生效时间)和 notAfter(失效时间)。OpenSSL 在握手时验证当前系统时间是否落在 [notBefore, notAfter] 区间内。如果系统时间比 notBefore 还早(“证书还没生效”)或比 notAfter 还晚(“证书已过期”),握手直接失败。
最危险的场景:Let’s Encrypt 证书的自动续期任务依赖系统时间判断"证书快过期了该续了"。如果系统时间比真实时间慢,续期任务以为"证书还有效不急",但真实时间下证书已经过期。等用户访问时报错,你才发现证书过期了,但自动续期任务因为时间错误根本没触发。
防范措施:
- 监控证书剩余有效期(不依赖本地时间,用
openssl s_client连远程服务读证书) - 续期任务加容错——即使时间判断"不用续",也定期强制检查一次
- 时间偏差超过 30 秒就告警,别等出了事才发现
# 不依赖本地时间的证书过期检查
echo | openssl s_client -connect example.com:443 -servername example.com 2>/dev/null \
| openssl x509 -noout -dates
# notBefore=Jul 19 00:00:00 2026 GMT
# notAfter=Oct 17 00:00:00 2026 GMT
把这段做成监控脚本,从外部探测证书有效期,不依赖被监控机器的本地时间。
生产实践建议
1. 分层时间源架构。 大集群不要所有机器都连外网 NTP。搭 1-2 台内网时间服务器,对外连权威源,对内提供服务。内网源挂了还有 local stratum 兜底,保证内网时间一致。一致性比绝对准确重要。
2. 虚拟机和物理机分开管。 物理机时钟稳定,chrony 默认配置即可。虚拟机时钟抖动大,需要缩短轮询间隔、加大 makestep 容忍。容器靠宿主机,别在容器里跑 chrony。
3. 监控不只看服务状态。 systemctl status chronyd 显示 active 不代表时间同步正常。监控要读 chronyc tracking 的实际偏差值,偏差超阈值就告警。
4. 警惕时间跳变。 makestep 会导致时间跳变,对某些应用有冲击(数据库、定时任务、日志顺序)。金融交易系统建议关闭 makestep,用 slew 模式缓慢调整。但首次部署或偏差很大时,跳变比缓慢调整更实际。
5. 闰秒是个坑。 闰秒发生时(通常 6 月底或 12 月底),23:59:59 后会插一个 23:59:60。很多系统处理不了这个秒,直接报错或崩溃。chrony 支持 leapsectz 指令处理闰秒,但建议在闰秒前测试一遍。Google 的做法是"涂抹闰秒"——把一秒的偏差分散到 24 小时内逐渐修正,避免瞬间跳变。
总结
时间同步是分布式系统的基础设施,但它太"底层"了,平时没人关注,出了事就是大事。我见过因为时间偏差导致支付系统停摆两小时的案例,也见过 K8s 集群节点因时钟不同步被大规模驱逐的故障。这些问题的共同点是:排查时从没第一时间怀疑时间——因为"时间还能错?"
chrony 替代 ntpd 是趋势,配置不复杂,但参数调优有讲究。makestep、rtcsync、driftfile 是三个必配项,多源冗余和 local stratum 兜底是生产标配。虚拟机和容器场景有各自的坑,记住一条核心原则:容器靠宿主机,虚拟机靠宿主机+chrony 双保险。
监控要做,而且要监控真实的时钟偏差值,不是只看服务进程活着没。时间偏差超过 1 秒就要 P0 告警——这个阈值不是为了吓人,是因为 1 秒偏差足以让 TLS 证书校验、Kerberos 认证、分布式锁全部出问题。
最后一点经验:时间问题排查时,先看系统时间准不准,再看服务配置。很多人一上来就查 chrony 配置、翻日志,最后发现是系统时间被手动改过或 RTC 没同步。从最简单的开始排查。
参考资料与致谢
本文在撰写过程中参考了以下资料,感谢原作者的贡献:
- chrony如何配置多个NTP服务器实现高可用时间同步? — CSDN问答,chrony 选源算法与多源高可用配置范式
- 5分钟搞定CentOS7时间同步:chronyd配置全攻略 — CSDN专栏,chronyd 核心配置与 ntpq 命令详解
- 如何在Linux中配置系统的本地时钟同步 — PHP中文网,chrony 服务检查与配置验证实战
- Linux系统时间同步配置_ntp与chrony实践 — PHP中文网,chrony 与 ntpd 对比及故障排查要点
- 主_旁路由时间不同步引发HTTPS证书批量失效:chrony高精度同步配置规范 — CSDN专栏,时间不同步导致 TLS 证书失效的根因分析与 PTP 硬件时间戳配置
- 北京时间服务器IP如何精准同步系统时间? — CSDN问答,时间同步异常的多维根因分析矩阵
- 2026年容器安全NTP服务配置实践指南 — 人人文档,容器环境下 NTP 服务的挑战与配置实践