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

  • 相关阅读:
    Atitit.随时间变色特效 ---包厢管理系统的规划
    Atitit.request http乱码的设计防止 检测与解决最近实践p825 attilax总结.doc
    Atitit.request http乱码的设计防止 检测与解决最近实践p825 attilax总结.doc
    atitit.薄伽梵歌overview  attilax 读后感
    Atitit。 《吠陀》 《梨俱吠陀》overview 经读后感  是印度上古时期一些文献的总称
    Atitit。 《吠陀》 《梨俱吠陀》overview 经读后感  是印度上古时期一些文献的总称
    atitit.薄伽梵歌overview  attilax 读后感
    Atitit 《摩奴法典》overivew 读后感 不是由国王 颁布的,而是 僧侣编制
    Atitit 《摩奴法典》overivew 读后感 不是由国王 颁布的,而是 僧侣编制
    Atitit.执行cli cmd的原理与调试
  • 原文地址:https://www.cnblogs.com/hjh188/p/4321336.html
Copyright © 2011-2022 走看看