<rt id="bn8ez"></rt>
<label id="bn8ez"></label>

  • <span id="bn8ez"></span>

    <label id="bn8ez"><meter id="bn8ez"></meter></label>

    【譯】構(gòu)建可擴(kuò)展的Java EE應(yīng)用(一)

    原文地址: http://www.theserverside.com/tt/articles/article.tss?l=ScalingYourJavaEEApplications

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

    我一直為一家互聯(lián)網(wǎng)性質(zhì)的實驗室工作,這個實驗室采用我們公司最新的大型服務(wù)器環(huán)境為合作伙伴的產(chǎn)品和解決方案免費做性能測試,我工作的部分就是幫助他們在強大的CMT和SMP服務(wù)器上進(jìn)行性能調(diào)優(yōu)。

    這些年來,我已經(jīng)為不同的解決方案測試了數(shù)十種java應(yīng)用。許多的產(chǎn)品都是為了解決同樣的領(lǐng)域問題,因此這些產(chǎn)品的功能基本都是類似的,但在可擴(kuò)展性上表現(xiàn)的卻非常不同,其中有些不能擴(kuò)展到64 CPU的服務(wù)器上運行,但可以擴(kuò)展到20臺服務(wù)器做集群運行,有些則只能運行在不超過2 CPU的機(jī)器上。

    造成這些差別的原因在于設(shè)計產(chǎn)品時的架構(gòu)愿景,所有的具備良好擴(kuò)展性的java應(yīng)用從需求需求階段、系統(tǒng)設(shè)計階段以及實現(xiàn)階段都為可擴(kuò)展性做了考慮,所以,你所編寫的java應(yīng)用的可擴(kuò)展能力完全取決于你的愿景。

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

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

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

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

    這篇文章的第一部分來討論下垂直擴(kuò)展Java應(yīng)用。

    如何讓Java EE應(yīng)用垂直擴(kuò)展

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

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

    為什么會這樣呢?首先,如果你的系統(tǒng)是多進(jìn)程或多線程的,并且已經(jīng)用盡了CPU的資源,那么在這種情況下增加CPU通常能讓應(yīng)用很好的得到擴(kuò)展。

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

    但在我的實驗室中,我發(fā)現(xiàn)很多的產(chǎn)品并不能充分的使用CPU,有些應(yīng)用在8 CPU的服務(wù)器上只能使用到不到20%的CPU,像這類應(yīng)用即使增加CPU也提升不了多少的。

    熱鎖(Hot Lock)是可擴(kuò)展性的關(guān)鍵障礙

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

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

    ..
    "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 關(guān)鍵字強制執(zhí)行器串行的執(zhí)行synchronized中的動作。如果很多線程競爭同樣的同步對象,那么只有一個線程能夠執(zhí)行同步塊,而其他的線程就只能進(jìn) 入blocked狀態(tài)了,如果此時沒有其他需要執(zhí)行的線程,那么處理器就進(jìn)入空閑狀態(tài)了,在這種情況下,增加CPU也帶來不了多少性能提升。

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

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

    盡可能的縮短同步塊

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

    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);
    }
    }

    上面的代碼片段是為了當(dāng)更新"schema"變量時保護(hù)這個共享的變量。但獲取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);
        }
    }
    
    

    減小鎖的粒度

    當(dāng)你使用"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 == nullreturn false;
             
    else return schema.update(nodeName,nodeAttributes);
         }

         
    public boolean synchronized updateTreeNodes() {
             
         }
    }
    對比Code list 2中的代碼,這段代碼就顯得更糟糕些了,因為當(dāng)調(diào)用"updateSchema"方法時,它鎖定了整個
    對象,為了獲得更好的粒度控制,應(yīng)該僅僅鎖定"schema"變量來替代鎖定整個對象,這樣其他不同的方法就可
    以保持并行執(zhí)行了。

    避免在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)

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

    在Java SE 5.0中使用lock free的數(shù)據(jù)結(jié)構(gòu)

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

    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"塊也是非常的短。但是鎖是非常重量級(當(dāng)鎖被其他線程持有時,線程會去頻繁嘗試獲取鎖)的,吞吐量會下降,并且同步鎖的競爭也是很昂貴的。

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

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

    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;
    }

    首先,我們從地址上讀取一個值,然后執(zhí)行幾步操作來產(chǎn)生新的值(例子中只是做加1的操作),最后使用CAS方式來將地址中的舊值改變?yōu)樾轮?。如果在時間片段 內(nèi)地址上的值未改變,那么CAS操作將成功。如果另外的線程同時修改了地址上的值,那么CAS操作將失敗,但會檢測到這個操作失敗,并在while循環(huán)中 進(jìn)行重試。CAS最好的原因在于它是硬件級別的實現(xiàn)并且非常輕量級,如果100個線程同時執(zhí)行這個increment()方法,最糟糕的情況是在 increment方法執(zhí)行完畢前每個線程最多嘗試99次。

    在Java SE 5.0和以上版本的java.util.concurrent.atomic包中提供了在單個變量上lock-free和線程安全操作支持的類。這些原子 變量的類都提供了比較和交換的原語,它基于各種平臺上可用的最后的native的方式實現(xiàn),這個包內(nèi)提供了九種原子變量,包 括: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采用了原子變量來直接實現(xiàn)wait-free算法,而像ConcurrentHashMap則采用
    ReentrantLock來實現(xiàn)必要的鎖,而ReentrantLock則是采用原子變量來維護(hù)所有等待鎖的線程隊列。

    在我們實驗室中一個最成功的關(guān)于lock free算法的案例發(fā)生在一個金融系統(tǒng)中,當(dāng)將"Vector"數(shù)據(jù)結(jié)構(gòu)替換為"ConcurrentHashMap"后,在我們的CMT機(jī)器(8核)性能提升了超過3倍。

    競爭條件也會導(dǎo)致可擴(kuò)展性出現(xiàn)問題

    太多的"synchronized"關(guān)鍵字會導(dǎo)致可擴(kuò)展性出現(xiàn)問題。但在某些場合,缺少"synchronized"也會導(dǎo)致系統(tǒng)無法垂直擴(kuò)展。缺少"synchronized"會產(chǎn)生競爭場景,在這種場景下允許兩個線程同時修改共享的資源,這有可能會造成破壞共享數(shù)據(jù),為什么我說它會導(dǎo)致可擴(kuò)展性出現(xiàn)問題呢?

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

    產(chǎn)生這個死循環(huán)的根源在于對一個未保護(hù)的共享變量 -- 一個"HashMap"數(shù)據(jù)結(jié)構(gòu)的操作。當(dāng)在所有操作的方法上加了"synchronized"后,一切恢復(fù)了正常。檢查"HashMap"(Java SE 5.0)的源碼,我們發(fā)現(xiàn)有潛在的破壞其內(nèi)部結(jié)構(gòu)最終造成死循環(huán)的可能。在下面的代碼中,如果我們使得HashMap中的entries進(jìn)入循環(huán),那 么"e.next()"永遠(yuǎn)都不會為null。

    Code list 7:

    public V get(Object key) {
        
    if (key == nullreturn 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()以及其他對外暴露的方法都會有這個風(fēng)險,這算jvm的bug嗎?應(yīng)該說不是的,這個現(xiàn)象很早以前就報告出來了(詳細(xì)見:http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6423457)。Sun的工程師并不認(rèn)為這是bug,而是建議在這樣的場景下應(yīng)采用"ConcurrentHashMap",在構(gòu)建可擴(kuò)展的系統(tǒng)時應(yīng)將這點納入規(guī)范中。

    非阻塞 IO vs. 阻塞IO

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

    在最近的5個月內(nèi),我們實驗室有兩個Java EE項目測試對比了基于傳統(tǒng)的阻塞I/O構(gòu)建的服務(wù)器和非阻塞I/O構(gòu)建的服務(wù)器上的性能。他們選擇了Tomcat 5作為基于阻塞I/O的服務(wù)器,Glassfish作為基于非阻塞I/O的服務(wù)器。

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

    Concurrent Users
    Average Response Time (ms)
    Tomcat
    Glassfish
    5
    30
    138
    15
    35
    142
    30
    37
    142
    50
    41
    151
    100
    65
    155


    從測試結(jié)果來看,Glassfish的性能遠(yuǎn)低于Tomcat??蛻魧Ψ亲枞鸌/O能夠帶來的提升表示懷疑,但為什么那么多的文章以及技術(shù)報告都告訴大家NIO具備更好的性能和可擴(kuò)展性呢?

    當(dāng)在更多的場景進(jìn)行測試后,隨著NIO的能力逐步的展現(xiàn)出來,他們改變了觀點,他們做了以下的測試:

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

    2、模擬更多的并發(fā)用戶,從1000到10000;

    3、在不同的硬件環(huán)境上進(jìn)行測試,從2 CPU、4 CPU到16 CPU。

    以下的圖為在4 CPU服務(wù)器上的測試結(jié)果:

    Figure 1: Throughput in a 4CPU server

    傳統(tǒng)的阻塞I/O為每個請求分配一個工作線程,這個工作線程負(fù)責(zé)請求的整個過程的處理,包括從網(wǎng)絡(luò)讀取請求數(shù)據(jù)、解析參數(shù)、 計算或調(diào)用其他的業(yè)務(wù)邏輯、編碼結(jié)果并將其返回給請求者,然后這個線程將返回到線程池中供其他線程復(fù)用。Tomcat 5采用的這種方式在應(yīng)對完美的網(wǎng)絡(luò)環(huán)境、簡單的邏輯以及小量的并發(fā)用戶時是非常高效的。

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

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

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

    Figure 2: scalability test result


    單線程任務(wù)問題

    幾個月前我們實驗室測試了一個基于Java EE的ERP系統(tǒng),它其中的一個測試場景是為了產(chǎn)生非常復(fù)雜的分析報告,我們在不同的服務(wù)器上測試了這個應(yīng)用場景,發(fā)現(xiàn)竟然是在最便宜的AMD PC服務(wù)器上擁有最好的性能。這臺AMD的服務(wù)器只有兩個2.8HZ的CPU以及4G的內(nèi)存,但它的性能竟然超過了昂貴的擁有8 CPU和32G內(nèi)存的SPARC服務(wù)器。

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

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

    重新調(diào)整架構(gòu)和重寫代碼的解決方案是一個耗時并且容易出現(xiàn)錯誤的工作。在我們實驗室中的一個項目中采用了JOMP來為其單線程的任務(wù)獲得并行性。JOMP是一個基于線程的SMP并行編程的Java API。就像OpenMP,JOMP也是根據(jù)編譯指示來插入并行運行的代碼片段到常規(guī)的程序中。在Java程序中,JOMP 通過//omp這樣的指示方式來表示需要并行運行的部分。JOMP程序通過運行一個預(yù)編譯器來處理這些//omp的指示并生成最終的java代碼,這些 java代碼再被正常的編譯和執(zhí)行。JOMP支持OpenMP的大部分特性,包括共享的并行循環(huán)和并行片段,共享變量,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也是關(guān)注于loop-level和集合的并行運算,研究如何同時執(zhí)行不同的迭代。為了并行 化,兩個迭代之間不能產(chǎn)生任何的數(shù)據(jù)依賴,這也就是說,不能依賴于其他任何一個執(zhí)行后產(chǎn)生的計算結(jié)果。要編寫一個JOMP程序并不是容易的事。首先,你必 須熟練使用OpenMP的指示,同時還得熟悉JVM對于這些指示的內(nèi)存模型映射,最后你需要知道在你的業(yè)務(wù)邏輯代碼的正確的地方放置正確的指示。

    另外一個選擇是采用Parallel Java。Parallel Java,就像JOMP一樣,也支持OpenMP的大部分特性;但又不同于JOMP,PJ的并行結(jié)構(gòu)部分是通過在代碼中調(diào)用PJ的類來實現(xiàn),而不是通過插 入預(yù)編譯的指示,因此,"Parallel Java"不需要另外的預(yù)編譯過程。Parallel Java不僅對于在多CPU上并行有效,對于多節(jié)點的擴(kuò)展能力上也同樣有效。以下的代碼是"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-1new 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]);
                                    }
                                }
                            }
                        });
                    }
                }
            });

    擴(kuò)展使用更多的內(nèi)存

    內(nèi)存是應(yīng)用的重要資源。足夠的內(nèi)存對于任何應(yīng)用而言都是關(guān)鍵的,尤其是數(shù)據(jù)庫系統(tǒng)和其他I/O操作頻繁的系統(tǒng)。更多的內(nèi)存意味著更大的共享內(nèi)存空間以及更大的數(shù)據(jù)緩沖,這也就使得應(yīng)用能夠更多的從內(nèi)存中讀取數(shù)據(jù)而不是緩慢的磁盤中讀取。

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

    回到Java應(yīng)用在給予更多的內(nèi)存時是否可以擴(kuò)展的問題上,答案是有些時候是的。太小的內(nèi)存會導(dǎo)致gc頻繁的執(zhí)行,足夠的內(nèi)存則保證JVM花費更多的時間來執(zhí)行業(yè)務(wù)邏輯,而不是進(jìn)行g(shù)c。

    但它并不一定是這樣的,在我們實驗室中出現(xiàn)的真實例子是一個構(gòu)建在64位JVM上的電信系統(tǒng)。使用64位JVM,應(yīng)用可以突破32位JVM中4GB內(nèi)存的限制,測試時使用的是一臺4 CPU/16G內(nèi)存的服務(wù)器,其中12GB的內(nèi)存分配給了java應(yīng)用使用,為了提高性能,他們在初始化時就緩存了超過3,000,000個的對象到內(nèi)存中,以免在運行時創(chuàng)建如此多的對象。這個產(chǎn)品在第一個小時的測試中運行的非常快,但突然,系統(tǒng)差不多停止運行了30多分鐘,經(jīng)過檢測,發(fā)現(xiàn)是因為gc導(dǎo)致了系統(tǒng)停止了半個小時。

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

    在我們實驗室中測試過的以內(nèi)存為中心的Java應(yīng)用中,我們發(fā)現(xiàn)具備有如下特征:

    1、每個請求的處理過程需要大量和復(fù)雜的對象;
    2、在每個會話的HttpSession對象中保存了太多的對象;
    3、HttpSession的timeout時間設(shè)置的太長,并且HttpSession沒有顯示的invalidated;
    4、線程池、EJB池或其他對象池設(shè)置的太大;
    5、對象的緩存設(shè)置的太大。

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

    為了擴(kuò)展這些以內(nèi)存為中心的java應(yīng)用,你需要多個jvm實例或者多臺機(jī)器節(jié)點。

    其他垂直擴(kuò)展的問題

    有些Java EE應(yīng)用的擴(kuò)展性問題并不在于其本身,有些時候外部系統(tǒng)的限制會成為系統(tǒng)擴(kuò)展能力的瓶頸,這些瓶頸可能包括:

    • 數(shù)據(jù)庫系統(tǒng):這在企業(yè)應(yīng)用和web 2.0應(yīng)用中是最常見的瓶頸,因為數(shù)據(jù)庫通常是jvm線程中共享的資源。因此數(shù)據(jù)庫執(zhí)行的效率、數(shù)據(jù)庫事務(wù)隔離的級別將會很明顯的影響系統(tǒng)的擴(kuò)展能力。我 們看到很多的項目將大部分的業(yè)務(wù)邏輯以存儲過程的方式放在數(shù)據(jù)庫中,而web層則非常的輕量,只是用來執(zhí)行下數(shù)據(jù)的過濾等,這樣的架構(gòu)在隨著請求數(shù)的增長 后會出現(xiàn)很多的擴(kuò)展性問題。
    • 磁盤IO和網(wǎng)絡(luò)IO。
    • 操作系統(tǒng):有些時候系統(tǒng)擴(kuò)展能力的瓶頸可能會出現(xiàn)在操作系統(tǒng)的限制上,例如,在同一個目錄下放了太多的文件,導(dǎo)致文件系統(tǒng)在創(chuàng)建和查找文件時變得非常的慢;
    • 同步logging:這是一個可擴(kuò)展性的常見問題。在有些案例中,可以通過采用Apache log4j來解決,或者采用jms消息來將同步的logging轉(zhuǎn)為異步執(zhí)行。

    這些不僅僅是Java EE應(yīng)用的問題,對于所有平臺的所有系統(tǒng)而言同樣如此。為了解決這些問題,需要從系統(tǒng)的各個層面來從數(shù)據(jù)庫管理員、系統(tǒng)工程師和網(wǎng)絡(luò)分析人員處得到幫助。

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

    posted on 2008-07-07 23:15 BlueDavy 閱讀(7803) 評論(16)  編輯  收藏 所屬分類: Internet

    評論

    # re: 【譯】構(gòu)建可擴(kuò)展的Java EE應(yīng)用(一) 2008-07-08 01:57 bottom

    在我們實驗室中一個最成功的關(guān)于lock free算法的案例發(fā)生在一個金融系統(tǒng)中,當(dāng)將"Vector"數(shù)據(jù)結(jié)構(gòu)替換為"ConcurrentHashMap"后,在我們的CMT機(jī)器(8核)性能提升了超過3倍。

    vector 和 map 是兩種不同的結(jié)構(gòu),我估計是原來用vector的contains之類的方法,當(dāng)然很慢。如果把vector換成普通HashMap,我估計性能會提高2.x倍。再把普通HashMap換成ConcurrentHashMap,性能提高剩下的(1-x)。3倍性能的提高主要是從list -> map的轉(zhuǎn)換得來的,與concurrent關(guān)系不大。

      回復(fù)  更多評論   

    # re: 【譯】構(gòu)建可擴(kuò)展的Java EE應(yīng)用(一) 2008-07-08 08:30 岑文初

    動作很快么,呵呵.昨天我才在theserverside看到,你今天就出中文版了。  回復(fù)  更多評論   

    # re: 【譯】構(gòu)建可擴(kuò)展的Java EE應(yīng)用(一) 2008-07-08 10:25 Robin's Java World

    他們中大部分人認(rèn)為可以借助昂貴的意見來縮小性能問題。

    看了半天這句沒看懂,一看原文才知道,是字打錯了。正確的似乎為:
    他們中大部分人認(rèn)為可以借助昂貴的硬件來縮小性能問題。  回復(fù)  更多評論   

    # re: 【譯】構(gòu)建可擴(kuò)展的Java EE應(yīng)用(一) 2008-07-08 10:50 路過

    不錯,翻譯得也很好啊。感謝。  回復(fù)  更多評論   

    # re: 【譯】構(gòu)建可擴(kuò)展的Java EE應(yīng)用(一) 2008-07-08 12:32 BlueDavy

    @Robin's Java World
    ...汗,不好意思....
    馬上修正  回復(fù)  更多評論   

    # re: 【譯】構(gòu)建可擴(kuò)展的Java EE應(yīng)用(一) 2008-07-09 13:05 javafuns

    不錯,支持你
    有些地方的翻譯似乎與原文有些出入  回復(fù)  更多評論   

    # re: 【譯】構(gòu)建可擴(kuò)展的Java EE應(yīng)用(一) 2008-07-09 20:35 網(wǎng)站設(shè)計

    看了半天這句沒看懂,一看原文才知道,是字打錯了。  回復(fù)  更多評論   

    # re: 【譯】構(gòu)建可擴(kuò)展的Java EE應(yīng)用(一) 2008-07-09 20:35 網(wǎng)站建設(shè)

    原來沒錯。正確的  回復(fù)  更多評論   

    # re: 【譯】構(gòu)建可擴(kuò)展的Java EE應(yīng)用(一) 2008-07-09 22:39 漢辰

    Synchronized static method 并不會鎖住所有的實例對象,而只是這個類的Class對象。

    所以Synchronized static method 并不是一個最糟糕的設(shè)計,這個類的非static同步方法仍然可以同時被不同的線程訪問。

    原文也不正確,已有讀者指出,作者也已改正。  回復(fù)  更多評論   

    # re: 【譯】構(gòu)建可擴(kuò)展的Java EE應(yīng)用(一) 2008-07-10 15:43 BlueDavy

    @漢辰
    恩,多謝指正,不過原文中最早寫的是這個問題只會出現(xiàn)在Java 1.4以下的版本中,在Java 5以上版本則沒有這個問題。  回復(fù)  更多評論   

    # re: 【譯】構(gòu)建可擴(kuò)展的Java EE應(yīng)用(一) 2008-07-10 18:39 44you

    我一個外行也看得明明白白,覺得很實在,期待第二篇  回復(fù)  更多評論   

    # re: 【譯】構(gòu)建可擴(kuò)展的Java EE應(yīng)用(一) 2008-07-10 20:27 nan

    在TSS上已經(jīng)讀到原文,沒有想到這么快就翻譯出來了,感覺scalability 翻譯成可伸縮性是否更恰當(dāng)點?  回復(fù)  更多評論   

    # re: 【譯】構(gòu)建可擴(kuò)展的Java EE應(yīng)用(一)[未登錄] 2008-07-11 10:36 BlueDavy

    @nan
    恩,是滴,最準(zhǔn)確的說法應(yīng)該是可伸縮性。  回復(fù)  更多評論   

    # re: 【譯】構(gòu)建可擴(kuò)展的Java EE應(yīng)用(一) 2008-07-29 10:49 Jack.Wang

    翻譯的很好了!  回復(fù)  更多評論   

    # re: 【譯】構(gòu)建可擴(kuò)展的Java EE應(yīng)用(一) 2009-02-23 23:12 seonj

    還是看中文舒服,英文太菜會  回復(fù)  更多評論   

    # re: 【譯】構(gòu)建可擴(kuò)展的Java EE應(yīng)用(一) 2009-09-11 13:08 啊時代發(fā)生大幅

    受益匪淺  回復(fù)  更多評論   

    公告

     









    feedsky
    抓蝦
    google reader
    鮮果

    導(dǎo)航

    <2008年7月>
    293012345
    6789101112
    13141516171819
    20212223242526
    272829303112
    3456789

    統(tǒng)計

    隨筆分類

    隨筆檔案

    文章檔案

    Blogger's

    搜索

    最新評論

    閱讀排行榜

    評論排行榜

    主站蜘蛛池模板: 一级人做人爰a全过程免费视频 | 羞羞漫画在线成人漫画阅读免费| 永久免费av无码网站yy| 一级毛片a免费播放王色| 理论秋霞在线看免费| 黄色毛片免费在线观看| 日韩a毛片免费观看| 人禽伦免费交视频播放| 久香草视频在线观看免费| 一个人免费观看日本www视频| 国产免费久久精品丫丫| 在线观看免费无码视频| 日韩插啊免费视频在线观看| 1000部无遮挡拍拍拍免费视频观看| 91免费国产精品| 中文字幕无码视频手机免费看| 两个人看www免费视频| 黄色网址在线免费| 嘿嘿嘿视频免费网站在线观看| 香蕉97超级碰碰碰免费公| 久久青草免费91线频观看不卡| 99久9在线|免费| 免费精品国产日韩热久久| 免费看香港一级毛片| 免费h成人黄漫画嘿咻破解版| 中文字幕影片免费在线观看| 成人性生交大片免费看午夜a| 免费**毛片在线播放直播| 亚洲午夜久久久影院| 亚洲va在线va天堂va不卡下载| 亚洲成a人片7777| 国产亚洲欧美在线观看| 精品人妻系列无码人妻免费视频| 99热这里只有精品免费播放| 成年女人喷潮毛片免费播放| 亚洲精品视频在线观看你懂的| 老司机亚洲精品影院| 亚洲人成网站999久久久综合| www免费黄色网| 国产h视频在线观看网站免费| 国产真实伦在线视频免费观看|