深夜两点半,值班手机的告警声把我从沙发上拽起来。监控面板上,某海外区域的"数据写入成功率"曲线正以一个相当陡峭的角度往下掉,同时用户回显时延的中位数曲线却在一路蹿升。群里已经有人在问:"是不是大面积故障?"但奇怪的是,中国区和其他区域的指标一切正常,只有海外区域的用户受到影响。这种"只看地域不看服务"的报错方式,往往比全盘崩溃更让人头疼——它意味着问题藏在你平时不太会注意的某个角落,链路越长,排查起来越费劲。
那段时间我正好负责这套跨区域系统的稳定性维护,接到告警后连夜把日志、监控、抓包数据全部翻了一遍。整个过程持续了将近七个小时,中间走过不少弯路,最后定位到的问题也很有代表性。这篇文章就把整个排查过程完整复述一遍,把中间用到的命令、思路和踩过的坑都摊开来说。如果你是做后端开发、运维或SRE的,尤其手上还管着跨地域业务,这篇内容应该能帮你避开不少同类问题。
1. 现场还原:数据丢在哪一环,延迟又何来
故障描述永远是排障的第一手情报,但是用户描述往往带着情绪和误判,不能全信,要自己去做技术验证。
1.1 现象:"请求成功了,但数据消失了"
这次故障最早是客服工单先爆出来的。用户反馈:在页面上提交了一条内容,界面显示"提交成功",但刷新之后内容不见了;更有意思的是另一些用户反馈,提交之后等了好几秒才出现成功提示,中间页面一直处于"处理中"的状态。
后台监控到的数据也很清晰:
- 写入成功率从正常值 99.95% 掉到了 86% 左右,这是一个非常明显的劣化;
- 接口 P99 时延从 480ms 拉高到了接近 8 秒;
- 错误码并没有大量增加,因为失败的请求大多没有走到返回错误码那一步,而是直接超时或被链路中间层吞掉了;
- 资源水位却非常奇怪:应用服务器的 CPU 和内存几乎毫无波澜,数据库也没有明显的慢查询风暴。
这套系统是典型的"边缘入口加中心处理"结构:海外用户先就近接入边缘节点,再通过专线或者公网访问部署在中心机房的网关、应用、缓存和数据库。正常情况下,海外用户会分配到离自己最近的边缘节点,然后从那里走一条优化的内部链路到中心机房。
测试一个用户的请求时,我发现这个用户的流量绕了一段很长的路径,数据包在中转节点之间的往返时延已经达到 900 多毫秒,正常应该在一两百毫秒以内。从那一刻起,我基本认定这一次不是一个单点故障,而是从用户到机房之间某个环节的路由、调度或者链路出了问题。
1.2 影响面评估:是全部用户还是部分用户
在拉群之前,我先把影响面收敛了一下。怎么快速判断是全量还是局部?看两个维度:维度一是"听起来像片区的客户投诉",维度二是"监控指标有没有按地域分桶"。
这套系统在网关层做了多区域标签,所以我可以直接按 region 字段把写入成功率拆开看。拆完以后发现,主要集中在这个海外片区,其他片区(包括中国区、另一个海外区)的写入成功率几乎纹丝不动。这样范围就清晰了:不是中心机房全挂,而是只影响某一片海外用户。
这非常关键。如果范围没有先圈住,后面你很容易被"全网故障"的架势带偏,在中心机房反复找问题,浪费时间。
另外,我还顺手看了 CDN 厂商在对应地区的状态页。状态页这种东西平时不一定准,但值得扫一眼,如果基础设施商自己都挂出来了,那排查重心又不一样了。这次的检查结果是一切正常。
2. 链路排查:从用户侧推进到机房层
范围锁定了,接下来就是沿着一条用户的请求链路,从客户端一步步走到机房,看阻塞和丢包发生在哪个环节。
2.1 先看 DNS 和接入节点调度
跨域场景的第一关,就是域名解析。域名解析的结果决定了用户会被导到哪个边缘节点。如果解析结果把海外用户导到了另一个片区的节点,用户流量就可能跨越大半个地球,拓扑乱套,延迟必然爆炸。
我用了一个典型的排查做法,在自己机器上用公共 DNS 去解析,同时让海外片区的测试同学帮忙从本地运营商 DNS 去解析,看返回的 IP 是否一致。结果很有意思:
- 公共 DNS 返回的 IP 是片区 A 节点;
- 本地运营商 DNS 返回的 IP 却指向了片区 B 节点;
- 而片区 B 节点到中心机房的链路,恰好存在比较明显的拥塞。
这就解释了为什么回显延迟能拉到数秒:用户的请求被解析到了劣势节点,接着走拥塞链路回到中心机房,一来一回的 RTT 已经接近秒级,再加上应用层处理超时重试,延迟自然越来越过分。
DNS 层面的调度失效,是这类"只有部分用户遭殃"故障里最常见的原因之一。它跟客户端本地缓存、运营商缓存刷新机制、以及我们自己在域名服务商那里配置的线路策略都有关系。当地运营商没有按要求刷新新记录时,老记录就会一直残留,直到 TTL 过期。
2.2 抓包验证:数据包到底转了几圈
DNS 的信息只说明"解析可能是错的",还不能直接定罪。我随手在边缘节点 A 和 B 上都抓了一份数据包,看了一下从用户侧过来的 TCP 连接究竟落在了哪个节点。
抓包命令倒非常简单,典型的长这样:
tcpdump -i eth0 tcp port 443 and host <用户出口IP> -w /tmp/client_443.pcap -c 5000抓完以后用 Wireshark 或者 tshark 分析往返时延。当场就能看到,从同一用户出口 IP 到节点 B 的 TCP 握手时延是 220ms 左右,到节点 A 只有 80ms。差距非常直观:节点 B 并不是这个用户的最佳接入点。
顺着链路再往内层看,从节点 B 到中心机房的 TCP 时延又比 A 高出一截。A 到中心机房 40ms,B 到中心机房 300ms。这个 300ms 的段就非常可疑,拥塞链路多半在这里。
2.3 应用层日志:写入到底成功没有
网络层的情况看明白了,再回应用层核对。每个写入请求处理的完整链路大概是:边缘节点 → 网关 → 业务后端 → 缓存 → 数据库。我在网关和业务后端拉出同一个 requestId 的日志,仔细过了一遍时间戳,结果是:
- 网关收到请求的时间早于业务后端的实际处理时间,中间隔了大概 640ms,说明请求在网关到后端的连接上等了一会儿;
- 业务后端执行写入成功,数据库也返回了成功;
- 但返回给用户的响应数据包,在边缘节点出方向又阻塞了很久。
这里出现了一个很微妙的细节:数据库写入已经成功了,用户端却迟迟收不到回显。站在用户角度,就会觉得是"卡了";如果客户端设置了较短的超时时间,用户还会主动重发,于是同一个内容被写入两次甚至三次,如果没有幂等处理,就是另一类数据重复问题。
我顺手又查了一下缓存的回源逻辑,因为用户刷新页面时一般会先查缓存,缓存未命中再回源到数据库。之前提到有一部分用户反馈"提交成功但刷新后内容消失",很有可能是写了主库,但缓存由于失败回源或者淘汰策略没有读到最新内容。
查下来果然,那条链路的缓存回源率异常升高,群里有人已经在问:"是不是大面积故障?"但奇怪的是,中国区和其他区域的指标一切正常,只有海外区域的用户受到影响。这种"只看地域不看服务"的报错方式,往往比全盘崩溃更让人头疼——它意味着问题藏在你平时不太会注意的某个角落,链路越长,排查起来越费劲。
3. 真正的根因:调度失效叠加链路拥塞的连锁反应
到这一步,大量现象已经能串联起来了。但要准确描述根因,还需要把两个看起来各自独立的问题放到同一个时间轴里去看。
3.1 解析调度失效是怎么造成的
先看调度失效。智能 DNS 解析策略本身就有个前提:用户用的是符合规范的递归解析服务,配合正常的 TTL 机制。但这次覆盖到该片区的场景是:
- 用户侧大量使用的是本地运营商递归 DNS,它对老记录特别"念旧",缓存刷新机制不够积极;
- 我们在域名服务商侧修改了一次线路加权配置,但新的记录并未即时覆盖全部递归节点;
- 部分用户客户端本身也有本地 DNS 缓存,在旧 TTL 还没有过期前,会持续命中旧记录。
于是用户流量继续源源不断地打到旧节点 B。而节点 B 的服务健康状况当时已经因为另一条专线故障而劣化,链路拥塞逐步加剧。
这里值得补一句:DNS 调度出问题,不一定是不调度,而是"旧调度没有及时退出"。在新旧记录交接的窗口期,边缘节点本身的熔断和退避机制如果没有做扎实,客户端是不会知道自己该换路的。
3.2 拥塞链路上的 TCP 重传,如何放大了延迟
链路拥塞带来的直接后果,是 TCP 层的重传次数激增。而在 B 节点到中心机房这一段,中间还叠加了运营商级 NAT 设备和中转代理,丢包之后重传所等待的超时时间相当长,这就让单个请求从发送到收到响应的 RTT 持续被拉大。
从抓包里看到的典型场景是:同一个数据包重传了三次,每次间隔在 200ms 到 600ms 之间,最终累计延迟轻松突破 2 秒。再叠加应用层的重试机制,用户侧感知到的等待时间从原来的 500ms 直接变成 6 至 8 秒。
所以很多情况下,用户感受到的"回显延迟",不是代码性能变差了,而是网络链路变差了、重传和排队放大了耗时。
3.3 "数据丢失"真面目:部分请求超时后,客户端误判并重发
再回头看"数据丢失"这个表象。其实数据库真实的写入条数并没有大量减少,减少的是"用户成功感知到写入完成"的比例。具体来说是:
- 业务后端收到请求,正常写库成功;
- 响应包在回程链路上超时未到达用户端;
- 用户端认为提交失败,启动重试;
- 重试请求同样卡在这条拥堵链路上;
- 部分请求排队过长,最终被边缘网关的全局超时配置主动断开。
用户在界面上看到的,是自己提交的内容没有回显,刷新后因为缓存回源率也出了问题,内容一会儿在、一会儿不在,感官上就是"数据丢失"。
这种"假丢失"和真正的存储节点宕机导致的丢失有本质区别。前者数据已经在库里,只是感知链路断了;后者是真的没有持久化。排查时要抓重点先做数据校验:直接查数据库确认数据还在不在。这个步骤能帮你快速分清到底是"没写进去"还是"写进去了看不到"。
4. 修复与止损:从临时切换到底层优化
先说结论:这类故障通常不会只有一个根因,DNS 调度失效和链路拥塞互相叠加、互相放大,所以修复也要分层次来。
4.1 立即止损:把用户流量切入健康节点
第一件事不是修代码,而是先把这批用户的流量从劣势节点 B 导走。
操作上分两步:
- 在域名服务商侧将片区用户的解析记录强制切到节点 A,并把 TTL 临时调低到 30 秒,加速新记录生效;
- 在边缘节点 B 上关闭对当前片区的写入路由,让新请求不再进入该节点;对新请求,边缘网关直接回 302 或 307 重定向到 A 的接入地址。
切量后大概十分钟,监控上的写入成功率曲线开始回拉。半小时内恢复到 99% 以上。这个动作证明:核心问题不是中心服务能力,而是接入链路。
实际操作中有一个细节:切量不要太激进。如果节点 A 的容量本来就不够,把 B 的全部流量都塞过去,会造成 A 的过载。这次因为 A 预留了弹性容量,所以整体切换还算平顺,但我见过太多因为切量过猛把健康节点拖垮的案例。先把 30% 的流量切过来观察两分钟,再逐步放量到全量,更稳。
4.2 超时、重试与幂等的重新设计
止损完成后,需要对应用的超时和重试机制做一次调整。这类机制的配置平时很难验证效果,只有当链路抖动发生时,才会暴露出各种问题。
- 客户端请求超时时间从原来的 3 秒调到了 8 秒,避免链路抖动时客户端频繁主动断开;
- 边缘节点到后端的超时上限略微放宽,防止正常的慢请求被误杀;
- 在关键写入接口上,把幂等做得更完整。请求 ID 在客户端生成,服务端用唯一索引做去重,保证即便用户重试多次,数据库里也不会有重复数据。
这里要特别强调幂等的重要性。跨域场景下,网络抖动几乎是不可避免的,客户端重试是一个常态。如果写入接口没有幂等,一次用户可见的"超时"就会造成后端多条重复数据,后续对账和补偿的成本非常高。
幂等的实现不一定要很重,常见的做法是:在业务表里加一个 request_id 字段,并建 unique index。写入时先按 request_id 查一下,存在就直接返回旧结果,不存在才真正写库。
-- 伪代码:基于 request_id 的幂等写入 insert into biz_record (id, request_id, content, status) values (#{id}, #{requestId}, #{content}, 1) -- 如果 request_id 已存在,唯一索引冲突,则直接 select 出已有记录返回4.3 缓存回源与一致性优化
缓存这一层的调整同样重要。之前提到,用户刷新时出现内容一会儿有一会儿没有,是因为缓存未命中回源时,链路拥堵导致回源失败率上升,大量请求降级为空结果。
这里意识到一个现象:很多情况下,业务侧写数据库成功后,缓存更新失败被静默吞掉了。日志里虽然有 warn,但日常没人看。于是用户读的时候,缓存里是旧值或者空,一直等缓存过期才会回到新值。
针对这个故障,我的处理策略是:
- 写入时更新缓存不再静默失败,至少要有日志和指标,能让值班的人感知到缓存更新异常率;
- 读取链路增加了"缓存 miss 时穿透到数据库并重建缓存"的逻辑,同时对重建动作加分布式锁,防止缓存雪崩式的回源;
- 把缓存的 key 时效跟业务场景对齐,这个场景的内容型数据要求强一致,所以直接把缓存过期时间从 10 分钟调整为 30 秒,宁可回源压力大一点,也要保证用户看到的内容尽量接近最新。
4.4 链路层面:双路冗余与自动切换
既然动到了链路,那专线的冗余和自动感知自然要摆上台面。
国内访问海外链路、或者不同海外区域互访,本质上是跨运营商、跨国界的网络传输,链路质量受很多因素影响,并不完全可控。这次是想彻底解决,最简单直接的办法是再拉一条备用线路,做主备切换。
但光有线路还不够,还要有自动切换能力。要不然下次链路再抖动,我们又是后知后觉,等用户骂上门才知道。
我给这套系统加了一个链路质量探活,按照 10 秒一次的频率去探测每个边缘节点到中心机房的 RTT 与丢包率,当连续 6 个探测周期超过阈值时,自动把该节点标为不健康并触发 DNS 层面的流量切换。
这套策略实现后,后续同样的问题再出现时,最快可以在 60 秒内把流量切换到备用路径,而不是像这次一样靠人肉盯监控。
5. 常见问题与排查技巧实录
故障处理完了,把这次用到的排查技巧和遇到的坑整理成一份速查表。这些内容平时在文档里未必写得全,但都是实操中真正管用的东西。
5.1 跨域故障排查的既定顺序
我建议所有跨域类故障都按这个顺序来,不要再从应用代码的汪洋大海里捞针:
- 先圈定影响范围:按地域、按用户分组、按客户端类型分桶看指标;
- 再看 DNS 解析差异:用公共 DNS 和本地 DNS 对比,确认接入节点是否一致;
- 抓包验证链路时延:TCP 握手时延、TLS 握手时延、首包时延,三层数据逐个对照;
- 查应用层全链路日志:同一个 requestId,从入口到出口的所有时间戳排开;
- 最后才是看代码和配置:超时、重试、幂等、缓存策略。
这五步做完,80% 的跨域问题都能找到方向。我的经验是,如果一上来就扑到代码里找 bug,往往会陷入"应用层看起来一切正常"的假象,浪费大量时间。
5.2 几个高频排查命令速查
这些命令都是 Linux 环境下常用的,直接给出一个精简版列表方便快速参考。
# 对比不同 DNS 的解析结果 dig @8.8.8.8 example.com A dig @<本地运营商DNS> example.com A # 抓包并计算握手时延 tcpdump -i eth0 tcp port 443 and host <目标IP> -w /tmp/issue.pcap -c 10000 # 分析 pcap 中的 TCP 流信息 tshark -r /tmp/issue.pcap -q -z io,stat,1 # 检查当前到目标节点的路由跳数和时延 mtr -rwzb -c 60 <目标IP> # 看链路丢包和 RTT 的持续变化 ping <目标IP> | awk '{print $NF}' | sort -n | tail -5如果用 mtr 看到一个中间节点连续丢包,不要急着下结论,有可能是中间节点的 ICMP 限速导致假丢包。更可信的数据是看最终目标节点的实际丢包率,以及 TCP 重传比例。
5.3 那些容易踩的坑
- 不要只信状态页。基础设施厂商的状态页更新滞后是常态,本次故障中看到 CDN 状态页正常,但实际链路已经在劣化;
- 用户说"数据丢失"时,先查数据库记录,别急着改代码。先确认数据在不在,再排查是"响应链路"还是"写入链路"的问题;
- DNS 新记录生效需要时间。TTL 设太长会让故障残留更久,治理时要有把 TTL 缩短的意识,但也要做好递归节点不完全遵守 TTL 的心理准备;
- 连接池超时配置要跟链路时延匹配。跨域接口的 RTT 本来就高于本地接口,连接池读超时设得太短,会频繁中断慢请求,引发雪崩。
5.4 排查工具与告警的改进方向
这次故障之后,我把监控告警补了两块,个人认为很有价值。
第一块是"分地域成功率"而不是总成功率。总成功率 99.9%,分地域一看可能某片区已经掉到 80% 以下。总指标太平滑,地域性故障经常被掩盖。现在所有核心接口都按 region 标签配置成功率与延迟告警,单地域劣化也能第一时间报警。
第二块是"链路质量探活"。从每个边缘节点到中心机房、以及边缘节点之间的专用探针,上报 RTT 和丢包率,而不是只依赖业务请求的间接表现。这样链路问题可以在用户感知明显变差前被捕捉到,而不用等到客服工单堆起来。
6. 写在最后的经验总结
这次故障让我印象最深的一点是:所谓的"数据丢失",绝大多数情况下都不是数据库把数据弄丢了,而是用户和数据库之间的链路某一环断了,导致"感知丢失"。当用户告诉你数据消失了,先别急着怀疑存储层的可靠性,先把目光放到那条又长又绕的链路上。
还有就是,跨域系统的稳定性工作,不能只停留在"机房内部高可用"这个层面。域名解析、边缘节点接入、运营商链路、客户端重试策略,这些看似跟"核心服务"无关的环节,恰恰是跨域体验最容易出问题的地方。服务端就算做到 99.999% 可用,只要用户和它之间隔着一段不稳定的链路,用户感受到的可用性依然可能惨不忍睹。
后来跟团队复盘的时候我们形成了一个共识:以后做跨域系统,第一件事就是把链路的可观测性建好。对跨域业务来说,网络链路绝对不是"基础设施部门的事",而是和数据库、缓存一样需要产品级关注的核心依赖。链路治理好,你的高可用架构才真正踩在实地上。