序言
在http中,上传下载文件总是一个比较耗时的过程,特别是大文件的时候,从而在http1.1之后就产生这样的数据传输方式。
分块传输也就是将一个大文件划分为不同的chunk进行传输,从而客户端收到之后,再进行拼接成完整的数据。
分块传输编码
本来也是无需接触这种东西的,奈何在问题排查的时候,一不小心就遇到了。
1 获取请求和响应的大小
在使用nginx的时候,如果要将对应的请求和响应进行保存到日志中,从而进行流量回放或者是安全扫描,那么就会遇到很大的body或者响应很大的情况,并且这种数据都属于敏感数据了,从而需要进行加密存储,在nginx的lua中,一旦直接保存,那么遇到上传下载的文件、视频,内存直接爆炸。
在这种场景下,如果是虚拟机,可能还好点,如果是容器,那么就会占用三倍的内存,如果你的limit不是那么大,那么直接这个worker process就会被oom杀死,从而会影响正常的业务请求。在error log中,可以看到worker process 被kill的日志,使用ps的时候,可以看到进程的pid发生了多次的变化。
那么怎么来减缓这种情况呢,第一反应是根据http请求头中content-length来进行判断响应的大小,如果超过1M,那么就对这个响应进行截断,从而防止nginx的worker process被oom杀死,但是,并没有那么简单,好像问题只是稍微缓解了,并未解决。
后面才发现情况很多,如果是分块传输编码,也就是使用chunk的形式来进行传输数据的时候,这个时候是没有这个头的,分块传输,也就是transfer-encoding:chunked,这个时候其实就是无法判断响应的大小,而且格式是分块的大小,然后是数据,最后以特殊的0大小的分块结尾。
从而只能使用nginx的变量中的upsteam_response的大小来进行判断,从而进行数据截断。
分块传输大小,是为了将大块的文件进行切割传输,无法确定大小的情况,例如视频
2 SSE
在现在的技术中,流式数据使用的越来越多了,例如sse,那么在nginx中需要进行特殊配置才能使用,也就是全局变量proxy_buffering 为off,在nginx的配置中,默认是开启的,也就是会把大的响应进行开启,然后缓存在nginx的磁盘中,然后一次性的发送给客户端。
默认开启的目的,是因为nginx和upsteam一般在同一段网络中,从而数据传输较快,有利于提高吞吐,节省后端的资源,因为像tomacat这种,不能支持太多的并发连接,从而使用nginx进行缓冲,当关闭之后,其实对客户端来说,会及时的收到响应,也就是rt会降低,两者各有优劣。
要想良好的支持流式数据,老老实实的关闭proxy_buffering,也就是不缓存上游的数据,这个还吃磁盘的iops,一不小心,就会让rt抖动一下,而且这种方式也比较耗费内存,关闭之后,对于nginx来说,也是比较节省内存的,在大量业务使用的nginx中,可以关闭此项。
3 curl下载文件报错
在使用curl进行下载文件的时候,可能会出现如下报错:
curl transfer closed with remaining to read
当你使用抓包工具进行抓包的时候,发现好像也很正常,都是传输了数据,然后关闭了连接,而curl的报错信息表示,数据在读取的时候,突然关闭了连接,而且是数据的大小和文件的大小差不多一样大的时候,然后就中断了。
在这个时候,需要检查抓包的信息,看看chunk的数据是否发送了最后的0大小的块,也就是:
0\r\n (十六进制编码30 0d 0a)
检查一下代码,看看是否正确,这个主要是和chunked发送的格式有关,还有可能就是检查最后一个包,包的大小和数据是否正确,在这种场景下,一般是忘记了发送最后的结束符。
4 其他
在进行大文件传输下载的时候,除了分块传输编码,其实还有其他的集中类型也是无法确定content lenth的,一种是像视频的拖动,也就是范围的请求的range,还有是多段数据传输的multipart等,这些都是通过http头来进行控制,可以自己写一段代码,然后抓包看看对应的头部信息。
风言风语
大文件的传输有各种不同的方式,例如可以开启压缩,例如分段传输,支持断点续传等等,基本上都是通过不同的http头来实现的。
在权力面前,所有的挣扎都是徒劳的,就像分块传输,已经订好了对应的规则,而一旦不遵守,那么就会直接报错了。
在不同的场景下,有的参数可能需要变化,例如大家都认为proxy_bffering开启之后比较好,实际上,流式的关闭更佳,rt敏感的场景更佳。