问题排查 - 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 编码传输(它可以将数据分成多个数据块进行传输,从而解决长度未知的实体数据传输问题)