背景
最近在做的一個(gè)項目,兩個(gè)java進(jìn)程之間會(huì )涉及一個(gè)大數據量的傳遞過(guò)程,基本都是圖片文件,(做了壓縮后還是會(huì )比較大,最大的有超過(guò)600MB)。其次這兩個(gè)java進(jìn)程是在跨機房,比如中國和美國機房,網(wǎng)絡(luò )待框也就幾百kB。
這就是本文的項目背景
分析
1. 600MB的文件,都是A進(jìn)程運行時(shí)根據需要生成的(下載需要的圖片文件)。所以無(wú)法預先處理,而且公司總圖片文件都是以TB計算,所以全量同步的方案也不靠譜
2. 從A進(jìn)程到B進(jìn)程的數據傳遞,首先想到用socket進(jìn)行傳遞,但單socket的數據同步無(wú)法滿(mǎn)足需求,多線(xiàn)程數據傳遞會(huì )涉及數據的切片和數據的合并等,代碼相對會(huì )比較復雜
老的項目實(shí)現:
- A先臨時(shí)保存文件到一指定目錄
- A進(jìn)程機器上啟動(dòng)一個(gè)http服務(wù)(比如nginx,lighttpd)
- B進(jìn)程外部調用一個(gè)多線(xiàn)程下載客戶(hù)端,下載數據到一臨時(shí)目錄
- B進(jìn)程等下載完成后,再操縱臨時(shí)目錄的數據
項目的工程代碼都是以java,引入了多線(xiàn)程服務(wù)和下載客戶(hù)端之后,增加了項目的部署和維護成本。所以在項目重構時(shí),想的一個(gè)辦法是用嵌入式的jetty,去替換nginx提供http服務(wù)。
過(guò)程
涉及到多線(xiàn)程下載和斷點(diǎn)續載,首先得了解一個(gè)Http協(xié)議的內容。
Byte Ranges: 文檔http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.35.1
大致的內容,可以在Http的Header頭中進(jìn)行添加,可以指定文件下載byte的start和end的位置,幾個(gè)例子:
- bytes=100-499
- bytes=-300
- bytes=100-
- bytes=1-2,2-3,6-,-2
最后,通過(guò)tcpdump進(jìn)行數據抓包分析,多線(xiàn)程下載客戶(hù)端的Http協(xié)議的內容?;镜乃悸肥歉鶕€(xiàn)程數,計算出每個(gè)線(xiàn)程的bytes-range,然后每個(gè)線(xiàn)程發(fā)起一個(gè)獨立的請求。后端每個(gè)處理線(xiàn)程單獨處理bytes-range的數據下載。
至于斷點(diǎn)續傳,其實(shí)也相對比較簡(jiǎn)單了。就是記錄好分出去的每個(gè)bytes-range成功與否,失敗的重新再做,已經(jīng)做完的可以直接跳過(guò)。
抓取的Http協(xié)議內容:
- GET /source.tar.gz HTTP/1.1
- User-Agent: aria2/1.13.0
- Accept: */*
- Host: 10.20.156.49:8080
- Pragma: no-cache
- Cache-Control: no-cache
- Range: bytes=258998272-387973119
java版的多線(xiàn)程下載支持
一提到做java版的多線(xiàn)程下載,首先就會(huì )想到j(luò )etty和tomcat。tomcat只能以外部server的方式進(jìn)行啟動(dòng),和nginx沒(méi)有太多的區別。
最后我選擇了jetty,并在項目中做為嵌入式進(jìn)行啟動(dòng),提供http多線(xiàn)程下載服務(wù)。
按照前面的分析,要做多線(xiàn)程下載,無(wú)非就是要實(shí)現一個(gè)bytes-range的處理。還好我用的jetty版本(7.0.1)已經(jīng)解析了bytes-range,具體解析類(lèi):InclusiveByteRange
再仔細翻了下它的代碼,發(fā)現jetty已經(jīng)默認提供了一個(gè)servlet支持多線(xiàn)程下載,就是DefaultServlet,甚喜。
最后,按照我項目的需求適當的裁剪了一些代碼,最后完成了:DownloadServlet,具體代碼見(jiàn)附件。
類(lèi)中使用了java版的sendfile,推薦看一下:http://stackoverflow.com/questions/1605332/java-nio-filechannel-versus-fileoutputstream-performance-usefulness
將jetty引入做為嵌入式啟動(dòng)的步驟
1. 引入相關(guān)的jar
- <dependency>
- <groupId>com.alibaba.external</groupId>
- <artifactId>server.jetty.jetty-servlet</artifactId>
- <version>${jetty_verion}</version>
- </dependency>
- <dependency>
- <groupId>com.alibaba.external</groupId>
- <artifactId>server.jetty.jetty-xml</artifactId>
- <version>${jetty_verion}</version>
- </dependency>
- <dependency>
- <groupId>com.alibaba.external</groupId>
- <artifactId>server.jetty.jetty-server</artifactId>
- <version>${jetty_verion}</version>
- </dependency>
2. 配置jetty.xml (我選擇了xml的配置方式,但沒(méi)有使用war包,我只需要一個(gè)Http服務(wù)功能即可)
- <Configure id="Server" class="org.eclipse.jetty.server.Server">
-
- <!-- =========================================================== -->
- <!-- Server Thread Pool -->
- <!-- =========================================================== -->
- <Set name="ThreadPool">
- <!-- Default queued blocking threadpool -->
- <New class="org.eclipse.jetty.util.thread.QueuedThreadPool">
- <Set name="minThreads">10</Set>
- <Set name="maxThreads">250</Set>
- </New>
- </Set>
-
- <!-- =========================================================== -->
- <!-- Set connectors -->
- <!-- =========================================================== -->
- <!-- -->
- <Call name="addConnector">
- <Arg>
- <New class="org.eclipse.jetty.server.bio.SocketConnector">
- <Set name="port"><Property name="jetty.bio.port" default="8080"/></Set>
- <Set name="forwarded">true</Set>
- <Set name="forwardedHostHeader">ignore</Set>
- <Set name="forwardedServerHeader">ignore</Set>
- <Set name="acceptQueueSize">256</Set>
- <Set name="statsOn">false</Set>
- <Set name="maxIdleTime">600000</Set>
- <Set name="lowResourcesMaxIdleTime">5000</Set>
- <Set name="requestHeaderSize">8192</Set>
- <Set name="responseHeaderSize">8192</Set>
- </New>
- </Arg>
- </Call>
- <!--
- <Call name="addConnector">
- <Arg>
- <New class="org.eclipse.jetty.server.nio.SelectChannelConnector">
- <Set name="host"><Property name="jetty.host" /></Set>
- <Set name="port"><Property name="jetty.port" default="8080"/></Set>
- <Set name="forwarded">true</Set>
- <Set name="forwardedHostHeader">ignore</Set>
- <Set name="forwardedServerHeader">ignore</Set>
- <Set name="maxIdleTime">600000</Set>
- <Set name="Acceptors">2</Set>
- <Set name="acceptQueueSize">256</Set>
- <Set name="statsOn">false</Set>
- <Set name="confidentialPort">8443</Set>
- <Set name="lowResourcesConnections">2000</Set>
- <Set name="lowResourcesMaxIdleTime">5000</Set>
- <Set name="requestHeaderSize">8192</Set>
- <Set name="responseHeaderSize">8192</Set>
- </New>
- </Arg>
- </Call>
- -->
- <!-- =========================================================== -->
- <!-- Set handler Collection Structure -->
- <!-- =========================================================== -->
- <Set name="handler">
- <New id="Handlers" class="org.eclipse.jetty.server.handler.HandlerCollection">
- <Set name="handlers">
- <Array type="org.eclipse.jetty.server.Handler">
- <Item>
- <New id="ServletHandler" class="org.eclipse.jetty.servlet.ServletContextHandler">
- <Set name="contextPath">/</Set>
- <Call name="addServlet">
- <Arg>com.alibaba.otter.task.biz.common.jetty.DownloadServlet</Arg>
- <Arg>/*</Arg>
- </Call>
- <Get name="initParams">
- <Put name="org.eclipse.jetty.servlet.Default.resourceBase">/tmp/</Put>
- <Put name="org.eclipse.jetty.servlet.Default.gzip">false</Put>
- </Get>
- </New>
- </Item>
- </Array>
- </Set>
- </New>
- </Set>
-
- <!-- =========================================================== -->
- <!-- extra options -->
- <!-- =========================================================== -->
- <Set name="stopAtShutdown">true</Set>
- <Set name="sendServerVersion">false</Set>
- <Set name="sendDateHeader">true</Set>
- <Set name="gracefulShutdown">1000</Set>
- </Configure>
說(shuō)明: 主要的配置見(jiàn)handler,配置了對應的DownloadServlet
3. 啟動(dòng)入口 (使用了xml配置后就灰常的簡(jiǎn)潔了)
- Resource jetty_xml = Resource.newSystemResource("jetty/jetty.xml");
- XmlConfiguration configuration = new XmlConfiguration(jetty_xml.getInputStream());
- Server server = (Server) configuration.configure();
- server.start();
測試
最后選擇了幾個(gè)多線(xiàn)程下載的客戶(hù)端進(jìn)行了測試,我這里選擇了aria2c(http://aria2.sourceforge.net/) 和 axel(http://www.axel.com/uk2/)
aria2c測試
參數:
- --no-conf -x 10 -s 10 -j 10 --timeout=600 --max-tries=5 --stop=1800 --allow-overwrite=true --enable-http-keep-alive=true --log-level=warn
下載1.1GB的文件:
apache : 28s
nginx : 27s
jetty : 27s
axel測試
參數:
- -n 10 -a -v <span style="white-space: normal;"> </span>
下載1.1GB的文件:
apache : 87s
nginx : 87s
jetty : 88s
總結
并沒(méi)有做非常詳盡的性能測試,不過(guò)從幾次跑的結果來(lái)看,基本上也有數了。
- jetty實(shí)現的servlet性能基本和nginx,apache下載接近。而且測試過(guò)程中瓶頸已經(jīng)不在應用本身,基本都在網(wǎng)絡(luò )帶寬上了,我是百MB網(wǎng)卡,基本可以滿(mǎn)負荷運轉。
- jetty的nio和bio版本,nio在context switch切換上會(huì )相對比較多(因為有大量的READ/WRITE事件響應,線(xiàn)程切換反而不如bio來(lái)得少),建議部署bio模式
- 多線(xiàn)程下載aria2c工具的確不錯,推薦使用
后續,會(huì )嘗試使用java寫(xiě)一個(gè)多線(xiàn)程的客戶(hù)端,如果性能還ok的話(huà),可以直接替換aria2c,到時(shí)候就是一些jar包,沒(méi)有了外部軟件的依賴(lài),部署和維護也會(huì )相對比較簡(jiǎn)單。