1. 内网为什么会需要一台自建的 NTP 时间服务器
凌晨两点被值班电话叫起来,说集群里三台机器的日志时间戳差了十几分钟,排障时根本拼不出完整的调用链——这种场景我遇到过不止一次。很多人对时间的印象停留在"每台机器自己会跟手机一样自动校准",但真实的数据中心里,Linux 服务器的时间管理是一件需要专门规划的事。这篇内容就是讲清楚 NTP 时间服务器在 Linux 上怎么搭建、怎么配置、怎么验证、怎么排错,从单台小机器到几百个节点的集群都适用。不管你是刚接触 Linux 的运维新人,还是已经在管一批服务器但时间同步一直靠"凑合"的老手,下面这些步骤基本都能直接照着抄。
先把概念说清楚。NTP(Network Time Protocol)是一套用来在网络上传递标准时间的协议,默认走 UDP 123 端口,它做的事本质上是三件:测量本机与上游的时间差、测量网络往返延迟、然后按算法把本机时钟"拉"到接近上游的水平。而"NTP 时间服务器"指的是在内网里承担分发时间这个角色的那台(或那几台)机器——它自己向上游对时,然后对下层的服务器、交换机、数据库、业务容器提供对时服务。这套结构存在的意义很直接:如果全网几十台机器各自去公网对时,一旦出口网络抖动或者上游地址失效,各机器会朝着不同方向漂移,最后就是日志错位、证书校验失败、主从复制判断异常。
1.1 时间漂移带来的真实麻烦
大部分人对时间误差的容忍度是"差不多就行",但服务器环境里"差不多"的代价可能很贵。我先列几个我实际处理过的例子,你对照自己的环境看看有没有踩过。
第一类是日志与链路追踪失效。微服务架构下一次请求会横跨七八个服务,如果 A 机器比 B 机器慢 8 秒,你的日志平台按时间排序出来的调用关系就是反的,排查故障时会被彻底带偏。这个问题在单体应用时代不明显,一旦上了分布式就变成硬伤。
第二类是认证与安全组件报错。很多基于票据的认证机制对时间窗口非常敏感,比如 Kerberos 默认允许的时钟偏差是 5 分钟,超过这个范围票据直接被判定无效,表现就是用户莫名其妙登录不上、集群服务无法互访。TLS 证书校验也会受影响,客户端时间比证书生效时间早,就会报"证书尚未生效"。
第三类是数据库与中间件的判断逻辑错乱。MySQL 主从复制在部分场景下依赖时间戳判断,Kafka 的消息时间戳、Redis 的过期时间、分布式锁的租期计算,全都默认各节点时钟基本一致。时间差一大,会出现"锁还没到期就被判定过期"导致并发写冲突,或者"消息刚发出去就被判定为过期数据"这种很难复现的问题。
第四类是计费、审计、风控类业务直接出错。这类业务往往有明确的时间窗口要求,时间不准意味着账算错了,这种问题的修复成本远高于提前把时间同步做好。
所以我的观点很明确:内网时间同步不是"锦上添花",而是基础设施的第一层地基。而自己的 NTP 服务器的价值在于,你有了一个可控的、稳定的、不依赖外网瞬时状态的内部时间源,哪怕公网出口断了,内网的对时链条也不会断。
1.2 chrony 与 ntpd、systemd-timesyncd 怎么选
Linux 上做时间同步,现在主要有三个候选,我把实际对比列出来:
| 方案 | 典型发行版默认 | 收敛速度 | 虚拟化表现 | 适用场景 |
|---|---|---|---|---|
| chrony | RHEL 8+/CentOS 8+/SUSE | 快,秒级到分钟级 | 好,专为不规律唤醒优化 | 推荐首选,服务端客户端都合适 |
| ntpd | CentOS 7 及更早 | 慢,可能几十分钟 | 一般,虚拟机频繁唤醒时容易积累偏差 | 老系统兼容 |
| systemd-timesyncd | Ubuntu 18.04+ 默认 | 中等 | 一般 | 只做简单客户端,功能很薄 |
结论很清楚:今天要在 Linux 上搭 NTP 时间服务器,直接选 chrony。原因不是"新",而是它解决了一个老 ntpd 解决不好的问题——虚拟机和容器环境下系统不会一直连续运行,可能被暂停、被迁移、被快照恢复,ntpd 的算法假设时钟是连续走的,一旦睡眠后唤醒,它需要很长时间才能重新收敛,甚至长期保持一个固定偏差;chrony 在这方面做了针对性优化,几分钟内就能把偏差压到毫秒级。
还有一个现实原因:chrony 同时具备服务端和客户端能力,配置文件语法简洁,命令行工具chronyc的信息密度比ntpq高,排错时能少查很多文档。至于 systemd-timesyncd,它只适合"我这台机器随便对一下时就行"的场景,它不支持作为服务端为其他机器提供时间,功能上是个阉割版,所以搭建时间服务器时第一件事就是把 Ubuntu 上默认的它关掉——这个坑后面单独讲。
1.3 Stratum 层级到底意味着什么
理解Stratum(层级)这个概念,对排查问题非常关键。NTP 是分层结构的:
- Stratum 0是参考时钟本身,比如原子钟、GPS 授时接收机,它们不直接跑 NTP 协议。
- Stratum 1是直接连接 Stratum 0 的服务器,属于国家级或大型机构的时间源。
- Stratum 2从 Stratum 1 取时间,Stratum 3 从 Stratum 2 取,依此类推,最大到 15,16 就表示不可用。
你在内网自建的那台服务器,正常情况下会显示为 Stratum 3 或 Stratum 4。这个数字大小本身不影响精度——决定精度的是网络延迟和上游质量,不是层级数字。但层级有两个实际用途:一是排查时看到 Stratum 突然变成 16,说明同步丢失了;二是配置里那个local stratum 10参数,意思是当所有上游都不可达时,本机把自己的层级设为 10 继续对外提供时间,保证客户端不至于完全断供。这个参数用得好是保险,用得不好是隐患,后面会详细说。
2. 动手前的准备:环境盘点与上游时间源挑选
直接上手改配置文件是最容易翻车的做法。我在正式动手前一定会做三件事:确认系统版本和虚拟化平台、挑好上游时间源并测试连通性、清理掉会互相打架的服务。这三步花十分钟,能省掉后面两小时的排查。
2.1 系统版本、虚拟化平台与时钟源确认
先看系统,因为配置文件的路径在不同发行版上是不一样的,这是新手最容易卡住的地方:
cat /etc/os-release uname -r两条命令分别看发行版和内核版本。记住这个差异:RHEL/CentOS/Rocky/AlmaLinux 的配置在/etc/chrony.conf,Debian/Ubuntu 的配置在/etc/chrony/chrony.conf。别问我为什么不一样,反正历史上就这么分的,写文档和做自动化的时候必须区分。
接着看虚拟化平台,因为它决定了你的时钟源策略:
systemd-detect-virt cat /sys/devices/system/clocksource/clocksource0/available_clocksource cat /sys/devices/system/clocksource/clocksource0/current_clocksourcesystemd-detect-virt会告诉你是 kvm、vmware、microsoft(Hyper-V)还是 none。后面两条是看可用的时钟源和当前时钟源。经验值是这样的:
- KVM 环境下优先用
kvm-clock,它在半虚拟化下提供更稳定的时钟,比默认的tsc或hpet抗漂移能力强得多。 - VMware 环境下装好
open-vm-tools,它带的时间同步功能会配合宿主机工作,但注意不要和 chrony 抢着改时间,一般建议让 chrony 主导,vmware-toolbox-cmd 的时间同步关掉。 - 物理机就用默认的
tsc一般没问题,高精度场景可以考虑acpi_pm以外的选项,但绝大多数业务没必要折腾。
提示:容器内的机器不要试图在容器里跑 chrony 改时间,容器共享宿主机内核时钟,容器内改时间会失败或者只影响自己命名空间。正确做法是在宿主机上同步时间。
顺便把时区也确认一下,因为它和 NTP 是两件完全不同的事:
timedatectl这条命令的输出里有Local time、Universal time、RTC time、Time zone以及System clock synchronized和NTP service两个状态位。NTP 同步的永远是 UTC 时间,时区只是显示层的换算。我见过有人因为服务器显示时间差 8 小时,就以为 NTP 坏了,其实是时区设成了 UTC 而他期待的是东八区。改时区用timedatectl set-timezone Asia/Shanghai,和 NTP 一点关系都没有。
2.2 上游时间源的选择与连通性自测
自建的服务器自己也得有"老师"。上游时间源的选择原则就三条:网络延迟低、长期稳定、最好不止一个。
常见的做法是选两到四个上游,组成一个小的候选池。选择时有几个考虑点:如果这台机器在内网且能访问公网,可以用公共时间源;如果内网有隔离要求,就用单位已有的上级时间服务器或者带授时功能的设备。配置多个上游的好处是 chrony 会自动做筛选,剔除掉延迟大或者行为异常的那个,避免单点。
配置前一定要先测连通性,别改完配置再发现根本不通。最直接的测试方式是用chronyc自带的探测,或者临时用ntpdate -q(老工具,很多新系统已经不预装,只是用来测):
# 临时安装测试工具(CentOS 系) yum install -y ntpdate ntpdate -q 上游地址-q表示只查询不设置时间,输出会告诉你是哪个层级、偏差多少秒。如果这个命令能出结果,说明 UDP 123 的出方向是通的。这一点极其重要——很多人只记得在防火墙上放行入方向的 123 端口给客户端用,却忘了服务器自己也要能出方向访问上游的 123 端口,结果就是服务端起来了,客户端也能连上它,但它自己永远同步不上,层级一直是 16。这个坑我踩过,也见过太多人在群里问"为什么我的服务器状态是初始化"。
2.3 端口、防火墙与冲突服务的清理
防火墙这块,原则是按需放行、限定来源:
# firewalld(RHEL 系) firewall-cmd --permanent --add-service=ntp firewall-cmd --reload firewall-cmd --list-services # 或者更精细地只放行某个网段 firewall-cmd --permanent --add-rich-rule='rule family="ipv4" source address="192.168.10.0/24" port port="123" protocol="udp" accept'Ubuntu 上用 ufw 的话是ufw allow from 192.168.10.0/24 to any port 123 proto udp。如果用的是云主机,还要记得在云平台的安全组里放行,这一层很多人会漏掉,表现为本机防火墙都关了还是不通。
关于 SELinux,chrony 本身有现成的策略,一般不需要额外处理,用getenforce看下状态即可。如果确实遇到权限拒绝,可以用ausearch -m avc -ts recent找具体的拒绝记录再针对性处理,不要一上来就setenforce 0。
最后是清理冲突服务,这一步必须做:
# Ubuntu/Debian 上关掉 systemd-timesyncd systemctl disable --now systemd-timesyncd # 老系统上如果装了 ntpd 也要关掉 systemctl disable --now ntpd 2>/dev/null || true为什么必须关?因为chronyd 和 systemd-timesyncd 会争抢同一个时钟调整接口,两个进程同时往两个方向拧时钟,结果就是时间反复横跳,你在chronyc tracking里会看到偏移量一直在正负之间跳,怎么都收敛不了。这个现象很有迷惑性,看起来像"上游有问题",实际上是本地两个服务在打架。
3. 服务端配置:chrony.conf 逐行拆解
准备工作做完,进入正题。这一章我会把配置文件拆到每一行,讲清楚每个参数为什么这么写,而不是给你一份"复制粘贴就能用"的配置了事——因为网络环境千差万别,理解了参数才能自己调。
3.1 安装与配置文件位置差异
安装本身没难度:
# RHEL/CentOS/Rocky/AlmaLinux yum install -y chrony # 或者新版本 dnf install -y chrony # Debian/Ubuntu apt update && apt install -y chrony装完之后先别急着改,强烈建议先备份原配置:
cp /etc/chrony.conf /etc/chrony.conf.bak.$(date +%F) # Ubuntu 上是 cp /etc/chrony/chrony.conf /etc/chrony/chrony.conf.bak.$(date +%F)备份这个动作看起来像仪式感,但当你改了一堆参数发现同步不上、想回滚的时候,有个带日期的备份文件能救你一命。我个人的习惯是每次改动前都备份,文件名带上日期和改动原因,半年后回头看能快速回忆起来当时为什么改。
配置文件的注释以#开头,语法是"指令 参数"。下面这份是我在多个内网环境里沉淀下来的服务端配置模板,逐行解释放在后面:
# 上游时间源,多个以备冗余 server ntp1.example.internal iburst minpoll 4 maxpoll 6 server ntp2.example.internal iburst minpoll 4 maxpoll 6 # 公网备用,内网完全隔离时删掉 server ntp.aliyun.com iburst # 记录晶振频率偏差,加快重启后的收敛 driftfile /var/lib/chrony/drift # 允许对时的网段 allow 192.168.10.0/24 allow 10.20.0.0/16 # 前 3 次校准时,偏差超过 1 秒直接步进 makestep 1.0 3 # 定期把系统时间写入硬件时钟 rtcsync # 上游全部不可用时,以 stratum 10 继续对内提供服务 local stratum 10 # 关闭客户端功能,只做服务端(可选) # port 0 # 日志目录 logdir /var/log/chrony3.2 关键指令一行一行讲透
server与pool的区别。server指定一个具体的主机名或 IP,pool指定一个域名池,chrony 会解析出多个地址并自动轮换。内网自建时间源通常用server更可控;如果你的上游是公共的池化域名,用pool更方便。但要注意一个坑:不要在同一份配置里同时用很多个pool,也不要既用pool又重复列出池里的地址,否则同一台机器被走了两次,chrony 的算法会误判。我一般控制上游总数在 3 到 4 个。
iburst这个参数值得单独说。默认情况下 chrony 启动后要等一段时间才会发出第一个探测包,然后逐步收敛,整个过程可能好几分钟。加上iburst后,启动瞬间它会以 2 秒间隔连发 8 个包,让初次同步在几秒内完成。对于需要快速恢复的服务器,这个参数几乎必加。
minpoll和maxpoll是轮询间隔,单位是指数,实际间隔是 2 的 n 次方秒:
| 参数值 | 实际间隔 | 适用场景 |
|---|---|---|
| 4 | 16 秒 | 内网低延迟,收敛快,负载略高 |
| 6(默认最小值) | 64 秒 | 通用折中 |
| 8 | 256 秒 | 稳定环境,降低开销 |
| 10(默认最大值) | 1024 秒(约 17 分钟) | 极稳定环境,抗抖动要求不高 |
内网环境网络抖动能压到亚毫秒级别,所以我把 poll 区间设在 4 到 6,让服务器能快速跟踪上游变化。如果是跨机房或者跨运营商的链路,抖动量级大,把 maxpoll 放小反而会让算法被噪声带偏,这时候更合理的做法是保持默认的 6 到 10,靠长时间平均来抵消抖动。
driftfile记录的是本机晶振相对于标准频率的偏差。有了这个文件,chronyd 重启后不用从头慢慢摸索,能直接带着上次的频率修正量起步,收敛时间能缩短一个量级。注意这个文件所在目录必须对 chronyd 进程可写,否则启动会报错。
makestep 1.0 3是两个参数:阈值和次数。含义是"在启动后的前 3 次时钟更新中,如果偏差超过 1 秒,就直接步进(瞬间跳过去),之后只做 slew(缓慢平滑调整)"。为什么要区分?因为瞬间跳变会导致应用侧感知到时间倒流或跳跃,对数据库、消息队列这类有时间依赖的服务不友好;而 slew 的调整速率非常慢,chrony 默认大约每秒 0.5 毫秒,也就是说 5 秒的偏差要慢慢纠将近 3 小时。所以合理的策略是:开机初期允许步进,快速把大偏差抹平;运行期只允许平滑调整,避免影响业务。
注意:如果一台机器已经运行很久、时间偏差又很大,直接重启服务触发步进可能会影响业务。这种时候更稳的做法是先在维护窗口内用
chronyc makestep手动步进一次,或者用date -s手动设置后立刻启动 chronyd。
allow是权限边界,没写allow时 chrony 默认只响应本机查询。这里的网段一定要写成客户端实际的来源网段,别图省事写0.0.0.0/0全放开,那样一旦这台机器暴露在更大网络上,就会变成一个公开的时间服务,既浪费带宽也有安全风险。多个网段就写多行。
local stratum 10是个双刃剑。它的作用是:当所有上游都失联时,本机仍然以一个较高的层级号继续对外服务,这样客户端不会彻底失去时间源。但它有个前提——本机时钟本身得靠得住。如果这台机器的时钟漂得厉害,你又开了这个选项,那它就会把错误的时间分发给整个内网,比"断供"更糟糕。所以我的做法是:只在物理机上开,或者在有硬件时间源、有冗余上游的情况下开;纯虚拟机且没有可靠上游的,宁愿让它报错也不要开。
rtcsync让系统按固定周期把系统时间写回硬件时钟(RTC)。这样机器断电重启后,BIOS 里的时间不会差得太离谱,能给 chronyd 一个更好的起点。
port 0这个指令是把 NTP 服务端口关掉,让这台机器只做客户端不做服务端。如果你的机器不需要对别人提供服务,加上它等于关掉了一个不必要的监听端口,安全上更干净。
3.3 启动、自启与首次同步的验收动作
配置改完,启动服务:
systemctl enable --now chronyd systemctl status chronyd看到active (running)只是第一步,真正的验收要看同步状态。等 30 秒到 1 分钟让iburst完成初次探测,然后执行:
chronyc sources -v输出里有一个关键列是行首的符号,含义如下:
| 符号 | 含义 |
|---|---|
^* | 当前选中的同步源,一切正常 |
^+ | 可用的备选源,被纳入候选 |
^- | 被合并算法排除的源 |
^? | 不可达或尚未完成探测 |
^x | 被判定为错误时钟的源 |
看到^*才叫成功。如果全是^?,说明探测包没出去或者没回来,回去检查上游地址和出方向防火墙。如果一直有源但选不出来,看chronyc tracking里的层级和偏移。
再执行chronyc tracking,这是我排错时看得最多的一条命令:
chronyc tracking几个字段的解读:Reference ID是当前同步的上游标识;Stratum是你的层级;System time是本机相对上游的偏移量,这个是核心指标,正常应该在毫秒级甚至微秒级;Last offset是上一次更新的偏移;RMS offset是历史偏移的均方根,反映稳定性;Frequency是晶振频率偏差,单位 ppm;Update interval是当前轮询间隔;Leap status显示Normal就对了。
最后确认监听状态:
ss -lunp | grep 123应该能看到 chronyd 监听在 UDP 123 上。到这里,服务端就算搭好了。
4. 客户端接入:从单台手工到批量下发
服务端好了,接下来是让它真正产生价值——把客户端接上来。这一步在小规模环境里手工做没问题,几十台以上就必须走自动化,否则一定会漏机器。
4.1 Linux 客户端的两种接法
客户端有两套常见的实现:chrony 和 systemd-timesyncd。如果你的机器上装的是 chrony,配置和服务端几乎一样,只是不需要allow、local这些服务端指令:
server 192.168.10.10 iburst driftfile /var/lib/chrony/drift makestep 1.0 3 rtcsync改完systemctl restart chronyd,再用chronyc sources -v和chronyc tracking验收,标准和服务端一样。
如果机器上是 systemd-timesyncd(Ubuntu 默认就是),配置走的是另一套:
# /etc/systemd/timesyncd.conf [Time] NTP=192.168.10.10 FallbackNTP=192.168.10.11然后执行:
systemctl restart systemd-timesyncd timedatectl看System clock synchronized: yes和NTP service: active两行。timesyncd 的缺点是信息不透明,你只能看到"同步了没有",看不到偏差具体是多少。所以我个人的偏好是:只要有条件,客户端也统一换成 chrony,理由是排错的时候能看到偏移量、能看到当前用的是哪个源,省下来的时间远超安装成本。管理异构系统的痛苦,一半来自不同实现的行为差异。
这里还有个容易忽略的细节:客户端和服务端必须在 UDP 123 上双向通畅。客户端出方向要能到服务端的 123,服务端回包的路径也要通。云环境下如果客户端在另一个安全组,记得两个方向都配。
4.2 Windows 客户端接入实操
内网里总会有几台 Windows 服务器,它们同样可以对接 Linux 上的 chrony,因为 NTP 是标准协议,跨平台没问题。操作在管理员权限的命令行里执行:
w32tm /config /manualpeerlist:"192.168.10.10,0x8" /syncfromflags:manual /update net stop w32time net start w32time w32tm /resync w32tm /query /status0x8这个标志位表示使用客户端模式,对于普通客户端来说是最合适的。执行完w32tm /query /status后关注几项:Source应该显示你的内网服务器地址,Stratum应该是一个合理的数字,Last Successful Sync Time应该是刚刚。
Windows 的时间服务有个特点:它默认的校时行为比较保守,偏差很大时可能慢慢磨。如果遇到死活同步不上的情况,可以先用w32tm /query /configuration确认配置生效了,再用w32tm /stripchart /computer:192.168.10.10 /samples:5直接看和服务器的时间差。这条命令很好用,能立刻判定是网络不通还是别的问题。
4.3 大规模批量下发与硬件时钟落盘
到了几十台以上的规模,手工改配置就是灾难,必须上自动化。用 Ansible 的话,一份最小化的模板加一个任务就够:
# playbook: sync-ntp.yml - hosts: all become: yes vars: ntp_server: 192.168.10.10 tasks: - name: 部署 chrony 客户端配置 template: src: templates/chrony-client.conf.j2 dest: /etc/chrony.conf owner: root group: root mode: '0644' notify: restart chronyd - name: 确保 systemd-timesyncd 关闭 systemd: name: systemd-timesyncd state: stopped enabled: no ignore_errors: yes - name: 启动并设置开机自启 systemd: name: chronyd state: started enabled: yes handlers: - name: restart chronyd systemd: name: chronyd state: restarted模板文件里就一行有效的:server {{ ntp_server }} iburst。这里有个实操要点:Ubuntu 和 RHEL 的配置路径不同,写模板时要按发行版分支处理,否则会出现"任务显示成功但配置根本没生效"的情况。我一般用 Ansible 的when: ansible_os_family == 'Debian'做判断。
另外别忘了批量验收。跑完自动化后,可以用一条临时命令扫描所有机器当前偏移量:
for h in $(cat hosts.txt); do echo -n "$h: " ssh -o ConnectTimeout=3 $h "chronyc -c tracking | awk -F, '{print \$5}'" donechronyc -c tracking是 CSV 格式输出,第 5 个字段就是本机相对上游的偏移秒数(这个格式在 chrony 3.2 及以上可用)。把结果汇总一下,超过阈值的机器单独处理,比挨个登录快得多。
关于硬件时钟落盘:现代系统上rtcsync会周期性把系统时间写回 RTC,一般不用手工干预。但如果你需要立即固化一次(比如马上要断电维护),可以执行hwclock --systohc。反过来的命令是hwclock --hctosys,从硬件时钟读回系统,这个一般在开机脚本里由系统自动完成,手动执行要谨慎,因为硬件时钟可能不准。
5. 排查实录:对不上时的定位顺序
这一章是我最想写的部分,因为前面所有配置都顺利的话,你根本不会来搜"排查"。真正的价值在于出问题的时候知道先看哪里。
5.1 先分清是"没同步"还是"同步了但不准"
这是两个完全不同的问题,处理路径也不一样,所以第一步是分类。
判断方法:执行chronyc sources -v。如果行首全是^?,那是根本没连上,方向往网络和上游地址查。如果有^*,说明同步关系建立了,但chronyc tracking里的System time偏移量始终很大(比如几百毫秒甚至几秒),那是同步了但收敛不了,方向往时钟源、虚拟化、参数配置查。
我见过最典型的误判是:机器显示^*已选中,用户就认为没问题了,结果业务侧的日志还是错乱。原因是他看的是同步状态,没看偏移量。^*只代表"我找到了可信的老师",不代表"我已经学会并且和老师完全一致"。这个区别一定要记住。
5.2 五类高频故障的现场处置
故障一:服务端起来了,但自己层级一直是 16。
排查顺序:先看chronyc sources -v是不是全^?,再测出方向连通性。八成是出方向 UDP 123 没放行。用ntpdate -q或者直接nc -u -z -v 上游IP 123测。另外注意 DNS 问题,如果你在配置里写的是域名,而服务器的 DNS 不好使,解析失败也会导致全^?。这种情况直接换成 IP 最稳。
故障二:客户端连上了,但偏移量一直在几百毫秒上下晃。
这种情况多半是上游配置太多且质量参差。chrony 在多个源之间做筛选,如果某个源网络抖动严重,它会频繁重选,导致估计值抖动。解决方法是精简候选源到 3 个以内,并且优先选延迟低的。可以用chronyc sourcestats -v看每个源的统计信息,重点关注Std Dev(标准差)这一列,标准差大的源直接干掉。
故障三:虚拟机时间越跑越偏,重启后好一阵又不行。
虚拟机的时钟依赖宿主机,如果宿主机负载高或者启用了 CPU 频率调节,虚拟机时钟会跟着漂。处理方式有三层:确认时钟源是kvm-clock(KVM 环境);确保open-vm-tools或对应的集成组件已安装且时间同步不会和 chrony 打架;把 poll 间隔设小一点,让 chrony 更频繁地纠偏。还有一个很多人不知道的点——虚拟机的 CPU 被长时间"偷走"(steal time 高)会导致时钟走慢,这时候要在宿主机层面解决资源争抢,光调 chrony 治不了根。
故障四:配置改完重启服务,还是老样子。
先确认你改的是当前生效的那个配置文件。Debian 系上是/etc/chrony/chrony.conf,很多人在/etc/chrony.conf里改了半天,实际读的是前者。确认方法:systemctl cat chronyd看服务定义里引用了哪个路径,或者ps -ef | grep chronyd看启动参数。这个坑我至少踩过两次,每次都浪费半小时。
故障五:客户端和服务端时间差了十几分钟,完全同步不上。
时间偏差过大时,chrony 出于安全考虑会拒绝同步(防止被恶意源一下子把时钟带飞)。这时候需要手动干预。可以先在客户端停掉服务,手动把时间调到接近值,再启动:
systemctl stop chronyd date -s "$(date -d '2026-01-01 10:00:00')" systemctl start chronyd或者用chronyc makestep强制步进一次。注意在生产环境上手动改时间要极其谨慎,最好在维护窗口内做,改完立刻确认业务侧没有异常。
5.3 常见现象速查表
把上面这些整理成一张表,出问题的时候可以直接对着找:
| 现象 | 最可能的原因 | 处置动作 |
|---|---|---|
全部源显示^? | 出方向 123 未放行 / DNS 失败 | 测连通性,配置改 IP |
源显示^*但偏移大 | 上游质量差 / poll 设置不当 | 精简上游,调整 poll |
| Stratum 显示 16 | 完全未同步 | 回到"全部^?"的排查路径 |
| 重启后短暂失步 | drift 文件丢失或不可写 | 检查/var/lib/chrony/drift权限 |
| 偏移量正负反复跳 | 多个时间服务在打架 | 关闭 timesyncd 或 ntpd |
| 虚拟机持续漂移 | 时钟源不合适 / steal time 高 | 切 kvm-clock,查宿主机负载 |
| 客户端偶尔全断 | 上游单点故障 | 增加冗余上游,考虑local stratum |
| 配置改了不生效 | 改错了配置文件路径 | 用systemctl cat确认路径 |
提示:排查时养成"先看状态、再看偏移、最后看日志"的顺序。
journalctl -u chronyd -n 100 --no-pager能看到 chronyd 的启动和运行日志,很多参数错误会在启动时直接报出来,比瞎猜快得多。
6. 长期运维:监控、限速与安全边界
搭起来只是开始,一套时间同步体系如果没人盯着,早晚会悄悄出问题。这一章讲的是让它长期稳定运行该做的事。
6.1 用一条命令做偏移量监控告警
时间偏移是个缓慢变化的量,出问题往往没有明显症状,所以必须做监控。最简单的方式是写个脚本,定时采集偏移量,超过阈值就告警:
#!/bin/bash # check_ntp_offset.sh THRESHOLD=0.1 # 单位秒,超过 100 毫秒告警 OFFSET=$(chronyc -c tracking 2>/dev/null | awk -F, '{print $5}') if [ -z "$OFFSET" ]; then echo "CRITICAL: chronyc 无输出,服务可能异常" exit 2 fi ABS=$(awk -v o="$OFFSET" 'BEGIN{print (o<0)?-o:o}') if awk -v a="$ABS" -v t="$THRESHOLD" 'BEGIN{exit !(a>t)}'; then echo "WARNING: 时间偏移 ${OFFSET} 秒,超过阈值 ${THRESHOLD} 秒" exit 1 fi echo "OK: 时间偏移 ${OFFSET} 秒" exit 0这个脚本丢给 Zabbix、Prometheus 的 textfile collector 或者任何能执行脚本的监控系统都行。阈值定多少合适?我的经验是:普通业务机器 100 毫秒足够,对时间敏感的集群(比如跑分布式数据库的)可以压到 10 毫秒以内。不要定得太死,比如 1 毫秒,那样会天天告警,最后没人看告警才是最大的风险。
除了偏移量,还值得监控两个指标:chronyc tracking里的Stratum是否正常(突然变大说明上游丢了),以及chronyc activity里在线客户端的数量是否有异常下降(说明可能有网络分区)。
6.2 ratelimit 与访问控制
如果这台服务器同时对大量客户端提供服务,要防止它被滥用。NTP 协议有个经典的安全问题是"放大攻击"——攻击者伪造源地址向服务器发一个很小的请求,服务器回一个更大的响应,把攻击流量导向受害者。虽然内网风险低,但作为规范实践,建议加上限速:
ratelimit interval 3 burst 8 leak 2含义是大致限制同一个客户端在短时间内的高频请求。具体数值要根据客户端规模调整,客户端多的时候设太严会导致正常校时被拒,所以上线前先观察chronyc serverstats的输出,看实际请求频率。
访问控制方面,回到那个原则:allow只写需要的网段。如果内网做了分区,时间服务器应该部署在能被各区域访问到的位置,而不是让防火墙开一堆穿墙规则。另外如果环境里有加密要求,chrony 4.0 以上支持 NTS(Network Time Security),可以对时间同步链路做认证加密,配置上需要在服务端和客户端共享密钥,复杂度比普通配置高一截,建议在确实有合规要求时再上。
还有一个实用的加固点是密钥认证。传统的对称密钥方式在 chrony 里通过keyfile配置:
keyfile /etc/chrony.keys密钥文件里写10 SHA1 HEX:你的十六进制密钥,然后服务端和客户端在server或allow相关配置里引用这个 key 编号。这种方式能防止客户端连到伪造的时间源,但密钥管理本身是个负担,规模大了不好维护,所以实际中用得不多,知道有这条路就行。
6.3 迁移、冗余与几个我踩过的坑
最后分享几个实际运维里攒下来的经验,都是文档里不会写的。
关于冗余。只搭一台时间服务器就是单点,它一挂全网时间开始漂。规模稍大的环境建议至少两台,客户端配置里把两台都写上,chrony 会自动选优。但要注意,两台服务端最好从不同的上游取时间,或者至少错开一部分上游,否则上游挂了它们一起挂,冗余等于没有。
关于迁移。换时间服务器地址是件麻烦事,因为客户端配置散落在各处。我的做法是:给时间服务器配一个内网域名(比如ntp.corp.internal),所有客户端都写域名而不是 IP。换机器时只改 DNS 解析,客户端完全不用动。这一个习惯能省掉无数次批量改配置的痛苦。
关于容器和 Kubernetes。节点上的时间同步和容器里的时间不是一回事。容器共享宿主机时钟,所以只需要保证每个节点的时间准确。但要注意某些容器镜像里预装了 ntp 相关组件,可能会尝试在容器内改时间,这种要么删掉组件要么在启动脚本里禁掉。
关于时间跳变。我再强调一次:运行期的时钟跳变对业务是有害的。有一次一台机器重启后makestep触发了步进,把时间往前跳了 2 秒,结果基于时间戳做幂等判断的服务出现了重复处理。后来的做法是:所有对时间跳变敏感的服务,都要能在代码层面容忍一定范围的时间回退,不能假设时间单调递增。这是架构层面的防御,比调 NTP 参数更根本。
关于验证周期。我给自己定的规矩是:新机器上线时必须验证时间同步状态;每季度抽查一批机器的偏移量;每次网络或防火墙变更后,重新确认 123 端口的双向连通性。这三条坚持下来,时间相关的问题基本就绝迹了。
说到底,NTP 这套东西技术上不复杂,真正难的是把它当成一件需要持续照看的基础设施,而不是配一次就忘的开关。我在多个环境里反复部署过这套方案,最后沉淀下来的核心就几句话:服务端选 chrony、上游至少三个且要测通、allow严格限制网段、makestep控制好步进窗口、客户端统一实现以便排查、偏移量一定要有监控。把这几点做到位,内网时间这件事就可以从你的待办清单里划掉了。