多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

线上服务超时排查:从TLS握手瓶颈到系统性根因定位

线上服务超时排查:从TLS握手瓶颈到系统性根因定位 1. 项目概述从一次线上超时引发的深度思考最近在负责的一个线上服务里我们接入了Claude Sonnet 5模型来处理一些智能问答和内容生成任务。本来一切运行得挺平稳但上周突然开始间歇性地出现请求超时响应时间从正常的2-3秒飙升到10秒以上甚至直接超时失败。团队里的第一反应很自然“是不是我们调用模型的代码逻辑有问题赶紧看看重试机制、超时设置或者是不是并发太高把服务打挂了”但我拦住了大家。在多年的线上问题排查经验里我形成了一个根深蒂固的习惯面对任何异常尤其是涉及外部依赖的异常第一件事绝对不是改自己的代码而是先停下来把完整的证据链理清楚。盲目修改代码往往是在用新的不确定性去覆盖旧的不确定性最后问题没解决还引入了新的bug。这次排查Claude Sonnet 5超时问题的全过程就是一个典型的案例。我们最终发现问题根源远非代码逻辑那么简单而是一系列基础设施、网络策略和配置细节共同作用的结果。这篇文章我就来详细拆解这次排查的思路、步骤和收获希望能给你带来一些启发。2. 问题初现与常规排查的局限性我们的服务架构比较典型一个Java写的Spring Boot应用部署在Kubernetes集群里通过HTTP客户端调用Claude Sonnet 5的API。超时告警最先从监控平台我们用的是PrometheusGrafana触发图表上清晰地显示特定Pod的P99响应时间曲线出现了明显的尖刺。2.1 第一轮“条件反射式”排查团队同学的第一轮排查几乎是所有研发的标准动作检查应用日志在Kubernetes里kubectl logs看了出问题Pod的日志发现大量ReadTimeoutException和ConnectTimeoutException指向我们使用的HTTP客户端这里是OkHttp。检查代码配置立刻去翻代码确认OkHttpClient的超时设置connectTimeout10s,readTimeout30s,writeTimeout10s。看起来似乎合理但为什么30秒的读超时会被触发怀疑点一模型服务方不稳定大家的第一猜测是Claude的API服务临时波动。我们检查了官方状态页面没有发现任何故障公告。同时其他同样调用该API的服务似乎没有大规模报错。怀疑点二我们的并发量太高查看应用的QPS监控发现虽然有一定增长但远未达到我们预设的单实例限流阈值。线程池监控也显示活跃线程数健康。准备行动基于以上有同学提出“是不是网络偶尔抖动我们把OkHttp的readTimeout再调大一点到60秒并且增加重试次数试试”到这里我喊了停。如果按照这个思路走下去我们很可能把readTimeout改成120秒然后增加一个带退避的重试机制。这或许能掩盖部分超时现象因为请求最终可能成功但会带来更严重的问题整体服务响应时间拉长系统资源被长时间挂起的请求占用潜在雪崩风险剧增。我们并没有找到真正的根因只是在用更大的容忍度去适配一个未知的问题。注意在面对外部服务超时时盲目调大客户端超时时间和增加重试是风险极高的操作。这相当于将系统稳定性的控制权完全交给了外部不可控的服务极易引发连锁故障。2.2 确立排查核心原则先证据后假设我要求团队暂时放下代码跟我一起执行一套证据链收集流程。核心原则是从客户端到服务端沿着请求的实际路径收集每一环的可观测性数据让数据说话而不是让直觉主导。我们需要回答几个关键问题超时是发生在TCP连接阶段还是连接建立后的数据收发阶段是网络链路的问题还是目标服务Claude API本身的问题是我们单个Pod的问题还是整个集群或区域的问题是否有规律如特定时间、特定请求参数3. 构建多维度的证据链收集体系理清思路后我们开始从多个维度系统性收集信息这就像破案时收集现场痕迹、物证和证人证言一样。3.1 网络层证据连接与传输的显微镜这是排查外部服务问题的重中之重。我们分几步走3.1.1 从应用内部捕捉网络细节仅仅有“ReadTimeoutException”是不够的。我们启用了OkHttp的完整事件监听器EventListener并记录下每个请求的以下关键时间点callStart: 调用开始dnsStartdnsEnd: DNS解析耗时connectStartconnectEnd: TCP连接建立耗时secureConnectStartsecureConnectEnd: TLS握手耗时requestHeadersStartrequestHeadersEnd: 发送请求头耗时requestBodyStartrequestBodyEnd: 发送请求体耗时responseHeadersStartresponseHeadersEnd: 接收响应头耗时responseBodyStartresponseBodyEnd: 接收响应体耗时通过日志汇总我们很快发现一个模式大部分超时请求在connectEnd和secureConnectEnd之间耗时极长有时超过20秒。这说明时间主要卡在了TLS握手阶段而不是数据传输阶段。这是一个至关重要的线索它将我们的注意力从“服务器处理慢”转移到了“建立安全连接慢”。3.1.2 从系统层面验证网络状况在出问题的Pod内我们执行了网络诊断命令ping和mtr持续测试到Claude API域名的网络延迟和路由情况排除基础网络丢包和路由异常。curl配合详细输出和计时curl -w \ntime_namelookup: %{time_namelookup}\ntime_connect: %{time_connect}\ntime_appconnect: %{time_appconnect}\ntime_starttransfer: %{time_starttransfer}\ntime_total: %{time_total}\n -v https://api.anthropic.com。这里的time_appconnect就对应TLS握手时间我们复现了应用内观察到的长时间等待。tcpdump抓包分析在Pod内对相关端口进行抓包使用Wireshark分析。这是最直接的证据。抓包显示客户端我们发出了Client Hello但等待很久才收到服务器的Server Hello有时甚至等不到。这直接证实了TLS握手环节的问题。3.2 基础设施与环境证据容器世界的隐藏变量我们的服务跑在Kubernetes里这引入了额外的复杂性。我们需要排除K8s本身或容器运行时的问题。3.2.1 检查节点资源与状态kubectl describe node查看节点是否存在内存压力MemoryPressure、磁盘压力DiskPressure或PID压力。资源压力可能导致进程调度迟缓间接影响网络栈处理。kubectl top pod确认问题Pod的CPU和内存使用率是否正常是否存在突增。登录到宿主机检查dmesg系统日志看是否有OOM Killer杀进程或网络相关的错误日志。3.2.2 检查网络策略与服务网格我们使用了Calico作为CNI并定义了网络策略NetworkPolicy。需要确认策略是否错误地阻塞或限制了到外部API的流量。通过检查Calico的Felix组件日志和策略规则得以确认。如果使用了服务网格如IstioSidecar代理可能是瓶颈。需要检查istio-proxy容器的资源使用率和日志。3.2.3 检查DNS解析在容器内多次执行nslookup api.anthropic.com和dig api.anthropic.com观察解析结果是否一致、是否有延迟。Kubernetes的CoreDNS问题或容器/etc/resolv.conf配置不当可能导致解析缓慢。3.3 外部依赖证据理解第三方的行为我们不能假设第三方服务永远完美。除了看状态页我们还需要更主动地探查。3.3.1 设计差异化测试地域测试从不同地理区域的公有云VPC或跳板机用相同参数调用API对比延迟。这有助于判断是否是对方服务在特定区域有问题或是我们到对方特定入口的网络链路有问题。规格测试发送不同max_tokens参数从小到大的请求观察超时是否与请求/响应体大小相关。虽然TLS握手问题与此无关但这是排除“数据处理慢”假设的必要步骤。认证测试检查我们的API Key是否临近速率限制或配额耗尽。有些服务在接近限制时可能会延迟响应而非直接拒绝。3.3.2 利用可观测性工具如果Claude提供更详细的API监控如请求ID、分阶段耗时应尽可能在请求中带上唯一标识并联系其技术支持提供具体时间戳和请求ID以便对方在后台查询日志。4. 证据链汇聚与根因定位经过上述全方位的证据收集我们手头有了以下关键信息应用日志与监听器数据超时集中在TLS握手阶段time_appconnect异常高。抓包分析客户端发出Client Hello后服务器响应Server Hello严重延迟。环境对比同一K8s集群内其他不调用Claude的服务网络正常从集群外直接测试到api.anthropic.com的TLS握手也正常。节点与Pod状态资源使用率正常无压力告警。网络策略经核查规则允许Egress流量到外部HTTPS端口。差异化测试从问题Pod所在节点测试到其他外部HTTPS服务如https://httpbin.orgTLS握手同样缓慢。证据链在这里汇聚并指向了一个明确的方向问题不是Claude Sonnet 5服务本身也不是我们的业务代码逻辑甚至不是通用的外网访问问题。问题局限于从我们特定Kubernetes集群的某些Pod尤其是运行在特定节点上的Pod发起的所有HTTPS连接的TLS握手阶段。这个范围大大缩小了。结合“特定节点”这个线索我们与基础设施团队一起深挖。最终根因浮出水面公司网络安全团队近期在底层网络设备上更新了针对出向HTTPS流量的深度包检测DPI和安全策略。这些策略设备在处理TLS 1.3的完整握手特别是某些密码套件时在特定高负载情况下会出现性能瓶颈导致握手延迟激增。我们的服务恰好部署在了受影响的网络分区且调用Claude API强制使用TLS 1.3触发了这个瓶颈。5. 解决方案与优化实践找到根因后解决方案就清晰了而且完全不需要改动业务代码短期规避与网络安全团队协作将我们服务所在K8s节点集的出向IP地址暂时从导致性能瓶颈的深度检测策略中排除或调整为宽松模式立即恢复了正常。长期解决推动基础设施团队升级相关网络设备的固件或优化DPI策略配置从根本上解决兼容性和性能问题。配置优化虽然此问题与代码无关但我们借此机会复审了HTTP客户端的配置做了一些加固连接池确保连接池ConnectionPool大小设置合理避免频繁创建新连接每次创建都需TLS握手。超时分层我们设置了更精细的超时。connectTimeout包含TCP和TLS握手设置为5秒readTimeout根据Claude API业务逻辑最大允许时长设置为30秒。这样一旦TLS握手在5秒内失败就能快速失败而不是等待30秒。重试策略为连接异常如ConnectTimeoutException,SSLHandshakeException配置了非幂等的、短间隔的退避重试如最多2次间隔1秒。因为这类错误可能是瞬时的网络波动或策略设备抖动。而对于readTimeout则需非常谨慎通常不自动重试除非业务逻辑保证幂等性因为它可能意味着服务端已处理但响应慢重试会导致重复操作。5.1 构建预防性的可观测体系这次排查给我们最大的启示是不能等问题发生再临时抱佛脚。我们系统化地增强了可观测性在应用指标中新增TLS握手耗时直方图通过OkHttpEventListener将secureConnectEnd - secureConnectStart的时间暴露给Prometheus。这样可以在Grafana面板上直接监控TLS握手时间的P95、P99值设立预警阈值。标准化HTTP客户端诊断日志将包含各阶段耗时的请求摘要日志以结构化格式JSON输出并采样记录到日志中心。当出现超时告警时能第一时间查询到具体是哪个阶段慢了。建立外部依赖健康度看板在Grafana中为像Claude API这样的关键外部依赖创建独立看板监控其请求成功率、延迟区分连接延迟、TLS延迟、处理延迟、错误码分布。这能快速定位问题是全局性的还是局部性的。6. 通用故障排查思路总结与工具链这次排查虽然围绕Claude Sonnet 5但其中蕴含的思路和工具是通用的适用于任何线上故障尤其是涉及网络的故障。我将其总结为一个可复用的排查框架6.1 排查核心心法假设驱动数据验证第零步止血与保留现场如有必要先扩容、重启个别实例以恢复服务但务必保留至少一个故障现场的Pod/节点用于分析避免“死无对证”。第一步清晰定义问题现象是慢是错是挂影响面多大单个用户、单个服务、单个区域、全局第二步提出假设基于经验列出所有可能的原因代码Bug、配置错误、资源不足、网络问题、依赖服务故障、中间件问题等。第三步收集证据按照从内到外、从应用到基础设施的顺序系统性收集数据验证或推翻每一个假设。第四步定位根因当所有证据指向同一个点时根因就找到了。很多时候根因是多个因素叠加如本次的网络策略特定TLS版本高负载。6.2 分层排查工具链你可以根据问题现象像查字典一样使用这些工具怀疑层面工具/命令查看目标应用层应用日志、APM如SkyWalking, Pinpoint、Metrics如Prometheus、线程Dumpjstack错误堆栈、慢方法、SQL、HTTP调用链、GC情况、线程阻塞容器/进程层kubectl logs/describe/exec,docker stats/logs,ps,top,vmstat,pidstat容器状态、资源限制、进程资源使用、系统负载网络层ping,mtr,traceroute,curl -w,telnet,netstat,ss,tcpdump,wireshark网络连通性、延迟、路由、连接状态、抓包分析系统层dmesg,sar,iostat,free,df内核日志、历史性能数据、磁盘I/O、内存、磁盘空间外部依赖第三方状态页、自身监控错误码、延迟、差异化测试确认依赖服务自身状态6.3 针对经典问题的排查捷径CPU飙高top找到进程top -Hp [pid]找到线程jstack [pid]或arthas查看线程栈定位热点代码。内存泄漏/OOM观察jstat -gcutil或Grafana中GC曲线用jmap -histo或MAT分析堆转储看哪些对象占用了大量空间且无法被回收。死锁jstack查看线程栈搜索“deadlock”关键词或使用arthas的thread -b命令直接查找死锁线程。网络连接池耗尽netstat或ss查看TIME_WAIT状态连接数结合HTTP客户端配置如最大连接数、存活时间分析。回到我们这次的问题正是遵循了“先证据后假设”的原则利用从应用到系统再到网络的层层工具才避免了在代码层做无用功精准地找到了基础设施层的隐蔽问题。记住在复杂的分布式系统里你看到的症状超时和疾病的根源网络策略性能瓶颈往往隔了好几层。一名优秀的工程师不仅要有写代码的能力更要有这种层层递进、刨根问底的系统性排查能力。下次当你遇到线上超时不妨也先深吸一口气说“别急咱们先把证据链理一理。”
返回列表