HTTP之body去哪儿了

问题描述 ??业务反馈Golang服务在解析请求参数的时候,偶现出现"EOF"错误,怀疑网关或者中间链路丢失了HTTP请求体,业务错误日志统计如下:
HTTP之body去哪儿了
文章图片

??说明一下,Golang服务基于gin框架,解析POST请求参数方式如下:

func Handle(c *gin.Context) { err:= c.ShouldBindJSON(&req) //出现err io.EOF }

??HTTP请求没有body时候,就会出现这种错误。多次确认,客户端日志显示请求都带有body,而且根据traceid查询某个异常请求的客户端日志,也显示带有body。
难道真是中间节点丢了请求体?可是不应该啊,网关(Nginx)在转发请求的时候,不可能丢失body啊,而且客户端请求都带有"Content-Length",如果网关没有收到请求body,校验HTTP请求不完整,也会直接返回400错误啊。
??查询网关access日志,显示request-body确实为空,说明网关接收到的请求确实没有body。
??需要说明一下,从客户端到Golang服务,整个访问链路为:client ——> ECDN ——> LVS ——> 网关Nginx ——> Golang服务
??LVS只是四层负载均衡,也不会是它的问题。腾讯云ECDN,有可能,需要找服务方帮忙排查下。
ECDN排查 ??业务日志查询出异常请求,提供请求url,请求时间,客户端IP给ECDN服务方,查询ECDN日志。结果显示,所有请求都是带有body,即使存在回源失败的情况,重试的时候也都带有body。
HTTP之body去哪儿了
文章图片

??红框中的两个数字,第一个是head-length,第二个是body-length。
??网关日志只能看到request-body为空,以及request_length,但是请求头以及请求体长度是看不到的。
??但是发现,正常请求时候,网关日志request_length = ECDN日志head-length + body-length;异常请求时候,网关日志request_length = ECDN日志head-length。大概率ECDN确实没有带body。
??另外确认,ECDN针对客户端的携带的header "Content-Length",也会转发给源站。网关节点修改日志格式,添加字段Content-Length;观察一段时间,请求正常时候Content-Length也是正常的,请求出错的时候Content-Length=0。
基本可以确认,ECDN转发过来的请求确实没有携带body,以及Content-Length=0。
??查找多个异常case,发现出错的时候,ECDN都存在出错重试情况。在ECDN修改配置,去掉重试之后,EOF错误再也没有了。
经ECDN服务方排查确认,重试逻辑存在bug,重试确实没有带body。
继续探索 ??为什么第一次请求会失败呢?ECDN服务方给出线索,失败情况日志显示的错误是"SSL Alert Close Notify。查询了解到,这错误是HTTPS在建立加密链接的时候,源站SSL_shutdown主动关闭链接导致。
??源站为什么会主动关闭链接呢?排查问题的时候,腾讯云ECDN方还进行了线上抓包,给出了部分抓包数据:
HTTP之body去哪儿了
文章图片

??由于是HTTPS加密数据,抓包并不能看到具体的数据,wireshark导入网站密钥之后,发现依然不能解密。最后才发现,加密算法采用的是ECDHE,wireshark不支持此类密文的解密。
??不过还是可以看到,第21号包返回的应该就是所谓的SSL Alert Close Notify,后面就是链接的FIN关闭了。
??再次全局分析一下,第一次请求在相对0时刻发出,第二次请求在相对时刻120秒发出。120秒好像有点熟悉,查看网关Nginx配置,发现:
http{ keepalive_timeout 120; }

【HTTP之body去哪儿了】??长连接keepalive_timeout配置刚好是120秒,即120秒之内没有请求的话,Nginx(这里就是源站)会主动断开链接。
Nginx keepalive处理 ??处理完成当前请求时候,如果是长连接Nginx会添加定时器,超时时间刚好为keepalive_timeout,超时之后,主动关闭当前长链接。
static void ngx_http_set_keepalive(ngx_http_request_t *r) { //超时后处理方法 rev->handler = ngx_http_keepalive_handler; ngx_add_timer(rev, clcf->keepalive_timeout); }static void ngx_http_keepalive_handler(ngx_event_t *rev) { if (rev->timedout || c->close) { ngx_http_close_connection(c); return; } }void ngx_http_close_connection(ngx_connection_t *c) { #if (NGX_HTTP_SSL) if (c->ssl) { if (ngx_ssl_shutdown(c) == NGX_AGAIN) { c->ssl->handler = ngx_http_close_connection; return; } } #endif }

??Nginx有两个配置可以影响源站主动关闭链接(都归属与ngx_http_core_module):
//等待多长时间内,还没有请求到达,关闭链接 Syntax:keepalive_timeout timeout [header_timeout]; Default:keepalive_timeout 75s; Context:http, server, location//接收多少次请求后关闭链接 Syntax:keepalive_requests number; Default: keepalive_requests 1000; Context:http, server, location This directive appeared in version 0.8.0.

??同样的Nginx ngx_http_upstream_module在代理转发的时候,也支持类似的配置(注意默认配置,以及配置引入的版本):
Syntax:keepalive_timeout timeout; Default: keepalive_timeout 60s; Context:upstream This directive appeared in version 1.15.3.Syntax:keepalive_requests number; Default: keepalive_requests 1000; Context:upstream This directive appeared in version 1.15.3.

??显然只要ECDN配置的keepalive_timeout以及keepalive_requests小于源站网关的配置即可,这样ECDN就会主动关闭长连接。
??不过貌似ECDN并不是基于Nginx实现的,已经沟通,建议实现类似的配置能力。

    推荐阅读