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