HTTP stream PUT and GET analysis
前言
目前正在从事云端存储和备份方面的工作,主要负责测试框架的开发和优化。软件技术人员对"stream"(流)这个词应该并不陌生,很多场景下,"stream"更是代表着性能上的优化。在web服务的开发应用中,HTTP body stream更是家喻户晓。各种开发语言几乎都提供有对HTTP实现的封装来实现对远端web服务的交互,某些高级类库更是提供了给开发人员方便使用的request stream和response stream的接口,只需要简单调用即可。
近期每天晚上自动的回归测试不是很稳定,经常有莫名中断地情况,主要发生在与云端服务进行大文件读写的时候。在对测试框架文件读写的部分的分析后发现,调用云端服务读写文件的时候,会将整个body读到内存。这种方式对小文件的处理,问题不是很大。但对于大文件,问题就出来了,会造成内存使用过大甚至溢出。于是对HTTP读写部分以流的方式进行优化,事实证明,对于机器硬件(尤其是内存)不是很高的情况下,如何降低使用内存还是很有必要的。
简单来说,HTTP body的流式读写不需要将整个文件读到内存,而是读一部分处理一部分,因此能有有效的降低内存的消耗。本文主要结合项目中遇到的问题,然后深入到http相关类库对body stream的实现(以ruby1.8和python2.7为例)做一个简短的分析。
1 HTTP stream PUT and GET
1.1 Ruby中HTTP stream的实现
HTTP基础类库的实现和封装通常会提供以下三个接口,获取远端资源的流程如下:
建立Connection => 发送Request(PUT, GET, POST, ...) => 获取Response
1.1.1 Stream Get
Ruby基础类库"net/http"提供了对http客户端的简单封装,利用这个类库,可以很方便的跟远端HTTP服务器进行交互:
require ‘net/http‘ require ‘uri‘ url = URI.parse(‘http://www.example.com/index.html‘) res = Net::HTTP.start(url.host, url.port) {|http| http.get(‘/index.html‘) } puts res.boy
这是类库给出的一个使用例子,这个例子本身没什么问题。实际使用中,我们需要从远端GET一个大文件并且保存到本地文件。在这个例子的基础上,我们稍作改动
File.open("localfile", "wb+") do |f| f.write(res.body) end
这样写功能没什么问题。仔细思考下,发现如果文件比较大,消耗内存也比较多,因为在往本地写文件的时候,文件已经在本地内存,上面例子中,返回的HTTPResponse对象时,已经将文件读到了内存。实际类库提供了另一种block读写的方式,只需要设置好callback,就可以做到边读边写,看下net/http.rb里的实现,当传入block的时候,会yield一个HTTPResponse对象,这个对象还没有对body进行读取:
1033 def request(req, body = nil, &block) # :yield: +response+ ... ...
1047 begin_transport req 1048 req.exec @socket, @curr_http_version, edit_path(req.path) 1049 begin 1050 res = HTTPResponse.read_new(@socket) 1051 end while res.kind_of?(HTTPContinue) 1052 res.reading_body(@socket, req.response_body_permitted?) { 1053 yield res if block_given? 1054 } 1055 end_transport req, res
利用上面request方法,我们便可以很容易的实现流式的写文件:
conn = Net::HTTP.new(host, port) req = Net::HTTP::Get.new(url) File.open(localfile, "wb+") do |f| conn.request(req) do |res| res.read_body |data| f.write(data) end end end
1.1.2 Stream Put
相对于Get来说,Put不需要用户自己去chunk by chunk读文件,因为基础类库都已经封装好了,只需要告诉类库你想普通的Put还是流式的Put,我们只需要这样写:
conn = Net::HTTP.new(host, port) req = Net::HTTP::Put.new(url) # 关键在于Put body的处理 # 普通Put # req.body = File.read(localfile)
# 流式Put # req.body_stream = File.open(localfile) req.body_stream = File.open(localfile) res = conn.request(req)
实际使用当中,当然不仅仅局限于文件,对于类文件(file like object)如StringIO都可以,因此我们写类库的时候,考虑应该更加全面。下面让我们看看‘net/http.rb‘是如何实现的:
1523 def exec(sock, ver, path) #:nodoc: internal use only 1524 if @body 1525 send_request_with_body sock, ver, path, @body 1526 elsif @body_stream 1527 send_request_with_body_stream sock, ver, path, @body_stream 1528 else 1529 write_header sock, ver, path 1530 end 1531 end 1535 def send_request_with_body(sock, ver, path, body) 1536 self.content_length = body.length 1537 delete ‘Transfer-Encoding‘ 1538 supply_default_content_type 1539 write_header sock, ver, path 1540 sock.write body 1541 end 1543 def send_request_with_body_stream(sock, ver, path, f) 1544 unless content_length() or chunked? 1545 raise ArgumentError, 1546 "Content-Length not given and Transfer-Encoding is not `chunked ‘" 1547 end 1548 supply_default_content_type 1549 write_header sock, ver, path 1550 if chunked? 1551 while s = f.read(1024) 1552 sock.write(sprintf("%x\r\n", s.length) << s << "\r\n") 1553 end 1554 sock.write "0\r\n\r\n" 1555 else 1556 while s = f.read(1024) 1557 sock.write s 1558 end 1559 end 1560 end
对于ruby 1.8的实现,从核心函数exec可以看出就是由body和body_stream来选择是否流式Put,而流式Put的实现,无非就是block by block读和写,每次处理1024字节(python 2.7 httplib.py的实现里,每次处理8192字节)。个人觉得,对于这个block大小,如果能以参数的形式提供给用户配置,根据实际的硬件和软件环境(比如socket write buffer),给出更加合理时间和空间上的优化。在后面实验部分,会在时间上和空间上做对比,本文主要专注在空间的优化分析。
下面再让我们看看python 2.7里这块的实现:
787 def send(self, data): 788 """send `data` to the server.""" ... ... 797 blocksize = 8192 798 if hasattr(data, ‘read‘) and isinstance(data, array): 799 if self.debuglevel > 0: print "sendIng a read()able" 800 datablock = data.read(blocksize) 801 while datablock: 802 self.sock.sendall(datablock) 803 datablock = data.read(blocksize) 804 else: 805 self.sock.sendall(data)
对比python和ruby的实现部分,不同点体现在:
- 接口参数:ruby中body和body_stream两个参数,python中data一个参数。相对来说python更加简洁。
- 数据上传方式:相对于python,ruby中增加了对"Transfer-Encoding: chunked"的支持(不是所有的web server都支持这种方式)。
其实不论是Python,还是ruby,或者其他语言对这一块的实现,原理都一样,实现部分大同小异。
1.2 实验对比
将流模式应用于现有的测试框架后,分别Put和Get一个500M的文件。实验过程中,观察内存变化情况,然后记录下整个过程中内存消耗峰值。结果数据表明,采用流模式后,Put和Get过程中消耗的内存明显降低,所消耗时间增加,尤其是Put。这里实验数据比较粗略,仅仅想总体感官上来看下内存消耗变化。其实影响内存和时间的因素很多,比如web服务器的性能,client端机器的配置,连接过程是否采用keep-alive等。
- 普通模式Put和普通模式Get
【Time cost】:(sec)
Put Get
36.623502 34.996696
【memory cost】:(free -m)
total used free shared buffers cached
Mem: 8010 5658 2351 0 682 1484
-/+ buffers/cache: 3492 4518
- 流模式Put和流模式Get
【Time cost】:(sec)
Put Get
74.852179 42.071823
【memory cost】:(free -m)
total used free shared buffers cached
Mem: 8010 3801 4209 0 680 1984
-/+ buffers/cache: 1137 6873
总结
不管哪种编程语言,几乎都提供对HTTP body stream的很好封装,因此平常我们写程序,不需要了解太多的细节,只需要简单调用即可。本文主要就基础类库的实现部分的某些片段,从内存的角度作一定的理解和分析,难免有理解错误和不到位之处,欢迎纠正。
郑重声明:本站内容如果来自互联网及其他传播媒体,其版权均属原媒体及文章作者所有。转载目的在于传递更多信息及用于网络分享,并不代表本站赞同其观点和对其真实性负责,也不构成任何其他建议。