NTP 的 step 和 slew 说的其实是同一件事的两种干法——把本机时钟和参考源之间的偏差抹掉。step 是把CLOCK_REALTIME一把拨到正确位置,slew 是让时钟走得快一点或者慢一点,慢慢蹭过去。很多人配了好几年 chrony,配置文件里那行makestep 1.0 3一直是复制粘贴来的,从没想过为什么是 1 秒、为什么是 3 次,也没想过把maxslewrate调大之后到底会发生什么。等到某天机房断电恢复,几十台机器带着几分钟的偏差一起上线,定时任务跑了两遍、日志时间倒流、分布式锁互相打架,才会回头认真看这两行参数。这篇就把 step 和 slew 从内核接口一路捋到配置文件,顺带把阈值怎么定、追平要多久、哪些负载经不起跳变这些实操问题讲透,适合正在维护在线服务、被时间问题坑过、或者单纯想把 chrony.conf 看明白的人。
1. 断电六小时后的 37 秒:跳变与平滑在校时里指什么
前几年遇到过一次很典型的事故。某机房市电中断,备用电池撑了没够,整排机器硬断电六个多小时,等电来了自动上电,几十台机器几乎同时起来。这些机器的主板电池大多到了年限,CMOS 走时早就飘了,起来之后和上游时间源一比,偏差从十几秒到七十多秒不等。麻烦的地方在于,这批机器的校时配置并不统一:一部分是老配置,makestep 1.0 3还留着;另一部分是后来被人“优化”过的模板,只留了server行,makestep被删掉了。
结果就是两种完全不同的表现。有makestep的那批,chronyd 起来几秒内把时间直接拨正,日志里一行干脆的记录,业务无感。没makestep的那批,chronyd 开始以很慢的速度把时钟往前赶,几十秒的偏差要追将近一个小时,在这一个小时里,这些机器和已经跳正的机器之间始终差着几十秒。那天真正出问题的不是时钟本身,而是那些“机器之间时间不一致”的连带效应:一台机器认为任务还没到点,另一台认为已经过点十分钟。
1.1 两种动作,其实是在处理两种不同形态的“错”
先把概念钉死。step 就是把墙钟的当前值直接改写成一个新值,从系统的角度看,CLOCK_REALTIME在某一瞬间从 T1 变成 T2,中间没有任何过渡。slew 是不改当前值,而是改“时钟走的速度”,让本机时钟在接下来的一段时间里比真实时间快一点或者慢一点,直到偏差被吃掉。
这个区别带来的后果非常不对称。step 是“瞬间正确,但过程有断裂”;slew 是“过程连续,但一段时间内仍然不准”。选哪个,本质上是在回答一个问题:你的系统更能容忍“一段时间内时间不准”,还是更不能容忍“时间轴上出现一个断点或回退”。
大部分在线服务怕的是后者。一个跳变,尤其是向后的跳变,会让很多基于时间戳做判断的逻辑陷入自我矛盾:先写进去的记录时间比后写进去的还新,令牌的签发时间在“未来”,租约看起来还没过期但其实早就该过期了。而刚开机、刚装好系统、刚从长时间离线状态恢复的设备,怕的是前者——偏差可能有几分钟甚至几小时,靠 slew 追要追到天荒地老。
1.2 为什么没有第三种选择
有人会问,能不能“又快又平滑”。严格来说不能,这是由物理约束决定的:时钟的频率可以在一定范围内调整,但调整幅度有上限,所以“每秒能追回多少偏差”有上限;想让偏差消失得更快,就只能突破频率调整的范围,那就变成直接改值,也就是 step。
所以内核只给了两把旋钮,一把叫“改频率”,一把叫“改当前值”。所有 NTP 实现——ntpd、chrony、systemd-timesyncd,乃至你自己写的校时脚本——做的事情无非是在这两把旋钮之间做选择,再决定什么时候用哪一把。理解了这一点,配置文件里那些参数就不再是玄学。
2. 偏差是怎么攒出来的,内核又靠什么让时钟“走慢一点”
要把阈值定得合理,得先知道偏差的量级。服务器上的时钟不是凭空产生误差的,它背后是一颗石英晶振在数数。晶振的频率会随温度漂、随老化漂、随供电电压漂,标称 100 MHz 的晶振实际可能是 100.0000123 MHz,这个相对偏差就是 ppm(百万分之一)。
2.1 晶振、温度与虚拟化:ppm 是个什么量级
普通主板上的晶振,常温下偏差大概在 ±20 ppm 以内,换算成时间是每天 1.7 秒左右。廉价一点的、老化严重的,100 ppm 也不稀奇,那就是每天 8.6 秒。机房温度控制得好,这个值会稳定一些;如果是没有空调的边缘机柜、车载设备、户外设备,一天之内温度变化几十度,偏差跟着一起摆。
虚拟化环境更麻烦。虚拟机的时钟本质上来自宿主的时钟,但 vCPU 被调度出去的时候,客户机里读到的时间会“卡住”,等调度回来再补偿,这个补偿往往不精确。负载越重,vCPU steal time 越高,时钟漂移越明显。我见过压力大的时候,一台虚拟机一天漂出去十几秒的。容器则更极端一点——容器共享宿主的内核时钟,容器自己不应该、也没有能力去校时。
一个粗略的量级参考:物理机每天漂 1 到 10 秒算正常,虚拟机每天漂 5 到 30 秒也常见,长期不通电靠 CMOS 电池走时的机器,一个月漂几分钟是常态。这些数字决定了你的
makestep阈值不可能定得很小。
2.2 adjtimex 暴露的三把旋钮
Linux 把这套能力通过adjtimex(2)这个系统调用暴露出来,参数结构体里有一堆字段,真正关键的是三个:offset、freq、modes。
freq是频率修正量,单位是 ppm(内核内部按 16 位小数放大,所以实际精度到 1/65536 ppm)。写进去之后,内核会按这个修正量调整时钟的走速,本机时钟就变快或者变慢一点点。这个修正量有上下限,Linux 内核里大约在 ±500 ppm 这个量级,具体常量在不同内核版本里可能略有差异,但数量级一直是这个。这就是“内核自己慢慢追”的物理天花板。
offset配合不同的modes位含义完全不同。带ADJ_OFFSET_SINGLESHOT标志时,表示“我给你一个偏差,你在后台慢慢消化掉”,这正是adjtime(3)库函数最终落到内核的形态。不带这个标志时,ADJ_OFFSET是喂给内核 PLL 的一个输入,由 PLL 自己根据时间常数(time_constant,默认 7)决定收敛速度。而ADJ_SETOFFSET是另一种东西——它是直接步进,把指定量加到当前时间上。
2.3 阶跃走 clock_settime,平滑走 adjtime
所以从系统调用层面看,两条路径非常清晰:
| 动作 | 典型接口 | 内核行为 | 对CLOCK_MONOTONIC的影响 |
|---|---|---|---|
| step | clock_settime(CLOCK_REALTIME)、settimeofday()、adjtimex(ADJ_SETOFFSET) | 直接改写墙钟基准 | 无影响,单调时钟继续按自己的节拍走 |
| slew | adjtime(3)、adjtimex(ADJ_OFFSET_SINGLESHOT)、adjtimex(ADJ_FREQUENCY) | 调整走速,逐步消化偏差 | 无影响,但墙钟与单调时钟的差值会变 |
这里有个经常被忽略的点:CLOCK_MONOTONIC不受 step 影响。内核的单调时钟基于另一个基准,settimeofday动不了它。这意味着sleep()、epoll_wait的超时、systemd 的定时器、大多数语言的time.monotonic(),在 step 发生时都是安全的,它们不会突然多睡或者少睡。真正被 step 打乱的是所有基于墙钟的判断——time.time()、日志时间戳、数据库里存的时间字段、证书有效期校验。
把这条记住,后面判断“某个服务会不会被跳变搞坏”就有了基本依据:看它依赖的是单调时钟还是墙钟。
3. 500 ppm 这条线:slew 究竟能追多快
很多人对 slew 的直觉是错的。以为“平滑同步”就是很快地把时间拉回来,只是过程平缓一点。实际算一下就知道,慢得离谱。
3.1 把 ppm 换算成“每秒追回多少毫秒”
ppm 是百万分之一。500 ppm 就是每秒钟时钟比正常快 500 微秒,也就是 0.5 毫秒。要追回 1 秒的偏差,需要 2000 秒,也就是 33 分钟。这个换算关系值得背下来,因为它直接决定了阈值该定在哪里。
| 频率修正上限 | 每秒追回 | 每分钟追回 | 追平 1 秒耗时 | 追平 60 秒耗时 |
|---|---|---|---|---|
| 500 ppm(ntpd 默认上限) | 0.5 ms | 30 ms | 约 33 分钟 | 约 33 小时 |
| 1000 ppm | 1 ms | 60 ms | 约 17 分钟 | 约 17 小时 |
83333 ppm(chrony 默认maxslewrate) | 83 ms | 5 秒 | 约 12 秒 | 约 12 分钟 |
| 1000000 ppm(chrony 允许的上限) | 1 秒 | 60 秒 | 约 1 秒 | 约 1 分钟 |
看完这张表就明白为什么 ntpd 的默认 step 阈值是 128 毫秒了。128 毫秒在 500 ppm 下只需要 256 秒就能追平,四分多钟,可以接受。如果阈值放到 10 秒,那就要追五个半小时,中间这段时间系统的时间一直是错的,不如直接跳。
3.2 ntpd 死守 500 ppm,chrony 为什么敢开到 8%
ntpd 的tinker maxslewrate默认就是 500 ppm 左右,它基本上只靠内核频率修正来追时间,所以快不起来。chrony 的思路不一样:它的maxslewrate默认值可以到 83333 ppm 这个量级,比内核能承受的频率修正范围大得多。
原因在于 chrony 追时间的方式不是单纯调频率。当需要的速率超过内核频率修正的范围时,它会把偏差拆开处理:一部分交给频率修正,剩下的部分通过密集的小幅偏移修正来补。这些修正量被控制在远小于makestep阈值的量级上,对上层应用表现出来的是“时钟走得快了一点”,而不是一次可见的跳变。换句话说,chrony 的高速 slew 本质上是“很多很多个极小的台阶”,宏观上看是斜坡。
这个设计让“平滑”变得实用了。默认配置下追 1 秒只要十几秒,追一分钟也就十来分钟。对于不能跳变但偏差又不小的场景,这是目前最省心的方案。
3.3 makestep 的阈值该由服务的容忍度倒推
阈值不是拍脑袋定的,应该反过来推:先确定你的服务能容忍多长时间的“时间不准”,再根据 slew 速率算出对应的偏差上限,这个上限就是makestep的阈值。
举个具体例子。假设你的服务要求时间误差始终不超过 2 秒,chrony 用默认 slew 速率大约 83 ms/s,那么从 2 秒误差追到 0 需要 24 秒,这 24 秒里误差是从 2 秒往下单调递减的,最大值就是 2 秒,符合要求。那阈值可以放到 2 秒甚至更大一点,因为超过阈值就步进,步进是瞬间归零,也不会超过 2 秒。但如果你的服务要求误差不超过 200 毫秒,阈值就该定在 200 毫秒附近,超了直接跳。
| 场景 | 建议makestep | 理由 |
|---|---|---|
| 普通物理机、边缘设备、离线部署 | makestep 1.0 3 | 偏差常见且量级不小,启动早期跳变成本最低 |
| 数据库主库、共识算法节点 | makestep 0.5 1,稳态期不允许再跳 | 稳态期一次回退可能破坏租约和写入顺序 |
| 日志采集、指标聚合的宿主 | makestep 1.0 3,但容器内一律禁用自己的校时 | 宿主统一校时,容器只跟随 |
| 计费、审计、账务类 | 阈值收紧,超限告警而不是自动跳 | 时间倒流造成的记录错乱不可逆 |
这里有个细节容易被误解:chrony 的makestep threshold limit里,limit是时钟更新的次数,不是秒数,也不是“系统启动后的前 N 次”。makestep 1.0 3的意思是:在 chronyd 启动后的前 3 次时钟更新中,如果偏差超过 1 秒就步进;3 次之后,即使偏差再大,也只做 slew。这个语义决定了它在“开机后很长时间才第一次拿到时间源”的场景下可能已经失效——因为那几次更新早就用完了。
如果需要“任何时候偏差超过阈值都跳”,用负数:makestep 1.0 -1。这在一些自动化部署里见过,图省事,但对在线服务是有风险的,因为这意味着运行期随时可能来一次跳变。
4. 谁经不起一次跳变,谁又等不起慢慢追
判断标准其实就一条:这个组件判断“现在”的时候,用的是墙钟还是单调时钟,用了之后做了什么。
4.1 分布式共识与租约:时间倒流代价最高的地方
共识类系统对时钟的依赖比大多数人想的要深。Raft 的选举超时通常用单调时钟,这块是安全的;但 leader lease、租约续期、跨节点的时间戳排序,往往落在墙钟上。一次向后的跳变会让节点认为自己的租约还有效,而实际上在真实时间里租约早就该到期了,于是出现两个 leader 同时认为自己有效的窗口。
数据库也类似。事务 ID、快照隔离的可见性判断、基于时间戳的冲突解决,这些逻辑如果遇到时间回退,可能出现“更新丢失”或者“读到不该读的版本”。很多数据库在文档里明确写了“不要在运行期回拨系统时钟”,但很少有团队真的当回事,直到真出事。
我自己的经验是:对于这类系统,slew 的代价是可控的(一段时间内时间偏一点),step 的代价是不可控的(可能出现违反不变量的状态)。所以宁可慢,也不能跳。阈值定小一点,只在启动早期允许一次跳变,运行期一律平滑。
4.2 票据、证书与定时任务:跳变后遗症
和共识系统相比,这些场景的后果轻一些,但更容易被忽视,而且往往是“跳变之后几小时才发作”。
时间向前跳,所有“还有 5 分钟过期”的东西瞬间变成“已过期”。缓存的 TTL 集体失效,鉴权票据全部需要重新申请,证书校验如果跳得够远会直接失败。如果跳变幅度大,还可能让一些“未来才生效”的配置提前生效。
时间向后跳,问题更隐蔽:已经过期的票据看起来又有效了,日志里出现重复的时间段,定时任务可能重复触发。cron用的是墙钟,一次回拨跨过某个整点,那个时间点的任务有可能被再触发一次,也可能被跳过。这类问题排查起来特别费劲,因为现象和原因隔得太远。
4.3 离线和冷启动:这时候不 step 就是自找麻烦
反过来,有些场景下坚持用 slew 是纯粹的自我折磨。刚从库存里拿出来上架的设备、停电几小时后恢复的机器、海运过来的车载终端,偏差可能到几分钟甚至几小时。用 500 ppm 追一小时偏差需要 83 天,等于永远追不上。
这种场景没有任何犹豫的余地,必须 step。而且最好是“启动时立刻跳一次,之后就老实平滑”。chrony 的做法是在配置文件里先写makestep,然后靠limit参数限制次数;更稳妥的方式是用chronyc makestep在上层编排脚本里显式触发一次,跑完再确认状态。
还有一个隐藏的坑:如果偏差大到超过某些实现的“恐慌阈值”,校时进程会直接拒绝调整,认为自己可能拿到了错误的时间源。ntpd 里这个阈值默认是 1000 秒,超过就 panic 退出;-g参数可以让它忽略一次。chrony 也有类似的保护机制,目的是防止一个错误的时间源把整批机器带偏——这个保护是有道理的,绕过它之前最好先确认时间源是可靠的。
5. 三套配置的落地写法与观测手段
理论说完了,落地其实就几行。下面按 chrony、ntpd、systemd-timesyncd 三套分别给。
5.1 chrony:makestep、maxslewrate、corrtimeratio
chrony 是现在的主流选择,配置也最直观。
# 时间源,至少给 4 个,方便做真假判定 pool time1.example.net iburst pool time2.example.net iburst # 前 3 次更新内,偏差超过 1 秒就直接步进,之后只平滑 makestep 1.0 3 # 平滑追时间的速率上限,默认 83333 ppm,一般不用动 # 如果机器对时间误差极度敏感,可以调小到 1000 以下,代价是追得更慢 maxslewrate 83333 # 修正时间与误差累积时间的比例,默认 3 # 意思是“这段误差累积了 T 秒,我最多花 3T 秒把它修完” corrtimeratio 3 # 拒绝偏差太大的源,防止被带偏 maxdistance 16.0 maxupdateskew 100.0几个参数的实际含义值得掰开说。corrtimeratio控制的不只是速度,还影响平滑过程中的抖动:比例越小追得越急,时钟频率变化越剧烈,对依赖高精度定时的应用(比如音频处理、高频采集)不友好;比例越大越温和,但时间不准的窗口拉长。默认 3 是个不错的折中。
makestep的limit如果写成负数,表示“任何一次更新都可以跳”,等价于放弃稳态保护。我看到过有团队为了“省事”直接写makestep 1.0 -1,结果某次上游时间源抽风,全网机器一起跳了 4 秒,比不跳还惨。
5.2 ntpd:-g、tinker step、tinker panic
ntpd 的玩法不太一样,它的 step 阈值是全局的,靠tinker调。
# ntpd.conf server time1.example.net iburst server time2.example.net iburst # 偏差超过 128 毫秒就步进,低于则平滑 tinker step 0.128 # 偏差超过 1000 秒认为是异常,直接退出保护 tinker panic 1000 # stepout:等待多久还不满足条件就放弃这次步进 tinker stepout 300启动脚本里通常会带-g,作用是允许第一次调整突破 panic 阈值,专门对付“开机时偏差极大”的情况。这个参数不要常驻,一般只在首次启动时用一次。
ntpd 的 slew 上限基本就是内核频率修正的范围,所以它对大偏差的处理策略非常干脆:要么跳,要么慢慢磨。这也是这些年它被 chrony 取代的主要原因之一。
5.3 观测:chronyc tracking 与 adjtimex -p 怎么读
配完了得能看出来它在干什么。chrony 的tracking是最重要的观测入口。
chronyc tracking几个关键字段:System time是当前相对参考源的偏差,正数表示本机慢、需要往前拨;Last offset是上一次测量的偏差;Frequency是本机时钟相对标称频率的实际偏移,单位 ppm,这个值能反映晶振的健康度;Residual freq是残余频率误差,正常应该接近 0;Skew是频率估计的不确定度;Update interval是当前轮询间隔。
判断是否在平滑过程中,看System time是不是在持续单调地向 0 靠近,同时Frequency偏离正常值较多。看到这种组合,说明 chronyd 正在主动加速或减速。
chronyc sources -vsources里的状态位很有用:^*是当前选中的源,+是可接受的备选,-是被排除的,?是不可达或还没测够。Reach是八位的可达性寄存器,全377表示最近八次都收到了。如果Reach在退化,说明网络或上游有问题,这时候讨论 step 还是 slew 没意义。
内核层面用adjtimex看。
adjtimex --print输出里的status是标志位组合,STA_PLL置位说明内核 PLL 在参与调整,STA_UNSYNC置位说明内核认为还没同步。offset是内核当前持有的待消化偏差(微秒),freq是频率修正量(ppm,带 16 位小数)。tick是内核每个时钟中断的实际长度,正常情况下接近 10000 微秒。
一个实用的自查动作:
System time一直在 0 附近小幅跳动、Frequency稳定、Skew很小,说明系统处于健康的稳态。如果System time反复出现几十毫秒的跳变,先怀疑上游链路抖动或者轮询间隔太长,别急着改makestep。
6. 排查链路:显示“已同步”但仍然出问题
最难受的情况不是校时失败,而是校时显示正常,业务却在报错。这种情况我遇到过几次,下面把一次完整的排查过程写出来,思路可以直接复用。
6.1 第一步:确认“同步”到底同步到什么程度
chronyc tracking显示Leap status: Normal、System time是几十微秒,看起来完美。但这只说明当前时刻的偏差小,不代表过去一段时间没发生过跳变。
这时候要看chronyc tracking里的Last offset和RMS offset。如果RMS offset比System time大一到两个数量级,说明最近有过一次较大的调整,正在或刚刚收敛。再看chronyc sourcestats,里面记录了每个源的偏移历史估计,能从频率和偏移的变化趋势里看出端倪。
紧接着翻系统日志。journald 里搜 chronyd 的关键字,能直接看到它记录的动作:System clock was stepped by X seconds、System clock wrong by X seconds, adjustment started。这一行就是铁证,比任何间接推断都可靠。
6.2 第二步:把“时间跳变”和应用异常对上时间线
确认了跳变发生的时刻,接下来的活是关联分析。这一步最费时间,但方法很简单:把跳变时刻前后几分钟的应用日志拉出来,看有没有集中的异常。
典型症状我有几个固定检查项。一是systemd的定时器日志,看有没有任务在跳变前后重复执行;二是应用的会话或令牌相关日志,看有没有“签发时间在未来”或者“已过期”的异常;三是数据库的慢查询和事务日志,看有没有异常的时间戳顺序;四是指标系统里的 rate 类指标,时间回退会让 rate 出现负值或者巨大的尖峰。
如果这几项里有明确的异常,且时间戳精确落在跳变的那几秒内,基本可以结案。
6.3 第三步:判断是“该跳没跳”还是“不该跳跳了”
定位到跳变之后,要判断这次跳变是不是符合预期。翻配置文件里makestep的limit值,结合 chronyd 的启动时间算一下——如果limit是 3,而这台机器已经运行了几个月,那就不该再跳了。如果它跳了,说明配置文件被改过,或者limit写成了负数。
另一种情况是该跳没跳。表现是时间一直慢慢追,业务一直在“时间略偏”的状态下运行。这时候看System time的绝对值和收敛速度,用 3.1 节的表算一下预计耗时,如果预计耗时超过业务可接受窗口,说明阈值定高了,该往下调。
还有一种更隐蔽的:makestep配置正确,但因为时间源不稳定,chronyd 一直没进入“可步进”的状态。chronyc sources里如果所有源都是?或者状态不满,chronyd 就不会做任何调整,包括步进。这种“静默失效”最难发现,因为它不报错。
6.4 容器和虚拟机里的额外变量
容器里跑chronyc tracking看到的其实是宿主内核的状态,容器本身并没有独立的时钟。如果容器镜像里还装了 chronyd 并且在跑,那就是两个进程抢同一把旋钮,行为会非常诡异。我见过容器里 chronyd 和宿主 chronyd 互相拉扯,导致时钟频率被反复写入不同值,Frequency字段来回跳。
正确的做法是容器内一律关闭校时,把CAP_SYS_TIME能力去掉,让容器只能读时间不能改时间。虚拟机则要注意宿主的时间同步策略——宿主自己在跳,客户机跟着一起跳,客户机里配的makestep根本拦截不住。
7. 几个踩过的坑和一个可以直接照抄的策略
先说我踩得最疼的一个。有次给一批边缘设备做统一配置,为了“保证时间准确”,把makestep写成了makestep 0.5 -1。上线一周后,上游时间源的一次短暂抖动导致这些设备判定偏差超过 500 毫秒,全部一起步进。这些设备上的采集程序用的是墙钟做数据分片,一次回退让部分数据被写进了“上一秒”的分片文件,跨分片聚合的时候出现了重复计数。事后复盘,问题不在上游,而在于我们给了校时进程太大的权限。
第二个坑是关于maxslewrate的。为了让时间追得快一点,我把它调到了 500000 ppm。追是快了,但机器上的音频采集出现了周期性的丢帧,因为时钟频率变化太剧烈,音频子系统按时间戳算出的缓冲区位置和实际硬件指针漂移了。后来把速率降到 5000 ppm 以下才恢复正常。高速 slew 不是免费的,它对依赖时间精度的实时处理是有代价的。
第三个坑比较冤。某次新装的一批机器时间怎么都同步不上,chronyc sources全是?。查了半天发现是防火墙把出向的 UDP 123 端口拦了,而校时进程本身不会因为收不到包就报错,它只是安静地等待。这个坑提醒我,排查时间问题要从“有没有拿到时间”开始,而不是从参数调优开始。
现在这套策略我已经用了两年多,没再出过事,可以直接拿去改:
- 所有机器统一用 chrony,禁用 systemd-timesyncd,容器内不装任何校时组件。
- 物理机和边缘设备用
makestep 1.0 3,只在启动早期允许跳变,运行期只平滑。 - 数据库、共识节点、账务系统用
makestep 0.3 1,并且把System time超过 100 毫秒的情况接进告警,让人工介入而不是靠自动跳变。 maxslewrate保持默认,除非有明确的“必须追得更快”的需求,并且评估过实时业务的影响。- 所有节点的
sources -v和tracking纳入监控,重点盯Reach退化、Skew变大、RMS offset异常这几种情况。
最后分享一个我觉得比参数更重要的小习惯:每次改动机器的校时配置,都顺手在变更记录里写一行“这台机器允不允许在运行期跳变”。听起来像废话,但真正出事的时候,这一行能帮你在一分钟内判断清楚,这次跳变是配置使然还是意外,而不是先去翻三个月前的提交记录。时间同步这件事,配置本身没多复杂,难的是让所有人都对“这台机器的时钟可以怎么动”有一致的预期。