资讯动态

服务器周期性卡顿元凶:NTP时间同步引发的惊群效应排查实录

发布时间:2026/9/24 6:05:49 来源:尧图企业网站定制
1. 这次“卡顿”到底是什么样1.1 故障现象与影响范围先交代一下环境一台 Ubuntu 22.04 服务器跑着一个内部 ERP 系统前端是 Nginx 反代 Spring Boot 服务后端接了 PostgreSQL另外用 Docker 跑了一个日志采集的 Java 容器。整体部署规模不大但属于业务核心节点一卡整个班组都跟着等。症状刚开始并不剧烈管理后台的 UI 界面偶尔卡一下点个按钮要转圈好几秒VSCode 里连接 SSH 远程服务器看日志时敲命令经常要等最诡异的是这个卡顿不是持续的而是周期性出现。每次大概持续半分钟到一分钟过后一切恢复正常看起来就像什么都没发生过一样。那种“看起来一切正常但你知道它不对劲”的感觉最让人抓狂。因为单看任何一项指标都正常可一到时间点系统就像被人按住了脖子。最初几天我们甚至怀疑是不是运维值班同事的本地网络问题直到发现三台不同的办公电脑、两种不同的远程工具都出现同样的情况才确认问题出在服务器侧。1.2 为什么说它“诡异”我先说结论这次根因排查整整花了三天不是因为工具不够而是因为问题的表现和常规监控完全脱节。服务器卡顿常规思路是看 CPU、内存、磁盘、网络这四件套。但这次每一样查出来都让人想摔键盘——CPU 空闲 99%负载长期在 0.1 以下内存用了不到一半磁盘 IO 完全没有瓶颈iowait 平时基本是 0网络流量也很小一个内部 ERP 系统能有多大的流量可业务方就是反馈卡而且很规律。后来我们把卡顿时间点拉出来比对发现基本集中在业务高峰时段但又没有明显的流量突增。这种感觉就像你去找医生看病说“我头疼”医生给你做了全套检查所有指标都在正常范围内可你的头就是疼。最难受的是你甚至开始怀疑自己是不是记错了症状。在这种时候与其继续凭感觉猜不如退一步把排查思路重新捋一遍。2. 排查思路先搭好框架再动手2.1 服务器卡顿排查看什么、按什么顺序查踩过太多次坑之后我现在的习惯是不管问题看起来多简单先给自己定一个排查顺序避免在某个分支上钻牛角尖。我的默认顺序是先看系统整体负载再看 CPU 和内存然后看磁盘和文件系统接着看网络和连接状态最后才进入应用层面。每一步都要有数据支撑而不是“我看了一眼感觉正常”。具体到命令层面我习惯先跑uptime看 load average再用top或htop看 CPU 使用率和负载进程用free -h看内存用df -h和iostat -x 1看磁盘空间和实际 IO 压力用ss -s和netstat看连接数和状态分布。这套组合拳对绝大部分“假卡顿”是有效的。但实际上真正让人头疼的从来不是“有指标异常”的卡顿而是“所有指标都正常”的卡顿。这也是为什么我特别强调不要只盯着数值还要看数值的时间分布和变化趋势。2.2 常规三板斧为什么不够用所谓“三板斧”就是 CPU、内存、磁盘三层排查。大多数服务器问题都能在这里找到答案但这次偏偏不是。原因在于很多隐蔽问题并不会直接表现为资源消耗过高。比如进程在等待某个锁、线程被阻塞在某个系统调用上、CPU 在执行自旋等待这些情况在top里看到的 CPU 使用率可能很高也可能很低。如果是在等待 IO 或者网络事件CPU 根本不会被打满但业务就是卡。还有一个很容易被忽略的点如果你排查的是虚拟机或者云服务器那么宿主机上的情况同样会影响到你的实例。比如 CPU steal虚拟 CPU 等待真实 CPU 的时间在虚拟机内部看 CPU 使用率很正常但实际计算能力已经打了折扣。这种“隐形消耗”不借助专门工具是看不到的。换句话说常规三板斧能解决的是“资源被谁吃掉了”这一类问题但卡顿往往不是“被吃掉”而是“被卡住”。所以排查思路必须从“看资源”转向“看状态”。2.3 工具选型与监控思路这次排查我用到了一批开源工具都是日常服务器运维中非常常见的这里列一下并说明各自的用途sar历史性能数据回放排查周期性卡顿特别有用可以看到卡顿发生那一刻各项指标的变化atop比top更细能记录一段时间内的进程级资源占用和状态变化iostat/vmstat/mpstat分别看磁盘 IO、内存分页和 CPU 各核使用情况适合精确定位是哪一类资源出现波动dmesg/journalctl内核日志和系统日志很多诡异问题其实在这里都有痕迹jstack/jstat针对 Java 应用看线程状态和 GC 情况strace跟踪系统调用能定位到进程到底卡在哪个调用点。我的建议是排查之前先把sar的数据拉出来看一遍。它天然记录了系统的历史状态可以帮你判断卡顿是不是真的“没征兆”。很多时候你回头看数据会发现其实早有迹象只是当时没有注意。3. 第一轮排查常规指标全部“正常”3.1 负载、CPU、内存全都正常第一天的排查比较常规。我先敲了uptimeload average 大概是 0.08、0.05、0.02低得感人。top看一眼CPU 空闲率 99%%Cpu(s)里 us、sy、wa、st 四项几乎全是 0。内存也毫无压力总共 16G 内存用了不到 7GSwap 甚至从来没被触碰过。这种结果让我一度怀疑是不是业务方误报。但既然反馈持续出现我只能继续往下走。我又用mpstat -P ALL 1看每个 CPU 核心的使用情况发现不仅整体空闲每个核也都是空闲的根本没有某一个核被打满的情况。于是排查重点暂时排除 CPU 和内存。这里要说一下如果某个核被打满通常会伴随明显的业务卡顿而且很容易定位但当所有核都空闲的时候反而要警惕——问题很可能不在计算资源上而在等待和阻塞上。3.2 磁盘与IO没有异常写入磁盘是我第二个怀疑对象。内部 ERP 系统有大量的报表导出和日志写入如果某块盘出现 IO 抖动业务卡顿非常正常。但iostat -x 1一圈看下来磁盘的%util基本在 1% 以下读写延时也在正常范围。df -h显示各分区剩余空间都充足没有文件系统被写满的情况。我不太放心又用iotop盯了一会儿确认没有进程在持续做大量的读写。当时甚至怀疑过会不会是数据库的 WAL 日志频繁 fsync但 PostgreSQL 的checkpoint时间间隔正常慢查询日志也没发现明显异常。磁盘方向暂时排除。但这里有个伏笔当时我检查的都是“当前”状态没有注意到卡顿是否与某些定时任务重合。周期性问题的特征就是“发作时异常、恢复后正常”如果你查的时候正好错过了发作窗口很容易得出“一切正常”的结论。3.3 网络与连接数流量不大但连接多既然本机资源都正常我转向网络。用ss -s看了一眼连接统计发现连接数其实不少TIME_WAIT 状态的连接接近两千这对于一个内部 ERP 系统来说偏高但还没到异常的程度。用iftop观察实时流量发现流量很小峰值也就几 MB/s完全没有网络拥塞的可能。当时我还担心是不是内网交换机端口有问题或者服务器网卡固件有 bug于是登录交换机看了一眼端口统计丢包和错误计数都正常。排查到这里第一天基本宣告结束结论是服务器本身看起来什么问题都没有。但业务方依然反馈卡顿而且我们通过监控平台发现了一个有意思的细节每次卡顿发生时Nginx 的请求响应时间会瞬间飙到十几秒但请求量并没有明显增加。这个线索让我意识到问题不在“来多少请求”而在“请求卡在哪里”。3.4 从应用日志里找线索第二天开始我把重点放在应用层面。先看 Spring Boot 的日志发现卡顿时间段内大量请求超时超时的服务集中在几个内部接口上。再往深挖发现这些接口都在调用一个公共的缓存组件而这个缓存组件在部分场景下会触发锁等待。这里要注意应用日志里看到的“超时”不一定是应用本身的问题。很多时候是底层资源或者系统调用卡住了应用层只是被动表现出超时。我见过不少人排查到这里就下结论说“代码有 bug”然后一头扎进代码里结果浪费大量时间。我当时的做法是先把超时服务的线程栈抓下来。用jstack连续抓了几次发现大量线程卡在sun.misc.Unsafe.park上也就是在等待锁。同时用jstat -gcutil看了 GC 情况FGCFull GC次数并不高但每次 Full GC 的耗时接近两秒对于一个经常要响应请求的服务来说两秒的 STWStop The World足以造成明显的卡顿。但我仍然不敢直接断定是 GC 问题因为 Full GC 是每隔一段时间才触发一次而业务反馈的卡顿是周期性的两者虽然时间上有些重合逻辑上还不够硬。4. 第二轮深挖从假象到真相4.1 怀疑网络问题ARP/IP冲突那点事第二天下午排查方向一度被带偏到网络层原因是dmesg里出现了几条很可疑的日志看起来像是网络地址冲突。当时的第一反应是内网有人手动配了 IP导致和这台服务器冲突了。如果真是这样那所有卡顿都能解释通——IP 冲突会导致 ARP 表反复刷新网络连接时断时续自然会卡。于是我们把交换机的 ARP 表、服务器上的 ARP 缓存全部翻了一遍也挨个排查了同网段的设备 IP。排查持续了几个小时期间确实发现了几个不规范配置但修改之后卡顿依旧。后来我才意识到那几条 ARP 日志其实是网络抖动时正常出现的日志并不是 IP 冲突只是我看得太急产生了误判。这也是一个很典型的教训在排查过程中不要看到一个可疑点就急着下结论除非你能完整解释所有现象。IP 冲突解释不了“管理界面卡顿但 SSH 偶尔能连上”这个现象因为真正的 IP 冲突会直接导致网络频繁断连而不是间歇性卡顿。4.2 怀疑容器内存与GCJava应用背锅记排除网络后我回到应用层继续深挖。因为之前jstat显示 Full GC 耗时异常我开始怀疑是不是 Docker 容器里的 Java 应用有问题。当时docker stats显示那个 Java 容器占用内存确实偏高接近容器限制的上限看起来很像堆内存不足导致频繁 GC。我就顺着这条线查了很久调整堆内存参数、查看 GC 日志、分析堆转储甚至把容器内存限制调大了一倍。但业务仍然卡而且卡顿时间点依然很规律。这时我发现一个被我忽略的细节每次卡顿时容器的 GC 并不是同时发生的而是稍微滞后几秒。换句话说是先卡顿后 GCGC 可能是卡顿的结果而不是原因。我又检查了 Kafka 消费组的 lag发现卡顿期间 consumer 的 lag 有明显上涨这进一步说明卡顿影响到了整个应用的所有线程而不仅仅是某一个接口。问题明显在更底层的地方。那两天晚上我一直在想一个 CPU、内存、磁盘、网络都正常的系统为什么会周期性卡顿直到第三天上午我无意中做了一个操作才把整个排查方向扭转到正确的轨道上。4.3 一次偶然的怀疑系统时间在“跳”那天我在观察系统日志时发现/var/log/syslog里有一些和系统时间同步相关的记录时间戳对应的节点正好和业务卡顿的时间点高度吻合。我顺手敲了timedatectl status发现服务器用的是 NTP 同步但chronyc tracking显示的偏移量有点不太对劲——系统时间在同步过程中出现了明显的跳跃。也就是说这台服务器的时间在每次同步时不是平滑地微调而是直接跳了一下。系统时间一跳所有依赖时间的逻辑全部被触发从服务端到客户端、从数据库到缓存全线抖动。那一刻我基本确定问题就出在这里。5. 根因确认NTP时间同步引发的“惊群”5.1 时间同步原理step与slew的区别先解释一下 NTP 时间同步的两种方式。一种是slew也就是渐进调整系统时间会以非常小的步长慢慢逼近真实时间对应用几乎无感另一种是step也就是直接跳跃系统时间瞬间拨到目标值。绝大多数 NTP 客户端在系统时间偏差超过一定阈值时比如 128 毫秒甚至更大会使用step方式因为渐进调整太慢根本追不上。这台服务器的情况正是如此内网 NTP 服务器本身不稳定导致系统时间跑偏的幅度越来越大每次同步时都触发了step而step一旦发生系统时间就在瞬间变了几个甚至几十秒。这对那些依赖时钟的组件来说是致命的。为什么会这样打个比方你在排队队伍每隔一段时间会突然往前窜一大截虽然最终你还是能到柜台前但中间所有人都会因为惯性撞在一起队伍反而乱套了。服务器里的线程也是这样时间一跳所有依赖时间的等待和超时逻辑被同时唤醒或重制造成瞬间的争抢和堆积。5.2 为什么时间跳变会让服务器“卡顿”时间跳变引发卡顿的机制说穿了就是“惊群效应”加“超时风暴”。一方面很多框架和组件会基于系统时间计算超时时间、缓存过期时间、会话有效期。时间突然跳变原本还没到期的缓存可能瞬间过期大量线程同时去重建缓存原本还没超时的连接可能瞬间超时大量线程同时去重连原本还在等待的任务可能瞬间判定失败大量线程同时发起重试。另一方面Spring Boot 内嵌的 Tomcat 连接池、数据库连接池、各种第三方客户端都有基于时间戳的心跳和空闲判断。时间一跳它们的判断逻辑全部乱套结果就是连接在短时间内全部重建。连接重建本身需要消耗 CPU 和网络资源再加上业务线程大量并发重试系统的处理能力瞬间被击穿表现出来就是卡顿。我们当时看到的 Full GC 耗时异常也是因为时间跳变导致大量对象瞬间变为垃圾JVM 不得已进行一次大清理。GC 不是根因只是时间跳变的“下游受害者”。5.3 修复方案与代码层面的加固定位到根因之后修复其实很快。第一步是更换时间同步策略把内网 NTP 服务器配置改成稳定的上级时间源并在 chrony 配置里增加参数限制系统在运行状态下不要轻易使用step方式调时优先采用slew方式渐变调整。具体配置如下# /etc/chrony/chrony.conf server ntp.internal.example.com iburst makestep 1 -1makestep 1 -1的含义是只有在系统时间偏差超过 1 秒时才允许 step而且只在启动阶段生效。正常运行期间即使有偏差也只用 slew 方式微调避免对运行中的服务造成冲击。第二步是清理和重建应用层的时间依赖。我检查了业务代码发现有几个工具类直接使用System.currentTimeMillis()做缓存过期判断和分布式锁超时计算这在时间跳变时会非常脆弱。后来统一改为单调时钟System.nanoTime()或者使用 Guava 的Ticker来处理相对时间计算。单调时钟不随系统时间跳变只反映两个时刻之间的间隔适合做超时和延迟计算。第三步是加监控。我在监控平台里加了系统时间偏移量的指标设置告警当偏移量超过 50 毫秒时提醒超过 200 毫秒时 page。同时也把 NTP 同步状态纳入日常巡检项确保类似问题能第一时间暴露。5.4 验证与效果修复之后我们继续观察了三天。卡顿现象彻底消失业务方反馈管理后台操作流畅SSH 远程连接也不再出现间歇性卡顿Kafka 消费 lag 也恢复平稳。复盘一下为什么根因“藏了三天”第一问题周期性出现每次发作时间很短常规巡检很容易错过现场第二系统资源指标在卡顿前后都正常按常规套路排查自然一无所获第三中间被 ARP 日志和 GC 异常这两个假象带偏了方向浪费了大量时间。而时间同步问题往往不会被列入常规排查清单如果不是偶然在 syslog 里看到可疑记录可能还要更久才能定位。6. 常见问题与排查技巧实录6.1 服务器卡顿排查的常见原因速查表这段时间的复盘让我整理出了一份服务器卡顿排查速查表不止针对这次时间同步问题也覆盖我以往遇到的各类情况现象特征可能原因重点排查对象CPU 高、load 高进程死循环、GC 频繁、资源竞争top、jstack、perf top内存高、Swap 使用内存泄漏、堆配置不合理free -h、jmap、docker statsiowait 高、磁盘 util 高磁盘瓶颈、大量小文件、备份任务iostat、iotop、dmesg网络连接多、延迟高连接未释放、DNS 慢、路由问题ss -s、iftop、traceroute周期性卡顿、资源正常定时任务、NTP 时间跳变、监控采集crontab -l、chronyc tracking、sar虚拟机卡顿、宿主机正常CPU steal、宿主机邻居干扰top看%st、宿主机监控UI 卡顿但 API 正常前端资源加载、后端接口慢浏览器 Network、Nginx access log容器应用卡顿容器资源限制、镜像层 IO、Docker 网络docker stats、docker inspect、dmesg这个表不一定能覆盖所有情况但能帮你快速圈定方向。6.2 排查看不见的问题有哪些“隐蔽观察窗口”我始终认为排查隐藏问题最有效的思路不是“多查几遍”而是“让数据说话”。有几个隐蔽观察窗口值得重点留意第一sar是最容易被低估的工具。它可以按分钟粒度回放历史数据哪怕问题已经过去了你依然能通过sar -q、sar -u、sar -r、sar -d看到当时系统的真实状态。我很多问题的定位都是从sar的异常回放开始的。第二内核日志dmesg里往往藏着时间戳。不管是网络地址冲突、磁盘 IO 错误、内存不足还是时间同步问题内核日志一般都会留下记录。关键是养成“先看时间戳、再比对业务卡顿时间点”的习惯。第三应用日志的“逆行排查”。卡顿发生时应用可能没有直接报错但你从日志里能看到线程卡在哪里。jstack连续抓几次线程栈对比线程状态的变化能快速定位是否在等待锁、等待 IO 或者执行 GC。第四对于虚拟化环境一定要关注 CPU steal 指标。top里%st字段如果持续大于 0说明宿主机资源已经紧张你的虚拟机性能会受到“隐形”影响。这种情况在虚拟机内部看 CPU、内存都正常但执行效率就是上不去。6.3 排查中容易踩的坑最后说几个这次踩过的坑都是真实教训。第一个坑是“看到一个可疑点就急着收网”。第一天看到 ARP 日志就往 IP 冲突方向钻结果浪费了整整半天。现在我的原则是任何结论至少要能解释两个以上的独立现象否则宁可先存疑。第二个坑是“只盯当前值不盯时间线”。周期性问题的最大特征就是“发作期”和“正常期”交替如果你没有在发作期抓到数据很容易被“正常状态”欺骗。最好的办法是拉一段足够长的时间窗口把指标按时间画出来看它和卡顿时间点是否重合。第三个坑是“修好了就完事不做验证”。我见过不少人改完配置后没有持续观察结果第二天又出问题。修复 NTP 配置后我坚持观察了三天并且每天都手动核对时间偏移量和业务响应时间确认稳定后才认为问题真正解决。第四个坑是“忽略容器和宿主机的边界”。排查 Docker 容器问题时不要只盯着容器内部宿主机的时间、网络、存储同样会影响容器。这次 NTP 问题影响的就不只是宿主机容器内的应用也一并遭殃因为 Docker 默认和宿主机共享时钟命名空间。最后说点个人的体会这次排查让我重新认识了“时间同步”这个平日里几乎不被关注的底层模块。服务器时间看似是小事实际上动一发而牵全身。后来我再遇到类似的“表面一切正常但实际卡得要命”的问题都会先看四个东西系统时间偏移量、CPU steal、内核日志、定时任务。这套组合拳下来大多数隐蔽问题的方向都能在短时间内锁定。排查卡顿问题很多时候拼的不是工具多先进而是能不能把表象背后的逻辑链条理清楚。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价