ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

HTTP实战排查笔记:连接复用、状态码与抓包分析

HTTP实战排查笔记:连接复用、状态码与抓包分析 如果你已经能说出 HTTP 和 HTTPS 的区别也知道 200、404、500 大概是什么意思那这篇就是写给你的。我这些年排查线上问题总发现很多同学对 HTTP 的理解停留在“状态码背熟”的阶段真遇到请求超时、连接复用异常、浏览器拦截、Docker 上不了镜像这类具体报错时还是会懵。HTTP 协议本身不复杂但它在真实系统里要跟 DNS、TCP、TLS、代理、浏览器策略叠在一起很多问题都是“看起来像 HTTP 问题实际上不是”。这篇算是我的 HTTP 补充笔记把那些教材没细讲、但实战里高频踩坑的点串一遍。1. 连接复用HTTP/1.1 keep-alive到底在复用什么1.1 一次HTTP请求到底消耗了哪些资源一个普通 HTTP 请求从浏览器发出底层要先做 DNS 解析找到 IP然后 TCP 三次握手建立连接接着发送请求行和请求头服务器返回响应正常情况下连接就关了。如果不用 keep-alive每次请求都要重复这个过程。这个“重复”的成本在局域网里不明显但在公网上、高并发下非常致命——TCP 三次握手多出来的一个 RTT往返时延加上 TCP 慢启动导致的前几个请求包不能充分利用带宽累积起来就是用户感知的“卡”。我在实际项目里曾经对服务做过压测把一个内网接口从 HTTP/1.0 请求改为 HTTP/1.1 keep-alive 后相同并发下的吞吐量直接提升了将近一倍。原因很简单连接不关闭TCP 拥塞窗口可以停在适合当前网络的状态不用每次从新连接的最小窗口重新爬升。所以 keep-alive 的本质是复用 TCP 连接多出来的状态信息不仅仅是省掉握手。很多新手把“连接复用”理解成“复用 HTTP 响应”这是不对的。HTTP 本身是无状态的同一个 TCP 连接上连续发多个请求每个请求之间没有必然关系。真正有状态的是 TCP 层的滑动窗口、拥塞窗口、序号空间。keep-alive 让这些状态能够跨请求延续省去重建成本这才是“复用”二字的准确含义。1.2 连接池、超时设置与TIME_WAIT服务端如果设置了 keepalive_timeout比如 Nginx 默认 65 秒在超时时间内的后续请求都会复用同一个 TCP 连接。但客户端如果大量短连接服务端会看到一堆 TIME_WAIT。反过来如果客户端有连接池连接池里的连接被服务端提前关闭客户端还继续发数据就会看到“Connection reset by peer”。常见的坑有两个代理层Nginx/LVS到后端服务的连接复用和后端自身的 keepalive 参数不一致导致后端连接被代理复用后后端的应用层协议比如 PHP-FPM 的 fastcgi已经超时释放代理再发请求过去就直接报 502。服务端 keepalive_timeout 设得太短长连接形同虚设设得太长又会占用大量文件描述符。生产环境我一般从 60 秒起步配合连接数上限观察。场景表现主要原因短连接服务端 TIME_WAIT 增多客户端未使用连接池或 HTTP/1.0连接被重置client: Connection reset by peer服务端主动断开客户端仍在复用502 Bad Gateway代理到后端请求失败后端连接池与代理复用策略不一致这里给一个 Nginx 侧常见的配置参考keepalive_timeout 60s; keepalive_requests 1000;keepalive_requests是单个连接最多能处理的请求次数。设成 1000 的意思是无论空闲超时到没到只要这个连接处理完 1000 个请求就主动关闭。这样做可以避免一个连接被某个客户端长期占着不放也能让连接定期刷新释放掉可能泄漏的内存和文件描述符。1.3 HTTP/2多路复用和HTTP/1.1连接复用的区别很多人以为 HTTP/2 的连接复用和 HTTP/1.1 的 keep-alive 是一回事其实差别很大。HTTP/1.1 的 keep-alive 只是复用 TCP 连接但同一时刻这个连接上只能跑一个请求等前一个响应完才能发下一个这就是队头阻塞。HTTP/2 在同一个 TCP 连接上引入了流stream的概念多个请求可以同时交错发送每个流有单独的 ID响应也按照流 ID 重组彻底解决了 HTTP/1.1 的请求排队问题。不过 HTTP/2 也有自己的坑如果底层 TCP 丢包由于 TCP 是可靠的所有流都得等丢掉的包重传完成反而比 HTTP/1.1 多个连接的表现更差。另外一些老旧代理中间件对 HTTP/2 支持不好会出现请求被重置的问题。所以连接复用这件事不是越高级越省心得看链路里每一跳的配合。2. 状态码不是背出来的400/401/403/404的实战区分2.1 400 Bad Request请求头字段太长别第一反应怪后端我看到的热搜词里有一条非常典型“HTTP Error 400. A request header field is too long.” 这个问题我踩过不止一次。从客户端看就是你代码里某个请求头塞了太大的东西最常出现在 Cookie 和 Authorization 上。有些单点登录系统会把用户权限列表整个塞进 Cookie动辄好几 KB有些接口把 JWT 塞在 Header 里如果角色信息太多JWT 也能到 10KB 以上。而 Nginx 默认的 large_client_header_buffers 是 4 个 8KB一旦超过就报 400。排查方法很简单用 curl 带上同样的请求头发给后端如果后端直连不报错而经过 Nginx 就报 400基本可以确认是 Nginx 层限制。调整参数large_client_header_buffers 4 16k;但我不建议一味调大。更好的做法是从客户端压缩头信息把用户会话 ID 之类的东西放到请求体或者改用 Cookie 的切片方案。这个报错的本质是服务器为了保护自身资源对请求头大小做了限制并不是服务器端程序写错了。2.2 401与403认证和授权不是一回事热搜词里有一条 git 远程仓库的报错“remote: http basic: access denied fatal: authentication failed for http://...”。这其实是典型的 HTTP Basic 认证失败服务器返回 401然后 git 客户端把 401 转成了“access denied”的字样。很多人分不清 401 和 403401 是“你没证明你是谁”403 是“我知道你是谁但这不让你进”。所以 git 拉代码时用户名密码错误是 401而不是 403如果你用公司域账号登录了系统但没加入某个代码仓库组那才可能是 403。这里有个实操经验当你在一个 HTTP 接口里同时做认证和权限校验时一定要先做认证再做授权。否则匿名用户访问一个需要权限的接口你返回 403客户端可能会误以为“我的凭据有问题”但实际上它压根没有提供凭据。很多前端拿到失败后弹出“账号密码错误”其实后端返回的是 403前端判断逻辑又只用状态码做提示就会误导用户。2.3 404到底在掩盖什么HTTP 状态码大全里 404 绝对是被误解最深的一个。很多新手以为 404 就是“路径写错了”但实际上后端完全可以对“存在但不该让你访问”的资源返回 404这叫信息隐藏。反过来有些系统为了诊断方便对未匹配路由会返回 200 加一个空 JSON这种设计我非常不推荐——它会让监控系统误以为接口正常掩盖掉路由换代的问题。我在实际排障时看到“HTTP/1.1 404”会先去区分是静态资源 404 还是 API 404。静态资源 404 通常是部署路径或缓存问题API 404 则可能是路由前缀、版本号、或服务发现注册失败。比如你在 Nginx 里配置了location /api但上游服务实际跑在/api/v1前端却调/api/v2后端直接返回 404。这时候抓包看 URL 才是关键。2.4 状态码记忆法与其背整个状态码大全不如按类记类别典型状态码一句话含义1xx100 Continue, 101 Switching Protocols还在协商别急2xx200 OK, 201 Created, 204 No Content成功了3xx301 Moved Permanently, 302 Found, 304 Not Modified换个地方或直接用缓存4xx400, 401, 403, 404, 405, 408, 429客户端有问题5xx500, 502, 503, 504服务器或网关有问题实际上线上最需要警惕的不是 500而是 200。因为 500 一定触发告警200 不会。很多慢请求、错误数据都是 200 里夹着业务错误码。状态码只能说明“HTTP 传输层”有没有成功不代表“业务逻辑层”成功这个是很多人忽略的点。3. 抓包分析用Wireshark看一次HTTP请求的完整生命周期3.1 Wireshark过滤器的几个高效姿势热搜词里有“wireshark抓包及分析http”这个场景我能聊很多。很多人打开 Wireshark 抓包直接填http过滤然后发现抓不到几个包因为现在绝大多数流量都是 HTTPSWireshark 默认只能看到 TLS 加密包看不到 HTTP 明文。但如果你抓的是本地联调环境或者 HTTP 测试流量以下过滤器足够用http只看 HTTP 层会隐藏掉 TCP 握手包tcp.port 80只看 80 端口流量tcp.stream eq 0锁定某一条 TCP 连接按序号跟完整会话http.request or http.response区分请求和响应我的习惯是先http看整体再右键某条请求选择“Follow - HTTP Stream”Wireshark 会把请求和响应拼接成原始文本非常直观。如果是 HTTPS 流量只能通过在浏览器或客户端里配置 SSLKEYLOGFILE 导出 TLS 密钥再在 Wireshark 的 Protocols-TLS 里设置否则看到的就是一堆密文。3.2 一个请求的时间线到底慢在哪我经常用 Wireshark 做慢请求定位。一次典型的 HTTP 请求在抓包里可以看到以下阶段DNS 查询包的特征是出现“标准查询”而且通常在请求之前。TCP 三次握手SYN、SYN-ACK、ACK 三个包。TLS 握手如果是 HTTPSClient Hello、Server Hello、证书、密钥交换这一步包数量最多。HTTP 请求客户端发一个GET /index.html HTTP/1.1的包。HTTP 响应服务端返回HTTP/1.1 200 OK后面跟着响应头和数据。如果用户反馈“接口要等 3 秒才返回”我会按包的间隔计时。三次握手阶段慢通常是网络链路或防火墙问题TLS 握手慢可能是证书链太多或 SSL 握手协商算法不合适HTTP 请求发出后到第一个响应包之间慢那才是后端处理慢。这个区分非常重要因为很多后端同学一看到“慢”就优化数据库实际上瓶颈可能在客户端到服务器之间的某个代理节点。3.3 用抓包理解CORS预检热搜词里有一条典型报错Access to XMLHttpRequest at http://127.0.0.1:8000/myapp/center from origin...这种 CORS 报错用 Wireshark 或浏览器开发者工具 Network 面板看会非常清楚。当你的前端运行在http://localhost:8080后端在http://127.0.0.1:8000只要前端代码用fetch或XMLHttpRequest发起跨域请求并且请求头里带了非简单字段比如Content-Type: application/json浏览器会先发一个OPTIONS请求叫预检。后端如果没返回Access-Control-Allow-Origin和Access-Control-Allow-Methods浏览器会直接拦下真正请求并在控制台输出上面那条报错。我在实践中发现很多人不理解为什么要先 OPTIONS其实这是浏览器的安全模型跨域请求默认不被信任浏览器先替前端问服务端“你允许我这个来源、这个方法、这些头部吗”服务端点头了才会发起真实请求。所以排查 CORS不要去改前端代码而是先看后端预检响应里有没有这三个头Access-Control-Allow-Origin: http://localhost:8080 Access-Control-Allow-Methods: GET, POST, OPTIONS Access-Control-Allow-Headers: Content-Type, Authorization如果用了 Nginx 代理还要确认Access-Control-Allow-Origin没有被覆盖掉。这个头在浏览器端是强制校验的服务端不返回前端代码写得再对也白搭。4. 系统报错里的HTTP细节Docker、apt、浏览器报错拆解4.1 Docker daemon 里的net/http报错热搜词里有一条特别经典error response from daemon: get https://registry-1.docker.io/v2/: net/http...。很多人在 Docker 拉镜像时看到这个第一反应是“网络问题”然后去重启 Docker其实不一定有用。这条报错的意思是 Docker daemon 作为 HTTP 客户端向 registry-1.docker.io 发 HTTPS 请求时底层 net/http 库返回了一个错误。可能是 TLS 握手失败、连接被重置、DNS 解析失败也可能是代理配置不对。排查顺序我是这样的先看 daemon 日志journalctl -u docker或tail -f /var/log/docker.log找到具体错误行很多情况下会把 TLS 错误细节打出来。curl -v https://registry-1.docker.io/v2/如果 curl 能通说明系统层面网络没问题问题可能出在 Docker 自身的 proxy 设置上。检查 Docker 配置的 registry-mirror 或 HTTP 代理/etc/docker/daemon.json里配置的proxies、registry-mirrors有时候镜像源切换后证书链不完整就会在 TLS 层直接断开。还有一条 Docker 报错也常出现在日志里docker search redis request returned 500 internal server error for api route and version http://%2f%2f.%2fpipe%2fdockerdesktoplinuxengine/v1.56/images/search?termredis, check if the server supports the requested api version。这个看起来是 HTTP 500实际上通常是 Docker Desktop 的本地 API 引擎版本和客户端 SDK 不匹配。它请求的是 Windows 路径下的管道 pipe而不是网络地址所以和普通 HTTP 服务完全两码事。解决办法基本都是升级 Docker Desktop或者清理旧版本残留的 CLI 配置而不是去调什么超时时间。4.2 apt和ROS源里的GPG错误热搜词里有两条系统的 HTTP 错误一条是w: gpg error: http://mirrors.ustc.edu.cn/debian trixie inrelease...另一条是 ROS 2 的获取:1 http://packages.ros.org/ros2/ubuntu jammy inrelease [4,682 B] 错误:1。这两种报错看起来是包管理器的问题但根子还是 HTTP 会话里下载下来的InRelease文件没通过 GPG 签名验证。我之前在配置 Linux 服务器时也踩过这个坑。系统时间不对、GPG key 过期、网络中间设备修改了响应内容都会导致签名校验失败。排查步骤先确认系统时间date如果偏差超过几分钟GPG 验证直接就失败。更新 GPG key对 Debian 系用apt-key adv --keyserver keyserver.ubuntu.com --recv-keys KEYID对 ROS 源用curl -s https://raw.githubusercontent.com/ros/rosdistro/master/ros.asc | gpg --dearmor -o /usr/share/keyrings/ros-archive-keyring.gpg。把源里的http://改成https://很多公网镜像都有 HTTPS 可用能在传输层减少内容被篡改的概率。这里有个容易被忽略的细节InRelease文件本身是通过 HTTP 明文下载的GPG 签名仅仅保证“文件内容没被改”但产地和下载过程不保证。所以在公网环境使用 HTTPS 源是更稳妥的做法。4.3 HSTS为什么浏览器拒绝访问一个能用的HTTP站点热搜词里有这句由于此站点使用 HTTP 严格传输安全,因此你目前无法继续访问此站点。这是 HSTSHTTP Strict Transport Security在起作用。简单说某个域名在之前访问时给浏览器返回过Strict-Transport-Security: max-age31536000浏览器把这个域名记进安全列表之后就算你手动输入http://浏览器也会强制升级成https://。如果此时证书无效或过期它不会像普通 HTTPS 那样给你“继续访问”选项而是直接拒绝。这个机制本意是防止用户在 HTTP 阶段被劫持但本地开发时很讨厌。比如你之前在某个域名下启用了 HSTS后来把证书撤了想临时用 HTTP 调试浏览器死活不让你进。解决办法在 Chrome 地址栏输入chrome://net-internals/#hsts在“Delete domain security policies”里删除对应域名。或者换一个没上过 HSTS 的本地域名/加端口比如http://127.0.0.1:8080因为 HSTS 一般不会作用于 IP 地址。开发测试服务器记得别随便下发 HSTS 头尤其是includeSubDomains参数一旦带上子域名也会跟着被强制 HTTPS。4.4 端点配置错误HTTP客户端报错的高频原因热搜词里还有一条很长your endpoint configuration is wrong; for more details see: http://wiki.apac...。这种“端点配置错误”经常出现在云平台 SDK、Kafka 客户端、或内网服务注册中心里。报错本身不一定是 HTTP 状态码但本质是 HTTP 客户端把请求发到了一个错误 URL。我遇到过的三类情况一是 base URL 配置里多了一个路径前缀比如http://api.example.com/v1然后 SDK 内部又拼接了/v1最终请求变成/v1/v1二是环境变量没生效代码读到了默认端点根本不是你配的那个三是认证 token 的生成接口和业务接口用了不同的端点配置写错导致先认证就失败。解决这类问题的通用思路是在代码入口把最终请求的完整 URL 打印出来肉眼比一比胜过猜半天。5. 排查HTTP报错的五板斧5.1 先复现保留现场遇到任何 HTTP 报错不要急着重启服务。先用 curl 原样复现一次注意带上请求头、请求体和 Cookie。curl -v会打印完整请求和响应过程包括 DNS 解析、TCP 连接、TLS 握手、发送的请求头、返回的响应头。这一条能解决大概一半的“看起来像 HTTP 问题”。对于浏览器里的报错打开开发者工具 Network 面板看具体的请求 URL、请求头、响应状态和响应体。很多 CORS、HSTS、400 报文在这里能一眼定位。5.2 分清是哪一个环节的错误一个 HTTP 请求跨越的环节太多客户端、代理、网关、DNS、负载均衡、后端应用。报错信息里的措辞其实常常暴露位置。比如“Nginx 502”是代理到后端失败“Connection refused”是目标端口没监听“SSL certificate problem”是证书链问题根本不是 HTTP 层问题。我建议用抓包或者日志把链路拆开至少找出“报错发生在哪个组件”再动手。5.3 不要忽略响应头HTTP 响应头里有很多排障信息。Server告诉你是谁响应的X-Cache告诉你有没有命中 CDNDate和本地时间对比可以发现时间偏移Content-Length与实际收到的 byte 数不一致说明响应被截断Retry-After告诉你限流后多久能重试。很多接口报错正文里只有一句“server error”但响应头已经把原因写得很清楚了。5.4 把HTTPS证书检查放在前面现代 HTTP 流量基本都是 HTTPS所以遇到“网络问题”时先确认证书链完整性。openssl s_client -connect host:443 -servername host可以快速查看证书链、过期时间、TLS 版本。很多 Docker、apt、curl 的报错都是因为系统信任的 CA 仓库里缺少中间证书导致 TLS 握手失败表现却是“HTTP 请求失败”。5.5 时间同步是隐藏杀手系统时间不准会引发一连串问题SSL 证书“未生效”、GPG 签名校验失败、Token 过期判断错乱、日志时间对不上。排查 HTTP 问题时顺手跑一下chronyc tracking或者date确认时间偏差在合理范围能省去后面一大半折腾。时间同步这个排查项是我处理过的所有“莫名其妙”报错里出现频率最高的之一如果你也卡在某个 HTTP 报错上查了两小时没头绪先去同步一下系统时间可能就有答案了。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进