當(dāng)并發(fā)用戶數(shù)明顯的開始增長,你可能會不滿意一臺機器所能提供的性能,或者由于單個JVM實例gc的限制,你沒法擴展你的java應(yīng)用,在這樣的情況下你可以做的另外的選擇是在多個JVM實例或多臺服務(wù)器上運行你的系統(tǒng),我們把這種方法稱為水平擴展。
請注意,我們相信能夠在一臺機器的多個JVM上運行系統(tǒng)的擴展方式是水平擴展方式,而非垂直擴展方式。JVM實例之間的IPC機制是有限的,兩個JVM實例之間無法通過管道、共享內(nèi)存、信號量或指令來進(jìn)行通訊,不同的JVM進(jìn)程之間最有效的通訊方式是socket。簡而言之,如果JavaEE應(yīng)用如果擴展到多個JVM實例中運行,那么大多數(shù)情況下它也可以擴展到多臺服務(wù)器上運行。
隨著計算機越來越便宜,性能越來越高,通過將低成本的機器群組裝為集群可以獲得超過那些昂貴的超級計算機所具備的計算能力。不過,大量的計算機也意味著增加了管理的復(fù)雜性以及更為復(fù)雜的編程模型,就像服務(wù)器節(jié)點之間的吞吐量和延時等問題。
Java EE集群是一種成熟的技術(shù),我在TSS上寫了一篇名為“
Uncoverthe Hood of J2EE Clustering”的文章來描述它的內(nèi)部機制。
從失敗的項目中吸取的教訓(xùn)
采用無共享的集群架構(gòu)(SNA)
Figure 3: share nothing cluster
最具備擴展性的架構(gòu)當(dāng)屬無共享的集群架構(gòu)。在這樣的集群中,每個節(jié)點具備完全相同的功能,并且不需要知道其他節(jié)點存在與否。負(fù)載均衡器(LoadBalancer)來完成如何將請求分發(fā)給這些后臺的服務(wù)器實例。由于負(fù)載均衡器只是做一些簡單的工作,例如分派請求、健康檢查和保持session,因此負(fù)載均衡器很少會成為瓶頸。如果后端的數(shù)據(jù)庫系統(tǒng)或其他的信息系統(tǒng)足夠的強大,那么通過增加更多的節(jié)點,集群的計算能力可以得到線性的增長。
幾乎所有的JavaEE提供商在他們的集群產(chǎn)品中都實現(xiàn)了HttpSession的failover功能,這樣即使在某些服務(wù)器節(jié)點不可用的情況下也仍然能夠保證客戶端的請求中的session信息不丟失,但這點其實是打破了無共享原則的。為了實現(xiàn)failover,同樣的session數(shù)據(jù)將會被兩個或多個節(jié)點共享,在我之前的文章中,我曾經(jīng)推薦除非是萬不得已,不要使用session failover。就像我文章中提到的,當(dāng)失敗發(fā)生時,sessionfailover功能并不能完全避免錯誤,而且同時還會對性能和可擴展性帶來損失。
使用可擴展的session復(fù)制機制
為了讓用戶獲得更友好的體驗,有些時候可能必須使用sessionfailover功能,這里最重要的在于選擇可擴展的復(fù)制型產(chǎn)品或機制。不同的廠商會提供不同的復(fù)制方案 -有些采用數(shù)據(jù)庫持久,有些采用中央集中的狀態(tài)服務(wù)器,而有些則采用節(jié)點間內(nèi)存復(fù)制的方式。最具可擴展性的是成對節(jié)點的復(fù)制(paired nodereplication),這也是現(xiàn)在大部分廠商采用的方案,包括BEA Weblogic、JBoss和IBMWebsphere,Sun在GlassfishV2以及以上版本也實現(xiàn)了成對節(jié)點的復(fù)制。最不可取的方案是數(shù)據(jù)庫持久session的方式。在我們實驗室中曾經(jīng)測試過一個采用數(shù)據(jù)庫持久來實現(xiàn)session復(fù)制的項目,測試結(jié)果表明如果session對象頻繁更新的話,節(jié)點在三到四個時就會導(dǎo)致數(shù)據(jù)庫崩潰。
采用collocated部署方式來取代分布式
Java EE技術(shù),尤其是EJB,天生就是用來做分布式計算的。解耦業(yè)務(wù)功能和重用遠(yuǎn)程的組件使得多層的應(yīng)用模型得以流行。但對于可擴展性而言,減少分布式的層次可能是一個好的選擇。
在我們實驗室曾經(jīng)以一個政府的項目測試過這兩種方式在同樣的服務(wù)器數(shù)量上的部署 - 一種是分布式的,一種是collocated方式的,如下圖所示:
Figure 4: distributed structure
Figure 5: collocated structure
結(jié)果表明collocated式的部署方式比分布式的方式更具備可擴展性。假設(shè)你應(yīng)用中的一個方法調(diào)用了一堆的EJB,如果每個EJB的調(diào)用都需要loadbalance,那么有可能會因為需要分散到不同的服務(wù)器上進(jìn)行調(diào)用導(dǎo)致你的應(yīng)用崩潰,這樣的結(jié)果就是,你可能做了很多次無謂的跨服務(wù)器的調(diào)用。來看更糟糕的情況,如果你的方法是需要事務(wù)的,那么這個事務(wù)就必須跨越多個服務(wù)器,而這對于性能是會產(chǎn)生很大的損害的。
共享資源和服務(wù)
對于用于支撐并發(fā)請求的Java EE集群系統(tǒng)而言,其擴展后的性能取決于對于那些不支持線性擴展的共享資源的操作。數(shù)據(jù)庫服務(wù)器、JNDI樹、LDAP服務(wù)器以及外部的文件系統(tǒng)都有可能被集群中的節(jié)點共享。
盡管JavaEE規(guī)范中并不推薦,但為了實現(xiàn)各種目標(biāo),通常都會采用外部的I/O操作。例如,在我們實驗室測試的應(yīng)用中有用文件系統(tǒng)來保存用戶上傳的文件的應(yīng)用,或動態(tài)的創(chuàng)建xml配置文件的應(yīng)用。在集群內(nèi),應(yīng)用服務(wù)器節(jié)點必須想辦法來復(fù)制這些文件到其他的節(jié)點,但這樣做是不利于擴展的。隨著越來越多節(jié)點的加入,節(jié)點間的文件復(fù)制會占用所有的網(wǎng)絡(luò)帶寬和消耗大量的CPU資源。在集群中要達(dá)到這樣的目標(biāo),可以采用數(shù)據(jù)庫來替代外部文件,或采用SAN作為文件的集中存儲,另外一個可選的方案是采用高效的分布式文件系統(tǒng),例如Hadoop DFS(http://wiki.apache.org/hadoop/)。
在集群環(huán)境中共享服務(wù)很常見,這些服務(wù)不會部署到集群的每個節(jié)點,而是部署在專門的服務(wù)器節(jié)點上,例如分布式的日志服務(wù)或時間服務(wù)。分布式鎖管理器(DLM)來管理集群中的應(yīng)用對這些共享服務(wù)的同步訪問,即使在網(wǎng)絡(luò)延時和系統(tǒng)處理失敗的情況下,鎖管理器也必須正常操作。舉例來說,在我們的實驗室中測試的一個ERP系統(tǒng)就碰到了這樣的問題,他們寫了自己的DLM系統(tǒng),最終發(fā)現(xiàn)當(dāng)集群中持有鎖的節(jié)點失敗時,他們的locksystem將會永遠(yuǎn)的持有鎖。
分布式緩存
我所碰到過的幾乎所有的JavaEE項目都采用了對象緩存來提升性能,同樣所有流行的應(yīng)用服務(wù)器也都提供了不同級別的緩存來加速應(yīng)用。但有些緩存是為單一運行的環(huán)境而設(shè)計的,并且只能在單JVM實例中正常的運行。由于有些對象的創(chuàng)建需要耗費大量的資源,我們需要緩存,因此我們維護(hù)對象池來緩存對象的實例。如果獲取維護(hù)緩存較之創(chuàng)建對象而言更劃算,那么我們就提升了系統(tǒng)的性能。在集群環(huán)境中,每個jvm實例維護(hù)著自己的緩存,為了保持集群中所有服務(wù)器狀態(tài)的一致,這些緩存對象需要進(jìn)行同步。有些時候這樣的同步機制有可能會比不采用緩存的性能還差,對于整個集群的擴展能力而言,一個可擴展的分布式緩存系統(tǒng)是非常重要的。
如今很多分布式緩存相關(guān)的開源java產(chǎn)品已經(jīng)非常流行,在我們實驗室中有如下的一些測試:
1個基于JBoss Cache的項目的測試;
3個基于Terracotta的項目的測試;
9個基于memcached的項目的測試;
測試結(jié)果表明Terracotta可以很好的擴展到10個節(jié)點,并且在不超過5個節(jié)點時擁有很高的性能,但memcached則在超過20個服務(wù)器節(jié)點時會擴展的非常好。
Memcached
Memcached是一個高性能的分布式對象緩存系統(tǒng),經(jīng)常被用于降低數(shù)據(jù)庫load,同時提升動態(tài)web應(yīng)用的速度。Memcached的奇妙之處在于它的兩階段hash的方法,它通過一個巨大的hash表來查找key =value對,給它一個key,就可以set或get數(shù)據(jù)了。當(dāng)進(jìn)行一次memcached查詢時,首先客戶端將會根據(jù)整個服務(wù)器的列表來對key進(jìn)行hash,在找到一臺服務(wù)器后,客戶端就發(fā)送請求,服務(wù)器端在接收到請求后通過對key再做一次內(nèi)部的hash,從而查找到實際的數(shù)據(jù)項。當(dāng)處理巨大的系統(tǒng)時,最大的好處就是memcached所具備的良好的水平擴展能力。由于客戶端做了一層hashing,這使得增加N多的節(jié)點到集群變得非常的容易,并不會因為節(jié)點的互連造成負(fù)載的增高,也不會因為多播協(xié)議而造成網(wǎng)絡(luò)的洪水效應(yīng)。
實際上Memcached并不是一款java產(chǎn)品,但它提供了Java client API,這也就意味著如果你需要在JavaEE應(yīng)用中使用memcached的話,并不需要做多大的改動就可以從cache中通過get獲取值,或通過put將值放入cache中。使用memcached是非常簡單的,不過同時也得注意一些事情避免對擴展性和性能造成損失:
不要緩存寫頻繁的對象。Memcached是用來減少對數(shù)據(jù)庫的讀操作的,而非寫操作,在使用Memcached前,應(yīng)先關(guān)注對象的讀/寫比率,如果這個比率比較高,那么采用緩存才有意義。
盡量避免讓運行的memcached的節(jié)點互相調(diào)用,對于memcached而言這是災(zāi)難性的。
盡量避免行方式的緩存,在這樣的情況下可采用復(fù)雜的對象來進(jìn)行緩存,這對于memcached來說會更為有效。
選 擇合適的hashing算法。在默認(rèn)的算法下,增加或減少服務(wù)器會導(dǎo)致所有的cache全部失效。由于服務(wù)器的列表hash值被改變,可能會造成大部分的 key都要hash到和之前不同的服務(wù)器上去,這種情況下,可以考慮采用持續(xù)的hashing算法(http://weblogs.java.net /blog/tomwhite/archive/2007/11/consistent_hash.html) 來增加和減少服務(wù)器,這樣做可以保證你大部分緩存的對象仍然是有效的。
Terracotta
Terracotta(http://www.terracottatech.com/)是一個企業(yè)級的、開源的、JVM級別的集群解決方案。JVM級的集群方案意味著可以支撐將企業(yè)級的Java應(yīng)用部署部署到多JVM上,而且就像是運行在同一個JVM中。Terracotta擴展了JVM的內(nèi)存模型,各虛擬機上的線程通過集群來與其他虛擬機上的線程進(jìn)行交互(Terracotta extendsthe Java Memory Model of a single JVM to include a cluster of virtualmachines such that threads on one virtual machine can interact withthreads on another virtual machine as if they were all on the samevirtual machine with an unlimited amount of heap.)。
Figure6: Terracotta JVM clustering
采用Terracotta來實現(xiàn)集群應(yīng)用的編程方式和編寫單機應(yīng)用基本沒有什么差別,Terrocotta并沒有特別的提供開發(fā)者的API,Terracotta采用字節(jié)碼織入的方式(很多AOP軟件開發(fā)框架中采用的技術(shù),例如AspectJ和AspectWerkz)來將集群方式的代碼插入到已有的java語言中。
我猜想Terrocotta是通過某種互連的方式或多播協(xié)議的方式來實現(xiàn)服務(wù)器和客戶端JVM實例的通訊的,可能是這個原因?qū)е铝嗽谖覀儗嶒炇覝y試時的效果:當(dāng)超過20個節(jié)點時Terracotta擴展的并不是很好。(注:這個測試結(jié)果僅為在我們實驗室的測試結(jié)果,你的結(jié)果可能會不同。)
并行處理
我之前說過,單線程的任務(wù)會成為系統(tǒng)可擴展性的瓶頸。但有些單線程的工作(例如處理或生成巨大的數(shù)據(jù)集)不僅需要多線程或多進(jìn)程的運行,還會有擴展到多節(jié)點運行的需求。例如,在我們實驗室測試的一個JavaEE項目有一個場景是這樣的:根據(jù)他們站點的日志文件分析URL的訪問規(guī)則,每周產(chǎn)生的這些日志文件通常會超過120GB,當(dāng)采用單線程的Java應(yīng)用去分析時需要耗費四個小時,客戶改為采用HadoopMap-Reduce使其能夠水平擴展從而解決了這個問題,如今這個分析URL訪問規(guī)則的程序不僅運行在多進(jìn)程模式下,同時還并行的在超過10個節(jié)點上運行,而完成所有的工作也只需要7分鐘了。
有很多的框架和工具可以幫助Java EE開發(fā)人員來讓應(yīng)用支持水平擴展。除了Hadoop,很多MPI的Java實現(xiàn)也可以用來將單線程的任務(wù)水平的擴展到多個節(jié)點上并行運行。
MapReduce
MapReduce由Google的Jeffrey Dean和SanjayGhemawat提出,是一種用于在大型集群環(huán)境下處理巨量數(shù)據(jù)的分布式編程模型。MapReduce由兩個步驟來實現(xiàn) -Map:對集合中所有的對象進(jìn)行操作并基于處理返回一系列的結(jié)果,Reduce:通過多線程、進(jìn)程或獨立系統(tǒng)并行的從兩個或多個Map中整理和獲取結(jié)果。Map()和Reduce()都是可以并行運行的,不過通常來說沒必要在同樣的系統(tǒng)同樣的時間這么來做。
Hadoop是一個開源的、點對點的、純Java實現(xiàn)的MapReduce。它是一個用于將分布式應(yīng)用部署到大型廉價集群上運行的Lucene-derived框架,得到了全世界范圍開源人士的支持以及廣泛的應(yīng)用,Yahoo的Search Webmap、AmazonEC2/S3服務(wù)以及Sun的網(wǎng)格引擎都可運行在Hadoop上。
簡單來說,通過使用“HadoopMap-Reduce”,"URL訪問規(guī)則分析"程序可以首先將日志文件分解為多個128M的小文件,然后由Hadoop將這些小文件分配到不同的Map()上去執(zhí)行。Map()會分析分配給它的小文件并產(chǎn)生臨時的結(jié)果,Map()產(chǎn)生的所有的臨時結(jié)果會被排序并分配給不同的Reduce(),Reduce()合并所有的臨時結(jié)果產(chǎn)生最終的結(jié)果,這些Map和Reduce操作都可以由Hadoop框架控制來并行的運行在集群中所有的節(jié)點上。
MapReduce對于很多應(yīng)用而言都是非常有用的,包括分布式檢索、分布式排序、web link-graphreversal、term-vector per host、web訪問日志分析、索引重建、文檔集群、機器智能學(xué)習(xí)、statisticalmachine translation和其他領(lǐng)域。
MPI
MPI是一種語言無關(guān)、用于實現(xiàn)并行運行計算機間交互的通訊協(xié)議,目前已經(jīng)有很多Java版本的MPI標(biāo)準(zhǔn)的實現(xiàn),mpiJava和MPJ是其中的典型。mpiJava基于JNI綁定native的MPI庫來實現(xiàn),MPJ是100%純java的MPI標(biāo)準(zhǔn)的實現(xiàn)。mpiJava和MPJ和MPIFortran和C版本提供的API都基本一致,例如它們都對外提供了具備同樣方法名和參數(shù)的Comm class來實現(xiàn)MPI的信息傳遞。
CCJ是一個類似MPI通訊操作的java庫。CCJ提供了barrier、broadcast、scatter、gather、all-gather、reduce和all-reduce操作的支持(但不提供點對點的操作,例如send、receive和send-receive)。在底層的通訊協(xié)議方面,CCJ并沒有自己實現(xiàn),而是采用了JavaRMI,這也就使得CCJ可以用來傳遞復(fù)雜的序列化對象,而不僅僅是MPI中的原始數(shù)據(jù)類型。進(jìn)一步看,CCJ還可以從一組并行的processes中獲取到復(fù)雜的集合對象,例如實現(xiàn)了CCJ的DividableDataObject接口的集合。
采用不同的方法來獲取高擴展能力
有很多的書會教我們?nèi)绾我設(shè)O的方式來設(shè)計靈活架構(gòu)的系統(tǒng),如何來使服務(wù)透明的被客戶端使用以便維護(hù),如何采用正常的模式來設(shè)計數(shù)據(jù)庫schema以便集成。但有些時候為了獲取高擴展性,需要采用一些不同的方法。
Google設(shè)計了自己的高可擴展的分布式文件系統(tǒng)(GFS),它并不是基于POSIXAPI來實現(xiàn)的,不過GFS對于用戶來說并不完全透明。為了使用GFS,你必須采用GFS的API包。Google也設(shè)計了自己的高可擴展的分布式數(shù)據(jù)庫系統(tǒng)(Bigtable),但它并不遵循ANSISQL標(biāo)準(zhǔn),而且其中的概念和結(jié)構(gòu)和傳統(tǒng)的關(guān)系數(shù)據(jù)庫幾乎完全不同,但最重要的是GFS和Bigtable能夠滿足Google的存儲要求、良好的擴展性要求,并且已經(jīng)被Google的廣泛的作為其存儲平臺而使用。
傳統(tǒng)方式下,我們通過使用更大型的、更快和更貴的機器或企業(yè)級的集群數(shù)據(jù)庫(例如RAC)來將數(shù)據(jù)庫擴展到多節(jié)點運行,但我有一個我們實驗室中測試的socialnetworking的網(wǎng)站采用了不同的方式,這個應(yīng)用允許用戶在網(wǎng)站上創(chuàng)建profiles、blogs,和朋友共享照片和音樂,此應(yīng)用基于JavaEE編寫,運行在Tomcat和Mysql上,但不同于我們實驗室中測試的其他應(yīng)用,它只是希望在20多臺便宜的PCServer上進(jìn)行測試,其數(shù)據(jù)模型結(jié)構(gòu)如下:
Figure7: Users data partitions
這里比較特殊的地方子礙于不同的用戶數(shù)據(jù)(例如profile、blog)可能會存儲在不同的數(shù)據(jù)庫實例上,例如,用戶00001存儲在服務(wù)器A上,而用戶20001存儲在服務(wù)器C上,分庫的規(guī)則以一張元信息的表的方式存儲在專門的數(shù)據(jù)庫上。當(dāng)部署在Tomcat的JavaEE應(yīng)用希望獲取或更新用戶信息時,首先它會從這張元信息的表中獲取到需要去哪臺服務(wù)器上獲取這個用戶,然后再連到實際的服務(wù)器上去執(zhí)行查詢或更新操作。
用戶數(shù)據(jù)分區(qū)和這種兩步時的動作方式可以帶來如下的一些好處:
擴展了寫的帶寬:對于這類應(yīng)用而言,blogging、ranking和BBS將會使得寫帶寬成為網(wǎng)站的主要瓶頸。分 布式的緩存對于數(shù)據(jù)庫的寫操作只能帶來很小的提升。采用數(shù)據(jù)分區(qū)的方式,可以并行的進(jìn)行寫,同樣也就意味著提升了寫的吞吐量。要支持更多的注冊用戶,只需 要通過增加更多的數(shù)據(jù)庫節(jié)點,然后修改元信息表來匹配到新的服務(wù)器上。
高可用性:如果一臺數(shù)據(jù)庫服務(wù)器down了,那么只會有部分用戶被影響,而其他大部分的用戶可以仍然正常使用;
同時也會帶來一些缺點:
由于數(shù)據(jù)庫節(jié)點可以動態(tài)的增加,這對于在Tomcat中的Java EE應(yīng)用而言要使用數(shù)據(jù)庫連接池就比較難了;
由于操作用戶的數(shù)據(jù)是兩步式的,這也就意味著很難使用ORMapping的工具去實現(xiàn);
當(dāng)要執(zhí)行一個復(fù)雜的搜索或合并數(shù)據(jù)時,需要從多臺數(shù)據(jù)庫服務(wù)器上獲取很多不同的數(shù)據(jù)。
這個系統(tǒng)的架構(gòu)師這么說:“我們已經(jīng)知道這些缺點,并且準(zhǔn)備好了應(yīng)對它,我們甚至準(zhǔn)備好了應(yīng)對當(dāng)元信息表的服務(wù)器成為瓶頸的狀況,如果出現(xiàn)那樣的狀況我們將會把元信息表再次劃分,并創(chuàng)建出一個更高級別的元信息表來指向眾多的二級元信息表服務(wù)器實例。“
參考
Scalability definition in wikipedia:
http://en.wikipedia.org/wiki/ScalabilityJavadoc of atomic APIs:
http://java.sun.com/j2se/1.5.0/docs/api/java/util/concurrent/atomic/package-summary.htmlAlan Kaminsky. Parallel Java: A unified API for shared memory and cluster parallel programming in 100% Java:
http://www.cs.rit.edu/~ark/20070326/pj.pdfOMP-an OpenMP-like interface for Java:
http://portal.acm.org/citation.cfm?id=337466Google MapReduce white paper:
http://labs.google.com/papers/mapreduce-osdi04.pdfGoogle Bigtable white paper:
http://labs.google.com/papers/bigtable-osdi06.pdfHadoop MapReduce tutorial:
http://hadoop.apache.org/core/docs/r0.17.0/mapred_tutorial.htmlMemcached FAQ:
http://www.socialtext.net/memcached/index.cgi?faqTerracotta:
http://www.terracotta.org/關(guān)于作者
Wang Yu目前在Sun的ISVEGroup小組工作,擔(dān)任的職位為Java工程師和架構(gòu)咨詢師,他承擔(dān)的職責(zé)包括支持本地的ISVs,為一些重要的Java技術(shù)例如JavaEE、EJB、JSP/Servlet、JMS和web services技術(shù)提供咨詢,可以通過wang.yu@sun.com聯(lián)系他。