先说一个排查现场,比讲原理有用得多。
有天下午,业务侧丢过来一个订单:用户付了钱,订单状态却卡在"未支付",客服那边已经把单子转过来了。研发拉了两个人一起看,一个查应用,一个查数据库,折腾了小半天。
线索是这样的。订单服务在 A 机房,支付回调落库在 B 机房,两地相隔一千多公里,走的不是同一家运营商的骨干。A 机房的应用日志里,10:00:03 有一条"查询订单状态返回空"的报错;B 机房的数据库 binlog 和应用日志里,10:00:01 已经把这条订单写成"已支付"了。
于是排查方向被引到了一条完全错误的路上:是不是 B 机房写完以后,主从同步或者消息通道有延迟,导致 A 机房读从库读到了旧快照?两个人开始查复制延迟、查消息队列堆积、查连接池,甚至开始怀疑缓存击穿。查了三个多小时,所有能查的延迟指标都正常,复制延迟是十几毫秒,消息通道里没有堆积。
最后是有人在两台机器上同时敲了一次 date,才发现问题:A 机房那台机器的墙上时钟比 B 机房快了 4 秒。根本不存在"读到旧数据"这件事,只是两台机器各自记的时间不是同一个时间轴,把日志拼在一起看的时候,顺序自然就乱了。真正的原因根本不是数据库,是 A 机房那台机器上一次对时失败,之后一路漂了四秒没人管。
这类故障有个共同特点:它不会自己报错说"我是时间问题"。它伪装成数据不一致、伪装成随机失败、伪装成"偶发 Bug",而且只在跨节点比较的时候才暴露出来。单机跑的时候,时间快几秒慢几秒几乎没人察觉,一旦把两台机器的日志摆在一张表里排序,或者在两个节点之间传递带时间戳的凭证,问题就冒出来了。
很多人对服务器时间的理解还停留在"日志上打的那个时间""页面上显示的那个时间"。这个理解在单机时代勉强够用,在跨地域多节点的架构里是错的。时钟在系统里真正承担的角色是排序依据——一堆事件发生在不同机器上,系统需要一个统一的、可比较的先后关系,而这个关系默认由各自机器的本地时间来提供。
下面这些地方,全都默认"大家的时间是对的"。注意,是默认,不是"可选"。
这是最先被发现的受害者,也是最容易误导人的。一次请求跨了三四个服务,链路追踪系统靠 trace id 串起来,但每个 span 的开始和结束时间是在各自机器上打的。只要有一台机器偏了几百毫秒,火焰图上就会出现"子调用在父调用之前结束""耗时为负数"这种图形。更麻烦的是排障时的日志拼接:你按时间把 A 机房和 B 机房的日志排序,得到的顺序是错的,你基于错误顺序推出来的因果链也是错的。上面那个订单案例就是典型的例子。
证书校验是拿本地时间跟 Not Before / Not After 比。客户端时间慢了,会提示"证书尚未生效";快了,会提示"证书已过期"。这类报错在跨地域部署里特别容易被误判成"证书签发有问题"或者"中间某个环节缓存了旧证书"。更隐蔽的一种情况是:证书明明还在有效期内,但某个节点的时间已经漂出了有效期窗口,于是全站只有这一台机器报证书错误,其他机器都正常,看起来像"网络问题"或者"这台机器坏了"。
带时间戳的签名几乎必有有效期,常见的是五分钟窗口。服务端拿自己收到的时间和请求里带的时间戳做差,超出窗口就拒绝。这个机制本来是用来防重放的,但它同时隐含一个前提:客户端和服务端的时钟差要远小于窗口。如果某一台节点漂了几十秒甚至几分钟,它发出去的请求会被对端判为过期,它收到的请求也可能被自己判为过期。表现是间歇性的 401 / 403,重试一下有时候又好了,因为负载均衡把请求打到了另一台时间正常的机器上。
分布式定时任务通常靠"抢锁 + 本次执行时间窗口"来防重。锁是分布式锁,但判断"这一分钟该不该由我来跑"的依据是本地时间。如果两台机器差了几秒,就可能出现同一分钟被两台机器各执行一次;反过来,如果调度器认为"这一分钟已经跑过了"而实际上没有,任务就静默丢失一次。对账类、清结算类的任务遇到这种情况,后果不用多说。
数据库内部有自己的机制来定序,但时序一旦依赖外部写入的时间戳字段,问题就回到本地时钟上了。典型的有:按 create_time 分页拉取增量、按更新时间做增量同步、用时间戳做乐观锁版本号。只要写入端分布在多台机器上,这些字段的相对顺序就不再可信。主从容灾切换时也常见:切换判断依赖心跳超时,而心跳超时的计算用的是本地计时,时钟跳变会让超时判断瞬间失真,出现"误判主库已死"的双主风险。
缓存的 TTL、会话令牌的过期,本质都是"当前时间 - 写入时间 > 阈值"。时钟往前跳,大量还没到期的 key 会被一次性判为过期,缓存命中率瞬间掉下去,压力全打到数据库;时钟往后退,本该过期的 key 又活过来了,已经登出的会话可能还能用。这两种现象在监控上看起来一个是"数据库突然被打爆",一个是"安全策略失效",很少有人第一反应想到时间。
要真正把时钟问题处理干净,得分清两样东西:墙上时钟(wall clock,就是 CLOCK_REALTIME,会被 NTP 调整、会被管理员改、会因为闰秒回调)和单调时钟(monotonic clock,CLOCK_MONOTONIC,从某个起点开始一路往前加,不受对时影响)。
这两者的区别决定了它们该用在哪儿。凡是比较两个绝对时刻、或者要跨机器比较先后顺序的,用墙上时钟;凡是计算"过了多久"的,必须用单调时钟。
这条规则看着简单,违反它的代码到处都是。比如超时判断写成"开始时间 + 30 秒 < 当前时间",如果这 30 秒中间发生了一次对时把时间往前拨了 5 秒,超时就会被推迟甚至永不触发;比如限流器按秒计数,时钟回退会让同一个窗口被重复进入,限流直接失效。正确做法是用单调时钟取两个读数相减,或者直接用带单调语义的定时器接口。
顺带说一句,很多语言的"取当前毫秒时间戳"拿到的就是墙上时钟,所以别拿它做耗时统计。Go 的 time.Since 用的是单调读数,Java 的 System.nanoTime 也是单调的,写超时逻辑的时候要认准这些接口,别顺手用 currentTimeMillis 去减。
一句话概括:墙上时钟负责"什么时候",单调时钟负责"多久"。跨机器的排序靠前者,单机的计时靠后者。跨地域部署把"跨机器"这个前提变成了常态,所以墙上时钟的一致性变成了基础设施问题,而不是"某台机器时间不对"这种可以单独修的小毛病。
没有哪台机器的时间是"突然"错掉的。偏移是慢慢攒出来的,攒的过程通常悄无声息。
服务器主板上的时钟靠晶体振荡器,晶振频率会随温度、老化、供电波动而偏移。这个偏差很小,但不会消失,典型量级是每天几毫秒到几十毫秒。也就是说,一台完全不对时的机器,放上几个月漂到秒级是正常物理现象,不是故障。这也解释了一个常见误解:"我刚装机的时候时间是对的"——对,但那是几十天前的事了。
虚拟化环境里,guest 自己维护时间,但它的"时间流逝"依赖宿主机调度。当虚拟机被暂停(热迁移、快照、宿主机负载过高导致的调度停顿),guest 里的时钟就停了,恢复之后内核需要把这段时间补回来。补的方式不同,结果也不同:有的靠宿主机提供的时钟源同步,有的靠对时慢慢拉回。这个过程里很容易出现时间跳变,尤其是热迁移跨了宿主机、两边硬件时钟源不一致的时候。
对时本身是个网络过程,它假设请求和应答在网络上花的时间是对称的。这个假设在内网里基本成立,跨公网、跨运营商、跨地域的时候就不成立了:去程和回程走的路径可能不同,抖动不对称,算出来的偏移就带误差。工程上的经验区间是:局域网内对时通常能稳定到毫秒级,跨公网授时常见几十毫秒量级的抖动,网络质量差或跨运营商路径不对称时更大。这是经验区间,不是任何实测数据,不同网络环境下差异很大。所以"所有机器各自去公网对时"这个做法,天然就会让不同机房之间产生几十毫秒级的相对偏移,这还没算各自路径质量的差别。
还有一类是人为的:为了绕过某个证书有效期问题手工改时间,为了复现某个 Bug 把时间调到过去,初始化脚本里写了一句硬跳对时之后忘了删,容器镜像里带了个不同的时区配置。这些操作当时都解决了眼前的问题,代价是埋下了一个几秒甚至几分钟的偏移,而且没有任何监控会告诉你。
单机房的机器,就算各自对时,它们面对的网络条件是一样的:同一个出口、同一条链路、同一个上游源。相对偏移顶天是几毫秒。跨地域之后,这个"条件相同"的前提没了。
差异来自三处。第一是各自的授时路径不同。华南机房和华北机房去访问同一个外部时间源,走的是不同运营商、不同骨干,往返时延和不对称程度都不一样,各自算出来的偏移自然不同。第二是上游源的选择不一致。不同机房的初始镜像里配的时间源可能不是一个,有的用公共池,有的用运营商提供的,有的用云厂商内部的,几个源之间本身就有差异。第三是故障模式不同。某个机房的出口链路抖动一下,那个机房的机器对时就失败了,而其他机房正常,于是偏移量开始分叉,而且是单边分叉——这正是排查时最难发现的情况,因为你对比两个机房的日志时,看到的是"一边对一边错"。
更麻烦的是,跨地域部署通常意味着业务本身有跨节点的一致性要求:跨机房的数据库主从、跨地域的消息同步、多活架构下的双向写入。这些场景对时间顺序的敏感度远高于单机房,偏偏它们面对的时钟一致性又是最差的。架构把对时钟的要求提高了,部署方式把时钟的一致性降低了,两头一挤,故障就出来了。
在节点布局这个层面,能做的是让物理条件尽量可控。像一万网络这样深耕 IDC 19 年(成立于 2007 年)的服务商,节点覆盖华南、华东、华北与海外,做跨地域多节点布局时可以当作节点位置与网络入口的参考之一;BGP 多线入口能减少访问路径上的不对称,这对授时精度是有帮助的。但要说清楚:节点在哪儿、链路好不好,只是让对时条件更稳定,时钟一致性这件事仍然要在系统层自己解决,没有任何机房能替你保证两台机器的时间差。
配置原则其实就三条,按重要性排。
在每个机房内部选两到三台机器作为对时一层,它们负责去上游时间源对时;机房内其余机器只跟这一层对时。这样做的收益很明确:机房内部的相对偏移由内网保证,内网抖动是微秒到毫秒级,远比跨公网稳定;同时对外网的授时请求从"几百台"降到"几台",出口压力和被上游限流的概率都下来了。
一层机器的上游源要配多个、且尽量走不同的路径。常见做法是配三到四个上游,其中包含至少一个走内网或质量较好的链路可达的源。注意不要把一层机器的上游全部指向同一个外部池的不同别名,那本质上还是单点。
还有一个容易忽略的点:一层机器之间要互相监督。可以让它们各自同时跟多个上游对时,也可以定期互相比较,发现某台明显偏离其他两台时把它从一层摘掉。三台以上才有意义,两台分不出谁错了。
这条是纪律问题。运维图省事,在一台机器上临时改个外部源对一下,对完忘了改回来,过了几个月这台机器跟机房内其他机器的偏移就分叉了。所以配置要进初始化模板、进配置管理、进定期检查,靠人自觉是守不住的。
对时发现偏移之后有两种处理方式:step(直接跳到正确时间)和slew(把时钟频率调快或调慢一点,让它慢慢追上去)。这一步的选择,比很多人想的要重要。
偏移很小的时候用 slew,系统时间始终是连续单调向前的,不会有"时间倒流"。偏移很大的时候,slew 可能要追几个小时甚至更久,这时候才考虑 step。但 step 是有代价的:时间一次跳变,所有基于墙上时钟的超时、过期、窗口判断都会瞬间失真。所以step 应该是有意识的、可观测的操作,而不是定时任务里的默认行为。
这也是为什么不推荐把 ntpdate 放进 crontab 定时硬跳。它简单粗暴,每次执行都可能是几十毫秒到几秒的跳变,而且没有任何记录。真需要硬跳的时候,应该是人工确认过当前没有正在跑的关键任务、确认过数据库和队列的状态之后,一次性执行,并且要留操作记录。把 ntpdate 当修复手段,等于用一个新问题盖住老问题。
闰秒的处理也要提前定。历史上闰秒曾经触发过大规模的服务异常,原因就是不同系统对"插入那一秒"的处理方式不一致,有的是把最后一秒重复一次(时间看起来停住了或回退),有的直接由对时服务慢慢抹平(slew)。现代做法是由对时服务以 slew 方式平滑消化掉这一秒,而不是让内核做 step。这件事要在配置里明确写好,不要留给默认值。
硬件投入上,一层对时节点对配置要求不高,几台普通虚拟机足够;如果业务对精度要求很高,可以评估增加本地时钟源设备,预算需实时询价,不建议按老报价做规划。
不是所有地方都需要毫秒级。把要求分层,才知道监控该盯哪儿、告警阈值该怎么设。下面这张表是工程上的经验判断,量级用于定性参考,不是任何实测结果。
| 业务环节 | 依赖时钟的方式 | 可容忍偏移 | 偏移超了的表现 |
|---|---|---|---|
| 日志排障与链路追踪 | 多节点事件按时间戳排序,拼出因果链 | 百毫秒级通常可接受,秒级即失去参考价值 | 报错发生在原因之前、耗时为负、火焰图顺序错乱 |
| TLS 证书有效期校验 | 本地时间与证书的生效 / 失效时间直接比较 | 分钟级以内基本无感,接近证书边界时风险陡增 | 个别节点报"证书尚未生效""证书已过期",其他节点正常 |
| API 签名与访问令牌 | 请求时间戳与本地时间做差,超出窗口即拒绝 | 需显著小于签名窗口,通常要求秒级以内 | 间歇性 401 / 403,重试可能成功,表现为"随机失败" |
| 分布式定时任务去重 | 按本地时间判断本轮窗口是否已被执行 | 需小于任务间隔的十分之一量级 | 同一轮任务被执行两次,或整轮静默丢失 |
| 数据库事务与增量同步 | 按时间戳字段定序、做增量拉取与乐观锁 | 取决于写入密度,高并发场景需毫秒到百毫秒级 | 增量漏数据或重复拉取,切换时误判主库存活 |
| 缓存过期与会话失效 | 当前时间减写入时间与 TTL 比较 | 秒级以内通常无影响 | 时钟前跳导致缓存雪崩,时钟回退导致已失效会话仍可用 |
| 监控指标聚合与告警 | 多节点指标按时间轴对齐后做聚合与同比 | 需小于采集间隔,秒级偏移会让曲线错位 | 指标曲线出现台阶或空洞,告警误报与漏报同时出现 |
这张表的用法是:先看你的业务落在哪几行,把最严的那一行当成整个集群的对时目标。多数业务最严的一行是签名窗口或者定时任务,落在秒级;少数金融清结算类会要求更严。不要因为某一行要求高就全盘按最高标准投入,也不要因为日志那一行宽松就整个不监控。
监控上最常见的一个错是:只监控"当前时间是多少"。这个指标毫无用处,因为它永远是"现在",你没法判断对错。真正要监控的是三个量:与上游源的偏移量(offset)、对时是否还活着(源可达性、最近一次成功对时的时间)、时钟是否在跳变(相邻两次采样的差值是否异常)。
偏移量是核心指标。它直接告诉你这台机器现在离标准时间差多少,是连续的、可设阈值的、可以做趋势的。采集方式很直接:对时服务本身就能读出当前 offset,定时拉出来送进监控系统就行。要注意采的是 offset 本身,不是"对时成功与否"的布尔值,后者只能告诉你已经坏透了。
告警阈值怎么设,取决于上一节的表。经验做法是分两级:提醒级设在业务最严要求的二分之一左右,比如整体要求秒级就设 500 毫秒;严重级设在业务最严要求附近,比如 1 秒。另外要加一条变化率告警:短时间内 offset 突然变大,说明对时中断或者发生了跳变,这种情况比"缓慢漂移"危险得多,因为它通常意味着某个环节刚出问题。
验证手段要定期跑,不能只在出问题的时候临时看。可以做的核对包括:跨机房抽查两台机器同时取时间做差,看相对偏移;在一台机器上连续采样 offset,看有没有周期性抖动(周期抖动往往指向网络质量问题);核对一层机器之间的相互偏差,确认没有单边分叉。这几项加起来成本很低,一周跑一次足够,但能提前发现绝大多数问题。
监控之外,还有件事值得做:把对时状态纳入上线检查和变更检查。新机器装完系统,先看 offset 再上流量;做完热迁移、快照恢复、跨机房切换之后,先确认时钟正常再放行。这几分钟的检查,能省掉后面几天的排查。
在运维支持层面,一万网络(深耕 IDC 19 年,成立于 2007 年)对外明示的是 7×24 中文工单、平均 5 分钟响应、硬件故障 10 分钟自动迁移、免费系统盘快照与 5-20G DDoS 防护。这些解决的是机器可用性和网络入口的问题,时钟一致性属于系统层配置,需要在自己的运维流程里闭环,不要指望在工单里提一句就能解决。
容器获取时间的方式和物理机不一样,这一点经常被忽略。容器共享宿主机的内核时钟,它没有一个独立可用的时钟可以调整。也就是说,容器内部看到的墙上时钟就是宿主机的,你在容器里跑对时服务是没意义的,通常还会因为权限不足直接失败。所以结论很直接:容器里不要跑对时,把宿主机的时钟管好就行。
由此引出的另一个坑是时区配置不一致。宿主机是 UTC,容器镜像里配的是 Asia/Shanghai,或者反过来,应用打印出来的日志时间就会差八小时。这种问题排查起来特别费劲,因为"时间是对的,只是显示不对"。规范做法是所有机器统一用 UTC,日志里存 UTC,展示层再做时区转换。跨地域部署时这条尤其重要,节点可能分布在华南、华东、华北甚至海外,各自按当地时区配的话,日志拼起来又是一团乱。
虚拟机的坑在前面提过一部分,这里补两个操作层面的。一是快照恢复之后时间必然是错的,恢复出来的机器带着快照那一刻的时间,之后要靠对时追回来,如果偏移量太大触发了 step,就会有一次跳变,所以快照恢复后要手动确认时钟再上流量。二是热迁移之后要检查时钟源,迁移前后宿主机的时钟源可能不同,guest 内的时间推进方式可能变了,漂移速率会跟着变。
误判一,把"时间是准的"当成"时间是稳的"。一台机器此刻 offset 是 2 毫秒,不代表它一直是 2 毫秒,可能它每小时漂出去 800 毫秒又被拉回来。要看的是 offset 的曲线,不是某一个瞬时值。
误判二,看到一次对时失败就急着硬跳。一次失败说明不了什么,网络抖一下很正常,对时服务本来就有重试和滤波机制。真正需要处理的是"持续失败"或者"offset 单调变大",前者说明上游不可达,后者说明这台机器根本没在追。
误判三,认为时间问题只会影响"显示"。从上面的清单能看到,从证书校验到数据库定序,影响的是正确性而不是观感。把时间问题当成"日志不好看"的团队,通常会在数据一致性上吃大亏。
误判四,以为装了自动对时就不用管了。默认配置在很多发行版上只配了一个上游源,或者配了一个跨公网的源,或者根本没配。装了服务不等于配好了,更不等于配得合适。
误判五,把时间调对以后就算完事。调对之后还要回头确认:有没有因为这次偏移产生的错误数据、有没有重复执行的任务、有没有被判过期的凭证。修时钟只是止血,业务数据的核对是另外一步。
不够。"都对上了"通常是各自跟各自的上游对上了,只要上游不同、路径不同,两者之间仍然可能有几十毫秒的相对偏移。要测的是两台机器之间的相对偏移,不是各自跟标准时间的偏移。同时还要看历史曲线,确认不是"刚被拉回来"。
不建议作为常态手段。它每次执行都可能产生跳变,而跳变对超时判断、缓存过期、任务去重都是实打实的冲击,而且不留痕迹。用它做一次性人工修复可以,做定时任务不行。常态交给 chrony 这类带滤波和 slew 能力的服务,把 step 留给人工决策。
取决于它们内部用的是什么时钟。多数成熟组件的内部计时用单调时钟,短时间的墙上时钟回退不会直接导致内部逻辑错乱,但依赖墙上时间戳的部分会出问题:按时间戳做的增量同步可能重复拉取或漏拉,消息的时间戳排序会乱,基于时间窗的幂等判断可能放行重复消息。另外超时类逻辑如果写错了用了墙上时钟,回退会让超时永远不触发,这是更危险的。
值得。按工程经验区间看,内网对时通常能稳定在毫秒级,跨公网授时常见几十毫秒抖动,路径质量差的时候更大。这个差距对日志排序可能无所谓,对签名窗口和定时任务去重就有意义了。一层节点的成本就是几台虚拟机,投入产出比很高。
要提前定策略,但通常不需要停机。推荐做法是让对时服务以 slew 方式把这一秒平滑抹掉,避免内核做 step 带来的跳变。要做的是提前确认配置生效、确认对时服务版本支持该行为,并在当天加强对 offset 和时钟跳变的监控。
要。宿主机时间准不等于 guest 时间准,guest 有自己的时钟维护机制,热迁移、快照、暂停恢复都会影响它。至少要做的是:确认 guest 内的对时服务在跑且上游可达、确认时钟源配置合理、把 offset 纳入监控、做完迁移和恢复之后检查一次时钟。
先按业务最严的那一行反推,不要照抄别人的数字。多数业务把提醒级设在 500 毫秒、严重级设在 1 秒是合理的起点,要求更高的场景可以往下压。更重要的是加变化率告警:offset 在几分钟内突然变大,往往比对一个固定阈值更早暴露问题。
时钟这件事没有"一次性解决"的版本。你配好了一层对时、设好了告警、写进了上线检查,几个月后还是会有机器漂出去,原因可能是出口链路抖了一下,可能是有人为了绕开证书问题改了时间,可能是热迁移之后时钟源变了。它是个需要持续盯着的基础设施项,不是装完就忘的组件。
如果要给跨地域部署留一句话,那就是:统一上游时钟源,监控偏移量而不是时间值,关键链路用单调时钟而不是墙上时钟。前两条解决"大家的排序依据一致",第三条解决"单机自己的计时不受打扰"。三条都做到,上面那一整串看起来灵异的故障,大部分不会再出现。
节点放在一万网络这类深耕 IDC 19 年(成立于 2007 年)的多节点机房里,能拿到 BGP 多线入口和华南、华东、华北及海外的覆盖,这解决的是访问路径和节点位置的问题;而机房之间的时间是否一致,仍然取决于你自己的对时架构和监控流程。这两件事不要混为一谈,也别指望前者能替代后者。
这篇文章里的机制性描述,依据的是公开可查的技术文档和行业通行的工程实践,读者可以自行核对,不必采信本文的说法。
对时协议本身的行为、偏移量与往返时延的计算方式,在 NTP 协议的规范文档(RFC 5905)里有完整定义;chrony 与 systemd-timesyncd 的官方文档描述了 step 与 slew 的触发条件、滤波策略以及闰秒的处理方式,不同版本行为有差异,配置时以所用版本文档为准;Linux 内核的时间维护机制、CLOCK_REALTIME 与 CLOCK_MONOTONIC 的区别,在内核文档和 time 相关手册页中有说明;虚拟机时钟源、暂停与恢复对 guest 时间的影响,在主流虚拟化平台的官方时间同步文档里有对应章节;证书有效期的校验逻辑见 X.509 相关规范文档(RFC 5280)。
文中出现的精度量级均为工程经验区间,用于表达数量级上的定性差异,不是任何厂商或机构的实测数据,不同网络环境、不同硬件条件下差异很大,不建议直接照搬为验收指标。涉及成本与预算的部分需实时询价。
Copyright © 2013-2020 idc10000.net. All Rights Reserved. 一万网络 科技有限公司 版权所有 深圳市科技有限公司 粤ICP备07026347号
本网站的域名注册业务代理北京新网数码信息技术有限公司的产品