前言
目前正在從事雲端存儲和備份方面的工作,主要負責測試框架的開發和優化。軟件技術人員對"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的很好封裝,因此平常我們寫程序,不需要了解太多的細節,只需要簡單調用即可。本文主要就基礎類庫的實現部分的某些片段,從內存的角度作一定的理解和分析,難免有理解錯誤和不到位之處,歡迎糾正。