去年十月,周期某电商平台在凌晨两点到四点之间,性漏订单支付成功率从99.7%掉到了61%。周期技术团队排查了六个小时,性漏最后发现问题出在一台NTP服务器上——它的周期时钟漂移了4.3秒,导致分布式锁的性漏过期时间计算出现偏差,两个节点同时认为自己持有锁,周期互相覆盖了对方的性漏写入。
这不是周期什么高深的安全漏洞,没有黑客入侵,性漏没有SQL注入。周期就是性漏一个时间同步的小问题,每隔37天左右会触发一次。周期因为那台服务器的性漏晶振在温度变化下会有周期性误差累积,到了某个阈值,周期漂移量刚好越过分布式锁的容错边界。
我管这类东西叫“周期性漏洞”。它跟传统意义上的bug不太一样。传统bug是你写错了代码,逻辑有缺陷,一跑就崩。周期性漏洞是代码本身没写错,逻辑也自洽,但它在特定时间窗口内会失效。平时一切正常,监控指标漂漂亮亮,然后到了某个周期节点,啪,炸了。
2012年闰秒那天,Reddit、Mozilla、LinkedIn 同时挂了。Linux内核的闰秒处理逻辑跟某些应用层的时间计算撞在一起,CPU负载直接飙到100%。事后大家改代码、打补丁,但根本原因是:我们整个技术栈对时间的假设太脆弱了。
你写代码的时候默认时间是均匀流逝的,每秒就是每秒。但真实世界里,NTP同步会跳变,闰秒会插入,虚拟机迁移会导致时钟偏移,容器共享内核时钟但又有自己的时区配置。这些因素叠加在一起,时间就不再是单调递增的了。
更麻烦的是,周期性漏洞往往藏在依赖链的深处。你用的那个缓存库,它内部有个定时清理任务,每30分钟跑一次。你用的那个消息队列,它的心跳检测间隔是15秒。你用的那个数据库,它的MVCC快照有版本号回绕的问题。单独看每个组件都没毛病,但它们的时间周期如果形成了某种共振关系,就会在特定时刻同时出问题。
我见过最离谱的一个案例:某金融系统每年3月和9月的第二个周二下午会短暂不可用。后来查出来,是因为那天有系统维护窗口,运维会重启一批服务器。重启导致NTP重新同步,时钟跳了0.8秒。而他们的风控引擎里有个滑动窗口计数器,依赖时间戳做去重。时钟一跳,窗口边界错位,大量请求被误判为重复,直接限流了。
这类问题很难在测试环境复现。你跑单元测试,时间是你mock出来的,想让它走多快就多快。你跑集成测试,环境稳定,NTP不会漂移,也没有闰秒。只有到了生产环境,几万台机器各自有自己的时钟偏差,网络延迟还不一样,才能真正触发。
怎么防?说实话,没有银弹。但有几件事可以做。第一,别信任系统时间做精确计算,用单调时钟。Java的System.nanoTime(),Go的time.Since(),都比currentTimeMillis()靠谱。第二,分布式锁的过期时间留足余量,别卡着理论值设。第三,监控时钟偏移量,把它当成一个跟CPU、内存同等重要的指标。
还有个更根本的问题:很多周期性漏洞的周期,比你的系统生命周期还长。比如某些时间戳回绕问题,32位系统2038年才炸,你代码可能2030年就重写了。但如果你写的是基础设施,是操作系统内核,是长期维护的协议实现,那就得认真对待。
时间这个东西,在计算机里从来都不是理所当然的。它是一堆硬件、协议、配置和假设拼凑出来的近似值。周期性漏洞就是这些近似值在特定时刻集体失效的结果。你没法消灭它,只能承认它的存在,然后在设计系统的时候,给时间留一点冗余和敬畏。