HTTP stream PUT and GET analysis

前言

目前正在從事雲端存儲和備份方面的工做,主要負責測試框架的開發和優化。軟件技術人員對"stream"(流)這個詞應該並不陌生,不少場景下,"stream"更是表明着性能上的優化。在web服務的開發應用中,HTTP body stream更是家喻戶曉。各類開發語言幾乎都提供有對HTTP實現的封裝來實現對遠端web服務的交互,某些高級類庫更是提供了給開發人員方便使用的request stream和response stream的接口,只須要簡單調用便可。html

近期天天晚上自動的迴歸測試不是很穩定,常常有莫名中斷地狀況,主要發生在與雲端服務進行大文件讀寫的時候。在對測試框架文件讀寫的部分的分析後發現,調用雲端服務讀寫文件的時候,會將整個body讀到內存。這種方式對小文件的處理,問題不是很大。但對於大文件,問題就出來了,會形成內存使用過大甚至溢出。因而對HTTP讀寫部分以流的方式進行優化,事實證實,對於機器硬件(尤爲是內存)不是很高的狀況下,如何下降使用內存仍是頗有必要的。python

簡單來講,HTTP body的流式讀寫不須要將整個文件讀到內存,而是讀一部分處理一部分,所以能有有效的下降內存的消耗。本文主要結合項目中遇到的問題,而後深刻到http相關類庫對body stream的實現(以ruby1.8和python2.7爲例)作一個簡短的分析。web

1 HTTP stream PUT and GET

1.1 Ruby中HTTP stream的實現

HTTP基礎類庫的實現和封裝一般會提供如下三個接口,獲取遠端資源的流程以下:編程

創建Connection => 發送Request(PUT, GET, POST, ...) => 獲取Responseruby

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進行讀取:less

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方法,咱們即可以很容易的實現流式的寫文件:python2.7

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,咱們只須要這樣寫:socket

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的很好封裝,所以日常咱們寫程序,不須要了解太多的細節,只須要簡單調用便可。本文主要就基礎類庫的實現部分的某些片斷,從內存的角度做必定的理解和分析,不免有理解錯誤和不到位之處,歡迎糾正。

相關文章
相關標籤/搜索