首页 / 资讯中心 / 文章详情

网络请求全链路:从DNS到HTTP/2的性能优化与排查实战

网络请求全链路:从DNS到HTTP/2的性能优化与排查实战 ★ FEATURED ARTICLE
1. 一个网络请求从发出到返回中间到底发生了什么先说一个我常用来考团队新人的场景你在手机App里刷到一条内容点了个赞屏幕上的红心立刻亮了。这个过程中你的手机到底做了哪些事很多人会回答“发了个请求给服务器服务器返回成功”。这话没错但离“真正理解网络请求”还差着一整条链路。拆开来看一次典型的HTTP请求至少经历了这么几个阶段触摸反馈之后客户端先做的是“找路”——DNS解析。你写代码时用的是https://api.example.com这种域名但网络传输层只认IP地址。手机需要先向DNS服务器询问“api.example.com的IP是多少”这个过程本身又是一次网络请求。如果App是第一次启动这条DNS解析的耗时常常在几十到几百毫秒不等慢的时候甚至能超过1秒。很多人排查接口慢只盯着服务器响应时间却忽略了DNS这一层。找到IP之后TCP连接开始建立。这个阶段就是大家常说的“三次握手”客户端发一个SYN包服务器回SYNACK客户端再回ACK。三次握手本身要消耗一个RTT往返时间。如果网络环境不好比如丢包重传这个阶段就可能卡住。很多接口超时问题不是服务器没响应而是TCP根本握不上手。连接建立后如果走的是HTTPS还有TLS握手。这一步往往比很多人想象中更耗时。TLS握手要协商加密套件、交换证书、生成会话密钥通常也得消耗一到两个RTT。我见过很多性能优化项目最后发现耗时大头根本不在业务代码里而是在TLS协商上——尤其是一台服务器部署了多张证书、或者客户端的CA证书链不完整的时候。然后是真正的HTTP请求发送与响应。客户端把请求行方法路径协议版本、请求头、请求体发出去服务器解析后路由到对应的处理函数业务代码执行完再按状态码、响应头、响应体返回。这一步才是大家眼里“接口调通了”的那一步。最后连接不会立刻断掉。除非服务器或客户端明确关了连接否则这个TCP连接会被放进连接池供后续请求复用。这也就是“Keep-Alive”机制的底层原理。我说这些不是为了掉书袋。想说明的是一次网络请求是整套链路协同的结果任何一个环节出问题表现都是“接口慢”或“接口失败”但排查方向完全不一样。这也是为什么我一直建议遇到网络问题先把问题分层——是DNS层、连接层、TLS层、传输层还是应用层分清楚再动手能省下大把无用功。2. 三个最容易被忽视的细节连接管理、超时配置与DNS缓存很多人在项目初期“调通接口”之后就再也不管网络层了直到线上报障才回来补课。根据我的经验线上网络类问题里有相当大比例根本不是服务器逻辑出错而是下面这三个细节没处理好。2.1 连接复用Keep-Alive才是高并发的基础先说一个直觉很容易骗人的点。很多刚入行的开发者以为“并发高就是多开连接”实际恰恰相反——高并发场景下连接复用才是保命的关键。HTTP/1.1在设计时就支持了Keep-Alive也就是在一个TCP连接上连续发送多个请求避免每个请求都重新走三次握手和TLS握手。我在优化过一个模拟项目X的网关层时做过实测短连接模式下单机吞吐只有不到2000 QPS连接建立的开销占了快一半的CPU时间改成连接池复用后同样一台机器吞吐直接翻了将近四倍P99延迟也从300毫秒降到了80毫秒左右。但连接复用不是“配了就完了”它有几个隐藏问题。比如连接池的最大连接数和空闲超时需要根据业务特征设置如果设置的空闲超时太短连接很快被回收复利率就低太长又可能占用大量文件描述符。另外一个更隐蔽的问题是连接复用容易把错误扩散——比如某个请求超时了但TCP连接本身还活着下一个请求如果继续复用这个半关闭的连接就可能出现“请求发出去了但收不到响应”的诡异现象。所以成熟的客户端库都会在检测到连接异常时主动废弃这条连接而不是继续复用。2.2 超时配置只设一个总超时是新手最容易犯的错我见过太多线上事故根因就是“超时时间设置不合理”。最典型的问题有两个超时设得太短导致本该成功的请求在慢网络下被误杀超时设得太长用户在弱网环境下卡到怀疑人生最终自己杀掉了App。正确的做法是分层设超时连接超时Connect Timeout只覆盖TCP握手和TLS握手阶段建议3秒左右。这个阶段失败通常是网络根本不通或服务器宕机不需要等太久。读取超时Read Timeout覆盖“请求发出后等待响应”的阶段建议可以根据业务特性调整。比如普通的查询接口10秒是底线如果是导出报表这类长任务可能要放到60秒甚至更长。写超时Write Timeout这是很多人忽略的一项。它保护的是请求体本身发送失败的情况比如弱网环境下上传大文件连接半开导致发送卡死。还有一个建议千万不要在业务代码里把超时写死成同一个值。在模拟项目X里我接手过一套老代码所有接口统一设置30秒超时结果就是本该快速失败的请求占了大量线程资源线程池被拖垮最终整个服务雪崩。后来按接口分级设置超时核心接口15秒、非核心接口30秒、长任务接口单独放宽才把稳定性拉回来。2.3 DNS缓存你代码里最隐蔽的“定时炸弹”DNS解析听起来是基础设施的事但它在客户端侧的坑特别多。最常见的问题是系统DNS缓存时间过长导致域名解析结果长期不变。一旦服务器IP发生了迁移或故障切换客户端还在拼命连接旧IP表现就是“App突然大面积请求失败但服务器明明好好的”。我在某个图像处理Demo里就踩过这个坑。当时把图片上传域名迁移到了新机房CDN和源站都切换完成后还是有大约10%的用户上传失败排查了一天最后发现是这些用户的本地DNS缓存还指着旧IP而旧IP的网关已经在迁移当天做了下线处理。处理方式有几个方向一是用HTTPDNS方案在应用层直接绕过本地DNS自己做解析和缓存管理二是在客户端缓存DNS结果并配合TTL做主动刷新三是至少要做到“请求失败时尝试重新解析一次”而不是死磕同一个IP。别指望用户会乖乖清缓存客户端层面的容错必须自己做。3. 幂等、重试与缓存决定线上稳定性的关键设计如果说上一节讲的连接、超时、DNS是网络请求的“地基”那这一节要讲的就是“骨架”——真正决定一个接口在真实场景下能不能扛住乱局的是幂等设计、重试策略和缓存机制。3.1 幂等性为什么“重试”会带来灾难很多人对“幂等”的理解停留在“GET请求是幂等的POST不是”这个说法太粗糙了。严格来说幂等指的是同一个请求执行多次和只执行一次产生的效果完全一致。为什么网络层特别需要关注这个因为网络是不可靠的超时会频繁发生。客户端超时了服务器到底有没有处理这个请求没人知道。于是客户端只能重试。这一重试就有讲究了如果接口不幂等用户点了一次支付按钮后端可能收到两次扣款指令结果就是扣了两次钱。我见过一个很典型的案例某跨平台系统的订单回调接口业务方没做幂等处理某次网络抖动导致消息队列重复投递一天之内生成了几百条重复订单财务对账对到怀疑人生。后来加的方案很朴素但也很有代表性每条请求带上全局唯一的请求ID服务端在Redis或数据库里按请求ID做去重重复请求直接返回第一次的结果。别看方案简单它能挡住绝大多数重试带来的脏数据。3.2 重试策略不是“失败就再来一次”重试本身是个好机制但盲目的重试比不重试更可怕。我先列几个基本判断什么情况下值得重试连接超时、TLS握手失败、5xx类错误、DNS解析失败——这些属于“服务器可能没处理成功”的情况可以重试。什么情况下不该重试请求体已经发送成功但没收到响应、4xx类业务错误、超时时间已经到了但服务器可能正在处理——这些情况重试很可能造成重复业务或无效请求。重试的次数和间隔怎么定我的建议是总重试次数不超过3次间隔用递增的退避策略比如1秒、2秒、4秒并且要加入随机抖动。这里有个数学上的道理如果大量客户端在同一时刻失败又用完全相同的间隔重试它们会形成一个“重试雪崩”把本来就被打垮的服务再压上一根稻草。退避抖动就是为了打破这种同步性。还有一个经常被忽略的细节重试要限制在同一个逻辑请求里不能无限扩散。比如A调用BB调用CC超时了A重试B也重试这个重试放大效应会把下游直接打挂。成熟的架构会在RPC链路上传递“重试预算”比如通过请求头传递剩余重试次数每一层消耗一次预算预算用完了就不允许再重试。3.3 缓存网络请求性能优化的第一手段我在性能调优时有一句口头禅“能不走网络的请求尽量别走。”这不只是节省带宽更是为了省掉整条链路的不确定性和延迟。HTTP协议自带了一套缓存语义只要后端配合客户端可以极大地减少重复请求。核心是这几个字段Cache-Control服务端告诉客户端这个响应能不能缓存、缓存多久。比如max-age60表示60秒内可以直接用缓存。ETag / If-None-Match服务端给资源一个版本标识客户端带上上次的ETag去请求如果资源没变服务端返回304不下载实际内容。Last-Modified / If-Modified-Since类似的逻辑基于时间戳判断。这里有个容易踩的坑Cache-Control设了no-cache不等于不缓存它只是要求“使用前必须先验证”。很多开发者搞混了导致明明想禁用缓存却反而让客户端每次都要发请求去服务器确认延迟没降反升。对于App端的网络请求更常用的做法是应用层自己维护缓存。比如我在优化某跨平台系统的接口时把所有“读多写少”的数据按用户维度做了本地内存双重缓存命中率从20%提到70%多整体接口平均耗时降了一半。缓存的设计原则就一条先明确数据的更新频率和一致性要求再选择缓存层级和过期策略。不是所有数据都适合缓存硬塞只会带来数据不一致的问题。4. 从“能通”到“好用”性能优化是我最推荐的动手方向网络请求能做到“稳定不出错”已经超过了相当多项目但要真正把体验做上去就得进入性能优化阶段。这也是我觉得每个人都可以亲手做、做完立刻能感知到成效的部分。4.1 先看清你的请求到底慢在哪优化的第一步不是“优化”是测量。我建议所有人在动手前先抓一份现状基线接口平均耗时、P95耗时、P99耗时、失败率、超时率以及——最关键的一环——耗时拆解。网络耗时可以拆成DNS解析时间、TCP连接时间、TLS握手时间、首字节时间TTFB服务器处理时间、内容下载时间这几段。拿到了拆解数据优化方向就很清晰了DNS解析慢考虑HTTPDNS、客户端缓存、减少域名数量。TCP连接慢开启连接池复用。TLS握手慢检查证书链完整性、考虑TLS会话复用。TTFB慢这是服务器端的业务处理时间网络层帮不了忙得去看后端逻辑。内容下载慢检查响应体大小、压缩情况、网络带宽。我遇到过很多“优化了半天没效果”的项目最后发现是把时间和资源用错了环节——比如后端逻辑明明要2秒却在拼命调连接池参数当然没有意义。4.2 HTTP/2当前性价比最高的网络升级项如果只让我选一个“性价比最高”的网络优化手段我会选升级到HTTP/2。它带来的核心能力有三个多路复用一个TCP连接里可以同时跑多个请求不再受HTTP/1.1“队头阻塞”的限制。以前浏览器对同一个域名最多开6个连接每个连接里请求还得排队HTTP/2把多个请求交错传输性能提升立竿见影。头部压缩HTTP头是很啰嗦的每次请求都要带一堆Cookie、User-Agent、Accept之类的字段。HTTP/2用HPACK对头部做压缩重复字段只传一次差异部分能省下不少字节。服务端推送Server Push服务器可以在客户端请求某个页面时主动把页面需要的配套资源CSS、JS一起推过来减少一次往返。不过这个特性实际落地效果有争议很多CDN在默认配置里反而会关掉它具体还是要看场景。从HTTP/1.1切到HTTP/2改动成本远比很多人想象的低。服务端开个TLS证书再加一行配置客户端只要换用支持HTTP/2的网络库连接层的事情就不用管了。我在优化某跨平台系统时做过一次全量升级仅此一项静态资源的加载时间平均下降了35%接口并发能力也涨了一截。4.3 弱网优化把“网络不好”变成“体验尚可”移动端场景下用户可能在电梯里、地铁上、地下车库网络质量差是常态而不是异常。弱网优化的思路不是“让网络变好”——这谁也做不到——而是让应用在网络差的时候不要那么糟糕。常见的手段包括请求合并Batching把多个小请求合并成一个大请求发出。比如用户进入一个页面传统做法是三个接口分别拉数据弱网下每个都要经历完整的握手和首字节等待。合并成一个接口后三次往返变成一次体验提升会很明显。数据压缩在带宽有限时减少传输字节数是硬道理。Gzip/Brotli压缩是基础项控制响应体大小、去掉冗余字段、精简JSON结构也能省下可观的流量。超时动态调整前面提到过分层设超时弱网环境下这种分层设置尤其重要。如果网络状态检测到当前信号差可以适当放宽读取超时信号恢复后再把超时收紧。很多成熟的App会用网络质量监控模块来做这个动态策略。离线优先Offline First把上一次成功拉取的数据缓存下来网络请求失败时先展示缓存让页面不至于白屏。这个策略在信息流、列表页场景里效果特别好我从实践中得到的心得是用户对“数据稍微旧一点”的容忍度远远高于对“页面空白”的容忍度。5. 网络请求排查的思路与常用工具遇到问题不乱翻代码网络请求的问题有个特点报错信息往往极其简略——“请求超时”“连接失败”“网络异常”就这五个字背后可能藏着十几种不同的原因。如果拿到报错就一头扎进业务代码里翻大概率是浪费时间。我自己在面对网络问题时遵循的是一套固定的排查思路。5.1 先复现再分层定位复现这个环节很多人会跳过但我建议一定不要省。因为稳定复现是定位问题的前提。你至少要搞清楚这个报错是偶发的还是必现的是特定网络环境Wi-Fi、4G/5G下才出现是特定设备、特定系统版本才有复现之后按我前面说的链路逐层排查DNS层dig或nslookup看解析是否正常解析出来的IP是否还活着。连接层用ping -c 4看基本连通性再检查目标端口是否开放可用telnet ip 端口或在客户端配置里临时指向测试IP验证。TLS层用带详细输出的方式看证书链很多客户端工具本身就能打印证书校验细节确认证书是否过期、域名是否匹配、是否缺少中间证书。应用层抓包看请求头、请求体、响应状态码。重点关注状态码——4xx是客户端问题5xx是服务端问题超时则要看是连接超时还是读取超时。5.2 三个我认为最顺手的排查工具首先是curl。别小看这个命令行工具它几乎是网络排查的“瑞士军刀”。我常用的几个参数# 完整输出请求和响应头部并加上耗时统计 curl -v -w DNS解析: %{time_namelookup}s\n连接建立: %{time_connect}s\nTLS握手: %{time_appconnect}s\n首字节: %{time_starttransfer}s\n总耗时: %{time_total}s\n https://example.com # 指定HTTP版本测试 curl --http2 -I https://example.com # 模拟慢速网络 curl --limit-rate 10k https://example.com这一套下来响应慢的问题基本能定位出是哪一层了。然后是抓包工具。Wireshark能做到全链路分析但它的学习曲线很陡峭日常调试我更常用的其实是Charles这类的图形化HTTP代理工具因为它能看到每个请求的详细信息、耗时还能直接断点修改请求和响应。移动端调试时把证书装进手机就能抓到真实的App网络流量非常适合排查“真机上的问题”和“模拟器上的问题”表现不一致的情况。还有一个是客户端日志。很多网络库都支持把网络层的内部日志打出来比如OkHttp的日志拦截器、Flutter的Dio日志拦截器。这类日志的颗粒度已经足够精细能直接看到每次请求的耗时和失败原因。在测试环境打开日志抓一轮比对着屏幕猜要有效得多。5.3 常见状态码到底在告诉你什么这个表格我经常在排查问题时当速查表用状态码含义常见排查方向200请求成功直接看响应体是否符合预期301/302重定向检查Location头是否正确客户端是否允许跟随重定向304未修改可直接用缓存检查缓存策略是否生效400客户端请求语法错误检查请求参数、请求头格式401未认证检查Token是否过期、认证头格式403无权限检查权限配置、IP白名单、防盗链404资源不存在检查URL路径是否写错、服务端路由是否匹配429请求过于频繁检查限流配置客户端是否触发了频率控制500服务器内部错误服务端日志是唯一突破口502/504网关错误/超时检查反向代理配置、上游服务是否存活特别要提醒一点很多人在状态码上会犯一个直觉性错误——看到4xx就觉得是服务端的锅。实际上4xx的定义就是“客户端发的东西有问题”责任方在调用方不要搞反了。6. 安全与合规视角下的网络请求别等出事才补课网络请求是数据进出应用的“大门”安全和合规问题也最容易在这一层暴露。这部分的优先级常常被低估但真要出问题往往就是大问题。6.1 HTTPS与证书校验是底线不是选配我见过不少项目为了省事直接把证书校验关掉或者干脆用HTTP。说句不好听的这是把自己家的钥匙挂在门口。中间人攻击在公共Wi-Fi环境下非常常见攻击者可以悄无声息地篡改你App流向服务器的数据——你在App里输入的账号密码等于直接送给了别人。正确的做法是生产环境一律使用HTTPS并且开启证书校验不要盲目信任所有证书。证书固定Certificate Pinning可以加但要慎重。它确实能提升安全性但也会带来证书更新时的运维成本。我建议至少对核心接口启用并且做好证书轮换预案。内部测试环境可以用自签名证书但一定要在代码里明确区分环境绝不能把测试用的证书校验逻辑带到生产环境。6.2 敏感数据的网络传输字段级别的红线现在主流合规要求里对敏感信息的保护是重点审查项。在技术层面有几个非常具体的注意事项不要用GET请求传敏感参数。GET的参数会出现在URL里而URL会被记入服务器访问日志、代理日志、浏览器历史记录等多处地方。密码、Token这类数据一旦进了URL就等于在多个地方留了副本。Token的存放位置很关键。放在本地存储里本身就容易被其他应用读取放在内存里则要考虑持久化。推荐的折中方案是用系统提供的安全存储能力如Android的EncryptedSharedPreferences、iOS的Keychain加上合理的过期时间。请求体加密可以但要谨慎。很多团队喜欢自己在应用层再加一层加密这本身没问题但加密方案若是自己拍脑袋发明的往往比不加密更危险。如果要做应用层加密请使用公开、成熟的标准算法比如AES-GCM并且密钥管理要独立于代码之外。6.3 请求头里的隐私痕迹要主动清理这一节想提醒一个很多团队完全没注意过的地方请求头会暴露很多设备、用户甚至行为特征。User-Agent里带了完整的系统版本、机型Accept-Language暴露语言和地区自定义Header里可能有内部账号体系信息。这些信息单个看问题不大但汇聚在一起就会形成设备指纹从合规上看属于“间接识别用户”的范畴。建议做一件很简单的事每个接口上线前先看一眼它发出的请求头把不必要的信息全部去掉。只保留服务端真正需要的最小集合。我参与的几个对外项目里都会强制要求开发在联调阶段用抓包工具检查一遍完整的请求头这项习惯后来帮我们规避了好几次合规审查风险。7. 从一次线上故障看网络请求的链路价值讲了这么多理论最后分享一个我印象很深的实战案例。这个案例几乎把前面几个章节的问题都串在了一起理解了它你对网络请求的认知就基本成形了。那是某跨平台系统的一次线上事故现象是用户端大面积报错提示“网络异常请稍后重试”但服务器监控显示各项指标都正常CPU、内存、带宽都不高。当时很多人第一反应是“客户端的问题”甚至怀疑是某个版本发版引入的Bug。排查过程是这样的第一步复现。从用户反馈里筛选出几个机器发现报错比例大约占8%且集中出现在移动网络环境、特定地区。第二步看客户端日志。发现有大量请求卡在“连接建立”阶段也就是TCP握手一直没成功。这推翻了“服务器没处理”的猜测——服务器根本没收到多少请求。第三步顺着连接层往下查。发现DNS解析结果全都指向一个IP但这个IP在几个小时前刚做过一次机房割接。问题浮出水面了服务端IP变更后客户端的本地DNS缓存还在使用旧地址而旧地址的防火墙规则已经移除导致连接直接被丢弃。第四步是验证和修复。我们把问题域切到HTTPDNS服务在客户端代码里做了“连接失败自动重解析一次”的容错逻辑同时调整了DNS TTL策略。上线后报错比例直接从8%降到接近0。这个case的核心价值在于它让我们意识到网络请求的稳定性从来不是某一层单独能保障的。DNS层、连接层、应用层、客户端与服务器之间的任何一处细小疏忽都可能以“网络异常”四个字呈现在用户面前。而你能做的就是把每一层都当成一块独立的、可以被观测和控制的积木——出了问题可以快速定位是哪一块歪了而不是整栋楼一起垮。这也是我这篇文章想传达的最后一个观点别把“网络请求”这四个字看成一个API调用那么简单。花点时间把这条链路上的每一环都吃透日后能帮你省下的排障时间会远超你当初学习这些内容所花的工夫。
阅读完成 · 觉得有帮助?
咨询建站