目前正在從事雲端存儲和備份方面的工做,主要負責測試框架的開發和優化。軟件技術人員對"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
HTTP基礎類庫的實現和封裝一般會提供如下三個接口,獲取遠端資源的流程以下:編程
創建Connection => 發送Request(PUT, GET, POST, ...) => 獲取Responseruby
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
相對於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的實現部分,不一樣點體如今:
其實不管是Python,仍是ruby,或者其餘語言對這一塊的實現,原理都同樣,實現部分大同小異。
將流模式應用於現有的測試框架後,分別Put和Get一個500M的文件。實驗過程當中,觀察內存變化狀況,而後記錄下整個過程當中內存消耗峯值。結果數據代表,採用流模式後,Put和Get過程當中消耗的內存明顯下降,所消耗時間增長,尤爲是Put。這裏實驗數據比較粗略,僅僅想整體感官上來看下內存消耗變化。其實影響內存和時間的因素不少,好比web服務器的性能,client端機器的配置,鏈接過程是否採用keep-alive等。
【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
【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的很好封裝,所以日常咱們寫程序,不須要了解太多的細節,只須要簡單調用便可。本文主要就基礎類庫的實現部分的某些片斷,從內存的角度做必定的理解和分析,不免有理解錯誤和不到位之處,歡迎糾正。