问题排查 - HTTP 流式上传失败

一、背景

我们的业务系统中有使用 OkHttp3 做大文件流式上传,即 Content-Type 为 application/octet-stream 的请求,但是在传大约 10G 左右的文件时出现了上传失败现象

客户端在 OkHttp3 的上传代码类似如下:

RequestBody requestBody = new RequestBody() {
    @Nullable
    @Override
    public MediaType contentType() {
        return MediaType.parse("application/octet-sampleStream");
    }

    @Override
    public void writeTo(BufferedSink sink) throws IOException {

        int len;
        try {
            while ((len = sampleStream.read(buffer)) != -1) {
                sink.write(buffer, 0, len);
            }
        } catch (Exception e) {
            log.error(e.getMessage());
            throw new IOException();
        } finally {
            sink.flush();
            sink.close();
        }
    }
};

二、请求链路

client(springboot okhttp3 ) ==> server-proxy(nginx) ==> server(springboot) ==> w-componant(springboot)

三、排查

3.1 现象收集 & 验证

现象1:每次 client 后端发文件上传请求时,server 后端会等待 1min ~ 2min 才在拦截器收到请求

现象2:server 后端收到请求的时间随着文件大小变大而变长

现象3:server ngx 日志来看,ngx 是可以立即收到请求

至此:得知大文件上传失败是因为 server 对请求的防重放验证未通过,防重放 60s,而客户端发送请求后,服务端过了 60s 才收到

期望:客户端发送请求,服务端应该是马上能收到,是什么原因导致服务端过了一段时间才收到请求?

3.2 进一步推测原因 & 验证

1、w-componant 有问题:client 从 w-componant 的共享 PVC 取数据,可能是 PVC 性能不行,取数据慢,或者 w-componant 对于流响应处理有问题

2、client 后端有问题:可能是没有及时 sink.flush(),导致数据一直处于缓冲区

3、server ngx 有问题:可能是 server ngx 把请求缓存了,再路由给 server 后端

4、sever 后端有问题:可能是 server 后端把请求缓存了,再进入防重复拦截器,导致超时

经过逐一验证,验证推测 3 为根本原因

3.3 结论

server ngx 对文件上传请求,包含 application/octet-stream 类的二进制请求,都会缓存,需要禁用请求缓存。目前 ngx 的禁用请求缓存配置有问题,没有生效;需要在 http 路由块中加入配置 proxy_http_version 1.1; 才会真正让 nginx 不缓存请求

四、解决方案

在 /inner 的 ngx 配置块中加入 proxy_http_version 1.1;

location ~ ^(/path_of_system) {
    proxy_pass http://server;
    client_max_body_size 50000M;
    proxy_max_temp_file_size 10240m;
    proxy_request_buffering off;
    proxy_connect_timeout 60s;
    proxy_send_timeout 60s;
    proxy_read_timeout 60s;
    proxy_http_version 1.1;
}

五、扩展问题

5.1 为什么 ngx 会缓存请求?

ngx 默认行为调研:https://serverfault.com/questions/768693/nginx-how-to-completely-disable-request-body-buffering

proxy_http_version 指令用来设置代理服务器连接后端服务器(注意这里与客户端无关,只与要路由的后端服务有关)时所使用的 HTTP 协议版本。在 Nginx 中,该指令默认值为 1.0。HTTP 1.0 是不支持长连接与 Chunked 编码传输的,那么 ngx 为了支持客户端 application/octet-stream 流式传输,就会缓存下数据,再路由给后端服务

因此开启 proxy_http_version 1.1; 便可以具备使用 长连接和 Chunked 编码传输的条件

5.2 HTTP 中什么时候会启用 Chenked 编码传输?

问题一讨论了 ngx 的行为和使用长连接和 Chunked 编码传输的条件,但只是条件,要触发这些条件,在于客户端:如果请求头部中没有指定 Content-Length 字段,而且也没有指定 Transfer-Encoding 字段为其他值,那么默认会使用 Chunked 编码方式传输请求体数据

由于我们在客户端发的是 application/octet-stream ,也不知道其他的头部,因此 ngx 就会使用 Chunked 编码传输(它可以将数据分成多个数据块进行传输,从而解决长度未知的实体数据传输问题)