前段时间线上一个用 LangChain 封装 LLM 调用的服务频繁报超时日志里全是 TimeoutError看着就像模型服务端挂了。实际排查下来问题根本不在模型而在于服务器的 DNS 解析和 iptables 规则而且这俩坑是连环出现的好不容易修好 DNSiptables 又把连接拖死了。这篇实录会把完整的排查路径、用到的命令、判断依据和踩坑过程都整理出来。如果你在用 LangChain 调 LLM 时遇到间歇性超时或者发现 curl 直连正常但代码里一直超时这篇文章应该能帮你省下不少时间。1. 现象与分析为什么 LangChain 会“间歇性超时”1.1 现场记录报错随机重试能活那天的现象很典型服务运行几个小时后开始抽风每 10 次请求里有两三次会报超时剩余请求虽然能成功但耗时从正常的 300ms 飙到 4s 以上。业务方反馈“时好时坏”监控看 CPU、内存都不高模型服务的健康检查也是绿的。一开始我怀疑是模型侧限流毕竟 LLM 服务经常有并发限制。但看了返回错误是TimeoutError不是RateLimitError说明请求根本没等到模型返回而是在更早的环节就被卡住了。重试几次偶尔能成功这种“间歇性”特征非常关键它排除了配置写死、认证失败这类必然失败的场景更像是在某个网络环节上随机丢包或等待超时。1.2 先别甩锅模型先做分层定位遇到 LangChain 调用 LLM 超时我习惯把问题拆成四层去看应用配置层LangChain 的请求参数、超时时间、重试次数、base_url 配没配错。DNS 解析层调用 LLM 服务时域名解析耗时、解析结果、本地 DNS 服务器是否可用。网络传输层TCP 连接建立、TLS 握手、数据包是否被丢弃或延迟。主机防火墙层iptables/nftables 规则、连接跟踪表 conntrack 是否异常。这个顺序不能乱。很多排查一上来就抓包看 TCP忽略了 DNS 解析的等待时间结果绕了一大圈。反过来很多问题也不是 LangChain 代码写错而是底层网络环境悄悄变了。1.3 用三个工具快速定位问题层我常用的三板斧是curl、dig、tcpdump。先用 curl 模拟请求观察耗时分布再用 dig 查解析耗时最后用 tcpdump 抓包看包是否发出、是否收到响应。# 模拟 LangChain 的请求观察总耗时和 DNS 解析耗时 curl -o /dev/null -s -w dns:%{time_namelookup} connect:%{time_connect} tls:%{time_appconnect} total:%{time_total}\n https://your-llm-gateway.example.com/v1/chat/completions # 如果域名解析慢能直接看到 time_namelookup 异常高 # 如果 connect 高重点查 TCP 和防火墙 # 如果 appconnect 高查 TLS 握手和中间设备这一步能快速把问题缩小到“DNS 解析”还是“TCP 连接”还是“数据交互”上。我当时跑出来的结果是time_namelookup忽高忽低高的时候能到 3s而time_connect在 DNS 慢的时候也一起慢。这就把矛头指向了 DNS。不过真正修完 DNS 之后又发现time_connect单独飙高这才会引出后面的 iptables 问题。2. DNS 这个坑连接还没开始就卡在寻址2.1 间歇性卡顿的真凶解析耗时飘忽很多超时排查忽略了 DNS因为我下意识觉得“DNS 能解析就行了慢一点感觉不出来”。但实际上LangChain 发起 HTTP 请求之前要先做域名解析如果这一步卡住整个请求就会超时。而且 DNS 解析耗时的波动非常大流量低的时候几十毫秒高峰期能到好几秒表现就是“间歇性超时”。那次的情况就是/etc/resolv.conf里配置的 DNS 服务器是一个内网地址但该 DNS 服务器对 LLM 服务所在的域名解析特别慢偶尔还丢包。更恶心的是系统里同时存在 systemd-resolved 和手工配置的 DNS 配置两套配置互相覆盖导致实际生效的 DNS 地址不是我认为的那个。2.2 现场排查命令与判断方法在服务器上依次执行以下操作很快就能确认 DNS 是否异常# 查看当前正在使用的 DNS 服务器 cat /etc/resolv.conf # 如果系统用 systemd-resolved 接管 DNS resolvectl status # 对比不同 DNS 服务器的解析耗时 dig 223.5.5.5 your-llm-gateway.example.com time5 tries2 | grep Query time dig 8.8.8.8 your-llm-gateway.example.com time5 tries2 | grep Query time # 用 time 命令看系统整体解析耗时 time getent ahosts your-llm-gateway.example.com我当时的输出很有代表性配置中的内网 DNS 解析耗时在 1.5s 到 3s 之间波动换用公共 DNS 后耗时稳定在 20ms 左右。这说明问题不在上游权威 DNS而在本地这台 DNS 服务器本身可能它本身有故障也可能它的上游配置有问题但无论如何只要在服务器上换掉这个慢速 DNSLangChain 的请求就能绕开这个大坑。2.3 修复 DNS 的正确姿势很多人改/etc/resolv.conf只改了一半重启网络服务后又被覆盖回去。这就是“大坑里的第二个坑”。Linux 下 DNS 配置的维护逻辑要看发行版传统方式是直接编辑/etc/resolv.conf但这个文件经常被 DHCP 或 NetworkManager 自动覆盖只改这个文件不持久。使用 Netplan 的 Ubuntu 系统要改/etc/netplan/xx.yaml在nameservers里的addresses列表加上 DNS IP。使用 systemd-resolved 的场景可以设置全局 DNSresolvectl dns eth0 223.5.5.5并且记得resolvectl flush-caches清理缓存。我当时为了避免再被覆盖直接修改了发行版对应的网络配置文件然后重启了 systemd-networkd确认cat /etc/resolv.conf显示的是目标 DNS 后才算真正修好。# 修改配置后清理 systemd-resolved 缓存 resolvectl flush-caches # 验证当前生效的 DNS resolvectl status | grep Current DNS Server注意不能只做cat /etc/resolv.conf检查因为 systemd-resolved 可能用 127.0.0.53 作为本地转发层实际解析逻辑在 resolved 里配置文件里的 nameserver 不一定等于实际生效的 nameserver。2.4 顺手验证第一层是否解决修完 DNS 后再用 1.3 里的 curl 命令跑一次curl -o /dev/null -s -w dns:%{time_namelookup} connect:%{time_connect} total:%{time_total}\n https://your-llm-gateway.example.com/v1/chat/completions正常情况time_namelookup应该在几十毫秒内。我当时看到 DNS 耗时恢复后以为问题结束了结果连续请求 30 次后发现time_namelookup稳定了但time_connect开始出现 1s 以上的尖刺而且请求依然有超时报错。这就到了第二个坑iptables。3. iptables 这个坑连上了却被防火墙“拖死”3.1 为什么 DNS 修好了还是超时DNS 恢复后域名解析正常但部分连接仍然超时。再用 curl 观察时现象从time_namelookup高变成了time_connect高。这表示域名已经解析成功但 TCP 三次握手那一步没有完成。正常情况下客户端发出 SYN 包后服务器会回 SYN-ACK。如果 SYN 包被防火墙静默丢弃客户端就会一直重传 SYN直到达到内核的 tcp_syn_retries 限制后报超时。由于 iptables 规则可能只针对特定源 IP 或目标端口不同请求受影响的概率不一样最终表现出来的就是“间歇性超时”。3.2 防火墙规则排查的完整流程先看当前 iptables 规则带上计数器和行号这样才能知道哪些规则真正命中了# 查看 filter 表的 INPUT/OUTPUT/FORWARD 链带命中计数 iptables -L -n -v --line-numbers # 查看 nat 表和 mangle 表 iptables -t nat -L -n -v --line-numbers iptables -t mangle -L -n -v --line-numbers # 查看当前所有连接跟踪状态 cat /proc/net/nf_conntrack | head -20我当时的排查过程是先用iptables -L -n -v发现 OUTPUT 链上有一条针对目标 IP 段的 DROP 规则命中次数还在不断增加。这条规则本意是拦截某个风险地址段但 ACK 规则的地址范围写得太宽把模型服务的出口 IP 也包进去了。由于 DROP 是静默丢弃没有 RST应用层只能一直等待最终表现为超时。这类问题最难搞的不是规则本身而是它隐藏在几十条规则中间容易看漏。所以别只看-L一定要加-v让命中计数告诉你哪些规则在生效。如果某条规则命中次数一直增长而且下一个动作是 DROP/REJECT大概率就是它。3.3 conntrack 连接跟踪表被塞满除了规则本身另一个高频坑是 conntrack 表满。Linux 的 netfilter 会为每个连接建立一条 conntrack 条目如果系统并发连接数超过net.netfilter.nf_conntrack_max新连接会被直接丢弃表现也是“间歇性 TCP 超时”。排查方式# 查看当前连接数与最大限制 sysctl net.netfilter.nf_conntrack_count sysctl net.netfilter.nf_conntrack_max # 查看是否有 conntrack 表满的日志 dmesg | grep nf_conntrack常见错误日志是nf_conntrack: table full, dropping packet。如果发现 count 已经顶到 max说明默认的 65536 不够用或者有大量 TIME_WAIT 连接堆积。临时调大sysctl -w net.netfilter.nf_conntrack_max131072 echo net.netfilter.nf_conntrack_max131072 /etc/sysctl.conf同时可以缩短连接跟踪条目的超时时间比如把net.netfilter.nf_conntrack_tcp_timeout_established从默认的 432000 秒降到一个更合理的值减少条目堆积。这一步在 LangChain 这类客户端并发场景下非常关键因为每一个 HTTP 请求都会建立连接如果没有及时回收conntrack 很容易被打满。3.4 临时放行与长期修复当确认 iptables 规则误伤了 LLM 服务 IP 后我先把规则调整到只针对目标网段而不是一刀切掉所有流量。为了测试假设我临时在更靠前的位置插入一条 ACCEPT 规则# 临时放行测试是否能恢复 iptables -I OUTPUT 1 -d llm-service-ip -j ACCEPT # 确认没问题后再删除误伤的 DROP 规则 iptables -D OUTPUT rule-number测试阶段可以直接iptables -F清空规则但这个方法风险极大很容易把当前 SSH 连接也断掉。我在实际操作中一般是先加 ACCEPT再验证最后删除错误规则绝不在没把握的时候直接 flush 整个表。如果你确认某条规则要长期保留一定要把最终规则写进发行版对应的持久化文件比如iptables-save /etc/iptables/rules.v4或者迁移到 nftables 的配置文件中。否则服务器重启后规则恢复原样坑会再踩一遍。4. LangChain 侧的超时配置与重试优化4.1 给 ChatOpenAI 等封装设置合理的 timeoutDNS 和 iptables 都解决后问题基本消除但 LangChain 侧的“客户端保护”还是要做好。默认的超时时间可能长达几十秒一旦底层网络再次抖动应用就会长时间挂起。更合理的方式是在初始化时就显式传入超时时间from langchain_openai import ChatOpenAI llm ChatOpenAI( modelgpt-4o-mini, timeout30, # 连接和读取超时 max_retries2, # 网络错误重试次数 max_retry_interval10, )注意不同 LangChain 版本对超时参数的命名不一样有些版本还是request_timeout所以写完最好用inspect.signature(ChatOpenAI.__init__)确认一下。如果你用的是langchain_ollama、langchain_anthropic或其他 LLM 封装也建议查一下对应的参数名。超时值不要设置太短否则模型推理慢一点的时候也会被误杀。个人经验是连接超时 10s、整体读超时 30s 是一个比较稳妥的起点。4.2 连接复用与并发控制超时问题还有一个隐藏来源是连接池不够用。LangChain 底层请求基于 httpx 或 requests默认连接池大小有限。当并发请求较多时后来的请求只能在池外新建连接新建连接一旦碰上网络抖动就报超时。可以尝试增加连接池大小并复用会话import httpx from langchain_openai import ChatOpenAI client httpx.Client( limitshttpx.Limits(max_connections50, max_keepalive_connections20), timeouthttpx.Timeout(30.0), ) llm ChatOpenAI( modelgpt-4o-mini, http_clientclient, timeout30, max_retries2, )这个做法相当于把“每次请求都新建连接”改成“复用已有的存活连接”能明显降低 DNS 和 TCP 握手带来的超时概率。尤其是你已经确认 DNS 和防火墙没问题时这种优化能让整体耗时再降一个档次。4.3 重试策略和鉴权信息的注意事项超时和限流要分开处理。超时通常是网络层问题可以重试限流是业务层问题重试要按Retry-After来做。LangChain 的max_retries处理前者还可以但如果你自己写循环重试一定要加上指数退避不然本身就是给网络加压。另外我在这次排查中发现日志里把 API Key 打出来了。排查超时问题时会看请求上下文但请求头里的鉴权信息不应该出现在业务日志里。推荐的做法是# 从环境变量读取不要在代码和日志里硬编码 export LLM_API_KEYsk-xxxx代码中读取方式import os api_key os.environ.get(LLM_API_KEY) if not api_key: raise RuntimeError(LLM_API_KEY not set)日志输出时用自定义 filter 把Authorization头替换成***。这个问题和超时本身无关但排查过程中顺手解决掉能避免后面更大的安全风险。5. 排障工具箱与经验复盘5.1 排查命令速查表问题层快速命令判断标准应用配置python -c import inspect; print(inspect.signature(ChatOpenAI.__init__))超时参数是否设置合理base_url 是否写错DNS 解析dig dns_server domain time5 tries2Query time 是否超过几百毫秒系统 DNS 配置cat /etc/resolv.conf/resolvectl statusnameserver 是否为预期地址TCP 握手curl -w time_connect:%{time_connect}\n urltime_connect 是否异常高抓包确认tcpdump -nn -i eth0 host llm-ip and tcp port 443是否只有 SYN 没有 SYN-ACK防火墙规则iptables -L -n -v --line-numbers是否命中 DROP/REJECT 且计数增长conntracksysctl net.netfilter.nf_conntrack_count/dmesg | grep nf_conntrackcount 是否接近 max最终验证连续请求 100 次统计耗时分布无超时P95 耗时稳定5.2 容易忽略的三个细节第一DNS 配置修改后必须确认实际生效。很多系统里/etc/resolv.conf是软链到 systemd 或 NetworkManager 的直接编辑会被覆盖要用发行版对应方式修改。第二iptables 规则不仅看 filter 表还要看 nat 表。有些超时是 NAT 规则把请求转发到了错误的后端表现为后端返回异常或连接被重置但 filter 表却完全正常。第三conntrack 表满不一定能在 iptables -L 里看到因为被丢掉的包根本没有建立连接。所以dmesg日志和nf_conntrack_count监控必须提前配好否则问题只能在半夜爆发时临时查。5.3 从这次事故里总结的几点体会第一LangChain 报超时的时候不要只盯着模型侧也不要只盯着代码逻辑先用 curl 把各阶段耗时拆开能直接少走一半弯路。整个过程下来“DNS iptables”两个问题的症状几乎一样都是超时但排查路径完全不同没有分层定位的话只能靠瞎猜。第二防火墙规则最好有注释、有时效、有变更记录。我后来看历史命令才知道那条误伤的 DROP 规则是三天前加上的当时只想着封网段没注意覆盖范围结果连模型服务的出口 IP 也一起封了。规范规则变更流程比出事后再翻 iptables 高效太多。第三所有网络参数建议写成监控项。DNS 解析耗时、conntrack 使用率、TIME_WAIT 连接数这些指标平时看着不起眼但关键时刻能帮你判断是“逐渐恶化”还是“瞬时抖动”。这次如果提前有 conntrack 使用率监控甚至能在用户报障之前就发现问题。那次之后我把这些指标全部加到了监控面板上后面再没有因为这类问题半夜爬起来。
