程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> JAVA編程 >> 關於JAVA >> 構建可擴展的Java EE應用(一)

構建可擴展的Java EE應用(一)

編輯:關於JAVA

對於一個具備使用價值的應用而言,其使用者有可能會在一段時間內瘋狂的增 長。隨著越來越多的關鍵性質的應用在Java EE上運行,很多的Java開發者也開始 關注可擴展性的問題了。但目前來說,大部分的web 2.0站點是基於script語言編 寫的,對於Java應用可擴展能力,很多人都抱著質疑的態度。在這篇文章中, Wang Yu基於他本身在實驗室項目的經驗來展示如何構建可擴展的java應用,同時 ,基於一些在可擴展性上做的比較失敗的項目給讀者帶來構建可擴展java應用的 實踐、理論、算法、框架和經驗。

我一直為一家互聯網性質的實驗室工作,這個實驗室采用我們公司最新的大型 服務器環境為合作伙伴的產品和解決方案免費做性能測試,我工作的部分就是幫 助他們在強大的CMT和SMP服務器上進行性能調優。

這些年來,我已經為不同的解決方案測試了數十種java應用。許多的產品都是 為了解決同樣的領域問題,因此這些產品的功能基本都是類似的,但在可擴展性 上表現的卻非常不同,其中有些不能擴展到64 CPU的服務器上運行,但可以擴展 到20台服務器做集群運行,有些則只能運行在不超過2 CPU的機器上。

造成這些差別的原因在於設計產品時的架構願景,所有的具備良好擴展性的 java應用從需求需求階段、系統設計階段以及實現階段都為可擴展性做了考慮, 所以,你所編寫的java應用的可擴展能力完全取決於你的願景。

可擴展性作為系統的屬性之一,是個很難定義的名詞,經常會與性能混淆。當 然,可擴展性和性能是有關系的,它的目的是為了達到高性能。但是衡量可擴展 性和性能的方法是不一樣的,在這篇文章中,我們采用wikipedia中的定義:

可擴展性是系統、網絡或進程的可選屬性之一,它表達的含義是可以以一種優 雅的方式來處理不斷增長的工作,或者以一種很明白的方式進行擴充。例如:它 可以用來表示系統具備隨著資源(典型的有硬件)的增加提升吞吐量的能力。

垂直擴展的意思是給系統中的單節點增加資源,典型的是給機器增加CPU或內 存,垂直擴展為操作系統和應用模塊提供了更多可共用的資源,因此它使得虛擬 化的技術(應該是指在一台機器上運行多個虛擬機)能夠運行的更加有效。

水平擴展的意思是指給系統增加更多的節點,例如為一個分布式的軟件系統增 加新的機器,一個更清晰的例子是將一台web服務器增加為三台。隨著計算機價格 的不斷降低以及性能的不斷提升,以往需要依靠超級計算機來進行的高性能計算 的應用(例如:地震分析、生物計算等)現在可以采用這種多個低成本的應用來 完成。由上百台普通機器構成的集群可以達到傳統的基於RISC處理器的科學計算 機所具備的計算能力。

這篇文章的第一部分來討論下垂直擴展Java應用。

如何讓Java EE應用垂直擴展

很多的軟件設計人員和開發人員都認為功能是產品中最重要的因素,而性能和 可擴展性是附加的特性和功能完成後才做的工作。他們中大部分人認為可以借助 昂貴的硬件來縮小性能問題。

但有時候他們是錯的,上個月,我們實驗室中有一個緊急的項目,合作伙伴提 供的產品在他們客戶提供的CPU的機器上測試未達到性能的要求,因此合作伙伴希 望在更多CPU(8 CPU)的機器上測試他們的產品,但結果卻是在8 CPU的機器上性 能反而比4 CPU的機器更差。

為什麼會這樣呢?首先,如果你的系統是多進程或多線程的,並且已經用盡了 CPU的資源,那麼在這種情況下增加CPU通常能讓應用很好的得到擴展。

基於java技術的應用可以很簡單的使用線程,Java語言不僅可以用來支持編寫 多線程的應用,同時JVM本身在對java應用的執行管理和內存管理上采用的也是多 線程的方式,因此通常來說Java應用在多CPU的機器上可以運行的更好,例如Bea weblogic、IBM Websphere、開源的Glassfish和Tomcat等應用服務器,運行在 Java EE應用服務器中的應用可以立刻從CMT和SMP技術中獲取到好處。

但在我的實驗室中,我發現很多的產品並不能充分的使用CPU,有些應用在8 CPU的服務器上只能使用到不到20%的CPU,像這類應用即使增加CPU也提升不了多 少的。

熱鎖(Hot Lock)是可擴展性的關鍵障礙

在Java程序中,用來協調線程的最重要的工具就是 synchronized這個關鍵字 了。由於java所采用的規則,包括緩存刷新和失效,Java語言中的synchronized 塊通常都會其他平台提供的類似的機制更加的昂貴。即使程序只是一個運行在單 處理器上的單線程程序,一個synchronized的方法調用也會比非同步的方法調用 慢。

要檢查問題是否為采用synchronized關鍵字造成的,只需要像JVM進程發送一 個QUIT指令(譯者注:在linux上也可以用kill -3 PID的方式)來獲取線程堆棧 信息。如果你看到類似下面線程堆棧的信息,那麼就意味著你的系統出現了熱鎖 的問題:

... ... ...
"Thread-0" prio=10 tid=0x08222eb0 nid=0x9 waiting for monitor entry [0xf927b000..0xf927bdb8]
at testthread.WaitThread.run(WaitThread.java:39)
- waiting to lock <0xef63bf08> (a java.lang.Object)
- locked <0xef63beb8> (a java.util.ArrayList)
at java.lang.Thread.run(Thread.java:595)
... ... ...

synchronized 關鍵字強制執行器串行的執行synchronized中的動作。如果很 多線程競爭同樣的同步對象,那麼只有一個線程能夠執行同步塊,而其他的線程 就只能進入blocked狀態了,如果此時沒有其他需要執行的線程,那麼處理器就進 入空閒狀態了,在這種情況下,增加CPU也帶來不了多少性能提升。

熱鎖可能會導致更多線程的切換和系統的調用。當多個線程競爭同一個 monitor時,JVM必須維護一個競爭此monitor的線程隊列(同樣,這個隊列也必須 同步),這也就意味著更多的時間需要花費在JVM或OS的代碼執行上,而更少的時 間是用在你的程序上的。

要避免熱鎖現象,以下的建議能帶來一些幫助:

盡可能的縮短同步塊

當你將線程中持有鎖的時間盡量縮短後,其他線程競爭鎖的時間也就變得更短 。因此當你需要采用同步塊來操作共享的變量時,應該將線程安全的代碼放在同 步塊的外面,來看以下代碼的例子:

Code list 1:

public boolean updateSchema(HashMap nodeTree) {
synchronized (schema) {
String nodeName = (String)nodeTree.get("nodeName");
String nodeAttributes = (List)nodeTree.get("attributes");
if (nodeName == null)
return false;
else
return schema.update(nodeName,nodeAttributes);
}
}

上面的代碼片段是為了當更新"schema"變量時保護這個共享的變量。但獲取 attribute值部分的代碼是線程安全的。因此我們可以將這部分移至同步塊的外面 ,讓同步塊變得更短一些:

Code list 2:

public boolean updateSchema(HashMap nodeTree) {
String nodeName = (String)nodeTree.get("nodeName");
String nodeAttributes = (List)nodeTree.get("attributes");
synchronized (schema) {
if (nodeName == null)
return false;
else
return schema.update(nodeName,nodeAttributes);
}
}

減小鎖的粒度

當你使用"synchronized"時,有兩種粒度可選擇:"方法鎖"或"塊鎖"。如果你 將"synchronized"放在方法上,那麼也就意味著鎖定了"this"對象。

Code list 3:

public class SchemaManager {
private HashMap schema;
private HashMap treeNodes;
...
public boolean synchronized updateSchema(HashMap nodeTree) {
String nodeName = (String)nodeTree.get("nodeName");
String nodeAttributes = (List)nodeTree.get("attributes");
if (nodeName == null) return false;
else return schema.update(nodeName,nodeAttributes);
}

public boolean synchronized updateTreeNodes() {
......
}
}

  

對比Code list 2中的代碼,這段代碼就顯得更糟糕些了,因為當調 用"updateSchema"方法時,它鎖定了整個

對象,為了獲得更好的粒度控制,應該僅僅鎖定"schema"變量來替代鎖定整個對 象,這樣其他不同的方法就可

以保持並行執行了。

避免在static方法上加鎖

最糟糕的狀況是在static方法上加"synchronized",這樣會造成鎖定這個class的 所有實例對象。

--------------------------------
at sun.awt.font.NativeFontWrapper.initializeFont(Native Method)
- waiting to lock <0xeae43af0> (a java.lang.Class)
at java.awt.Font.initializeFont(Font.java:316)
at java.awt.Font.readObject(Font.java:1185)
at sun.reflect.GeneratedMethodAccessor147.invoke(Unknown Source)
at sun.reflect.DelegatingMethodAccessorImpl.invoke (DelegatingMethodAccessorImpl.java:25)
at java.lang.reflect.Method.invoke(Method.java:324)
at java.io.ObjectStreamClass.invokeReadObject (ObjectStreamClass.java:838)
at java.io.ObjectInputStream.readSerialData (ObjectInputStream.java:1736)
at java.io.ObjectInputStream.readOrdinaryObject (ObjectInputStream.java:1646)
at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1274)
at java.io.ObjectInputStream.defaultReadFields (ObjectInputStream.java:1835)
at java.io.ObjectInputStream.readSerialData (ObjectInputStream.java:1759)
at java.io.ObjectInputStream.readOrdinaryObject (ObjectInputStream.java:1646)
at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1274)
at java.io.ObjectInputStream.defaultReadFields (ObjectInputStream.java:1835)
at java.io.ObjectInputStream.defaultReadObject (ObjectInputStream.java:452)
at com.fr.report.CellElement.readObject(Unknown Source)
... ...

當使用Java 2D來為報表生成字體對象時,開發人員放了一個native的static 鎖在"initialize"方法上,不過這是sun JDK 1.4中才會出現的,在JDK 5.0中, 這個static lock就消失了。

在Java SE 5.0中使用lock free的數據結構

在Java中,"synchronized"關鍵字是一個較簡單、並且相對來說比較好用的協 作機制,不過同時對於管理一個簡單的操作(例如增加統計值或更新一個值)來 說就顯得比較重量級了,就像以下的代碼:

Code list 4:

public class OnlineNumber {
private int totalNumber;
public synchronized int getTotalNumber() { return totalNumber; }
public synchronized int increment() { return ++totalNumber; }
public synchronized int decrement() { return --totalNumber; }
}

以上的代碼只是用來鎖定非常簡單的操作,"synchronized"塊也是非常的短。 但是鎖是非常重量級(當鎖被其他線程持有時,線程會去頻繁嘗試獲取鎖)的, 吞吐量會下降,並且同步鎖的競爭也是很昂貴的。

幸運的是,在Java SE 5.0或以上版本,你可以在不使用native代碼的情況下 使用硬件級同步語義的wait-free、lock-free的算法。幾乎所有現代的處理器都 具有檢測和防止其他處理器並發修改變量的基礎設施。這些基礎設施稱為比較並 交換,或CAS。

一個CAS操作包含三個參數 -- 一個內存地址,期待的舊的值以及新的值。 如 果內存地址上的值和所期待的舊的值是同一個的話,處理器將此地址的值更新為 新的值;否則它就什麼都不做,同時它會返回CAS操作前內存地址上的值。一個使 用CAS來實現同步的例子如下:

Code list 5:

public int increment() {
int oldValue = value.getValue ();
int newValue = oldValue + 1;
while (value.compareAndSwap(oldValue, newValue) != oldValue)
oldValue = value.getValue();
return oldValue + 1;
}

首先,我們從地址上讀取一個值,然後執行幾步操作來產生新的值(例子中只 是做加1的操作),最後使用CAS方式來將地址中的舊值改變為新值。如果在時間 片段內地址上的值未改變,那麼CAS操作將成功。如果另外的線程同時修改了地址 上的值,那麼CAS操作將失敗,但會檢測到這個操作失敗,並在while循環中進行 重試。CAS最好的原因在於它是硬件級別的實現並且非常輕量級,如果100個線程 同時執行這個increment()方法,最糟糕的情況是在 increment方法執行完畢前每 個線程最多嘗試99次。

在Java SE 5.0和以上版本的java.util.concurrent.atomic包中提供了在單個 變量上lock-free和線程安全操作支持的類。這些原子變量的類都提供了比較和交 換的原語,它基於各種平台上可用的最後的native的方式實現,這個包內提供了 九種原子變量,包括:AtomicInteger;AtomicLong;AtomicReference; AtomicBoolean;array forms of atomic integer、long、reference;和atomic marked reference和stamped reference類。

使用atomic包非常容易,重寫上面code list 5的代碼片段:

Code list 6:

import java.util.concurrent.atomic.*;
...
private AtomicInteger value = new AtomicInteger(0);
public int increment() {
return value.getAndIncrement();
}
...

幾乎java.util.concurrent包中所有的類都直接或間接的采用了原子變量來替代 synchronized。像

ConcurrentLinkedQueue采用了原子變量來直接實現wait-free算法,而像 ConcurrentHashMap則采用

ReentrantLock來實現必要的鎖,而ReentrantLock則是采用原子變量來維護所有 等待鎖的線程隊列。

在我們實驗室中一個最成功的關於lock free算法的案例發生在一個金融系統 中,當將"Vector"數據結構替換為"ConcurrentHashMap"後,在我們的CMT機器(8 核)性能提升了超過3倍。

競爭條件也會導致可擴展性出現問題

太多的"synchronized"關鍵字會導致可擴展性出現問題。但在某些場合,缺少 "synchronized"也會導致系統無法垂直擴展。缺少"synchronized"會產生競爭場 景,在這種場景下允許兩個線程同時修改共享的資源,這有可能會造成破壞共享 數據,為什麼我說它會導致可擴展性出現問題呢?

來看一個實際的例子。這是一個制作業的ERP系統,當在我們最新的一台CMT服 務器(2CPU、16核、128芯)上進行性能測試時,我們發現CPU的使用率超過90%, 這非常讓人驚訝,因為很少有應用能夠在這款機器上擴展的這麼好。但我們僅僅 興奮了5分鐘,之後我們發現平均響應時間非常的慢,同時吞吐量也降到不可思議 的低。那麼這些CPU都在干嘛呢?它們不是在忙嗎,那麼它們到底在忙些什麼呢? 通過OS的跟蹤工具,我們發現幾乎所有的CPU都在干同一件事-- "HashMap.get()" ,看起來所有的CPU都進入了死循環,之後我們在不同數量的CPU的服務器上再測 試了這個應用,結果表明,服務器擁有越多CPU,那麼產生死循環的概率就會越高 。

產生這個死循環的根源在於對一個未保護的共享變量 -- 一個"HashMap"數據 結構的操作。當在所有操作的方法上加了"synchronized"後,一切恢復了正常。 檢查"HashMap"(Java SE 5.0)的源碼,我們發現有潛在的破壞其內部結構最終造 成死循環的可能。在下面的代碼中,如果我們使得HashMap中的entries進入循環 ,那麼"e.next()"永遠都不會為null。

Code list 7:

public V get(Object key) {
if (key == null) return getForNullKey();
int hash = hash(key.hashCode());
for (Entry<K,V> e = table[indexFor(hash, table.length)];
e != null;
e = e.next) {
Object k;
if (e.hash == hash && ((k = e.key) == key || key.equals(k)))
return e.value;
}
return null;
}

不僅get()方法會這樣,put()以及其他對外暴露的方法都會有這個風險,這算 jvm的bug嗎?應該說不是的,這個現象很早以前就報告出來了(詳細見: http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6423457)。Sun的工程 師並不認為這是bug,而是建議在這樣的場景下應采用"ConcurrentHashMap",在 構建可擴展的系統時應將這點納入規范中。

非阻塞 IO vs. 阻塞IO

Java 1.4中引入的java.nio包,允許開發人員在進行數據處理時獲取更好的性 能並提供更好的擴展性。NIO提供的非阻塞IO操作允許java應用像其他底層語言( 例如c)一樣操作IO。目前已經有很多NIO的框架(例如Apache的Mina、Sun的 Grizzly)了被廣泛的使用在很多的項目和產品中。

在最近的5個月內,我們實驗室有兩個Java EE項目測試對比了基於傳統的阻塞 I/O構建的服務器和非阻塞I/O構建的服務器上的性能。他們選擇了Tomcat 5作為 基於阻塞I/O的服務器,Glassfish作為基於非阻塞I/O的服務器。

首先,他們測試了一些簡單的JSP頁面和servlets,得到如下結果:(在一台4 CPU的服務器上)

Concurrent Users

Average Response Time (ms)

Tomcat Glassfish 5 30 138 15 35 142 30 37 142 50 41 151 100 65 155

從測試結果來看,Glassfish的性能遠低於Tomcat。客戶對非阻塞I/O能夠帶來 的提升表示懷疑,但為什麼那麼多的文章以及技術報告都告訴大家NIO具備更好的 性能和可擴展性呢?

當在更多的場景進行測試後,隨著NIO的能力逐步的展現出來,他們改變了觀 點,他們做了以下的測試:

1、比簡單的JSP、servlet更為復雜的場景,包括EJB、數據庫、文件IO、JMS 和事務;

2、模擬更多的並發用戶,從1000到10000;

3、在不同的硬件環境上進行測試,從2 CPU、4 CPU到16 CPU。

以下的圖為在4 CPU服務器上的測試結果:

Figure 1: Throughput in a 4CPU server

傳統的阻塞I/O為每個請求分配一個工作線程,這個工作線程負責請求的整個 過程的處理,包括從網絡讀取請求數據、解析參數、計算或調用其他的業務邏輯 、編碼結果並將其返回給請求者,然後這個線程將返回到線程池中供其他線程復 用。Tomcat 5采用的這種方式在應對完美的網絡環境、簡單的邏輯以及小量的並 發用戶時是非常高效的。

但如果請求包括了復雜的邏輯、或需要和外部的系統(例如文件系統、數據庫 或消息服務器)進行交互時,工作線程在其處理的大部分時間都會處於等待同步 的調用或網絡傳輸返回的狀態中,這個阻塞的線程會被請求持有直到請求處理完 畢,但操作系統需要暫停線程來保證CPU能夠處理其他的請求,如果客戶端和服務 器端的網絡狀況不太好的話,網絡的延時會導致線程被阻塞更長時間,在更糟的 狀況下,當需要keep-alive的話,當前的工作線程會在請求處理完畢後阻塞很長 一段時間,在這樣的情況下,為了更好的使用CPU,就必須增加更多的工作線程了 。

Tomcat采用了一個線程池,每個請求都會被線程池中一個空閒的線程進行處理 。"maxThreads"表示Tomcat 能創建的處理請求的最大線程數。如果我們 把"maxThreads"設置的太小的話,就不能充分的使用CPU了,更為重要的是,隨著 並發用戶的增長,會有很多請求被服務器拋棄和拒絕。在此次測試中,我們 將"maxThreads"設置為了1000(這對於Tomcat來說有些太大了),在這樣的設置 下,當並發用戶增長到較高數量時,Tomcat會創建很多的線程。大量的Java線程 會導致JVM和OS忙於執行和維護這些線程,而不是執行業務邏輯處理,同時,太多 的線程也會消耗更多的JVM heap內存(每個線程堆棧需要占用一些內存),並且 會導致更為頻繁的gc。

Glassfish不需要這麼多的線程,在非阻塞IO中,一個工作線程並不會綁定到 一個特定的請求上,如果請求被某些原因所阻塞,那麼這個線程將被其他的請求 復用。在這樣的方式下,Glassfish可以用幾十個工作線程來處理幾千的並發用戶 。通過限制線程資源,非阻塞IO擁有了更好的可擴展性,這也是Tomcat 6采用非 阻塞IO的原因了。

Figure 2: scalability test result

單線程任務問題

幾個月前我們實驗室測試了一個基於Java EE的ERP系統,它其中的一個測試場 景是為了產生非常復雜的分析報告,我們在不同的服務器上測試了這個應用場景 ,發現竟然是在最便宜的AMD PC服務器上擁有最好的性能。這台AMD的服務器只有 兩個2.8HZ的CPU以及4G的內存,但它的性能竟然超過了昂貴的擁有8 CPU和32G內 存的SPARC服務器。

原因就在於這個場景是個單線程的任務,它同時只能被一個用戶運行(並發的 多用戶執行在這個案例中毫無意義),因此當運行時它只使用了一個CPU,這樣的 任務是沒法擴展到多個處理器的,在大多數時候,這種場景下的性能僅取決於CPU 的運行速度。

並行是解決這個問題的方案。為了讓一個單線程的任務並行執行,你需要按順 序找出這個操作的過程中從某種程度上來講不依賴的操作,然後采用多線程從而 實現並行。在上面的案例中,客戶重新定義了"分析報告產生"的任務,改為先生 成月度報告,之後基於產生的這些12個月的月度報告來生成分析報告,由於最終 用戶並不需要“月度報告”,因此這些“月度報告”只是臨時產生的結果,但"月 度報告"是可以並行生成的,然後用於快速的產生最後的分析報告,在這樣的方式 下,這個應用場景可以很好的擴展到4 CPU的SPARC服務器上運行,並且在性能上 比在AMD Server高80%多。

重新調整架構和重寫代碼的解決方案是一個耗時並且容易出現錯誤的工作。在 我們實驗室中的一個項目中采用了JOMP來為其單線程的任務獲得並行性。JOMP是 一個基於線程的SMP並行編程的Java API。就像OpenMP,JOMP也是根據編譯指示來 插入並行運行的代碼片段到常規的程序中。在Java程序中,JOMP 通過//omp這樣 的指示方式來表示需要並行運行的部分。JOMP程序通過運行一個預編譯器來處理 這些//omp的指示並生成最終的java代碼,這些 java代碼再被正常的編譯和執行 。JOMP支持OpenMP的大部分特性,包括共享的並行循環和並行片段,共享變量, thread local變量以及reduction變量。以下的代碼為JOMP程序的示例:

Code list 8:

Li n k e dLi s t c = new Li n k e dLi s t ( ) ;
c . add ( " t h i s " ) ;
c . add ( " i s " ) ;
c . add ( " a " ) ;
c . add ( "demo" ) ;
/ / #omp p a r a l l e l i t e r a t o r
f o r ( S t r i n g s : c )
System . o u t . p r i n t l n ( " s " ) ;

就像大部分的並行編譯器,JOMP也是關注於loop-level和集合的並行運算,研 究如何同時執行不同的迭代。為了並行化,兩個迭代之間不能產生任何的數據依 賴,這也就是說,不能依賴於其他任何一個執行後產生的計算結果。要編寫一個 JOMP程序並不是容易的事。首先,你必須熟練使用OpenMP的指示,同時還得熟悉 JVM對於這些指示的內存模型映射,最後你需要知道在你的業務邏輯代碼的正確的 地方放置正確的指示。

另外一個選擇是采用Parallel Java。Parallel Java,就像JOMP一樣,也支持 OpenMP的大部分特性;但又不同於JOMP,PJ的並行結構部分是通過在代碼中調用 PJ的類來實現,而不是通過插入預編譯的指示,因此,"Parallel Java"不需要另 外的預編譯過程。Parallel Java不僅對於在多CPU上並行有效,對於多節點的擴 展能力上也同樣有效。以下的代碼是"Parallel Java"程序的示例:

Code list 9:

static double[][] d;
new ParallelTeam().execute (new ParallelRegion()
{
public void run() throws Exception
{
for (int ii = 0; ii < n; ++ ii)
{
final int i = ii;
execute (0, n-1, new IntegerForLoop()
{
public void run (int first, int last)
{
for (int r = first; r <= last; ++ r)
{
for (int c = 0; c < n; ++ c)
{
d[r][c] = Math.min (d[r][c],
d[r][i] + d[i][c]);
}
}
}
});
}
}
});

擴展使用更多的內存

內存是應用的重要資源。足夠的內存對於任何應用而言都是關鍵的,尤其是數 據庫系統和其他I/O操作頻繁的系統。更多的內存意味著更大的共享內存空間以及 更大的數據緩沖,這也就使得應用能夠更多的從內存中讀取數據而不是緩慢的磁 盤中讀取。

Java gc將程序員從繁瑣的內存分配和回收中解脫了出來,從而使得程序員能 夠更加高效的編寫代碼。但gc不好的地方在於當gc運行時,幾乎所有工作的線程 都會被掛起。另外,在gc環境下,程序員缺少調度CPU來回收那些不再使用的對象 的控制能力。對於那些幾乎實時的系統而言,例如電信系統和股票交易系統,這 種延遲和缺少控制的現象是很大的風險。

回到Java應用在給予更多的內存時是否可以擴展的問題上,答案是有些時候是 的。太小的內存會導致gc頻繁的執行,足夠的內存則保證JVM花費更多的時間來執 行業務邏輯,而不是進行gc。

但它並不一定是這樣的,在我們實驗室中出現的真實例子是一個構建在64位 JVM上的電信系統。使用64位JVM,應用可以突破32位JVM中4GB內存的限制,測試 時使用的是一台4 CPU/16G內存的服務器,其中12GB的內存分配給了java應用使用 ,為了提高性能,他們在初始化時就緩存了超過3,000,000個的對象到內存中,以 免在運行時創建如此多的對象。這個產品在第一個小時的測試中運行的非常快, 但突然,系統差不多停止運行了30多分鐘,經過檢測,發現是因為gc導致了系統 停止了半個小時。

gc是從那些不再被引用的對象回收內存的過程。不被引用的對象是指應用中不 再使用的對象,因為所有對於這些對象的引用都已經不在應用的范圍中了。如果 一堆巨大的活動的對象存在在內存中(就像3,000,000個緩存的對象),gc需要花 費很長的時間來檢查這些對象,這就是為什麼系統停止了如此長乃至不可接受的 時間。

在我們實驗室中測試過的以內存為中心的Java應用中,我們發現具備有如下特 征:

1、每個請求的處理過程需要大量和復雜的對象;

2、在每個會話的HttpSession對象中保存了太多的對象;

3、HttpSession的timeout時間設置的太長,並且HttpSession沒有顯示的 invalidated;

4、線程池、EJB池或其他對象池設置的太大;

5、對象的緩存設置的太大。

這樣的應用是不好做擴展的,當並發的用戶數增長時,這些應用所使用的內存 也會大幅度的增長。如果大量的活動對象無法被及時的回收,JVM將會在gc上消耗 很長的時間,另外,如果給予了太大的內存(在64位JVM上),在運行了相對較長 的時間後,jvm會花費相當長的一段時間在 gc上,因此結論是如果給jvm分配了太 多的內存的話,java應用將不可擴展。在大部分場合下,給jvm分配3G內存(通 過"-Xmx"屬性)是足夠 (在windows和linux中,32位的系統最多只能分配2G的內存 )的。如果你擁有更多的內存,請將這些內存分配給其他的應用,或者就將它留給 OS 使用,許多OS都會使用空閒的內存來作為數據的緩沖和緩存來提升IO性能。實 時JVM(JSR001)可以讓開發人員來控制內存的回收,應用基於此特性可以告訴JVM :“這個巨大的內存空間是我的緩存,我將自己來管理它,請不要自動對它進行 回收”,這個功能特性使得Java應用也能夠擴展來支持大量的內存資源,希望JVM 的提供者們能將這個特性在不久的將來帶入到免費的JVM版本中。

為了擴展這些以內存為中心的java應用,你需要多個jvm實例或者多台機器節 點。

其他垂直擴展的問題

有些Java EE應用的擴展性問題並不在於其本身,有些時候外部系統的限制會 成為系統擴展能力的瓶頸,這些瓶頸可能包括:

數據庫系統:這在企業應用和web 2.0應用中是最常見的瓶頸,因為數據庫通 常是jvm線程中共享的資源。因此數據庫執行的效率、數據庫事務隔離的級別將會 很明顯的影響系統的擴展能力。我 們看到很多的項目將大部分的業務邏輯以存儲 過程的方式放在數據庫中,而web層則非常的輕量,只是用來執行下數據的過濾等 ,這樣的架構在隨著請求數的增長 後會出現很多的擴展性問題。

磁盤IO和網絡IO。

操作系統:有些時候系統擴展能力的瓶頸可能會出現在操作系統的限制上,例 如,在同一個目錄下放了太多的文件,導致文件系統在創建和查找文件時變得非 常的慢;

同步logging:這是一個可擴展性的常見問題。在有些案例中,可以通過采用 Apache log4j來解決,或者采用jms消息來將同步的logging轉為異步執行。

這些不僅僅是Java EE應用的問題,對於所有平台的所有系統而言同樣如此。 為了解決這些問題,需要從系統的各個層面來從數據庫管理員、系統工程師和網 絡分析人員處得到幫助。

這篇文章的第二個部分將來探討水平擴展的問題。

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved