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的很好封装,因此平常我们写程序,不需要了解太多的细节,只需要简单调用即可。本文主要就基础类库的实现部分的某些片段,从内存的角度作一定的理解和分析,难免有理解错误和不到位之处,欢迎纠正。

郑重声明:本站内容如果来自互联网及其他传播媒体,其版权均属原媒体及文章作者所有。转载目的在于传递更多信息及用于网络分享,并不代表本站赞同其观点和对其真实性负责,也不构成任何其他建议。