每一个“灵异事件”的背后,都藏着一个被忽略的真相。

做技术这些年,我遇到过不少“诡异”的问题——现象看起来完全不合逻辑,日志里没有任何报错,所有常规手段都查不出原因。客户在催,领导在盯,而你面对的是一个“不应该发生”的问题。

这篇文章,记录几个我亲自解决过的复杂技术问题。不是为了炫耀技术能力,而是想说:那些让你怀疑人生的 Bug,往往只是你忽略了某个最基础的细节。 希望这些踩坑经历,能帮你在未来少走一些弯路。


病例一:K8s 集群的“幽灵”延迟

症状

一个在线服务,每天凌晨 2:00–2:15 之间,接口响应时间会从 50ms 飙升到 3–5 秒。白天一切正常,只有这个时间段出问题。监控显示 CPU、内存、网络 IO 都很平稳,没有明显的流量波峰。

客户怀疑是“定时任务抢资源”,但我们把所有定时任务都排查了一遍,没有一个在这个时间点运行。

诡异的地方在于:现象是准时的,但原因不是任何定时任务。

排查过程

第一周,我把所有常规手段都用了一遍:看慢日志、抓包、看 GC 日志、看数据库连接数……什么都没发现。2:00–2:15 的日志和正常时段几乎没有区别。

第二周,我开始怀疑是不是底层基础设施的问题。因为是在 K8s 上跑的,我决定在那个时间点登录到 Node 节点上看系统日志。

然后我发现了线索:dmesg 里有一条日志,时间戳恰好是 2:00 整:

kernel: CPU soft lockup - detected on CPU 2

“CPU soft lockup”意味着某个 CPU 核心被某个进程长时间占用,导致内核无法响应。但问题是,监控上 CPU 使用率明明很正常啊?

后来我才明白:CPU 使用率是平均值。 一个核心跑满 100%,其他核心空闲,平均值可能只有 12.5%。而我们的监控只看平均值,完美地隐藏了问题。

接下来就是找谁占用了 CPU。用 perf top 在问题时段观察,发现 kubeletcontainerd 在疯狂做某个操作。进一步排查,发现是 node-local-dns 缓存模块的一个 Bug——它在凌晨 2:00 会进行一次“健康检查”,但这个检查在某些内核版本上会触发死循环,导致 CPU 核心被锁死。

解决方案

升级 node-local-dns 到修复版本,同时调整了健康检查的间隔时间,避开业务高峰。问题解决。

复盘

这个案例给我的教训是:监控的平均值会骗人。 现在我在任何新项目里,都会强制加上“单核 CPU 使用率”的监控,不只看平均值。

另外,不要假设“现象准时出现就一定是业务层的问题”。有时候,底层基础设施的“定时任务”才是最隐蔽的杀手。


病例二:MySQL 主从延迟的“罗生门”

症状

一个电商系统,每到下午 3 点左右,主从延迟就会从 0 秒飙升到几百秒。数据库团队说是“写入量太大”,但业务团队坚称“这个时段根本没有大促”。

两边各执一词,问题拖了两个月没解决。

排查过程

我接手后,第一件事是去看慢查询日志。发现从库上有一个查询,平时只要 100ms,但在 3 点左右会变成 30 秒。

慢查询日志里记录了这个查询的 SQL,看起来没有任何问题——索引都在,执行计划也是正常的。这就很奇怪了:同一个 SQL,执行计划没变,为什么白天快、下午慢?

我决定在问题时段手动执行这个 SQL,然后用 EXPLAIN ANALYZE(MySQL 8.0 的一个功能,可以真实执行并返回每一步的耗时)看细节。

结果发现:这个查询虽然用了索引,但需要回表读取的数据量在问题时段突然变大了——因为主库在 3 点左右同步过来了一批数据,这批数据的某个字段值(订单状态)正好是这个查询要过滤的条件。

原来如此:这个查询在正常时段只扫描几百行,但在 3 点左右要扫描几十万行。

那么,为什么主库会在 3 点左右写入大量这种状态的数据?

继续追查主库的 binlog,发现 3 点左右有一个 UPDATE 操作,把一批“待支付”订单的状态更新为“已超时”。这个操作来自一个“订单超时清理”的定时任务——它确实在 3 点运行。

业务团队说“3 点没有大促”是对的,但他们忘了自己设了一个定时任务。

解决方案

两个方向:

  1. 优化那个查询,增加复合索引,避免回表;
  2. 把定时任务的时间改到凌晨业务低峰期。

复盘

这个案例的核心教训是:“没有大促”不代表“没有写入”。定时任务、数据清理、报表生成……这些“内务操作”往往是最容易被忽略的流量来源。

另外,“执行计划没变”不等于“性能没问题”。执行计划只告诉你用了什么索引,但不告诉你扫描了多少行。EXPLAIN ANALYZE 才是真正有用的工具。


病例三:Nginx 的“间歇性”502

症状

一个 Java 服务,上游用 Nginx 做反向代理。业务高峰期,Nginx 会随机返回 502,频率大概每小时几十次。

诡异的是:502 出现的时候,Java 服务的日志里没有任何错误。 也就是说,Nginx 认为后端出问题了,但后端觉得自己好好的。

排查过程

先看 Nginx 的 error.log:

upstream timed out (110: Connection timed out) while connecting to upstream

看起来是 Nginx 连接后端超时。但 Java 服务的端口明明是正常的,telnet 也能通。

抓包看一下。在 Nginx 服务器上用 tcpdump 抓包,发现了一个模式:502 发生的时候,Nginx 发出的 SYN 包,后端服务器没有回复 SYN-ACK。

也就是说,TCP 三次握手的第二步丢了。但为什么?

登录到 Java 服务所在的服务器,看 netstat -s 的输出:

101021 times the listen queue of a socket overflowed

这个数字太惊人了。listen queue overflow 意味着什么?当应用程序来不及处理新连接时,内核会把这些连接放在一个“半连接队列”里。如果这个队列满了,新的 SYN 包就会被直接丢弃——这就是我们的 SYN 包收不到 SYN-ACK 的原因。

那么,为什么应用程序来不及处理?看 Java 服务的线程池配置,acceptCount(Tomcat 的 backlog 参数)被设置成了 50,而业务峰值 QPS 是 2000。50 的队列,在高并发下瞬间就会被填满。

解决方案

acceptCount 从 50 调整到 2000,同时调整了 Linux 内核的 net.core.somaxconn 参数。

复盘

这个案例让我明白:502 不一定是后端挂了,也可能是后端太忙了,忙到连“我太忙了”这句话都没时间说。

另外,不要只看应用日志。 有时候,问题藏在内核的统计信息里。netstat -sss -lnt 这些命令,是排查网络问题的第一道防线。


病例四:Elasticsearch 的“数据消失”事件

症状

客户反馈:某个索引里的一部分数据“消失了”。但没有任何人执行过删除操作,也没有数据过期策略。就是某一天,这批数据凭空不见了。

排查过程

第一反应是:是不是有人误操作了?查了所有操作日志,没有人删除过数据。这就诡异了。

打开 Elasticsearch 的日志,发现了一个可疑的警告:

flood stage disk watermark exceeded

这是 Elasticsearch 的“磁盘保护机制”。当磁盘使用率达到 95% 以上时,ES 会强制将所有索引设置为 read_only_allow_delete 模式,禁止写入。但这里说的是“禁止写入”,不是“删除数据”啊。

继续看日志,发现更早的时候还有一条:

failed to create segment: disk full

原来,ES 在写入数据时,会先把数据写入内存缓冲,然后定期刷新到磁盘上的 segment 文件。如果磁盘满了,segment 文件写不进去,ES 会尝试把已经写入内存的数据“回滚”。但这个回滚过程有一个 bug(在 7.x 的某些版本中存在):在某些边界条件下,回滚操作会错误地标记一批已经持久化的数据为“可删除”,然后在下次 merge 时把它们物理删除。

解决方案

  1. 从快照中恢复数据(幸好有定期快照);
  2. 升级 ES 到修复版本;
  3. 调整磁盘水位线配置,在达到 85% 时就提前报警,而不是等到 95% 才触发保护。

复盘

这个案例的教训是:“只读模式”听起来无害,但在 ES 的复杂机制下,可能会触发意料之外的连锁反应。

另外,磁盘监控的阈值不能太保守。95% 才报警,意味着在报警之前,系统可能已经进入了危险状态。我现在所有的系统里,磁盘使用率达到 70% 就开始预警,80% 就是严重告警。


病例五:Kafka 消费积压的“元凶”

症状

一个实时数据处理 pipeline,Kafka 消费端的延迟(Consumer Lag)在业务高峰期会突然飙升,然后又自己降下来。监控显示消费者的处理速度并没有明显下降,但 Lag 就是涨上去了。

排查过程

这个问题的排查思路一开始就偏了——我们都以为“Lag 涨了”等于“消费慢了”。但监控数据显示消费速度是稳定的,这说不通。

后来我想到一种可能:Lag 的计算方式是“最新消息的 offset 减去当前消费的 offset”。如果“最新消息的 offset”突然跳涨,那么即使消费速度不变,Lag 也会涨。

也就是说,问题可能不是消费慢了,而是生产快了

看生产端的监控,果然发现:在 Lag 飙升的时间点,有一个生产者突然往这个 topic 里写入了大量数据。这个生产者来自一个“数据同步”服务,它在这个时间点正好在做一次全量同步——把一批历史数据全部重新灌入 Kafka。

业务团队说“我们没在大促”,但“数据同步”也是业务的一部分啊。

解决方案

  1. 把数据同步的写入目标从“生产 topic”改为“一个单独的 topic”,避免影响实时流;
  2. 对生产者的写入速度做限流,避免瞬间写入量过大;
  3. 调整 Kafka 的分区数,提高并行消费能力。

复盘

这个案例的核心教训是:Kafka Lag 的上涨,可能是生产端的问题,不一定是消费端的问题。

另外,监控不能只看“消费速度”,还要看“生产速度”。两个放在一起对比,很多问题就一目了然了。


结语:诊疗室的“药方”

回顾这些“疑难杂症”,我发现它们都有一个共同点:所有问题最终都指向了一个被忽略的基础环节。

  • “幽灵延迟”指向了被平均 CPU 掩盖的单核问题;
  • “主从延迟”指向了被忽略的定时任务;
  • “间歇性 502”指向了内核的 listen queue;
  • “数据消失”指向了磁盘保护机制的副作用;
  • “Kafka 积压”指向了生产端和消费端的认知偏差。

所以,如果你也在为一个“灵异事件”头疼,我的建议是:

  1. 从最基础的地方开始查——磁盘、网络、内核参数、系统日志。很多时候,问题不在“上层”的代码里,而在“下层”的基础设施里。

  2. 不要只看平均值——平均值会掩盖很多问题。看分布、看峰值、看单核、看单个实例。

  3. 把上下游一起看——一个系统的问题,可能是上游给的太多,也可能是下游消化太慢。

  4. 相信日志,但不要只相信应用日志——内核日志、系统日志、网络抓包,都是你的朋友。

  5. 如果一个问题持续了很久,问自己一个问题:“我是不是假设了某个东西是正常的?” 那个“正常”的东西,可能就是元凶。


希望这些“诊疗记录”能帮到你。如果你也遇到过什么匪夷所思的技术问题,欢迎在评论区分享——毕竟,技术人的快乐,有一半来自“这个 Bug 不是我一个人的”


本文所有案例均来自真实经历,部分细节做了脱敏处理。

Logo

腾讯云面向开发者汇聚海量精品云计算使用和开发经验,营造开放的云计算技术生态圈。

更多推荐