Nginx神奇的499竟然不在HTTP响应码标准内?快来了解一下!

本文涉及的产品
日志服务 SLS,月写入数据量 50GB 1个月
简介: Nginx神奇的499竟然不在HTTP响应码标准内?快来了解一下!

1 前言

高性能的HTTP和反向代理服务器,Nginx用来:

  • 搭建Web Server
  • 作负载均衡
  • 供配置的日志字段丰富,从各类HTTP头部到内部性能数据都有

Nginx的访问日志中,存在499状态码的日志。但常见4xx状态码只有400、400、401、403、404等,499并未在HTTP RFC文档。这499错误日志,在流量较大场景下,特别是面向Internet的Web站点场景下还是很常见 。

2 案例

某客户反馈:Nginx服务器连续几天记录较多499错误日志,之后几天趋零,再回升,整体状况不定。


经营的To C产品,跟手机端App协同。App会定时把消息上传到微信消息网关,后者再把这些消息推送到该客户的服务端(在公有云上)做业务处理,整体消息量约每日三十万条。对消息网关来说,这服务端就是一个Web回调接口:

499日志趋势:

由于大量499日志存在,客户非常担心业务已受影响,如终端消费者是否经常上传数据失败?499状态码本身意味着啥呢?查它在Nginx的 官方定义

NGX_HTTP_CLIENT_CLOSED_REQUEST | 499

client closed request(客户端关闭了请求)?说了跟没说没区别。啥引起“客户端关闭请求”?

解决问题办法,可能不在问题自身所处层面

应用层日志记录的只是表象。更深层次原因可能在更底层,如传输层或网络层。搞清499:

  • 不仅是理解这个499码底层含义
  • 而且通过排查,掌握一套 对HTTP返回码进行网络分析的方法。对维护好Nginx以及其他Web服务都有助

来抓包分析HTTP返回码真正含义。

3 锚定到网络层

软件文档已无法查清根因,所以要下沉到网络层排查。如你处理应用层故障,如HTTP异常返回码(4xx和5xx系列),也遇到应用层找不到答案,就抓包分析。

后文

  • “客户端”指微信消息网关
  • “服务端”指这客户在公有云的服务器

服务端 使用tcpdump工具抓包,Wireshark打开抓包文件。从抓包文件中一般寻找可疑报文。这次抓包有不少RST报文,过滤出典型的带RST报文的TCP流:

抓包文件可关注【JavaEdge】联系本人领取。

结尾处RST。这TCP流一定跟499日志有关系吗?得益于TCP/IP精妙分层设计,应用层只需通过系统调用,就可像使用文件IO那样使用网络IO,具体的网络细节都由内核处理。可由此也带来问题: 应用层视角无法“看到”具体的网络报文。

需根据关键信息确定应用层日志跟网络报文的对应关系。如这里,可确认上面这带有RST的TCP流,就是日志中记录的一条499日志记录。因为:

  • 客户端IP:日志中的remote IP跟抓包文件里面的IP符合
  • 时间戳:日志的时间戳也跟这个TCP流的时间吻合
  • 应用层请求:日志里的HTTP URL路径和这个TCP流里的URL相同

04也是类似方式找到应用日志跟报文对应关系。

真实抓包分析场景中,“如何把应用层问题跟网络层抓包关联”,始终是关键环节。也是令人困扰的关键技术障碍。所以这里方法可参考,再处理这种关键环节,也可根据上面提到的三维即IP、时间戳、应用层请求(包括URL和header),把应用层问题锚定到网络层数据包。


既然确定这个流就代表一次499事件,好好分析报文。

4 解密TCP流

TCP流示意图红框部分是重点。

报文1~3,表示TCP握手成功。

报文4(客户端发出),表示客户端向服务器发报文,报文里只包含HTTP header,其声明该请求为POST方法,但不含POST body。正常,因为HTTP协议规定数据先后顺序:先header(包含method、URL、headers),后body。所以,既然method和URL单独位于一个报文,按顺序body就在后续的报文。


报文5(服务端发出),确认报文:我(服务端)确认收到了你(客户端)发过来的报文4。


报文6(客户端发出),距上一报文2s。这报文被Wireshark标红,注释TCP Previous segment not captured:它之前的TCP报文段没被抓到。


之前的TCP报文”是啥?

就是按TCP序列号顺序,排在当前报文之前的报文。先关注右边红框FIN标志位,说明这是客户端主动关闭连接的报文。

目前报文情况:

明明HTTP POST请求的body(也称HTTP载荷)还没发过来,客户端就要关闭连接?好比朋友说:“我有个事情要你帮忙,嗯,拜~”,你刚听到上半句他的求助意向,还没听到这忙是个啥,他就跟你说再见。可能暂时看不出问题,先放放。

报文7(服务端发出)。服务器收到FIN+ACK报文(6号报文),但发现序列号不是它期待的309,而是777,于是服务器TCP协议栈判断:有一个长度为777-309=468的TCP段(TCP segment)丢失了。

按TCP约定,这时服务端只可确认其收到的字节的最后位置,在这里就是上次(报文5)的ACK位置。形式上,报文7就成了一个DupAck(重复确认)。

当客户端收到DupAck时,它就要长个心眼:“情况有点微妙,如果凑满3个DupAck可能有丢包”。

如凑满3个DupAck就重传的机制,被称为快速重传机制,12深入学习过。

报文4的TCP信息:

按TCP设计,客户端将发送的下一个报文的序列号(309)= 本次序列号(1) + 本次数据长度(308),即Next sequence number。


报文8(客户端发出),16s之久,客户端 重传 了这个报文,包含POST body,长度 468 字节。就跟777-309=468对应。


明明这468字节的报文是第一次出现,怎么算重传?因为,这个抓包文件是在服务端生成,所以它的视角无法看到多次传送同样这个报文的现象。但我判断,客户端抓包,一定可看到这个468字节的报文被试图传送多次。

服务端视角判断,一开始这报文应该是走丢,没到达服务端,所以没在这服务端抓包文件里。又因为过16s才到,很可能不是单纯一次重传,而是多次重传后才到达。因此确实属重传。


报文9,服务端对这POST body的数据包回复确认报文。


报文10,服务端发HTTP 400的响应报文给消息网关。这信息并没有被Wireshark直接按HTTP格式进行展示,但因HTTP是文本编码,所以可鼠标选中Transmission Control Protcol部分,在底下文本栏直接看到HTTP 400这段文本:

这HTTP 400报文也带FIN标志位,即服务端操作系统“图省事”,把应用层的应答数据(HTTP 400),跟os对TCP连接关闭的控制报文(这个FIN),合并在同一个报文。03提到的搭顺风车(Piggybacking),提升网络利用率。

这阶段报文:


从这些报文顺序来看,确实有问题:

  • 服务端先收到HTTP header报文,没收到期望的HTTP body报文,而是收到FIN报文(客户端试图关闭连接)。HTTP请求还没发到服务端,服务端回复HTTP响应更无从谈起,客户端就发FIN不符常理(
  • 服务端回复HTTP 400,并发送FIN关闭这连接
  • 客户端回复RST彻底关闭这连接
    客户端先发送了FIN,之后才发POST body。 全部过程:

服务端还没回复数据而客户端已经要关闭连接,按499官方定义,这种行为被Nginx判定499状态:

  • 对内表现为记录499日志
  • 对外表现为回复HTTP 400给消息网关

所以,在服务端的Nginx大量499日志条目;在消息网关那头,如果它也做Web日志,就是400报错。

5 从现象到本质

为什么客户端先发送FIN,然后才发送POST body?

回到Wireshark窗口,再关注6号报文:

离上个报文相差了2s。

往前看4号报文:

离3号报文相差3s。加起来,6号报文离TCP握手完成,正好隔 5s整

一般出现这种整数,就越可疑,因为如果是系统或网络错乱导致,时间分布应 随机,不可能卡在整数时间。经验看, 这和人为设置有关。于是客户仔细查看微信网关使用文档,发现5s超时设置。即若一个HTTP事务无法在5s内完成,就关闭这连接。

啥叫无法完成?

在这抓包里即:HTTP header报文发过去了,但HTTP body报文没一起过去(网络原因导致)。而由于初始阶段报文少, 无法凑齐3个DupAck

所以快速重传没有启动,只好依赖超时重传(12 讲),且这多次超时重传也失败,服务端只好持续等待这丢失的报文。5s后,客户端没收到服务端响应,就主动关闭这次连接(可以下次再试,这次就不继续干等)。


即该场景里Nginx 499错误日志主因:


消息网关—>服务器 方向上的一个TCP包丢失(案例里是HTTP POST body报文),引起服务端空闲等待

  • 消息网关有5s超时设置,即连接达到5s,消息网关就发FIN关闭连接

逻辑链:

  • 要解决499报错的问题,就需要解决5s超时
  • 要解决5s超时,就要解决丢包
  • 要解决丢包,就要改善网络链路质量

最根本解决方案,如何确保客户端到服务端的 网络连接 可靠稳定,使类似的报文延迟现象降到最低。只要不丢包不延迟,HTTP事务就能在5s内完成,消息网关就不会启动5s超时断开连接机制。


跟客户还有网关工程师配合,确实发现网关到公有云的一条链路有问题。更换为另外一条链路后,丢包率大幅降低,问题极大改善。虽然还是有极小比例的错误日志(约万分之一),但对客户已在可接受范围。


因为丢包,客户端FIN报文跟HTTP POST body报文一样,也可能丢失。不过,无论这FIN是否被服务端及时收到,这次HTTP事务本身也已在客户端记为失败。

链路丢包这种问题挺明显,为啥没及时发现?

  • 虽然对主要链路的整体状况有细致的监控,但这里的网关到客户的公有云服务属于“点到点”的链接,本身也属于客户自身的业务,公有云难以对这种情况做监控,理想情况是客户自己来实现监控。
  • 客户的消息量很大,哪怕整体失败比例不高,但乘以绝对的消息量,产生的错误的绝对数也就比较可观了。
  • 至于Nginx为何“创造”499状态码, Nginx源码 注释写得清楚。并非标新立异,而确实是为了弥补标准HTTP协议不足:
/*
 * HTTP does not define the code for the case when a client closed
 * the connection while we are processing its request so we introduce
 * own code to log such situation when a client has closed the connection
 * before we even try to send the HTTP header to it
 */
#define NGX_HTTP_CLIENT_CLOSED_REQUEST     499

6 总结

Nginx 499是Nginx定义状态码,不是RFC中定义HTTP状态码。表示“Nginx收到完整的HTTP request前(或者已经接收到完整的request但还没来得及发送HTTP response前),客户端试图关闭TCP连接”这种反常情况。

超时时间跟499报错数量也有直接关系。如果我们有办法延长消息网关的超时时间,比如从5秒改为50秒,那么客户端就有比较充足的时间去等待丢失的报文被成功重传,从而在50秒内完成HTTP事务,499日志也会少很多。

关注网络延迟对通信的影响。比如客户端发出的两个报文(报文3和报文4)间隔了3秒钟,这在网络通信中是个非常大的延迟。而造成这么大延迟的原因,会有两种可能:

  • 消息网关端本身是在握手后隔了3秒才发送了这个报文,属 应用层问题
  • 消息网关在握手后立刻发送了这个报文,但在公网上丢失了,微信消息网关就根据“超时重传”的机制重新发了这个报文,并于3秒后到达。属 网络链路问题

由于上面的抓包是在服务端做的,所以未到达服务器的包自然也不可能抓到,也就是无法确定是具体哪一种原因(客户端应用层问题或网络链路问题)导致,但这并不影响结论。

公网丢包现象不可能完全消失。千分之一左右公网丢包率是正常范围。由于:

  • 客户发送量较大(主因)
  • 微信消息网关设置5s超时相对较短(次要原因)

问题就会在这个案例中被集中暴露。

设置更长超时阈值(如50s)能解决?出错率会降低不少,但新问题:

  • 消息网关会有更多的资源消耗(内存、TCP源端口、计算能力等)
  • 消息网关处理事务的平均耗时会增加

所以,5s是权衡后的合适方案。

排查的方法论,对更广泛的应用层报错日志的排查,推荐:

  • 先查应用文档,初步确定问题性质,大体确定排查方向
  • 对比应用日志和抓取的报文,在传输层和网络层寻找可疑报文。这步,可采用比对策略找到可疑报文:
  • 日志中的IP跟报文中的IP对日志和报文的时间戳对应
  • 应用层请求信息和报文信息对应
  • 结合协议规范和报文现象,推导出根因

FAQ

  • 第7个报文是DupAck,为什么没触发快速重传?
  • 消息网关那头的应用日志应该不是499,那会是啥样日志?

本文由博客一文多发平台 OpenWrite 发布!

相关实践学习
日志服务之使用Nginx模式采集日志
本文介绍如何通过日志服务控制台创建Nginx模式的Logtail配置快速采集Nginx日志并进行多维度分析。
目录
相关文章
|
1月前
|
缓存 移动开发 安全
Web安全-HTTP响应拆分(CRLF注入)漏洞
Web安全-HTTP响应拆分(CRLF注入)漏洞
91 1
|
2月前
|
Shell
HTTP状态码解析:在Haskell中判断响应成功与否
HTTP状态码解析:在Haskell中判断响应成功与否
|
1月前
|
存储 JSON API
HTTP 请求与响应处理:C#中的实践
【10月更文挑战第4天】在现代Web开发中,HTTP协议至关重要,无论构建Web应用还是API开发,都需要熟练掌握HTTP请求与响应处理。本文从C#角度出发,介绍HTTP基础知识,包括请求与响应结构,并通过`HttpClient`库演示如何发送GET请求及处理响应,同时分析常见错误并提供解决方案,助你更高效地完成HTTP相关任务。
84 2
|
2月前
|
缓存 移动开发 安全
Web安全-HTTP响应拆分(CRLF注入)漏洞
Web安全-HTTP响应拆分(CRLF注入)漏洞
151 8
|
2月前
|
开发者
HTTP状态码是由网页服务器返回的三位数字响应代码,用于表示请求的处理结果和状态
HTTP状态码是由网页服务器返回的三位数字响应代码,用于表示请求的处理结果和状态
32 1
|
3月前
|
缓存 应用服务中间件 API
Nginx七层(应用层)反向代理:HTTP反向代理proxy_pass篇(三)
Nginx七层(应用层)反向代理:HTTP反向代理proxy_pass篇(三)
51 3
|
3月前
|
缓存 安全 应用服务中间件
Nginx七层(应用层)反向代理:HTTP反向代理proxy_pass篇(二)
Nginx七层(应用层)反向代理:HTTP反向代理proxy_pass篇(二)
63 1
|
3月前
|
运维 算法 应用服务中间件
运维系列.Nginx中使用HTTP压缩功能(一)
运维系列.Nginx中使用HTTP压缩功能(一)
67 1
|
3月前
|
缓存 应用服务中间件 nginx
[nginx]定制http头信息
[nginx]定制http头信息
|
3月前
|
数据采集 存储 监控
Haskell爬虫中日志记录:监控HTTP请求与响应
Haskell爬虫中日志记录:监控HTTP请求与响应