java集羣技術

序言

  面試的過程中,愛奇藝的面試官一直問我一些集羣的知識,因爲沒有做過相應的東西,卡殼了,今天百度了下,趕緊記錄下來。

越來越多的關鍵應用運行在J2EE(Java 2, Enterprise Edition)中,這些諸如銀行系統和賬單處理系統需要高的可用性(High Availability, HA),同時像Google和Yahoo這種大系統需要大的伸縮性。高可用性和伸縮性在今天高速增長的互連接的世界的重要性已經證實了。eBay於 1999年6月停機22小時的事故,中斷了約230萬的拍賣,使eBay的股票下降了9.2個百分點。

J2EE集羣是用來提供高可用性和伸縮性服務,同時支持容錯處理的一種流行的技術。但是,由於J2EE規範缺乏對集羣的支持,J2EE供應商實現集羣的方法也各異。這給J2EE架構師和開發人員帶來了很多困難。以下是幾個常見的問題:

  • 爲什麼帶集羣功能的商業J2EE服務器產品如此昂貴?(10倍於不帶集羣功能的產品)
  • 爲什麼基於單服務器環境構建的應用不能在集羣中運行?
  • 爲什麼應用在集羣環境中運行得很慢,但在非集羣環境中卻快得多?
  • 爲什麼集羣的應用移植到其他服務器中失敗?

理解這些限制和要素的最佳方法是學習他們的實現方式。

基本術語

在我們討論不同的集羣實現之前,先談談幾個概念。這有助於理解不同的J2EE集羣產品不同的設計結果和概念:

伸縮性(Scalability):

在一些大的系統中,預測最終用戶的數量和行爲是非常困難的,伸縮性是指系統適應不斷增長的用戶數的能力。提高這種併發會話能力的一種最直觀的方式就增加資源(CPU,內存,硬盤等),集羣是解決這個問題的另一種方式,它允許一組服務器組在一起,像單個服務器一樣分擔處理一個繁重的任務。

高可用性(High availability):

單一服務器的解決方案並不是一個健壯方式,因爲容易出現單點失效。像銀行、賬單處理這樣一些關鍵的應用程序是不能容忍哪怕是幾分鐘的死機。它們需要這樣一些服務在任何時間都可以訪問並在可預期的合理的時間週期內有響應。集羣方案通過在集羣中增加的冗餘的服務器,使得在其中一臺服務器失效後仍能提供服務,從而獲得高的可用性。

負載均衡(Load balancing):

負載均衡是集羣的一項關鍵技術,通過把請求分發給不同的服務器,從而獲得高可用性和較好的性能。一個負載均衡器可以是從一個簡單的Servlet或 Plug-Ins(例如一個Linux box利用ipchains來實現),到昂貴的內置SSL加速器的硬件。除此之外,負載均衡器還需執行一些其他的重要任務,如“會話膠粘”讓一個用戶會話始終存在一個服務器上,“健康檢查”用於防止將請求分發到已失效的服務器上。有些負載均衡器也會參與我們下面將要談到“失效轉移”過程。

容錯(Fault tolerance):

高可用性意味着對數據正確性的要求不那麼高。在J2EE集羣中,當一個服務器實例失效後,服務仍然是有效的,這是因爲新的請求將被冗餘服務器處理。但是,當一個請求在一個正在失效的服務器中處理時,可能得到不正確的結果。不管有多少個錯誤,容錯的服務應當能確保有嚴格的正確的行爲。

失效轉移(Failover):

失效轉移是集羣中用來獲取容錯能力的另一項關鍵的技術。當一個結點失效後,通過選擇集羣中的另一個結點,處理將會繼續而不會終止。轉移到另一個結點可以被顯式的編碼,或是通過底層平臺自動地透明地路由到另一個服務器。

等冪方法(Idempotent methods):

等冪方法是指這樣一些方法:重複用相同的參數調用都能得到相同的結果。這些方法不會影響系統狀態,可以重複調用而不用擔心改變系統。例如:getUsername()就是等冪的,而deleteFile就不是。當我們討論HTTP Session失效轉移和EJB失效轉移時,它是一個重要的概念。

什麼是J2EE集羣

一個天真的問題,不是嗎?但我仍要用幾句話和圖來回答它。通常,J2EE集羣技術包括"負載均衡"和"失效轉移"。

 

 
圖 1  負載均衡

如圖1所示,負載均衡意味着有許多客戶端向目標對象同時發出請求。負載均衡器在調用者和被調用者之間,分發請求到與原始對象相同的冗餘對象中。伸縮性和高可用性就是這樣得到的。

 

 
圖 2  失效轉移

如圖2所示,失效轉移與負載均衡不同。有時客戶端會連續發請求到目標對象,如果請求中間目標對象失效了,失效轉移系統將檢測到這次失敗,並將請求重定向到另一個可用的對象。通過這種方式可以獲得容錯能力。

如果你想知道更多的有關J2EE集羣的知識,你就會問到一個基本的問題,“什麼對象可以集羣?”和“在我的J2EE代碼中哪裏會發生負載均衡和失效轉移呢?”。這些都是用來理解J2EE集羣的非常好的問題。實際上,並不是所有的對象都能被集羣的,並且負載均衡和失效轉移並不是在J2EE代碼所有地方都能發生。看看下面的例子代碼:

 

 
圖 3  例子代碼

在Class A的bussiness()方法中,instance1可以負載均衡嗎?或是當其失效,可以失效轉移到其他B的實例上嗎?我想是不行的!對負載均衡和失效轉移來說,必須要有個攔截器在調用者和被調用者之間分發或重定向請求到不同的對象上。Class A和Class B的實例是運行在一個JVM中緊密耦合的,在方法調用間加入分發邏輯非常困難。

什麼類型對象可以被集羣?——只有那些可以被部署到分佈式拓樸結構中的組件。

在我的J2EE代碼中,什麼地方會有負載均衡和失效轉移?——只在你調用分佈式組件的方法時。

 

 
圖 4  分佈式對象

在如圖4所示的分佈式環境中,調用者和被調用者被分離在有明顯邊界的不同的運行容器中,這個邊界可以是JVM,進程和機器。

當目標對象被客戶端調用時,目標對象的功能是在容器中運行的(這就是爲什麼我們說它是分佈式的原因)。客戶端和目標對象通過標準的網絡協議通信。這些特性就爲一些機制提供了機會可以介入到方法調用之間實現負載均衡和失效轉移。

如圖4,瀏覽器通過HTTP協議調用JSP對象,JSP運行在WEB服務器中,瀏覽器只需要返回結果而不關心它是怎麼運行的。在上述場景中,一些東西就可以在瀏覽器與WEB服務器之間實現負載均衡和失效轉移的功能。在J2EE平臺,分佈式技術包括:JSP(Servlet),JDBC,EJB,JNDI,JMS,WEB Service等。負載均衡和失效轉移就發生在這些分佈式方法被調用時。在後續部分我們將詳細討論這些技術。

 

4 WEB層集羣實現
WEB層集羣是J2EE集羣的重要且基本的功能。WEB集羣技術包括WEB負載均衡和HTTP Session失效轉移。

4.1 WEB負載均衡 
J2EE提供商實現WEB負載均衡有許多方式。基本上,都一個負載均衡器被插入到瀏覽器和WEB服務器之間,如下圖所示。

 

 
圖 5  WEB負載均衡

負載均衡器可以是一臺硬件,如F5負載均衡器,或僅僅是另一臺有負載均衡Plug-Ins的WEB服務器,一個簡單的帶ipchains的Linux box可以很好的實現負載均衡。不管採用哪種技術,負載均衡器都有以下特性:

4.1.1 實現負載均衡算法

當客戶請求到來時,負載均衡器需要決定將如何分發到後臺服務器。流行的算法是Round-Robin、Random和Weight Based。負載均衡器盡力使每臺服務器實例都獲得相同的負載,但是上述算法沒有一個可以獲得理想的負載相同,因爲它們僅僅是依據發送到特定服務器的請求的個數。一些精密的負載均衡器實現了特殊的算法。它在分發請求之前將檢測服務器的工作負載。

  • 健康檢測

當一臺服務器失效了,負載均衡器應當檢測出失效並不再將請求分發到這臺服務器上。同樣,它也要檢測服務器是否恢復正常,並恢復分發請求。

  • 會話膠粘

幾乎所有的WEB應用程序都有一些會話狀態,可能是簡單的記住用戶是否登陸,或是包含你的購物車信息。因爲HTTP本身是無狀態的,會話狀態應當存在服務器的某個地方並與你當前瀏覽會話相關聯,這樣當你下次再請求相同WEB應用程序的頁面時可以很容易的重新獲取。當負載均衡時,最佳的選擇就是將特定的瀏覽器會話分發到上次相同的服務器實例中,否則,應用程序可能不能正確工作。

因爲會話狀態存儲在特定WEB服務器的內存中,“會話膠粘”對於負荷均衡非常重要。但是,如果其中某臺服務器實例因爲某種原因失效了(比如關機),那麼這臺服務器的會話狀態將要丟失。負載均衡器應當檢測到這個失效並不再將請求分發給它,但這些請求的會話狀態都因爲存放在失效的服務器中而丟失了所有信息,這就將導致錯誤。會話的失效轉移因此而生。

4.2 HTTP Session失效轉移 
幾乎所有流行的J2EE供應商都在他們的集羣產品中實現了Http Session失效轉移,用來保障當某臺服務器失效後會話狀態不會丟失,使客戶端請求能被正確處理。如圖6所示,當瀏覽器訪問有狀態的WEB應用程序(第 1 ,2步),這個應用程序可能在內存創建了會話對象用於保存信息以供後面的請求使用,同時,發送給瀏覽器一個唯一的HTTP Session ID用於標識這個會話對象(第3步),瀏覽器將這個ID保存Cookie中,並當它下次再請求同一WEB應用程序的頁面時,會將Cookie發還給服務器。爲了支持會話失效轉移,WEB服務器將在一定的時候把會話對象備份到其他地方以防止服務器失效後丟失會話信息(第4步)。負載均衡器檢測到這個失敗(第5,6步),並將後續的請求分發到裝有相同應用程序的服務器實例中(第7步),由於會話對象已經備份到其他地方了,這個新的服務器實例可以恢復會話(第8步)正確地處理請求。

 

 
圖 6  HTTP Session失效轉移

爲了實現上述功能,HTTP Session失效轉移將帶來以下問題:

  • 全局HTTP Session ID

如上所述,HTTP Session ID用於在特定的服務器實例中標識唯一的內存會話對象,在J2EE平臺,HTTP Session ID依賴於JVM實例,每個JVM實例擁有幾個應用程序,每個應用程序都爲不同的用戶管着許多會話,HTTP Session ID是在當前JVM實例用於訪問相關會話的關鍵。在會話失效轉移的實現中,要求不同的JVM實例不能產生兩個相同的HTTP Session ID,這是因爲當失效轉移發生時,一個JVM的會話將要備份並恢復到另一箇中,這樣,必須建立全局HTTP Session ID產生機制。

  •  如何備份會話狀態

如何備份會話狀態是區別J2EE服務器好壞的關鍵因素。不同的供應商有不同的實現,在後續部分我再詳細解釋。

  • 備份的頻率和粒度

會話的備份是消耗性能的,包括CPU,內存,網絡帶寬和寫入磁盤和數據庫的I/O,備份的頻率和備份對象的粒度將嚴重影響性能。

4.2.1 數據庫備份方式

幾乎所有的J2EE集羣產品都允許選擇將你的會話對象通過JDBC備份到關係數據庫中。如圖7所示,這種方式可以讓服務器實例非常簡單的在正確的時間序列化會話內容並寫到數據庫中。當發生會話轉移時,另一臺可用的服務器接過已失效的服務器工作,從數據庫中恢復所有的會話狀態。序列化對象是關鍵點,它使得內存會話數據可以持久化和傳輸。要了解更多有關Java對象序列化知識,請參考http://java.sun.com/j2se/1.5.0/docs/guide/serialization/index.html 。

 

 
圖 7  備份會話數據到數據庫

由於數據庫交易是非常昂貴的,這種方法主要缺點是當在會話中保存大量的或大的對象時限制了伸縮性,大多數使用數據庫會話持久化的服務器產品都提倡儘量減少用HTTP會話存儲對象,但這限制了你的應用程序的架構和設計,特別是你要使用HTTP會話緩存用戶數據時。

數據庫的方式也有一些優點:

  • 簡單,容易實現。分離的請求處理和會話備份處理使集羣更好管理和健壯。
  • 會話可以失效轉移到任何一臺服務器,因爲數據庫是共享的。
  • 當整個集羣失效時,會話數據依舊倖免。

4.2.2 內存複製方式 
因爲性能的原因,一些J2EE服務器(Tomcat,Jboss,WebLogic,WebSphere)提供了另一種實現:內存複製

 

 
圖 8  對會話狀態進行內存複製

基於內存的會話持久化將會話信息保存在一臺或是多臺備份服務器中,而不是保存數據庫中(如圖8)。這種方式因爲性能高而非常流行。同數據庫方式相比,直接在原服務器和備份服務器之間網絡通信是非常輕量的。同時注意在使用方式中,數據庫方式中的“恢復”階段是不需要的,因爲在備份後,所有會話數據都已經存在備份服務器的內存中了,已經可以處理請求。

“Java Groups”是當前Tomcat和Jboss集羣所使用的通信層。Java Groups是用於實現可靠組通信和管理的工具包。它提供了諸如“組成員協議”和“消息廣播”等核心特性,這些都對集羣的工作非常有用。有關Java Groups的信息,請參考:http://www.jgroups.org/javagroupsnew/docs/index.html 。

4.2.3 Tomcat方式:多服務器複製

內存複製也存在許多不同的方式,第一種方法就是將會話數據複製到集羣中的所有結點,Tomcat5採用的就是這種方式。

 

 
圖9  多服務器複製

如圖9所示,當一個服務器實例的會話改變後,將備份到其他所有的服務器上。當一臺服務器失效後,負載均衡器可以選擇其他任何一臺可用的服務器實例。但這種方式限制了伸縮性,如果集羣中有很多的服務器實例,那麼網絡通信的代價就不能被忽略,這將嚴重降低性能,並且網絡也將成爲系統的瓶頸。

4.2.4 WebLogic,Jboss和Websphere的方式:對等服務器複製

由於性能和伸縮性的原因,WebLogic,Jboss和Webshpere採用了其他方式實現內存複製。每臺服務器任意選擇一臺服務器備份其內存中的會話信息。如圖10所示。

在這種方式中,每臺服務器都有一臺自己的對等服務器,而不是其他所有的服務器,這種方式消除在集羣中加入過多服務器實例的話影響伸縮性的問題。

 

 
圖 10  對等服務器複製

儘管這種方式實現失效轉移有很高的性能和伸縮性,但它仍有一些限制:

  • 它給負載均衡器帶來了更多的複雜性。當一臺服務失效後,負載均衡器必須知道那臺服務是這臺己失效服務器的對等備份服務器。這將縮小了負載均衡器的選擇範圍,同時有些硬件也不能滿足這種要求。
  •  除了處理正常的請求外,服務器還將負責複製的任務。由於備份會話數據的任務也需要佔用CPU的週期,所以每臺服務器的請求處理能力也降低了。
  • 在沒有發生失效轉移的時候,備份服務器上大量用於備份的內存是個浪費。同時這也將增加了JVM GC的負擔。
  • 集羣中的服務器實例構成了複製對。這樣,當會話所在主服務器失效後,負載均衡器將會話轉移到備份服務器,使備份服務器處理兩倍的請求,這將造成備份服務器的性能問題。

爲了克服上面的4點問題,不同的軟件供應商採用了不同的方法,WebLogic採用的複製對不是對每臺服務器,而是對每個會話。當一臺服務器實例失效後,會話數據己經分散備份到多個備份服務器上,使失效的負載均勻地分佈。

4.2.5 IBM的方式:中央狀態服務器

Websphere採用不同的方式實現內存複製:備份會話信息到中央的狀態服務器,如圖11所示:

 

 
圖 11  中央狀態服務器複製

這與數據庫的解決方案非常類似,不同之處在於專用的“會話備份服務器”代替了數據庫服務器,這種方式結合了數據庫和內存複製兩種方式的優點。

  • 將請求處理和會話備份處理分開使用集羣更加健壯。
  •  所有的會話數據都備份到專用的服務器上,無需服務器浪費內存用於備份其他服務器的會話。
  • 因爲會話備份服務器是在服務器之間共享的,所有失效後可以轉移到任何一臺服務器上,這樣大多數據軟硬件負載均衡器都可以使用,更重要的是當一臺服務器失效後,負載將均勻的分佈到所有實例上。
  • 與重量級的數據庫連接相比,應用服務器與備份服務器之間Socket通信是輕量的。這樣就比數據庫的解決方案有更好的性能和更好的伸縮性。

然而,由於有恢復失效服務器會話數據的這麼一個階段,因此其性能肯定不如兩臺服務器直接複製解決方案,另外,多出來一臺備份服務器也增加了管理的複雜性。也可能由於單臺備份服務器造成性能瓶頸。

4.2.6 Sun的方式:特殊數據庫

 

 
圖 12  特殊數據庫複製

Sun JES應用服務器採用了別的方式實現會話失效轉移,如圖12所示,它看上去很像數據庫的方式,因爲它採用關係數據庫存儲會話並通過JDBC訪問所有會話數據。但是JES內部所使用的關係數據庫稱爲HADB,已經爲訪問會話做了特別優化,並且將幾乎所有的會話數據存在內存中。這樣,你可以說它更像中央狀態服務器的方式。

4.2.7 性能因素

考慮如下問題:一臺WEB服務器中可能運行着許多WEB應用,它們中每一個都可能被成百的併發用戶訪問,而每個用戶都會產生瀏覽器會話用於訪問特定的應用。所有會話信息都將備份以便服務器失效後能轉移到其他服務器實例中。更糟的是,會話會由於一次次的發生以下情況而變化,包括創建、失效、增加屬性、刪除屬性、修改屬性值。甚至是什麼都沒變,但由於有新的訪問而使最後訪問時間變了(由此判斷什麼時候失效會話)。因此,性能在會話失效轉移的解決方案中是個很大的因素。供應商通常會提供一些可調的參數改變服務器行爲,使之適應性能需求。

4.2.7.1 備份時機

當客戶端的請求被處理後,會話隨時改變。由於性能因素,實時備份會話是不明智的。選擇備份頻率需要平衡。如果備份動作發生得太頻繁,性能將急劇下降。如果兩次備份的間隔太長,那麼在這間隔之間服務器失效後,很多會話信息將會丟失。不管所有的方式,包括數據庫和內存複製,下面是決定備份頻率的常用的選項。

  •  按WEB請求

在WEB請求處理結束後,發生響應之前保存數據。這種方式能夠保證在失效後備份的會話數據是最新的。

  • 按固定的時間間隔

會話在後臺按固定的時間間隔保存。這種方式不能保證備份的會話數據是最新的。但由於不需在每次請求之後備份數據,因而有更好的性能。

4.2.7.2 備份粒度

當備份會話的時候,你還需要決定多少會話狀態需要保存。以下是不同產品所有采用的常用的策略。

  • 整個會話

每次都將保存所有會話。這種方式爲正確保存分佈式WEB應用的會話提供了最好保證。這種方式簡單,內存複製和數據庫存儲方式都默認採用這種方式。

  • 修改過的會話

當會話被修改過後,則備份整個會話。當“session.setAttribute()”或 “session.removeAttribute()”被調用後,則認爲會話被修改過。必須保證這些方法調用才修改會話屬性,這並不是J2EE規範後要求的。但卻是正確實現這種方法所需要的。備份修改過的會話減少了會話存儲的數量,那些僅僅是讀取會話屬性的請求將不會觸發會話備份的動作。這將帶來比備份整個會話更好的性能。

  • 修改過的屬性

僅僅是保存被修改過的會話屬性而不是整個會話。這將最小化備份的會話數據。這種方式帶來最好的性能及最小的網絡通信。爲了保證這種方式工作的正確性,必須依據以下的要點。首先,只能通過調用“setAttribute()”方法修改會話狀態,並且會話數據要被序列化和備份。其次,確保屬性之間沒有交叉引用。每個唯一的屬性鍵值下的對象圖應當被獨立地序列化和保存。如果兩個獨立的鍵值下的對象存在交叉引用,它們將不能夠正確的序列化和反序列化。例如圖13所示,在一個內存複製的集羣中,會話中存有“student”和“school”對象,同時“school”對象含有到“student”對象的引用,某一個時候“school”被修改後備份到備份服務器中。在序列化和反序列化之後,在備份服務器的被還原的“school”對象的版本將包含整個對象圖,包括其引用的“student”對象。但是“student”對象可以被獨立的修改,當它被修改後,僅僅只有它自己被備份。在序列化和反序列化之後,在備份服務器中還原“student”對象,但在此時,它將丟失與“school”對象的連接。儘管這種方式有最好的性能,但上述限制將影響WEB應用程序的架構和設計。特別是需要用會話保存緩存的複雜的用戶數據。

 

 
圖 13  會話複製中的交叉引用

4.2.8 其他失效轉移的實現

如前面章節所提到的,當會話備份時粒度對於性能非常重要。然而,當前的一些實現,不管是數據庫存儲還是內存複製,都將使用Java對象序列化技術來傳輸Java對象。這就打了個大印子,影響系統的性能,並給WEB應用程序的架構和設計帶來很多的限制。一些J2EE供應商便尋找一些特別的手段來更爲輕量地,小印子地實現WEB集羣,提供細粒度的分佈式對象共享機制用於提高集羣的性能。

4.2.8.1 Jrun的Jini技術

Jrun4將它的集羣解決方案構在Jini技術之上。Jini爲分佈式計算而生,它允許在一個單一的分佈式計算空間內創建“聯合”的設備或組件。 Jini提供查找,註冊,租用等分佈式系統服務,這對集羣環境非常有用。另一種稱爲JavaSpace的技術構建於Jini之上,它提供了一些用於實現集羣非常有價值的特性,如對象處理,共享,遷移等。要了解有關Jini和JavaSpace更多的信息,請參考http://java.sun.com/products/jini/2_0index.html

4.2.8.2 Tangosol的分佈式緩存

Tangosol Coherence提供了一個分佈式數據管理平臺,它可以嵌入到大多數流行的J2EE服務器中用於實現集羣環境。Tangosol Coherence同時也是提供了分佈式緩存系統用於在不同的JVM之間有效地共享Java對象。要了解有關Tangosol更多的信息,請參考http://www.tangosol.com

 

5 JNDI集羣實現

J2EE規範要求所有的J2EE容器必須提供JNDI規範的實現。JNDI在J2EE應用程序中的主要角色是用來提供一個間接層,這樣資源可以很容易被找到,而不用關心細節。這使得J2EE組件更加可複用。

擁用全特性的集羣的JNDI對於J2EE集羣是非常重要的。所有的EJB調用都開始於在JNDI樹上查找它的Home接口,J2EE供應商根據他們的集羣結構採用不同的方式實現JNDI集羣。

5.1 共享全局JNDI樹 
WebLogic和Jboss都有一個全局的,共享的,集羣範圍的JNDI Context供客戶端查找和綁定對象,綁定的全局JNDI Context中對象將通過IP廣播的方式在集羣中複製,這樣當一臺服務器實例停機後,被綁定的對象仍然可供查找。

 
圖 14  共享的全局JNDI

如圖14所示,共享的全局JNDI樹實際包括了所有本地JNDI結點上綁定的對象。集羣上每個結點都擁有自己的名稱服務器,它們與集羣中其他服務器相互複製所有的東西。這樣每個名稱服務器上都擁有其他名稱服務器對象樹的拷貝。這種冗餘結構使得全局JNDI樹高可用。

實際上,集羣的JNDI樹可以被用做兩個目的。可以被管理員用來部署對象和服務。在一臺服務中部署完EJB模塊或JDBC&JMS服務後,JNDI樹上的所有對象都將複製到其他服務器實例中。在運行期,程序可以JNDI API訪問JNDI樹存儲或者獲取對象,這些對象也將被全局複製。

5.2 獨立JNDI
Jboss和WebLogic都採用了共享全局JNDI樹的方式,Sun JES和IBM WebSphere等採用了每個服務器都擁有獨立的JNDI樹的方式。在使用獨立JNDI樹的集羣中,成員服務器不必知道或關心集羣中其他服務器。這是否意味着不想把JNDI集羣呢?所有EJB訪問都開始於在JNDI樹上查找它們的Home接口,如果沒有可集羣的JNDI樹,集羣就根本無用。

實際上,如果J2EE應用程序是相似的,獨立的JNDI樹仍然可以獲得高可用性。當集羣中所有實例都有相同的設置以及都部署相同的應用程序集,我們說集羣是“相似的”,這種情況下,一種被稱爲代理的特殊管理工具可以幫助我們獲取高可用性,如圖15所示:

 

 
圖 15  獨立JNDI

不管是Sun JES還是WebSphere都在集羣的實例上安裝了結點代理,當部署EJB模塊或綁定其他JNDI服務,管理控制員可以向所有的代理髮出命令,以此實現與共享全局JNDI相同的效果。

但是獨立JNDI的方案不能支持複製在運行期綁定或獲取的對象。有以下幾個原因:JNDI在J2EE應用程序中的主要角色是用來提供管理外部資源的間接層,並不是用來做數據存儲。如果有這樣的需求,可以採用具有HA(高可用性)特性的獨立的LDAP服務器或數據庫。Sun和IBM自己都有這樣擁有集羣特性的獨立的LDAP服務器產品。

5.3 中心JNDI
少數J2EE產品使用了中心JNDI方案,這種方案中名稱服務器駐留在單一服務器中,所有的服務器實例都註冊它們相同的EJB組件和管理對象到單一的服務器中。

名稱服務器實現了高可用性,這對客戶端是透明的。所有的客戶端都在這臺服務器中查找EJB組件,但是這種結構意味着複雜的安裝和管理,慢慢被多數供應商拋棄。

5.4 初始化訪問JNDI服務器 
當然客戶端要訪問JNDI服務器的時候,它們需要知道遠程JNDI服務器的主機名/IP地址和端口,在全局或是獨立JNDI樹的方案中,有多個JNDI服務器。客戶端第一次訪問時應該連接哪個呢?如何獲得負載均衡和失效轉移呢?

從技術上說,一個軟件或硬件負載均衡器可以設在遠程客戶端和所有的JNDI服務器之間執行負載均和失效轉移。但是,很少有供應商實現這種方式,這裏有些簡單的方案:

  • Sun JES和Jboss 實現JNDI集羣是在“java.naming.provider.url”屬性中設置一列用逗號分隔的URL,如 java.naming.provider.url=server1:1100,server2:1100:server3.1100:server4.1100 客戶端將挨個聯繫列表中的服務器,一旦其中一個響應了便中止。
  • Jboss同時也實現了自動發現的特性,當設置屬性串“java.naming.provider.url”爲空時,客戶端將試圖通過網絡廣播來發現在一個JNDI服務器。

6 EJB集羣實現

EJB是J2EE技術中重要的部分,並且EJB集羣是實現J2EE集羣最大的挑戰。

EJB技術是爲分佈式計算而生。它們可以在獨立的服務器中運行。Web服務器組件或富客戶端可以從其他的機器通過標準協議(RMI/IIOP)來訪問EJB。你可以象調用你本地Java對象的方法一樣調用遠程EJB的方法。實際上,RMI/IIOP完全掩蓋了你正在調用的對象是本地的還是遠程的,這被稱作本地/遠程透明性。

 
圖 16  EJB調用機制

上圖顯示了遠程EJB的調用機制。當客戶端想使用EJB,它不能直接調用,相反,客戶端只能調用一個被稱爲“stub”的本地對象,它扮演了到遠程對象代理的角色,並且有遠程對象相同的接口。這個對象負責接受本地方法調用,並且這些調用通過網絡代理到遠程EJB。這些對象在客戶JVM中運行,並且知道如何通過RMI/IIOP跨過網絡查找真正的對象。要了解有關EJB更多的信息,請參考http://java.sun.com/products/ejb/

爲解釋EJB集羣的實現,我們先看看在J2EE代碼中如何使用EJB的。爲了調用EJB,我們需要

  • 在JNDI服務器中查找EJBHome stub
  • 使用EJBHome stub查找或創建EJB對象,這樣獲得一個EJBObject stub
  • 在EJBObject stub上調用方法

負載均衡和失效轉移可以在JNDI查找時發生,這我們已在上面闡述了。在EJB stub(包括EJBHome和EJBObject)的方法調用時,供應商採用以下三種方式實現EJB負載均衡和失效轉移。

6.1 智能存根(Smart stub)
正如我們所知,客戶端可以通過存根對象(stub)來訪門遠程的EJB,這個對象可以通過JNDI樹獲取,甚至客戶端可能透明地從WEB服務器上下載存根類文件。

這樣存根就可以在運行期動態地用程序生成,而其類文件也不必在客戶端環境的classpath或庫中。(因爲它是可以被下載的)

 
圖 17  智能存根

如圖17所示,BEA WebLogic和Jboss通過在存根代碼中組合幾種行爲來實現EJB集羣,而這些都是在客戶端透明運行的(客戶端不需要了解這些代碼)。這種技術叫做智能存根。

智能存根確實很聰明,它包含一組它可以訪問的目標實例,可以檢測這些實例的任何失效,它也包含了複雜的負載均衡和失效轉移的邏輯,用來分發請求到目標實例。而且,如果集羣拓樸改變了的話(比如新增或刪除了服務器實例),存根可以更新它的目標實例清單來反映新的拓樸,而不需要手工重新配置。

使用智能存根實現集羣有以下優點:

  • 因爲EJB存根運行在客戶端,所以它可以節省許多服務器資源。
  • 負載均衡是在客戶端代碼中,並且與客戶端的生命週期相關。這樣就消除了負載均衡器的單點失效。如果負載均衡器失效了,就意味着客戶端也失效了,這種情況是能接受的。
  • 存根可動態的自動下載更新,這意味着零維護。

6.2 IIOP運行期庫 
Sun JES Application Server使用了另一種方法實現EJB集羣。負載均衡和失效轉移邏輯是在IIOP運行庫中實現的。比如,JES修改了“ORBSocketFactory”的實現,使它能支持集羣。如圖18所示。

 
圖 18  IIOP運行期

“ORBSocketFactory”的修改版有實現負載均衡和失效轉移的所有邏輯和算法,這樣就保持了存根乾淨和小。因爲是在運行庫中實現的,這樣就比存根的方式更容易獲得系統資源。但這種方式需要在客戶端運行一個特殊的運行庫,這樣就給與其他J2EE產品進行互操作時帶來了麻煩。

6.3 攔截器代理
IBM Webshpere採用了定位服務精靈(Location Service Daemon-LSD),它對EJB客戶端扮演了攔截器代理的角色,如圖19所示。


 

圖 19  攔截器代理

在這種方式中,客戶端通過JNDI查找獲取存根,這個存根將信息路由到LSD,而不是運行了EJB的應用程序服務器。這樣LSD接收到所有進來的請求,根據負載均衡和失效轉移的策略來判斷應該將它發送到哪個服務器實例。這種方式增加的安裝和維護集羣的額外的管理工作。

6.4 EJB的集羣支持 
要調用EJB的方法,要涉及到兩種存根對象,一是EJBHome接口,另一個是EJBObject接口。這意味着EJB潛在的需要在兩層上實現在負載均衡和失效轉移。

  • 當客戶端使用EJBHome存根創建或查找EJB對象時
  •  當客戶端調用EJB對象上的方法時。

6.4.1 EJBHome存根支持集羣

EJBHome接口用於在EJB容器中創建和查找EJB實例,而EJBHome存根是EJBHome接口的客戶端代理。EJBHome接口不需維持任何客戶端的狀態信息。這樣,來自不同EJB容器中的相同EJBHome接口對於客戶端來說是一致的。當客戶端執行create()或find()調用的時候,home存根根據負載均衡和失效轉移算法從多個相同的服務器實例中選擇一個,並將調用發送到該服務器的home接口上。

6.4.2 EJBObject存根支持集羣

當EJBHome接口創建一個EJB實例,它將返回EJBObject的存根給客戶端,並讓用戶調用EJB的業務方法。集羣中已有一組可用的服務器實例,並在上面的部署了bean,但是不能將EJBObject存根對象向EJBObject接口發出調用轉發到任意一個服務器實例上,這取決於EJB的類型。

無狀態會話Bean是最簡單的情況,因爲不涉及到狀態,所有的實例都可以認爲是一樣的,這樣對EJBObject的方法調用可負載均衡和失效轉移到任意一個服務器實例上。

集羣的有狀態會話Bean與無狀態會話Bean有一點不同,正如我們所知,有狀態會話Bean對於客戶端連續的請求會持有狀態信息。從技術上來說,集羣有狀態會話Bean與集羣HTTP Session是一樣的。在常規時間,EJBObject存根將不會把請求負載均衡到不同的服務器實例。相反,它將膠粘到最初創建的服務器實例上,我們稱這個實例爲“主實例”。在執行過程中,會話狀態會從主實例備份到其他服務上去。如果主實例失效了,備份服務器將接管它。

實體Bean本質上是無狀態的,儘管它可以處理有狀態的請求。所有的信息都將通過實體Bean本身的機制備份到數據庫中。這樣看起來實體Bean可以象無狀態會話Bean一樣很容易獲取負載均衡和失效轉移。但實際,實體Bean多數情況下是不負載均衡和失效轉移的。根據設計模式的建議,實體Bean 被會話外觀包裝。這樣,多數對實體Bean的訪問都是進程內會話Bean通過本地接口完成的,而不是遠程客戶端。這使得負載均衡和失效轉移沒有意義。

 

JMS和數據庫連接的集羣支持

除JSP,Servlet,JNDI和EJB之外,在J2EE中還有其他的分佈式對象。這些對象在集羣的實現中可能支持,可能不支持。

當前,一些數據庫產品,如Oracle RAC,支持集羣環境並可以部署到多複製,同步的數據庫實例中。然而,JDBC是一個高度狀態化的協議並且它的事務狀態直接與客戶端和服務器的 Socket連接綁定,所以很難獲取集羣能力。如果一個JDBC連接失效了,與該失效連接相關的所有JDBC對象也就失效了。客戶端代碼需要進行重連的動作。BEA WebLogic使用JDBC多池(multipool)技術來簡化這種重連過程。

JMS被多數J2EE服務器所支持,但支持得並不完全,負載均衡和失效轉移僅僅被JMS代理所實現,很少有產品在JMS Destination中的消息有失效轉移的功能。

 

8 J2EE集羣的神話

8.1 失效轉移可以完全避免錯誤——否定

在Jboss的文檔中,整個章節都在警告你“你真的需要HTTP會話的複製嗎?”。是的,有時沒有失效轉移的高可用性的解決方案也是可接受並且是廉價的。失效轉移並不是你想象的那麼強壯。

那麼失效轉移到底給你帶來了什麼?你可能想失效轉移可以避免錯誤。你看,沒有會話的失效轉移,當一個服務器實例失效後,會話數據將丟失而導致錯誤。通過失效轉移,會話可以從備份中恢復,而請求可以被其他服務器實例所處理,用戶根本意識不到失效。這是事實,但這是有條件的!

回想一樣我們定義的“失效轉移”,我們定義了一個條件是失效轉移是在“兩個方法調用之間”發生的。這是說如果你有兩個連續的對遠程對象的方法調用,失效轉移只會在第一調用成功後並且第二調用的請求發出前才能發生。

這樣,當遠程服務器在處理請求的過程中失效了會發生什麼呢?答案是:多數情況處理將會停止而客戶端將會看到錯誤信息。除非這個方法是等冪的(Idempotent),只有這個方法是等冪的,一些負載均衡器更智能,它會重試這些方法並將它失效轉移到其他實例上。

爲什麼“等冪”重要呢,因爲客戶端不知道當失效發生的時候請求被執行到什麼地方。是纔剛剛初始化還是差不多就要完成了?客戶端沒法判斷!如果方法不是等冪的,在相同方法上的兩次調用可能會兩次修改系統的狀態,而使得系統出現不一致的情形。

你可能想所有在事務中的方法都是等冪的,畢竟,如果錯誤發生,事務將被回滾,事務狀態的改變都將被複位。但事實上事務的邊界可能不包括所有的遠程方法調用過程。如果事務已經在服務器上提交了而返回給客戶端時網絡崩潰怎麼辦呢?客戶端不知道服務器的事務是否是成功了。

在一些應用程序中,將所有的方法都做成等冪的是不可能的。這樣,你只能通過失效轉移減少錯誤,而不是避免它們。拿在線商店爲例,假設每臺服務器可以同時處理100個在線用戶的請求,當一臺服務器失效了,沒有失效轉移的解決方案將丟失100個用戶的會話數據並激怒這些用戶。而有失效轉移的解決方案中,當失效發生的時候有20個用戶正在處理請求,這樣20個用戶將被失效激怒。而其他80個用戶正處於思考時間或在兩個方法調用之間,這些用戶可以透明地獲得失效轉移。這樣,你就需做以下的考慮:

  • 激怒20個用戶和激怒100個用戶造成影響的區別。
  • 採用失效轉移和不採用失效轉移產品成本的區別

8.2 獨立應用可以透明的遷移到集羣結構中——否定

儘管一些供應商宣稱他們的J2EE產品有這樣的靈活性。不要相信他們!事實你要在開始系統設計時就要準備集羣,而這將影響開發和測試的所有階段。

8.2.1 Http Session

在集羣環境中,如我前面提到的,使用HTTP Session有很多限制,這取決於你的應用程序服務器採用了那種會話失效轉移的機制。第一個重要的限制就是所有保存的HTTP Session中的對象必須是可序列化的,這將限制設計和應用程序結構。一些設計模式和MVC框架會用HTTP Session保存一些不序列化的對象(如ServletContext,EJB本地接口和WEB服務引用),這樣的設計不能在集羣中工作。第二,對象的序列的反序列化對性能的影響很大,特別是數據庫保存的方式。在這樣的環境中,應該避免在會話中保存大的或是衆多的對象。如果你採用了內存複製的方式,如前所述你必須小心在會話中屬性的交叉引用。其他在集羣環境中的主要區別是在會話不管任何屬性修改,你必須調用“setAttribute()”方法。這個方法調用在獨立的系統中是可選的。這個方法的目的是區別已修改的屬性和那些沒用到屬性,這樣系統可以只爲失效轉移備份必要的數據,從而提高性能。

8.2.2 緩存

我經歷過的大多數J2EE項目都用了緩存來提高性能,同時流行的應用程序服務器也都提供了不同程度的緩存用來加快應用程序的速度。但這些緩存都是爲那些典型的獨立環境設計的,只能在單JVM實例中工作。我們需要緩存是因爲一些對象很“重”,創建它需花費大量的時間和資源。因此我們維護了對象池用於重用這些對象,而不需要在後面創建。我們只有當維護緩存比創建對象更廉價時才能獲得性能的提高。在集羣環境,每個JVM實例都要維護一份緩存的拷貝,這些拷貝必須同步以維持所有服務器實例狀態的一致性。有時這種類型的同步會比沒有緩存帶來更糟的性能。

8.2.3 Static變量

當我們設計J2EE應用程序時,在架構上經常會使用一些設計模式。這些如“Singleton”的設計模式會用到靜態變量來在多對象之間共享狀態。這種方式在單服務中工作得很好,但在集羣環境將失效。集羣中的每個實例都會在它的JVM實例中維護一份靜態變量的拷貝,這樣就破壞了模式的機制。一個使用靜態變量的例子就是用它來保持在線用戶數。用靜態變量來保存在線用戶數是一個很簡單的辦法,當用戶進入或離開時就增加和減少它。這種方式在單服務器中絕對是好的,但在集羣環境將失效。在集羣中更好的方式是將所有狀態保存到數據庫。

8.2.4 外部資源

儘管J2EE規範不支持,但爲各種目的仍然會用外部I/O的操作。例如,一些應用會使用文件系統來保存用戶上傳的文件,或是創建一個動態配置的 XML文件。在集羣環境是沒有辦法來在其他實例之間來複制這些文件的。爲了在集羣中工作,辦法是用數據庫作爲外部文件的存放點,另外也可以使用SAN(存儲區域網,Storage Area Network)作爲存放點。

8.2.5 特殊服務

一些特殊的服務只在獨立的環境中才有意義,定時服務就一個很好例子,這種服務可以在一個固定的間隔時間有規律的觸發。定時服務常用於執行一些自動化管理任務。如日誌文件滾動,系統數據備份,數據庫一致性檢查和冗餘數據清理等。一些基於事件的服務也很難被遷移到集羣環境中。初始化服務就是個好例子,它只在整個系統啓動時才發生。郵件通知服務也一樣,它在一些警告條件下觸發。

這些服務是被事件而不是被請求觸發的,而且只被執行一次。這些服務使得負載均衡和失效轉移在集羣中沒多少意義。

一些產品準備了這些服務,如Jboss使用了“集羣單例設施”來協調所有實例,保證執行這些服務一次且僅有一次。基於你所選擇的平臺,一些特殊的服務可能會是把你的應用遷移到集羣結構中的障礙。

8.3 分佈式結構比並置結構更靈活——不一定

J2EE技術,尤其是EJB,天生就是用來做分佈式計算。解耦業務功能,重用遠程組件,這些使得多層應用非常流行。但是我們不能將所有的東西都分佈。一些J2EE架構師認爲Web層與EJB層並置得越近越好。這些計論後面會繼續。先讓我解釋一下。

 
圖 20  分佈式結構

如圖20所示,這是一個分佈式結構。當請求來了,負載均衡器將請求分發到不同服務器中的不同WEB容器,如果請求包含了EJB調用,WEB容器將重發EJB調用到不同的EJB容器。這樣,請求將被負載均衡和失效轉移兩次。

一些人看分佈式結構,他們會指出:

  • 第二次負載均衡沒有必要,因爲它不會使任務分配更平坦。每個服務器實例都有它們自己的WEB容器和EJB容器。把EJB容器用來處理來自其他實例WEB容器的請求比只在服務器內部調用並沒有什麼優勢。
  • 第二次失效轉移沒有必要,因爲它不能是高可用性。多數供應商實現J2EE服務器都會在同一服務器中運行的WEB容器和EJB容器放在一個JVM實例中。如果EJB容器失效了,多數情況下在同一個JVM中的WEB容器也將同時失效。
  • 性能將下降。想像一下對你的應用的一次調用包含一組對EJB的調用,如果你負載均衡了這些EJB,這將跨越每個服務器實例,導致許多不必要的服務器到服務器的交互。還有,如果這個方法在事務範圍內,那麼事務邊界將包含許多服務器實例,這將嚴重影響性能。

實際上在運行期,多數的供應商(包括Sun JES,WebLogic和Jboss)都會優化EJB調用機制,使請求首先選擇同一個服務器中的EJB容器。這樣,如圖21所示,我們只在第一層(WEB層)做負載均衡,然後調用相同服務器上的服務。這種結構我們稱之爲並置結構。技術上說,並置結構是分佈式結構的一種特例。

 
圖 21  並置結構

一個有趣的問題是,既然多數的部署在運行期都演進成了並置結構,爲什麼不用本地接口代替遠程接口,這將大提高性能。你當然可以,但是記住,當你使用本地接口後,WEB組件和EJB耦合得很緊,而方法調用也是直接的而不通過RMI/IIOP。負載均衡和失效轉移分發器沒有機會介入本地接口調用。 “WEB+EJB”整體處理負載均衡和失效轉移。

但不幸的是,在集羣中使用本地接口在多數J2EE服務器中有侷限性。使用本地接口的EJB是本地對象,是不可序列化的,這一個限制就使本地引用不能保存在HTTP Session中。一些產品,如Sun JES,會將本地接口區別看待,使它們可以序列化。這樣就可以用在HTTP Session中。

另一個有趣的問題是,既然並置結構這麼流行並且有好的性能,爲什麼還要分佈式結構呢?這在多數情況下是有道理的,但有時分佈式結構是不可替代的。

  • EJB不僅被WEB容器使用,富客戶端也會使用它。
  • EJB組件和WEB組件需在不同的安全級別上,並需要物理分離。這樣防火牆將被設置用於保護運行EJB的重要機器。
  • WEB層和EJB層極端不對稱使得分佈式結構是更好的選擇。比如,一些EJB組件非常複雜並且很消耗資源,它們只能運行在昂貴的大型服務器上,另一方面,WEB組件(HTML,JSP和Servlet)簡單得只需廉價的PC服務器就能滿足要求。在這種情況下,專門的WEB服務器可以用來接受客戶端連接請求,很快處理靜態數據(HTML和圖像)和簡單的WEB組件(JSP和Servlet)。大型服務器只被用來做複雜計算。這將更好的利用投資。

9 結論 
集羣與獨立環境不同,J2EE供應商採用不同的方法來實現集羣。如果你的項目爲做到高伸縮性而使用集羣,你應該在你的項目開始的時候就做準備。選擇符合你的需求的正確的J2EE產品。選擇正確的第三方軟件和框架並確保它們能支持集羣。最後設計正確的架構使得能從集羣中受益而不是受害。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章