关于go:HTTP之body去哪儿了

4次阅读

共计 2906 个字符,预计需要花费 8 分钟才能阅读完成。

问题形容

  业务反馈 Golang 服务在解析申请参数的时候,偶现呈现 ”EOF” 谬误,狐疑网关或者两头链路失落了 HTTP 申请体,业务谬误日志统计如下:

  阐明一下,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。

  红框中的两个数字,第一个是 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 方还进行了线上抓包,给出了局部抓包数据:

  因为是 HTTPS 加密数据,抓包并不能看到具体的数据,wireshark 导入网站密钥之后,发现仍然不能解密。最初才发现,加密算法采纳的是 ECDHE,wireshark 不反对此类密文的解密。

  不过还是能够看到,第 21 号包返回的应该就是所谓的 SSL Alert Close Notify,前面就是链接的 FIN 敞开了。

  再次全局剖析一下,第一次申请在绝对 0 时刻收回,第二次申请在绝对时刻 120 秒收回。120 秒如同有点相熟,查看网关 Nginx 配置,发现:

http{keepalive_timeout 120;}

  长连贯 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 实现的,曾经沟通,倡议实现相似的配置能力。

正文完
 0