http://www.infoq.com/cn/articles/hadoop-process-develop
http://www.infoq.com/cn/articles/hadoop-process-develop
引
最近關注Hadoop,因此也順便關注了一下Hadoop相關的項目。HBASE就是基于Hadoop的一個開源項目,也是對Google的BigTable的一種實現。
BigTable是什么?Google的Paper對其作了充分的說明。字面上看就是一張大表,其實和我們想象的傳統數據庫的表還是有些差別的。松散數據可以說是介于Map Entry(key & value)和DB Row之間的一種數據。在我使用Memcache的時候,有時候的需求是需要存儲的不僅僅是簡單的一個key對應一個value,可能我需要類似于數據庫表結構中多屬性的存儲,但是又不會有傳統數據庫表結構中那么多關聯關系的需求,其實這類數據就是所謂的松散數據。BigTable最淺顯來看就是一張很大的表,表的屬性可以根據需求去動態增加,但是又沒有表與表之間關聯查詢的需求。
互聯網應用有一個最大的特點,就是速度,功能再強大,速度慢,還是會被舍棄。因此在大訪問量的網站都采取前后的緩存來提升性能和響應時間。對于Map Entry類型的數據,集中式分布式Cache都有很多選擇,對于傳統的關系型數據,從MySQL到Oracle都給了很好的支持,唯有松散數據這類數據,采用前后兩種解決方案都不能最大化它的處理能力。因此BigTable才有了它用武之地。
HBASE作為Apache的開源項目,也是出于起步階段,因為其實它所依賴的Hadoop也不能說已經到了成熟階段,所以都有很大的發展空間,這也為我們這些開源愛好者提供了更多空間去貢獻。這里主要會談到HBASE的框架設計方面的知識和它的一些特點,不論是否采用HBASE去解決工作中的問題,一種好的流程設計總會給開發者和架構設計者帶來一些思想上的火花。
HBASE設計介紹
數據模型
HBASE中的每一張表,就是所謂的BigTable。BigTable會存儲一系列的行記錄,行記錄有三個基本類型的定義:Row Key,Time Stamp,Column。Row Key是行在BigTable中的唯一標識,Time Stamp是每次數據操作對應關聯的時間戳,可以看作類似于SVN的版本,Column定義為:<family>:<label>,通過這兩部分可以唯一的指定一個數據的存儲列,family的定義和修改需要對HBASE作類似于DB的DDL操作,而對于label的使用,則不需要定義直接可以使用,這也為動態定制列提供了一種手段。family另一個作用其實在于物理存儲優化讀寫操作,同family的數據物理上保存的會比較臨近,因此在業務設計的過程中可以利用這個特性。
看一下邏輯數據模型:
Row Key |
Time Stamp |
Column "contents:" |
Column "anchor:" |
Column "mime:" |
|
"com.cnn.www" |
t9 |
"anchor:cnnsi.com" |
"CNN" |
||
t8 |
"anchor:my.look.ca" |
"CNN.com" |
|||
t6 |
"<html>..." |
"text/html" |
|||
t5 |
"<html>..." |
||||
t3 |
"<html>..." |
上表中有一列,列的唯一標識為com.cnn.www,每一次邏輯修改都有一個timestamp關聯對應,一共有四個列定義:<contents:>,<anchor:cnnsi.com>,<anchor:my.look.ca>,<mime:>。如果用傳統的概念來將BigTable作解釋,那么BigTable可以看作一個DB Schema,每一個Row就是一個表,Row key就是表名,這個表根據列的不同可以劃分為多個版本,同時每個版本的操作都會有時間戳關聯到操作的行。
再看一下HBASE的物理數據模型:
Row Key |
Time Stamp |
Column "contents:" |
"com.cnn.www" |
t6 |
"<html>..." |
t5 |
"<html>..." |
|
t3 |
"<html>..." |
Row Key |
Time Stamp |
Column "anchor:" |
|
"com.cnn.www" |
t9 |
"anchor:cnnsi.com" |
"CNN" |
t8 |
"anchor:my.look.ca" |
"CNN.com" |
Row Key |
Time Stamp |
Column "mime:" |
"com.cnn.www" |
t6 |
"text/html" |
物理數據模型其實就是將邏輯模型中的一個Row分割成為根據Column family存儲的物理模型。
對于BigTable的數據模型操作的時候,會鎖定Row,并保證Row的原子操作。
框架結構及流程
圖1 框架結構圖
HBASE依托于Hadoop的HDFS作為存儲基礎,因此結構也很類似于Hadoop的Master-Slave模式,Hbase Master Server 負責管理所有的HRegion Server,但Hbase Master Server本身并不存儲HBASE中的任何數據。HBASE邏輯上的Table被定義成為一個Region存儲在某一臺HRegion Server上,HRegion Server 與Region的對應關系是一對多的關系。每一個HRegion在物理上會被分為三個部分:Hmemcache、Hlog、HStore,分別代表了緩存,日志,持久層。通過一次更新流程來看一下這三部分的作用:
圖2 提交更新以及刷新Cache流程
由流程可以看出,提交更新操作將會寫入到兩部分實體中,HMemcache和Hlog中,HMemcache就是為了提高效率在內存中建立緩存,保證了部分最近操作過的數據能夠快速的被讀取和修改,Hlog是作為同步Hmemcache和Hstore的事務日志,在HRegion Server周期性的發起Flush Cache命令的時候,就會將Hmemcache中的數據持久化到Hstore中,同時會清空Hmemecache中的數據,這里采用的是比較簡單的策略來做數據緩存和同步,復雜一些其實可以參照java的垃圾收集機制來做。
在讀取Region信息的時候,優先讀取HMemcache中的內容,如果未取到再去讀取Hstore中的數據。
幾個細節:
1. 由于每一次Flash Cache,就會產生一個Hstore File,在Hstore中存儲的文件會越來越多,對性能也會產生一定影響,因此達到設置文件數量閥值的時候就會Merge這些文件為一個大文件。
2. Cache大小的設置以及flush的時間間隔設置需要考慮內存消耗以及對性能的影響。
3. HRegion Server每次重新啟動的時候會將Hlog中沒有被Flush到Hstore中的數據再次載入到Hmemcache,因此Hmemcache過大對于啟動的速度也有直接影響。
4. Hstore File中存儲數據采用B-tree的算法,因此也支持了前面提到對于Column同Family數據操作的快速定位獲取。
5. HRegion可以Merge也可以被Split,根據HRegion的大小決定。不過在做這些操作的時候HRegion都會被鎖定不可使用。
6. Hbase Master Server通過Meta-info Table來獲取HRegion Server的信息以及Region的信息,Meta最頂部的一個Region是虛擬的一個叫做Root Region,通過Root Region可以找到下面各個實際的Region。
7. 客戶端通過Hbase Master Server獲得了Region所在的Region Server,然后就直接和Region Server進行交互,而對于Region Server相互之間不通信,只和Hbase Master Server交互,受到Master Server的監控和管理。
后話
對HBase還沒有怎么使用,僅僅只是看了wiki去了解了一下結構和作用,暫時還沒有需要使用的場景,不過對于各種開源項目的設計有所了解,對自己的框架結構設計也會有很多幫助,因此分享一下。
這部分內容是分布式計算開源框架Hadoop入門實踐的第二部分,講述了關于實際使用配置的內容.第三部分是對于集群配置的測試結果分析的部分,下周三應該會在InfoQ刊登.
http://www.infoq.com/cn/articles/hadoop-config-tip
第一部分已經在InfoQ.cn上刊登了第一部分
SIP的第四期結束了,因為控制策略的豐富,早先的的壓力測試結果已經無法反映在高并發和高壓力下SIP的運行狀況,因此需要重新作壓力測試。跟在測試人員后面做了快一周的壓力測試,壓力測試的報告也正式出爐,本來也就算是告一段落,但第二天測試人員說要修改報告,由于這次作壓力測試的同學是第一次作,有一個指標沒有注意,因此需要修改幾個測試結果。那個沒有注意的指標就是load average,他和我一樣開始只是注意了CPU,內存的使用狀況,而沒有太注意這個指標,這個指標與他們通常的限制(10左右)有差別。重新測試的結果由于這個指標被要求壓低,最后的報告顯然不如原來的好看。自己也沒有深入過壓力測試,但是覺得不搞明白對將來機器配置和擴容都會有影響,因此去問了DBA和SA,得到的結果相差很大,看來不得不自己去找找問題的根本所在了。
通過下面的幾個部分的了解,可以一步一步的找出Load Average在壓力測試中真正的作用。
CPU時間片
為了提高程序執行效率,大家在很多應用中都采用了多線程模式,這樣可以將原來的序列化執行變為并行執行,任務的分解以及并行執行能夠極大地提高程序的運行效率。但這都是代碼級別的表現,而硬件是如何支持的呢?那就要靠CPU的時間片模式來說明這一切。程序的任何指令的執行往往都會要競爭CPU這個最寶貴的資源,不論你的程序分成了多少個線程去執行不同的任務,他們都必須排隊等待獲取這個資源來計算和處理命令。先看看單CPU的情況。下面兩圖描述了時間片模式和非時間片模式下的線程執行的情況:
圖 1 非時間片線程執行情況
圖 2 非時間片線程執行情況
在圖一中可以看到,任何線程如果都排隊等待CPU資源的獲取,那么所謂的多線程就沒有任何實際意義。圖二中的CPU Manager只是我虛擬的一個角色,由它來分配和管理CPU的使用狀況,此時多線程將會在運行過程中都有機會得到CPU資源,也真正實現了在單CPU的情況下實現多線程并行處理。
多CPU的情況只是單CPU的擴展,當所有的CPU都滿負荷運作的時候,就會對每一個CPU采用時間片的方式來提高效率。
在Linux的內核處理過程中,每一個進程默認會有一個固定的時間片來執行命令(默認為1/100秒),這段時間內進程被分配到CPU,然后獨占使用。如果使用完,同時未到時間片的規定時間,那么就主動放棄CPU的占用,如果到時間片尚未完成工作,那么CPU的使用權也會被收回,進程將會被中斷掛起等待下一個時間片。
CPU利用率和Load Average的區別
壓力測試不僅需要對業務場景的并發用戶等壓力參數作模擬,同時也需要在壓力測試過程中隨時關注機器的性能情況,來確保壓力測試的有效性。當服務器長期處于一種超負荷的情況下運行,所能接收的壓力并不是我們所認為的可接受的壓力。就好比項目經理在給一個人估工作量的時候,每天都讓這個人工作12個小時,那么所制定的項目計劃就不是一個合理的計劃,那個人遲早會垮掉,而影響整體的項目進度。
CPU利用率在過去常常被我們這些外行認為是判斷機器是否已經到了滿負荷的一個標準,看到50%-60%的使用率就認為機器就已經壓到了臨界了。CPU利用率,顧名思義就是對于CPU的使用狀況,這是對一個時間段內CPU使用狀況的統計,通過這個指標可以看出在某一個時間段內CPU被占用的情況,如果被占用時間很高,那么就需要考慮CPU是否已經處于超負荷運作,長期超負荷運作對于機器本身來說是一種損害,因此必須將CPU的利用率控制在一定的比例下,以保證機器的正常運作。
Load Average是CPU的Load,它所包含的信息不是CPU的使用率狀況,而是在一段時間內CPU正在處理以及等待CPU處理的進程數之和的統計信息,也就是CPU使用隊列的長度的統計信息。為什么要統計這個信息,這個信息的對于壓力測試的影響究竟是怎么樣的,那就通過一個類比來解釋CPU利用率和Load Average的區別以及對于壓力測試的指導意義。
我們將CPU就類比為電話亭,每一個進程都是一個需要打電話的人。現在一共有4個電話亭(就好比我們的機器有4核),有10個人需要打電話。現在使用電話的規則是管理員會按照順序給每一個人輪流分配1分鐘的使用電話時間,如果使用者在1分鐘內使用完畢,那么可以立刻將電話使用權返還給管理員,如果到了1分鐘電話使用者還沒有使用完畢,那么需要重新排隊,等待再次分配使用。
圖 3 電話使用場景
上圖中對于使用電話的用戶又作了一次分類,1min的代表這些使用者占用電話時間小于等于1min,2min表示使用者占用電話時間小于等于2min,以此類推。根據電話使用規則,1min的用戶只需要得到一次分配即可完成通話,而其他兩類用戶需要排隊兩次到三次。
電話的利用率 = sum (active use cpu time)/period
每一個分配到電話的使用者使用電話時間的總和去除以統計的時間段。這里需要注意的是是使用電話的時間總和(sum(active use cpu time)),這與占用時間的總和(sum(occupy cpu time))是有區別的。(例如一個用戶得到了一分鐘的使用權,在10秒鐘內打了電話,然后去查詢號碼本花了20秒鐘,再用剩下的30秒打了另一個電話,那么占用了電話1分鐘,實際只是使用了40秒)
電話的Average Load體現的是在某一統計時間段內,所有使用電話的人加上等待電話分配的人一個平均統計。
電話利用率的統計能夠反映的是電話被使用的情況,當電話長期處于被使用而沒有的到足夠的時間休息間歇,那么對于電話硬件來說是一種超負荷的運作,需要調整使用頻度。而電話Average Load卻從另一個角度來展現對于電話使用狀態的描述,Average Load越高說明對于電話資源的競爭越激烈,電話資源比較短缺。對于資源的申請和維護其實也是需要很大的成本,所以在這種高Average Load的情況下電話資源的長期“熱競爭”也是對于硬件的一種損害。
低利用率的情況下是否會有高Load Average的情況產生呢?理解占有時間和使用時間就可以知道,當分配時間片以后,是否使用完全取決于使用者,因此完全可能出現低利用率高Load Average的情況。由此來看,僅僅從CPU的使用率來判斷CPU是否處于一種超負荷的工作狀態還是不夠的,必須結合Load Average來全局的看CPU的使用情況和申請情況。
所以回過頭來再看測試部對于Load Average的要求,在我們機器為8個CPU的情況下,控制在10 Load左右,也就是每一個CPU正在處理一個請求,同時還有2個在等待處理。看了看網上很多人的介紹一般來說Load簡單的計算就是2* CPU個數減去1-2左右(這個只是網上看來的,未必是一個標準)。
補充幾點:
1.對于CPU利用率和CPU Load Average的結果來判斷性能問題。首先低CPU利用率不表明CPU不是瓶頸,競爭CPU的隊列長期保持較長也是CPU超負荷的一種表現。對于應用來說可能會去花時間在I/O,Socket等方面,那么可以考慮是否后這些硬件的速度影響了整體的效率。
這里最好的樣板范例就是我在測試中發現的一個現象:SIP當前在處理過程中,為了提高處理效率,將控制策略以及計數信息都放置在Memcached Cache里面,當我將Memcached Cache配置擴容一倍以后,CPU的利用率以及Load都有所下降,其實也就是在處理任務的過程中,等待Socket的返回對于CPU的競爭也產生了影響。
2.未來多CPU編程的重要性。現在服務器的CPU都是多CPU了,我們的服務器處理能力已經不再按照摩爾定律來發展。就我上面提到的電話亭場景來看,對于三種不同時間需求的用戶來說,采用不同的分配順序,我們可看到的Load Average就會有不同。假設我們統計Load的時間段為2分鐘,如果將電話分配的順序按照:1min的用戶,2min的用戶,3min的用戶來分配,那么我們的Load Average將會最低,采用其他順序將會有不同的結果。所以未來的多CPU編程可以更好的提高CPU的利用率,讓程序跑的更快。
以上所提到的內容未必都是很準確或者正確,如果有任何的偏差也請大家指出,可以糾正一些不清楚的概念。
代碼修改如下:
JarOutputStream jos;
try
{
jos = new JarOutputStream(new BufferedOutputStream(new FileOutputStream(file)));
String f = "spring/sip-analyzer-dataSource.xml";
String dir = "spring/";
JarEntry je1 = new JarEntry(dir);
jos.putNextEntry(je1);
JarEntry je = new JarEntry(f);
jos.putNextEntry(je);
BufferedInputStream bis = new BufferedInputStream(new FileInputStream("D:/work/sip3/analyzer/src/conf.test/spring/sip-analyzer-dataSource.xml"));
int i = 0;
while ((i=bis.read())!=-1)
{
jos.write(i);
}
bis.close();
jos.closeEntry();
jos.close();
} catch ...
工作忙,有些許時間沒有更新Blog了,這次在開發監控模塊的時候遇到了這個問題,整個問題定位過程真是走了不少路,所以覺得有必要記錄下來分享一下。在我看來很多時候結果也許就很簡單一個原因,但是開發人員卻要探究很久,也許在找到了其他可實現業務邏輯方法的情況下,就會放棄尋找原因,這期間我也是一樣。
問題初現:
在服務集成平臺中需要新增一塊寫入數據庫的邏輯,因此考慮最簡便就是弄個Spring的BeanFactory來搞定這一切,誰知道,問題就這么出現了。很簡單,通過Spring的ClassPathXmlApplicationContext來構建BeanFactory,下面的語句大家應該很熟悉:
ctx = new ClassPathXmlApplicationContext("/spring/sip-*.xml");
通過通配符來載入ClassPath下的所有的符合規則的spring配置文件。然后在Eclipse中作完單元測試和集成測試,一切正常。然后用我們內部的打包部署,而這些配置文件都被打在Jar中作為lib庫依賴。結果啟動以后,在分析完日志需要寫入到數據庫的時候出現異常:
Could not resolve bean definition resource pattern [/spring/sip-*.xml]; nested exception is java.io.FileNotFoundException: class path resource [spring/] cannot be resolved to URL because it does not exist
就提示來說,就是沒有找到spring這個目錄,也就是在ClassPath下面就沒有找到資源。
第一次試圖解決問題:
以前調整過Jboss關于ClassLoader的配置,即自上而下搜索還是自下而上搜索,以及是否采用Web容器的ClassLoader,開始懷疑是否是這種修改造成的問題。修改了沒有問題,然后就設置斷點跟蹤Spring的ClassPathXmlApplicationContext的構造過程,發現Spring在分析此類通配類型的過程中,首先將前面的文件目錄和后面的具體通配文件分開,先定位文件目錄資源,也就是在定位文件目錄資源的過程中,找不到spring目錄,而出現了那個異常。看了代碼中也有對Jar的處理,但是在處理之前就出現了問題。
自己做了嘗試,將spring目錄和其內容解壓到Web的Classes目錄下運行正常,或者解壓到war下面也是正常的,這些地方其實都是ClassPath可以找到的,但是lib目錄下的jar也應該是可以找到的。在仔細跟蹤了代碼中最后載入這些資源的ClassLoader內的數據,所有的Jar都是包含在內的。
由于工作太多,因此將原有的打包模式作了修改,每次打包將這部分配置解壓到war下面,這樣就找到了可解決方案了,因此細致的緣由也就沒有再去追究。(如果不是后面再次遇到,這個問題就會在此了結)
問題再現:
監控模塊中需要新增一塊寫入數據庫的邏輯,在單元測試和集成測試通過的情況下出現了問題,由于此次是普通的J2SE的應用,所有的配置和依賴都打入在了Jar中,所以問題和前次一樣。
這次決定花一些時間好好找到問題所在,首先覺的Spring的資源載入應該不會不支持從Jar中載入,這是最基本的功能,因此再次打開了Spring的源碼。
問題二次定位:
先看看ClassPathXmlApplicationContext的類圖結構:
關鍵方法就是
try
{
// Try to parse the location as a URL...
URL url = new URL(location);
returnnew UrlResource(url);
}
catch (MalformedURLException ex)
{
// No URL -> resolve as resource path.
return getResourceByPath(location);
}
上面的代碼都是標準的j2se的代碼.作為URL通過字符串來構造,通常需要能夠首先獲得URL的資源全路徑,而在當前情況下發現到獲取資源的時候location還是保持了spring/的狀態,而沒有被替換成為所在jar的資源全路徑,那么就先作以下測試:
新建簡單的項目,然后在項目中加入包含spring配置的jar,然后作單元測試,測試代碼如下:
URL url = Thread.currentThread().getClass().getResource("/spring/");
未獲取到URL,出現異常。
URL url = Thread.currentThread().getClass().getResource("/spring/sip-analyzer-dataSource.xml");
正常獲取到了URL。
由此看來應該是在獲取Jar中的目錄資源路徑的時候出現問題導致后續載入出現問題,嘗試直接傳入具體的文件名:
ctx = new ClassPathXmlApplicationContext("/spring/sip-analyzer-dataSource.xml");
發現還是出現問題,在new URL的時候傳入的是沒有翻譯過的文件名,考慮在傳入的過程中就直接替換成為資源路徑,因此寫了一個簡單的方法:
publicstatic String[] getRealClassPath(String[] locationfile)
{
String[] result = locationfile;
for(int i = 0 ; i < locationfile.length; i++)
{
try
{
URL url = Thread.currentThread().getClass().getResource(locationfile[i]);
String file = url.getFile();
if (file.indexOf(".jar!") > 0)
result[i] = new StringBuffer("jar:").append(file.substring(0,file.indexOf(".jar!")+".jar!".length()))
.append(locationfile[i]).toString();
}
catch(Exception ex)
{}
}
returnresult;
}
在將構造工廠類修改為:
ctx = new ClassPathXmlApplicationContext(BaseUtil.getRealClassPath(new String[]{"/spring/sip-analyzer-dataSource.xml"}));
運行測試,正常啟動,這也就是又變成最原始的文件羅列的模式。問題雖然找到了解決方案,但是始終覺得很別扭,同時對于無法在Jar中載入配置資源的情況我一直都還是覺得應該不是Spring的問題。
峰回路轉:
晚上到家還是有點不死心,就直接建了個項目作單元測試,然后將一個自己建立的Jar加入到Classpath下面,作單元測試,結果大吃一驚。
URL url = Thread.currentThread().getClass().getResource("/test/");
URL url = Thread.currentThread().getClass().getResource("/test/test.txt");
都正常獲取到了資源,這完全推翻了我早先認為在Jar中無法獲得目錄作為資源的問題。然后把公司里面的項目重新打包然后加入到ClassPath下,驗證spring的目錄,出錯,目錄無法獲取,此時我確定看來應該不是應用的問題,而是環境問題。檢查了兩個Jar,看似沒有什么區別,將公司項目的Jar中的spring目錄拷貝到測試的jar中,然后作測試,可以找到目錄。那么問題完全定位到了Jar本身。通過RAR的壓縮工具看了一下兩個Jar的信息,除了顯示所謂的壓縮平臺不同(一個是DOS,一個是Unix)其他沒有任何區別。然后自己用RAR打了一個Jar以及在linux下打了一個Jar做了測試,兩個Jar內的目錄都是正常可以被獲取。
無意中我換了一下需要獲取的目錄名稱,也就是說在公司項目中有多個目錄在jar中,這次換成為ibatis目錄,正常獲取,看來不是Jar的格式。回想了一下,公司的打包工具是自己人寫的,其中提供了一個特性,如果一個項目內部的一些配置信息是需要讓調用它的第三方在編譯期配置,那么可以通過在第三方項目構建的過程中,動態的生成配置文件然后植入到被依賴的jar中。而spring這個目錄中由于那些數據庫的配置都是需要動態配置的,因此spring的那個目錄是后期被寫入的,而ibatis是早先就固化在項目中的。
由于我們的Jar在META-INF中都有INDEX.LIST文件,過去遇到過在JAR中自己手工放入一些文件由于沒有修改INDEX.LIST而導致雖然文件已經存在但是不會被發現,于是打開公司項目中的Jar,果然INDEX.LIST中只有ibatis,而沒有spring,看來是我的同事在寫入的時候沒有將INDEX.LIST更新。立刻將INDEX.LIST作了更新,測試spring目錄,結果依然出錯。看來這還不是問題的根本。
立刻問了我們開發打包工具的同事,向他們要寫入Jar的代碼,對方的回答是就是采用簡單的JarOutputStream來寫入,沒有什么特殊的。那我就開始懷疑是否是因為采用這種方式寫入到Jar中的目錄在被資源定位的時候會出現問題。于是寫了下面的代碼:
JarOutputStream jos;
try
{
jos = new JarOutputStream(new BufferedOutputStream(new FileOutputStream(file)));
String f = "spring/sip-analyzer-dataSource.xml";
File source = new File(f);
JarEntry je = new JarEntry(f);
jos.putNextEntry(je);
BufferedInputStream bis = new BufferedInputStream(new FileInputStream("D:/work/sip3/analyzer/src/conf.test/spring/sip-analyzer-dataSource.xml"));
int i = 0;
while ((i=bis.read())!=-1)
{
jos.write(i);
}
bis.close();
jos.closeEntry();
jos.close();
} catch ...
結果創建出來的Jar中的spring目錄無法被資源定位,同樣在這個Jar中直接拖入一個目錄test,然后刷新測試,test目錄可以被定位。
后續
就到了這個階段來看如果以上面這種方式寫入,對于目錄資源定位的卻存在問題。這個問題還沒有最終的肯定的結論,在我現在所有的試驗來看,不論是否有INDEX.LIST,或者INDEX.LIST,如果用程序寫入到Jar中,目錄作為資源定位都會出現問題(起碼是上面那種普通寫入方式)。
這種情況可能是由于這種寫法還有一些其他需要配置的,例如寫入到META-INF/INDEX.LIST中,或者就是J2SE現在API存在的一個問題。不過不管是什么問題,起碼值得引起重視,特別是現在類似于Spring框架載入Jar目錄下的配置。
在完成ASF集成REST以后,接到的任務就是要完成一個日志分析應用。需求沒有很明確,只是要有這么一個東西能夠滿足分析收集后的日志,將分析后的原始數據入庫,作為后期分析和統計使用。
在動手做之前,我還是給這個應用作了最基本的需求定義:靈活配置(輸入源,輸出目標,分析器的實現等),高效(并行任務分解)。就這兩點能夠做到,那么將來需求如何變化都可以適應。Tiger的Concurrent包是滿足后面那項最好的實現,因此打算好好的實踐一把,也就這部分Tiger的特性還沒有充分使用過,里面的線程池,異步服務調用,并發控制都能夠極好的完成并行任務分解的工作。也就是在這個過程中,看到了IBM開發者論壇上的一片文章,講關于《應用fork-join框架》,談到了在J2SE 7 的Concurrent包中將會增加fork-join風格的并行分解庫,其實這個是更細粒度的任務分解,同時能夠在當前多CPU的情況下提高執行效率,充分利用CPU的一種實現。無關的話不多說了,就寫一下整個設計和實現的過程以及中間的一些細節知識。
背景:
由于服務路由應用訪問量十分大,即時的將訪問記錄入庫對于路由應用本身以及數據庫來說無疑都會產生很大的壓力和影響。因此考慮首先將訪問信息通過log4j記錄在本地(當然自己需要定制一下Log4j的Appender和Filter),然后通過服務器的定時任務腳本來將日志集中到日志分析應用所在的機器上(這里通過配置可以決定日志是根據什么時間間隔來產生新文件)。日志分析應用就比較單純的讀取日志,分析日志,輸出分析結果(包括寫入數據庫或者是將即時統計信息存入到集中式緩存Memcached中)。網絡結構圖如下:
圖1 網絡結構圖
Concurrent概述:
Tiger出來也有些年頭了,但是每一個新的特性是否都在實際的工作中使用過,起碼我自己是沒有作到的,包括對于Concurrent包也只是看過,寫了幾個Test case玩一下,但具體使用到實際開發中還是比較少的。在這個工作之前,如果考慮要使用對象池或者線程池,那么一定會去采用apache的common pool,不過在現在jdk日益“強大”的基礎下,能夠通過jdk自己搞定的,就盡量不再引入第三方包了。看Java 的Doc很容易就理解了Concurrent,這里我只是大致的說一下幾個自己在應用中使用的接口:
BlockingQueue<E>:看看名字就知道了,阻塞式隊列,可以設置大小。適合于生產者和消費者模式,生產者在隊列滿時阻塞,消費者在隊列空時阻塞。在日志分析應用開發中被用于分析任務(生產者)和輸出任務(消費者)之間的分析結果存儲通道。
Callable<V>:任何需要執行的任務都可以定義成Callable,類似于線程的Runnable接口,可以被Service Executor指派給內部的線程異步執行,并且返回對象或者拋出異常。在日志分析應用開發中,非定時性的任務都定義成為此類型。
ConcurrentMap<K,V>:這個以前常常使用,因為效率要遠遠高于Collections.synchronizedCollection和synchronized。后面還會提到實踐中的幾個實用的技巧來防止在高并發的情況下出現問題。在日志分析應用中,此類型的Map作為保存日志文件分析狀態的緩存(日志文件分為兩種狀態:分析中,分析結束。如果不存在于Map中就認為尚未分析,那么將其納入Map然后啟動分析處理線程工作,如果存在于Map中標示為分析中,那么將不會再分析此文件,如果分析結束并且被輸出,將會標示此文件分析結束,異步清理線程將會定時根據策略刪除或移動文件)。
ExecutorService:內置線程池,異步執行指派任務,并可以根據返回的Future來跟蹤執行情況。在日志分析應用開發中,被用于非定時性任務執行。
ScheduledExecutorService:內置線程池,定時異步執行指派任務,并可以根據返回的Future來跟蹤執行情況。在日志分析應用開發中,被用于定時性任務執行。
以上就是被使用到的接口,具體實現策略配置就不在此贅述了。
整體結構設計:
整體設計還是基于開始設定的兩個原則:靈活配置,高效性(任務分解,并行流水線執行)。說到任務分解又會想起讀書時候的離散數學中關鍵路徑等等。任務分解還是要根據具體情況來分析和設計,不然并行不但不會提高效率,反而還降低了處理效率。
就日志分析來看,主要的處理過程可以分成這么幾個任務:
1. 檢查日志來源目錄,鎖定需要分析的文件。(執行需要時間很短,可通過定時間隔執行)。
2. 分析已經被鎖定的日志文件,產生分析結果。(執行需要時間根據日志文件大小來決定,因此需要線程異步執行,結果根據設定拆分成細粒度包,降低輸出線程等待時間)。
3. 檢查分析結果隊列。(執行需要時間很短,當前是配置了SingleThreadExecutor來執行檢查阻塞隊列的工作,同時獲取到分析結果包以后立刻創建線程來執行輸出任務)
4. 輸出分析結果,如果輸出成功,將分析過的日志文件在日志文件狀態緩存中的狀態更新為已分析。(執行時間根據輸出情況來定,當前實現的是批量輸出到數據庫中,根據配置來批量提交入庫,后續還會考慮實時統計到集中式Cache作為監控使用)。
5. 清理分析日志文件。(執行時間較短,設定了定時線程池執行清理任務,根據策略配置來執行清理和移動文件任務,并且清除在日志文件狀態緩存中的信息)
根據上面的分解可以看到,其實在單線程工作的過程中,容易造成阻塞而影響性能的主要是讀取,分析和寫出這三個過程的協調,一個一個讀取分析和寫出,性能一定低于讀取和分析并行工作,而分析完畢才寫出,性能一定低于分析部分,寫出部分。
同時由于細分各個任務,因此任務與任務之間的耦合度降低,可以運行期獲取具體的任務實現配置,達到靈活配置的目的。
下面就具體的看看整個流程,以及其中的一些細節的說明,這里根據下圖中的序號來逐一描述:
1. 配置了Schedule Executor來檢查日志所屬目錄中的日志文件,Executor的線程池大小以及檢查時間間隔都根據配置來設定。
Tip:定時任務可以設置delay時間,那么可以根據你的任務數量以及時間間隔來設定每一個任務的delay時間,均勻的將這些任務分布,提高效率。
2. 當Read Schedule被執行時,將會去檢查Analysis Log File State Concurrent Cache(也就是上面提到的ConcurrentMap)中是否存在此文件,如果不存在證明尚未分析,需要將其置入Cache,如果已經存在就去查詢其他文件。Tip:這里用了一點小技巧,通常我們對于此類操作應該做兩部分工作,get然后再put,但是這樣可能就會在高并發的情況下出現問題,因為這兩個操作不是一個原子操作。ConcurrentMap提供了putIfAbsent操作,這個操作意思就是說如果需要put的key沒有存在于Map中,那么將會把key,value存入,并且返回null,如果已經存在了key那么就返回key在map已經對應的值。通過if (resources.putIfAbsent(filename, Constants.FILE_STATUS_ANALYSISING) == null)就可以把兩個操作合并成為一個操作。
3. 日志讀取的工作線程完成鎖定文件以后,就將后續的工作交給Log Analysis Service Executor來創建分析任務異步執行分析操作,日志讀取工作線程任務就此完成。
4. Log Analysis Schedule是運行期裝載具體的接口實現類(采用的就是類似于JAXP等框架使用的META-INF/services來讀取工廠類,載入接口實現)。Analysis Schedule執行的主要任務就是分析文件,并且根據配置將分析結果拆分并串行的置入到Block Queue中,提供給輸出線程使用。
5. Receiver主要工作就是守候著Block Queue,當有數據結果產生就創建Write Schedule來異步執行輸出。
6. Log Writer Service Executor根據配置來決定內置線程池大小,同時在Receiver獲取到數據包時產生Write Schedule來異步執行輸出工作。
7. Write Schedule和Analysis Schedule一樣可以運行期裝載接口實現類,這樣提供了靈活的輸出策略配置。
Tips:在數據庫輸出的時候需要配置批量提交記錄最大數,分批提交提高性能,也防止過大結果集批量提交問題。
8. 寫出完成以后需要更新鎖定文件的狀態,標示成為已經分析成功。這里還遺留一點問題,在一個日志文件分包的過程中每一個包都回記錄隸屬于哪一個分析文件,文件的最后一個數據包將會被標示。在輸出成功以后會去檢查哪些包是文件最后數據包,更新此文件為已分析成功,如果出現異常,那么將會把這些文件狀態清除,接受下一次的重新分析。這里一個文件部分包提交暫時沒有做到事務一致,如果出現部分成功可能會重復分析和記錄。
9. 最后就是Clean Schedule被定時執行,根據策略來刪除或者移動已經被分析過的文件。
Tips:
ScheduledExecutorService內部可以配置線程池,當執行定時任務比較耗時,線程池中的線程都被占用的情況下,定時任務將不會準確的按時執行,因此設計過程中需要注意的是,定時任務一般是簡短的工作任務,如果比較耗時,那么應該結合ScheduledExecutorService和ExecutorService,定時任務完成必要工作以后將耗時工作轉交給ExecutorService創建的即時執行異步線程去處理,保證Schedule Executor正常工作。
圖2 流程結構設計
類圖:
圖3 類圖1
上面的類圖中主要描述的就是日志分析應用的三個主類:類似于控制臺的LogAnalyzer,具體內部資源管理類,配置類。(T表示采用泛型)
圖 4 類圖2
類圖2主要就是描述了在整個應用中所有的被分解可并行的任務定義。ClearSchedule是用來在控制臺輸入stop停止日志分析的時候,做后續資源回收工作的任務。CleanSchedule是用來清除被分析后的日志文件任務。ConsumerSchedule是阻塞隊列消費者任務。
其他還有一些輔助工具類以及工廠類和定義類就不畫了。
后話:
做這個設計和開發的過程中又好好的實踐了一些編程細節方面的內容,作為架構設計來說,需要多一些全局觀和業務觀,作為一個良好的開發者來說需要多實踐,多了解一些細節,在不斷學習和掌握各種大方向技術框架的同時,適當的了解一些細節也是一種很好的補充,同時也可以衍生思考。
REST風格的服務結合云計算的思想,會被使用的更為廣泛,而云計算其實就是一個問題分解和組合處理的過程,可以說是一種宏觀的問題解決策略。高效解決問題,提供服務,通過組合體現業務最大價值,就是互聯服務的最重要目的。
更多文章請訪問:http://blog.csdn.net/cenwenchu79/
假期結束,開始收心回來繼續工作。晚上有一個項目要發布,公司的同事突然打手機給我,說ASF的文件解析又出了上次的問題,希望盡快解決。
問題描述:
上一次問題:
多臺機器運行同一個分支的應用,但是有些機器正常,有一臺機器始終在啟動的時候報文件解析錯誤,從提示看來,主要是因為解析配置文件的時候校驗dtd失效,這臺機器無法連接外網。最后降低了我們內部的核心解析包,問題解決(或者讓這臺機器連接到外網)。(當時由于自己手頭工作比較多,也沒有在意,既然解決了就隨之過去了)
此次問題:
問題的提示和上次的類似,不過這次的機器時連接外網的。
問題查找:
解析出錯的文件是ASF(SCA的服務框架)的組件配置文件(composite文件),格式為xml的格式,解析方式是通過StAX標準來實現的。
按照上一次的解決方法,我將內部的tuscany0.998降級到tuscany0.997,解析正常。看了一下我對于這兩個版本升級作的修改,主要是支持了SCA框架中的Spring配置文件能夠使用import的標簽,內簽多個標準的spring文件。
跟蹤代碼內部發現,果然是在解析某幾個spring的配置文件時出現了問題,比較了一下ASF的Spring(正常解析)和標準的Spring配置文件,差別主要是在關于Xml的校驗申明的區別。ASF的Spring配置文件是由ASF Spring插件來自己解析的(采用Schema申明(固定的Target namingspace),因此早先所有的ASF的Spring我都要求大家采用Schema的校驗申明),而對于原來不是ASF的spring都是采用dtd的校驗方式申明(互相拷貝導致都是這樣)。下面就是兩種申明:
Schema:
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:sca="http://www.springframework.org/schema/sca"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/sca http://www.springframework.org/schema/sca/spring-sca.xsd">
Dtd:
<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN" "http://www.springframework.org/dtd/spring-beans.dtd">
早先由于在0.997版本中沒有支持import,因此也就不會去解析那些不是ASF的Spring文件,而現在因為需求支持了import所以需要解析那些原來不屬于ASF的Spring的配置文件。因此降低版本不是解決問題的辦法。
進一步跟進問題,發現是在解析Dtd的申明時候出現問題,拋出異常說連接超時。通過IE訪問了一下dtd的地址,的卻也是有問題,無法連接。看來是Spring的dtd的服務器出現了問題,導致了我們解析文件時候校驗無法正常,最終無法正常啟動。
問題解決:
這里先說一下最后解決的幾個方案,后面會有一些詳細的解釋和說明。
1. 升級ASF的Spring插件包,去除對于Xml的格式校驗。
XMLInputFactory xmlFactory = XMLInputFactory.newInstance();
//add by wenchu.cenwc cancel support dtd check
xmlFactory.setProperty(XMLInputFactory.SUPPORT_DTD, "false");
2. 將Dtd的校驗申明修改成為Schema的校驗申明。
3. 建立公司的Xml校驗服務器,控制管理dtd或者schema,將所有的xml Schema或者dtd申明指向該服務器。
問題延伸開來的思考:
就問題的解決方案來看這個問題的一些值得注意和思考的地方。
方案1:
當前對于XML解析來說,各種框架都已經統一的實現了StAX的標準,同時在jdk6得rt.jar中都已經將StAX API作為基礎框架API納入其內。而通常情況下,如果不配置是否校驗Xml,那么都將默認會主動校驗Xml,此時就會出現上面我所遇到的問題,如果當你依賴的Xml DTD 或者 Schema服務器出現問題,就會導致你本地應用可能受到影響。在Dtd的申明說明中,<!DOCTYPE rootElement PUBLIC "PublicIdentifier" "URIreference">紅色部分說可以在網絡出現問題或者網絡速度很慢的時候被部分xml解析器替代URIreference使用,不過我這邊沒有成功過。我通過自己屏蔽網絡連接來模擬環境的情況,都是無法通過的。(Schema比較奇怪,就算無法連接網絡還是可以正常的,這個后續需要繼續研究看看,或者有朋友對這個問題有了解請告知一下)
方案2:
其實早在2002年就已經有將dtd替換成為schema的趨勢了,兩者的區別和優劣網上的文章介紹了很多了,這兒不再羅列,其實最更本一點就是schema就是用xml來校驗xml,而dtd卻采用了另一套規則來校驗xml,其本身也是xml,擴展性,可讀性,學習曲線等等都次與schema。除了遺留系統,我個人看不出還有什么必要去使用老的dtd來校驗xml的格式。Spring的dtd服務的出現問題,也說明了其實對于dtd這種方式的校驗,spring也已經不會保證幾個9的穩定性。
Xml常常會被作為數據承載中介,使雙方能夠在跨平臺跨語言的情況下松耦合的交互信息,也是現在的SOA的實施基礎。那么雙方勢必需要有協議和數據格式規范來約束,schema作為dtd的新一代替代者已經廣為使用。另一方面,xsd也早已獨立于wsdl作為數據描述和可重用的數據描述說明被采用到各種互聯網應用。
看看國外的Facebook,亞馬遜,ebay等公司的REST風格的API,就可以清楚地了解到xsd十分適合作為輕量級的數據交互協議。在后續ASF中融入REST配置的實現中,也需要采用XSD這種Schema描述來實現數據交互解析。
因此替換掉Dtd的配置是遲早要做的一件事情,所以遲作不如早作,更避免拷貝引起的問題放大效果(不過這個問題由于要考慮QA和業務組的項目經理的顧慮,因此我只能做到的是建議)。
方案3:
看看Maven這些年這么火,其實在我們自己公司內部的antx同樣都是在做一件事情,就是對于第三方的依賴包的版本控制。對于開源項目依賴的管理其實很重要,作的好項目能夠很好的利用已有的成果,管理的不好就會被一些不太穩定的開源項目搞得頭破血流。
記得在上次三亞的聚會上談到了對于Tuscany的依賴,其實對于這個項目來說,如果要作為成熟的產品來說,那么勢必要獲取一個版本然后就作為穩定的依賴,而不是一味的升級更新,由于我們產品的特殊性以及早期的Tuscany的不成熟,因此我們僅僅只是使用了Tuscany的最核心解析文件框架部分,其他的插件都采取自己設計或者在原有設計上優化和更新的做法。當然這不是說對于所有的第三方依賴都是采取這樣的策略,其實如果不是基礎框架設計,僅僅只是應用級別的使用,只需要拿來主義就完全可以了。
回過頭來看,大家現在對于類庫的管理已經都很重視了,但是對于配置性的或者數據格式類的文件還沒有引起足夠的重視,不過看到很多朋友已經在本地作了這樣的工作,不過就我來看,如果能夠對dtd,schema作版本控制和服務器搭建,在長遠來看還是有一定的好處的,只是說根據各自的需求來做這樣的工作。
后續
問題出現的當天,我其實晚上就一直比較擔心,因為如果問題不解決,那么將會影響到很多項目組(框架被使用的越廣泛,自己所要承擔的責任越重大)。其實也是由于自己上次對于這個問題的不上心,導致了問題的再次出現。刨根問底是件好事,做我們這行的還是需要多問一些為什么,這樣就會少不少危急時刻的怎么辦了。
很多時候為什么程序員自己喜歡什么都自己做,因為掌握在自己手中的事情總是能夠解決,但是現在項目中對于第三方的依賴越來越多,在選擇和控制上必須慎之又慎,有時候依賴也是雙刃劍。