面试的时候经常被问到一个问题:你了解应用层吗?多数人的回答是能背出HTTP、DNS、FTP的端口号,或者把七层模型默写一遍。可一旦追问下去,比如HTTP/1.1为什么会出现队头阻塞、DNS从输入域名到拿到IP到底经过几步、FTP主动被动模式各自解决什么问题,回答就开始含糊了。这篇是这个系列的下篇,上篇聊了应用层的基本框架和HTTP的常规用法,这次把剩下那些容易被忽略但实际很关键的细节补上。12月3号我把前阵子线上排查的两个问题也整理进来了,你会发现应用层协议看起来简单,真正较真起来全是细节。适合刚学完计网原理的学生、准备面试的开发者,以及已经在做后端或运维、想快速定位网络问题的朋友。
1. HTTP:你天天在用的协议,藏着哪些反直觉的细节
1.1 连接管理:从短连接到多路复用,为什么这么拧巴
先说一个很多人忽略的点:HTTP本身不负责传输,它依赖TCP。所以HTTP的性能瓶颈,很大程度卡在连接建立方式上。
早期的HTTP/1.0,每次请求都要新建一个TCP连接,请求完就关闭。代价有多大?假设客户端在本地,访问一个比较远的服务器,网络往返时间(RTT)是50毫秒。三次握手需要2个RTT,也就是100毫秒。建立一个连接还没发数据,100毫秒已经出去了。一个网页如果包含几十个资源,都走短连接,光握手就是几秒的浪费。这种模式下,网页越复杂,体验越崩。
HTTP/1.1做了改进,默认开启持久连接,也就是Keep-Alive。一个TCP连接建立后,可以连续发送多个请求。这是质的飞跃,但问题并没有解决干净:HTTP/1.1规定,同一个连接上同一时刻只能处理一个请求。前一个请求没返回,后面的请求只能排队等着。这就是应用层的队头阻塞。浏览器为了缓解这个问题,通常会为一个域名开6个左右的并发连接,但这也只是把排队从一个队伍变成几个队伍,整体效率依然受限。
真正扭转局面的是HTTP/2。它引入多路复用,在一个TCP连接上划分出多个独立的流(Stream),不同流的请求和响应可以交错传输。应用层的队头阻塞没有了。但注意,HTTP/2依然跑在TCP上,TCP本身有一个丢包重传机制,一旦这个连接发生丢包,整个连接上的所有流都会被阻塞,直到重传完成。这个叫TCP层的队头阻塞,HTTP/2解决不了。
所以才有了HTTP/3,直接用QUIC协议,底层换成UDP,在用户态重新实现可靠传输和多路复用,每个流独立处理,某一路丢包不再影响其他流。为什么这个演进过程看着这么拧巴?因为HTTP诞生之初是给文档传输设计的,谁也没想到它会变成今天应用层的万能管道。后来的所有改进,本质上都是在跟历史包袱做斗争。
实际开发中最直接的建议是:如果还在维护老系统,优先确认长连接有没有生效。我见过不少内部系统,服务器和客户端之间经过一层代理,代理默认把Connection头改成close,导致每个请求都在重新建连。这时候不用急着上HTTP/2,先把Keep-Alive打通,延迟就能降一大截。
1.2 状态码:真正的业务信号
很多人对HTTP状态码的印象停留在200和404,但排障时最有价值的信息恰恰藏在那些“不常用”的状态码里。
先看一组最容易搞混的:301、302、307、308。301是永久重定向,语义是资源搬家了,浏览器和搜索引擎会更新缓存和索引。302是临时重定向,资源还在原地址。但这里有个隐藏的坑:很多浏览器和实现,遇到302时会把POST请求改写为GET请求,请求体就丢了。如果你在做API接口重定向,应该用307(临时,保留方法和请求体)或者308(永久,保留方法和请求体)。我以前对接过一家支付回调,对方返回302,结果POST的参数全丢了,排查了半天才发现是重定向语义不对。
再看网关类错误:502、503、504。很多同学以为它们差不多,其实排障方向完全不同。502是网关或代理从上游收到了无效响应,常见原因是上游服务挂了,或者返回了格式错误的报文。503是服务暂时不可用,通常是过载或维护期间,响应头里往往带Retry-After,告诉你多久后重试。504是网关等待上游响应超时,这个时候重点查上游的处理耗时和超时配置,而不是网关本身。这三个状态码混在一起排查,方向错了时间就白费了。
还有一些容易被误判成“错误”的状态码。比如204 No Content,请求成功了,但响应体为空。比如206 Partial Content,这是分段下载的正常语义,配合Content-Range头使用,你看到它不应该惊讶。再比如304 Not Modified,这是缓存协商成功的标志,说明资源没有变化,可以直接用本地缓存。我见过有同事看到线上日志一堆304,以为是异常,其实这正是缓存命中率高的表现,是好事。
说到304,就要提一下If-Modified-Since和ETag。前者基于时间,后者基于内容标识。后端返回304时不会带响应体,只带状态码和少量头信息,流量开销极小。在排查“页面不更新”的问题时,先看响应头里有没有ETag或Last-Modified,再确定是不是缓存策略没配对。
1.3 Header里容易被看漏的规则
HTTP Header是被忽视的重灾区,里面几个字段决定了线上很多诡异问题。
先说Cookie的安全属性。HttpOnly表示脚本无法读取,降低XSS风险;Secure表示只在HTTPS下传输;SameSite用于控制跨站请求是否携带Cookie。现在主流浏览器默认SameSite是Lax,意味着跨站请求很多场景下默认不带第三方Cookie。如果你在做单点登录或跨域对接,登录态忽然失效,先想想是不是这个属性的问题。
再讲Cache-Control,这里几乎每天都会踩坑:no-cache不等于no-store。no-cache的意思是“使用缓存前必须重新验证”,也就是允许缓存,但每次都要问一下服务器资源有没有变。no-store才是“完全不缓存,每次都要回源”。这两个混用,后果完全不同。把no-cache当成no-store用,白费了缓存,性能变差;把no-store当成no-cache用,敏感数据可能被写进缓存,属于安全事故。我在实际项目里看到过太多次这两个值的错配,检查响应头时务必看清楚。
还有Connection头。HTTP/1.1里用Connection: keep-alive或Connection: close来控制连接行为,但在HTTP/2里,Connection头是被禁止的,连接管理交给了更高层的机制。如果你的中间代理还在硬塞Connection头,很可能会和HTTP/2冲突。曾经有个案例,某团队升级到HTTP/2后,发现请求总是失败,最后查出来是代理改写了Connection头导致的。这类问题,抓包看响应头一眼就能发现。
2. DNS与DHCP:两张看不见的网
2.1 一次DNS解析到底走了几步
你在浏览器输入一个域名,到真正发出请求,中间至少经历过这么几层缓存:浏览器缓存、操作系统缓存、hosts文件、本地DNS服务器缓存。每一层命中,就少一次上游查询。这也是为什么“第一次打开慢,第二次就快了”的重要原因。
但缓存没命中呢?本地DNS服务器就要开始真正的查询了。它会先问根服务器:这个域名由哪个顶级域管理?根服务器告诉它,你去问顶级域服务器。然后问顶级域服务器:这个域名由哪个权威服务器管理?顶级域告诉它。最后问权威服务器:这个域名的IP是多少?权威服务器给出最终答案。这个过程中,本地DNS服务器代替你完成了所有查询工作,而你只发了一次请求。
这里涉及一个容易被忽略但真实存在的限制:CNAME记录和A记录通常在同一个主机名上不能共存。比如你给某个域名配了CNAME,就不能再给它配A记录。否则解析行为会变得不可预测。现在有些CDN会做CNAME展平技术来绕过这个限制,但底层逻辑依然存在。
TTL是你一定要关注的概念。它表示解析结果可以在缓存里存活多久。TTL太短,每次解析都要回源,延迟高;TTL太长,域名迁移时旧IP会被缓存继续使用很久。我做域名迁移的习惯是:提前一天把TTL从3600秒调到60秒,等旧记录全部过期后,再切换IP,切换完成后观察一段时间,再把TTL调回原来的值。这一套流程下来,绝大多数用户不会感觉到变更。
2.2 递归和迭代,到底谁在替你跑腿
很多人不理解递归查询和迭代查询的区别,其实一句话就能说清:递归查询是“我帮你跑腿,你等着”,客户端只向本地DNS服务器发一次请求,剩下的全由本地DNS服务器完成,最后把结果返回给你。迭代查询是“我给你指路,你自己去问”,本地DNS服务器向根服务器查询时,根服务器不给最终答案,只告诉它“你去找顶级域服务器”,它再问顶级域,顶级域又说“你去找权威服务器”,就这样一级一级问下去。
根服务器在这个链条里扮演的是“引路人”的角色。它不存具体域名和IP的映射关系,但知道顶级域服务器在哪里。根服务器在逻辑上只有13个节点,注意是逻辑上。通过任播技术,物理部署数量远超13个,全球分布,所以你不用担心根服务器过载。
还有一个细节值得记住:DNS默认走UDP 53端口,速度快,开销小。但当响应数据变大,超过一定体积时,会改用TCP或更大容量的UDP传输。一般我们做排查时,用UDP就够了,遇到解析超时或截断才需要去看TCP趋势。
排查DNS问题时,我推荐的做法是拿两个不同的DNS服务器做交叉对比。用nslookup分别指定不同的DNS服务器解析同一个域名,看返回的IP是否一致、耗时差多少。如果结果对不上,要么是本地DNS缓存了过期记录,要么是中间网络做了异常干预。后者的信号是公共DNS解析出的IP是正常的,而默认DNS返回的IP不对劲,并且剩余TTL特别短。发现这种情况,建议尽快调整DNS配置,并核对解析记录有没有被人动过手脚。
2.3 DHCP的四步曲与租约世界观
你把电脑插上网线,什么都没配置就能上网,这是DHCP在起作用。完整过程是一个四步握手的DORA流程。
第一步Discover,客户端在局域网内广播:有没有DHCP服务器?给我个地址。第二步Offer,网内的DHCP服务器收到后,回复一个可用的IP、子网掩码、网关等信息。第三步Request,客户端说,就用你给的这个配置。第四步Acknowledge,服务器确认,正式把IP租给你。
为什么不能少一步?因为广播环境下可能有多个DHCP服务器同时回应。客户端需要通过Request明确“我选了哪一台”,服务器通过Acknowledge确认“我同意把IP给你”。两步握手建立不了这种多方协商的信任关系,所以四步最优。
这里有个关键概念:租约。DHCP分配的IP不是永久的,而是有时间限制的。常见配置下租约以小时或天为单位,当租约过了一半时,客户端会自动发起续租。这个过程平时你感知不到,但它保证了IP地址能被回收和再利用,避免闲置浪费。
我在实际排障中遇到过几次IP冲突,现象是两台设备抢同一个内网IP,网络时通时断。查到最后,原因无一例外是有人手动配置了静态IP,而这个IP恰好落在DHCP的地址池里。面对这种情况,最有效的办法是登录管理后台查DHCP分配记录,同时挨个排查设备有没有手动配置。如果内网有多个网段,还要注意DHCP中继代理的配置,否则跨网段设备得不到地址。
3. 老牌协议FTP与邮件:还活着,而且活得比你想象中久
3.1 FTP主动与被动,差的是一个“端口”的信任问题
FTP和HTTP最大的差异在于它是双通道协议:一个控制连接走21端口,负责传指令;一个数据连接专门传文件内容。这个设计在其他协议里非常少见,也是FTP在互联网时代水土不服的根本原因。
主动模式是这样的:客户端通过21端口发PORT命令,告诉服务器“我临时开放了某个端口,你来连我这里”。服务器接到指令后,用自己的20端口主动去连接客户端的那个端口来传数据。问题在于,客户端往往在NAT设备或防火墙后面,它的那个端口外网根本连不进来。于是数据连接建立失败,文件传不上去。
被动模式(PASV)反过来了:服务器收到PASV命令后,自己开放一个随机端口,把这个端口告诉客户端,由客户端主动去连接。这个模式对客户端友好,不用担心入站连接被拦。但代价是服务器的防火墙要放行一个较大的端口范围,运维配置起来比较麻烦。常见的做法是,在FTP服务器上固定被动端口范围,比如50000到50100,然后在防火墙上只放行这段端口。
实操建议是:企业内网部署FTP服务,优先用被动模式;客户端在NAT后面,基本只能走被动模式。做防火墙策略时,被动端口范围一定要固定,不然重启后端口漂移,策略就失效了。还要注意,现代浏览器基本都移除了FTP支持,但很多嵌入式设备、自动化脚本、老业务还在使用FTP。你要是接手这类维护,先把主动被动模式理清楚,比查什么都有用。
3.2 SMTP、POP3、IMAP:一封信的完整旅程
邮件协议是应用层里分工最细的一组。SMTP负责发送和投递,POP3负责拉取,IMAP负责同步,三者各管一段。
用一个完整场景串起来:你在客户端写了一封邮件,点发送。客户端先通过SMTP协议,把邮件投递到你的邮件服务器,这一步通常走587端口做提交。你的邮件服务器收到后,检查收件人的域名,通过该域名的MX记录找到对方的邮件服务器地址,再用SMTP协议把邮件投递过去,这一步一般走25端口。对方邮件服务器把邮件存进收件人邮箱。收件人打开客户端,用IMAP或POP3把邮件读出来。
MX记录在这条链路里至关重要,它决定了邮件应该投递到哪台服务器。如果MX记录配置错误,邮件要么投不出去,要么会发到错误的服务器。排查“邮件发不出去”的问题,首先用nslookup查收件方域名的MX记录。之后再检查双方服务器能否通过25端口连通。现在很多服务器对25端口做了访问限制,需要确认在不在白名单里。
POP3和IMAP的区别也值得单独说。POP3是“把邮件下载到本地,服务器上通常就删掉了”,所以你在多台设备上收信,每台设备看到的邮件状态不一致。IMAP则会把邮件保留在服务器上,各设备之间同步状态,你在手机上已读的内容,在电脑上也是已读。如果用户反馈“换了个设备,邮件全不见了”,八九不离十是在用POP3。另外,SMTP发信还涉及SPF、DKIM这些校验机制,配置不当会被对方服务器判成垃圾邮件,这类问题光看端口不够,得看服务器的邮件日志。
4. 协议进化论:从HTTP/1.1到HTTP/3
4.1 HTTP/2:多路复用终于来了,但还不够
HTTP/2是一次大刀阔斧的改进,改动深入到了协议底层。它把原来的文本格式改成二进制分帧,一个连接内的所有数据都被拆分成帧,再分配到不同的流里传输。多个请求可以在同一个连接上并发,互不干扰。这就是多路复用,它让应用层的队头阻塞成为了历史。
同时,HTTP/2还带来两个实用特性。第一个是头部压缩(HPACK),HTTP/1.1的头部是纯文本重复传输,压缩完可以减少大量开销。第二个是服务端推送,服务器可以在客户端请求之前,主动把相关资源推给客户端。这个特性当初被寄予厚望,但后来的实际应用并不理想,很多浏览器和服务器出于各种考虑,对服务端推送的支持逐渐变得有限甚至移除。所以如果你在规划新服务,不要把这当做重点依赖。
但HTTP/2有一个致命的短板:它依然建立在TCP之上。TCP的可靠传输机制要求数据按序到达,一旦某个包丢了,后续到达的数据包只能先在缓冲区里等着,即使数据都到了,应用层也取不出来。这个现象就是TCP层的队头阻塞,它和多路复用带来的收益正好互相抵消了一部分。尤其在高丢包的弱网环境中,HTTP/2的表现有时反而不如HTTP/1.1开多个连接稳定。我实测过,在丢包率超过一定比例的网络里,HTTP/1.1的并发连接反而更有韧性,因为单个连接丢包不会影响其他连接。
部署HTTP/2有一个默认知识点:主流浏览器只支持跑在TLS之上的h2。所以启用HTTP/2的前提是配置好HTTPS证书。客户端和服务器之间通过ALPN在握手阶段协商协议版本,如果中间代理不支持ALPN,协商会失败,回退到HTTP/1.1。
4.2 HTTP/3与QUIC:用UDP重新定义应用层传输
HTTP/3回应了TCP的固有缺陷,索性绕开TCP,改用UDP,然后在用户态实现了一个叫QUIC的传输协议。QUIC在UDP之上重新实现了可靠性、乱序重排、多路复用、加密握手,甚至连接迁移。
最直观的收益有三个。一是建连更快,握手阶段结合了TLS 1.3,多数场景下能实现1-RTT建连,连接恢复时甚至可以做到0-RTT,比TCP+TLS的多次往返省太多。二是彻底解决队头阻塞,因为每个流都是独立、互不阻塞的,某个流丢包重传不影响其他流继续传输。三是连接迁移,手机从WiFi切到移动网络时,IP地址变了,TCP连接会断,必须重连。QUIC通过连接标识符来保持连接身份,即使IP变化,连接依然可以续用,体验不会中断。
不过HTTP/3也有现实的麻烦。UDP流量在网络里经常被限速或者丢弃,有些中间设备对UDP的兼容性很差。CDN服务商和云厂商的支持程度也在逐步完善中,不是所有场景都能直接受益。如果你要升级到HTTP/3,建议先在边缘节点做小范围灰度,观察延迟和丢包情况再全量放开。
5. 实战复盘:一次线上访问变慢的定位过程
5.1 现象与第一轮判断
12月初处理了一个比较典型的案例。某内部管理系统,业务侧反馈下午高峰时段偶尔打不开页面,刷新几次又恢复正常。因为不是持续不可用,第一轮排查把重点放在后端性能上。结果后端负载不高,慢查询日志里也没有明显异常,中间件CPU和内存都很平稳。这说明问题大概率不在应用处理逻辑上。
于是换了个角度,直接用curl的计时功能打开同一个接口,连续测了几次。发现慢请求主要集中在time_namelookup这个阶段,也就是DNS解析。正常的解析耗时在个位数毫秒级别,异常时能拉到几百毫秒甚至上秒。TCP连接、TLS握手、请求处理阶段反而都正常。到这里,排查方向基本锁定在DNS解析链路上。
5.2 抓包后发现的问题
用nslookup分别查询默认DNS和公共DNS的解析结果,发现了三个异常点。第一,同一个域名从不同的DNS服务器返回的IP不一样,默认DNS返回的是一个已经被下线的老地址。第二,这个老地址的剩余TTL非常短,导致部分用户每次解析都要重新向上游查询,而这次查询恰好卡在一个不稳定的上游节点上。第三,当默认DNS缓存过期时,重新查询会间歇性超时,于是同一时刻有的请求能拿到新IP,有的还在用过期缓存,外在表现就是一部分请求慢、一部分请求正常。
这个案例的教训是:应用层访问变慢,不代表应用层有问题。快速定位的关键是把网络阶段拆开,DNS耗时就是最容易在第一次排查中被忽略的阶段。很多同学遇到页面打开慢,第一反应是看数据库慢查询、查接口耗时,这没有错,但如果方向不对,很容易浪费大量时间。
5.3 修复与验证
修复步骤很直接。先把本地DNS服务器的上游配置改成一个延迟更低、更稳定的解析源,避免每次查询都走上游的不稳定节点。然后把域名的TTL从3600秒调低到300秒,让各节点缓存快速收敛。最后在应用层加了一层缓存,把频繁访问的域名解析结果在进程内保存,过期时间略短于TTL。
验证的时候,重复之前的测速方式,time_namelookup恢复了正常的个位数毫秒,高峰期的偶发慢请求也消失了。整个过程花了不到半天,但排查思路比操作手段更值得记录:慢请求出现时,先看是慢在哪一段,而不是先看是哪里报错。用curl -w把DNS、TCP、TLS、首字节、总耗时全部打出来,问题边界会立刻清晰。
6. 高频问题速查:应用层排障工具箱
6.1 值得记住的一组命令和参数
排障时用到的工具不需要多,关键是会用。curl是最常用的,不要只拿它发个GET看返回体,要会看细节:
curl -w "DNS:%{time_namelookup} TCP:%{time_connect} TLS:%{time_appconnect} 首字节:%{time_starttransfer} 总计:%{time_total}\n" -o /dev/null -s https://你的服务域名这条命令会输出请求各阶段的耗时。time_namelookup是DNS解析耗时,time_connect是TCP连接建立耗时,time_appconnect是TLS握手完成耗时,time_starttransfer是从开始到收到第一个字节的总时间,time_total是整个过程的总时间。对比这几个数字,就能定位慢在哪一段。
DNS排查用nslookup和dig。nslookup适合快速查询,dig能给出更多细节,包括查询耗时、TTL、权威服务器。抓包用tcpdump和Wireshark,线上环境没有图形界面就用tcpdump抓包,存成pcap文件再下载到本地分析。对应用层问题,Wireshark里最常用的过滤语法是http、dns、tcp.stream。
6.2 高频问题速查表
实战里见过的问题总结成一张表,排查时可以对照着来:
| 故障现象 | 优先怀疑点 | 排查动作 |
|---|---|---|
| 第一次访问慢,第二次快 | DNS缓存未命中、TCP/TLS握手开销 | 用curl -w看time_namelookup和time_connect |
| 偶发超时,刷新后正常 | DNS TTL过期、连接被回收、上游不稳定 | 连续多次curl,对比各阶段耗时波动 |
| 域名迁移后仍访问旧地址 | DNS缓存未过期 | 提前降低TTL,等待缓存收敛,查本机DNS缓存 |
| 页面资源不更新 | Cache-Control配置错位 | 看响应头,区分no-cache与no-store |
| POST请求重定向后数据丢失 | 302默认改写方法 | 改用307或308保留请求方法 |
| 报错502 | 上游服务无效响应 | 查后端服务存活状态,检查网关配置 |
| 报错504 | 上游响应超时 | 查后端慢查询,评估是否需要调大网关超时 |
| 上传文件失败 | 请求体超过限制 | 看响应头是否返回413,调整服务器上传大小 |
| 邮箱收不到信 | MX记录异常或25端口不通 | nslookup查MX,telnet测25端口 |
最后说点实在的。我在排查网络问题上的最大体会是,先把分层想清楚,再动手。应用层协议是离我们最近的,但也是问题最多的一层;很多时候不是协议本身出了问题,而是上层应用、中间代理、缓存策略之间配合出了问题。像这次补充的DNS案例,从表象看是页面打不开,实际根因在解析链路,跟后端代码一点关系都没有。遇到问题不要一上来就怀疑某个环节,先用工具把网络阶段拆开,很多坑其实一眼就能看穿。这个系列先记到这里,后续有新的排查案例,我再继续补。