2008-09-11 119 views
36

我正在構建一個需要擴展的java服務器。其中一個servlet將提供存儲在Amazon S3中的圖像。在java servlet中流式傳輸大文件

最近在加載的時候,我的虛擬機內存耗盡,當時我添加了代碼來提供圖像,所以我非常肯定,流式傳輸更大的servlet響應會導致我的麻煩。

我的問題是:在從數據庫或其他雲存儲中讀取數據時,如何編寫java servlet以將大型(> 200k)響應流式傳輸回瀏覽器?

我曾考慮將文件寫入本地臨時驅動器,然後產生另一個線程來處理流,以便tomcat servlet線程可以重新使用。這看起來好像會很重。

任何想法將不勝感激。謝謝。

回答

47

如果可能,您不應該將要提供的文件的全部內容存儲在內存中。相反,獲取數據的InputStream,並將數據分段複製到Servlet OutputStream。例如:

ServletOutputStream out = response.getOutputStream(); 
InputStream in = [ code to get source input stream ]; 
String mimeType = [ code to get mimetype of data to be served ]; 
byte[] bytes = new byte[FILEBUFFERSIZE]; 
int bytesRead; 

response.setContentType(mimeType); 

while ((bytesRead = in.read(bytes)) != -1) { 
    out.write(bytes, 0, bytesRead); 
} 

// do the following in a finally block: 
in.close(); 
out.close(); 

我同意toby,你應該改爲「將它們指向S3 url」。

至於OOM異常,你確定它與提供圖像數據有關嗎?假設您的JVM有256MB的「額外」內存用於提供圖像數據。在Google的幫助下,「256MB/200KB」= 1310.對於2GB「額外」內存(這些日子裏非常合理的數量),可以支持超過10,000個併發客戶端。即便如此,1300個併發客戶端也是相當大的一部分。這是你經歷的負荷類型嗎?如果沒有,您可能需要在別處尋找OOM異常的原因。

編輯 - 關於:

在這種情況下,使用圖像可以包含敏感數據...

當我通過S3文檔幾個星期前看,我注意到,你可以生成可以連接到S3 URL的超時密鑰。所以,你不必在S3上向公衆開放這些文件。我對技術的理解是:

  1. 初始HTML頁面有下載鏈接到你的web應用下載鏈接
  2. 你的web應用程序生成包括到期的一個關鍵的S3 URL上
  3. 用戶點擊,可以說, 5分鐘。
  4. 使用步驟3中的URL發送HTTP重定向到客戶端。
  5. 用戶從S3下載文件。即使下載時間超過5分鐘,這也可以工作 - 一旦下載開始,它可以繼續完成。
+0

嗯,由於沒有設置內容長度,所以servlet容器必須進行緩衝,因爲在流式傳輸任何數據之前需要設置內容長度頭。所以不知道你節省了多少內存? – 2012-02-01 15:54:36

+1

彼得,如果你不能直接指向用戶的雲服務URL,並且你想設置內容長度頭,並且你不知道大小,並且你不能查詢雲服務的大小,那麼我猜你的最好的辦法是首先流到服務器上的臨時文件。當然,在將第一個字節發送到客戶端之前,在服務器上保存副本可能會導致用戶認爲請求失敗,具體取決於雲 - >服務器傳輸需要多長時間。 – 2012-02-13 03:55:37

17

爲什麼你不把他們指向S3網址?從S3中獲取一個工件,然後通過自己的服務器將其流式傳輸給我,從而擊敗了使用S3的目的,即S3將帶寬和處理服務的圖像分流到Amazon。

0

你必須檢查兩件事情:

  • 你關閉流?非常重要的
  • 也許你正在給流連接「免費」。這個流不是很大,但同時有許多流可以竊取你所有的記憶。創建一個池,以便您不能同時運行一定數量的流
1

託比是正確的,如果可以的話,您應該直接指向S3。如果你不能,這個問題有點含糊地給出一個準確的迴應: 你的java堆有多大?當內存不足時,有多少個數據流同時打開?
你的讀寫/緩衝有多大(8K是好的)?
您正在從流中讀取8K,然後將8k寫入輸出,對不對?你不是試圖從S3讀取整個圖像,將它緩存在內存中,然後一次發送整個圖像?

如果使用8K的緩衝區,可以有1000個併發流中的堆空間〜8Megs去,所以你肯定做錯了什麼....

順便說一句,我沒有挑8K憑空,它是套接字緩衝區的默認大小,發送更多的數據,比如說1Meg,並且你將在擁有大量內存的tcp/ip堆棧上阻塞。

0

除了John建議的內容之外,還應該重複刷新輸出流。根據您的Web容器,可能會緩存部分甚至全部輸出並一次刷新它(例如,計算Content-Length標頭)。這會消耗相當多的記憶。

2

我非常同意toby和John Vasileff - 如果您可以容忍相關問題,S3非常適合卸載大型媒體對象。 (自己的應用程序的一個實例可以爲10-1000MB FLV和MP4執行此操作。)例如:沒有部分請求(字節範圍標題)。人們必須處理'手動',偶爾停機等。

如果這不是一個選項,約翰的代碼看起來不錯。我發現2k FILEBUFFERSIZE的字節緩衝區在microbench標記中效率最高。另一個選項可能是共享的FileChannel。 (FileChannels是線程安全的。)

也就是說,我還補充說,猜測導致內存不足錯誤的原因是經典優化錯誤。您可以通過嚴格的指標來提高成功的機會。

  1. 將-XX:+ HeapDumpOnOutOfMemoryError到您JVM啓動參數,以防萬一
  2. 荷載作用下運行的JVM( JMAP -histo <PID>
  3. 採用開放JMAP
  4. Analyize指標( jmap -histo out,或者讓jhat看看你的堆轉儲)。很可能你的內存不足來自意想不到的地方。

當然還有其他的工具在那裏,但JMAP &來與jHat與Java 5+「開箱即用」

我已經考慮文件寫入到本地臨時驅動器,然後產生另一個線程來處理流,以便tomcat servlet線程可以被重用。這看起來好像會很重。

啊,我認爲你不能這樣做。即使你可以,這聽起來很可疑。正在管理連接的tomcat線程需要控制。如果遇到線程匱乏,則增加./conf/server.xml中可用線程的數量。同樣,指標是檢測這種情況的方式 - 不要猜測。

問題:您是否也在EC2上運行?什麼是你的Tomcat的JVM啓動參數?

0

如果是這樣的靜態文件是獨立的,在自己的桶,你可以組織你的文件,今天最快的性能可能可以使用Amazon S3的CDN,CloudFront實現。

10

我見過很多像john-vasilef(當前接受的)答案的代碼,一個緊湊的while循環從一個流中讀取塊並將它們寫入其他流。

我所做的論點是反對不必要的代碼重複,轉而使用Apache的IOUtils。如果您已經在其他地方使用它,或者您正在使用的其他庫或框架已經在使用它,那麼這是一條已知並經過良好測試的單一線條。

在下面的代碼中,我將一個對象從Amazon S3流式傳輸到servlet中的客戶端。

import java.io.InputStream; 
import java.io.OutputStream; 
import org.apache.commons.io.IOUtils; 

InputStream in = null; 
OutputStream out = null; 

try { 
    in = object.getObjectContent(); 
    out = response.getOutputStream(); 
    IOUtils.copy(in, out); 
} finally { 
    IOUtils.closeQuietly(in); 
    IOUtils.closeQuietly(out); 
} 

具有適當流關閉的6行定義良好的模式似乎非常穩固。