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的很好封裝,因此平常我們寫程序,不需要了解太多的細節,只需要簡單調用即可。本文主要就基礎類庫的實現部分的某些片段,從內存的角度作一定的理解和分析,難免有理解錯誤和不到位之處,歡迎糾正。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM