对接第三方对象存储的服务最近总是有warning日志,远程上传的对象get请求返回的响应头中缺失content-length信息
go程序中获取到的content-length值为-1
之前被content-length值为-1坑过,具体场景为程序中使用 minio 作为sdk对接第三方对象存储
使用PutObject
方法完成远程上传,即通过http get获取源对象,直接上传,不进行本地转存,原本运行挺稳定,直到出现大量响应头信息缺失content-length的资源出现
当时使用的minio版本为github.com/minio/minio-go v6.0.8+incompatible,此版本中当不显式传入大于0的size(对应http请求返回的content-length)时,将采用分块上传的逻辑
但是每块的体积是500M,计算方式是最大对象体积/最大分块数
最开始是串行上传,所以并没有暴露问题,直到为了优化性能利用带宽,改成了并行上传时,OOM了,因为需要同时上传的对象数n*500M的内存,这啥配置顶得住
这个问题的解决方案是升级了minio版本到v7,可以配置默认的chunk大小,不再是500M,然后将有问题的源都改为支持返回content-length,同时对获取不到content-length的请求添加告警,包括chunked
所以再看到这个告警还是要重视的
结果大跌眼镜,是个内部服务提供的远程上传链接,有着不少诡异的清空,初期很是抓头
前提
Content-Length
Content-Length 表示请求/响应实体内容的长度(字节)
一般是自动生成的,并不需要手动指定修改,手动操作反而会引发错误,比如在chrome浏览器中发送ajax时进行操作,可以试试
- 当内容的实际字节数大于 Content-Length 时
- 当服务端接收的请求实体长度超过 Content-Length ,服务端会直接关闭连接,从而导致请求失败
- 当服务端返回的响应实体长度超过 Content-Length,超出 Content-Length 长度的内容将被丢弃
- 当内容的实际字节数小于 Content-Length 时
- 当服务端接收的请求实体长度小于 Content-Length,服务端会继续等待后面的数据,从而导致超时
- 当服务端返回的响应实体长度小于 Content-Length,浏览器会报错,如Chrome 浏览器会报 net::ERR_CONTENT_LENGTH_MISMATCH
可见Content-Length适用于消息体长度明确的情况,Content-Length 无法提前计算出来的情况则使用分段传输
Transfer-Encoding
分块传输编码(Chunked transfer encoding)是超文本传输协议(HTTP)中的一种数据传输机制,允许HTTP由网页服务器发送给客户端应用( 通常是网页浏览器)的数据可以分成多个部分。分块传输编码只在HTTP协议1.1版本(HTTP/1.1)中提供
Transfer-Encoding 和 Content-Length 是互斥的,如果同时出现,浏览器以 Transfer-Encoding 为准,在Go中,会从header中删除两个信息,然后设置TransferEncoding和ContentLength属性
具体可以参考net/http
源码
1 | // srs/net/http/transfer.go |
问题
- 不是100%出现
- 同样的接口,Go请求不返回content-length,但curl访问则返回
- 同样的代码,其它服务中的文件访问均会返回content-length
原因
1. 并不是每个请求都能复现
通过观察日志,正常返回content-length的请求,其content-length值均不大于255
通过创建特殊文件请求验证,确认只有大于等于256字节的文件被访问时才会出现不返回content-length的情况
1. curl表现不一致的问题
通过排查,定位到 compressed
这个配置上,如果显示的添加配置 curl --compressed
,则表现与Go请求一致,即获取到了chunked 响应
而compressed
是指定请求要压缩的,对接收到的响应也会自动进行解压缩。即默认请求是不进行压缩的,所以会返回content-length头
同理,如果Go请求时指定 Accept-Encoding 为 identity,可以达到同样的效果
2. 同代码不同表现的原因
由于有参照,通过控制变量发的对比发现唯一的不同是其他服务是内网访问,而出问题的服务配置了公网地址访问
那么问题就只能是出在了多余的公网访问这一步。而服务是k8s集群内的,那么就是使用的ingress-nginx配置上有文章了
先找到ingress的pod,然后查看pod的nginx配置
1 | $ kubectl get po -n ingress-nginx |
果然,发现了这些配置
1 | gzip on; |
其中gzip_min_length 256;
对应了问题1,响应达到256字节的请求才会出现状况的原因
而nginx中chunked_transfer_encoding
又是默认开启(on)的,所以导致文件被压缩后通过分块传输的方式对外响应,从而表现出content-length响应头信息丢失的现象
为什么gzip会导致分块传输 Transfer-Encoding: chunked ?
虽然原始的服务提供者的http响应是未分块且响应了正确的头信息 Content-Length 的
由于请求经过了ingress-nginx(或者其它形式的),根据其配置,超过 gzip_min_length 大小的响应body需要被压缩
header 是先于 body 被发送的,但应用 gzip 压缩的过程会导致 body 的体积变化,且压缩完成前是不知道其明确的体积的,所以此场景下只能选择分块传输
这都是因为nginx的gzip默认是动态压缩的,如果又要压缩,又要返回content-length,则需要额外的 gzip_static 模块 ngx_http_gzip_static_module 支持,添加模块后加入配置 gzip_static on;
即可实现
gzip_static 开启需要额外的存储空间,自行权衡
解决方法
- 指定不压缩
有一定的风险,毕竟不压缩会造成带宽的额外开销,如果确定影响不大,那么就可以放心设置1
2req, err := http.NewRequest("GET", url, nil)
req.Header.Set("Accept-Encoding", "identity") - 绕过nginx
已经分析到原始的服务返回是没有分块传输的,是ingress造成的这个现象,而又有直接内网访问的方案来解决,所以绕过它就好了