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

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

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

    莊周夢蝶

    生活、程序、未來
       :: 首頁 ::  ::  :: 聚合  :: 管理

        在初步確定CMS參數后,系統運行了幾天,今天嘗試在線上打開了GC日志,按阿寶同學的說法是gc日志的開銷比之jstat還小,打開之后發現確實影響很小。打開GC日志之后又發現幾個隱藏的問題比較有價值,這里記錄下。

       首先是系統在啟動的時候有一次System.gc()調用引起的full gc,日志輸出類似這樣:
    1.201: [Full GC (System) 1.201: [CMS: 0K->797K(1310720K), 0.1090540 secs] 29499K->797K(1546688K), [CMS Perm : 5550K->5547K(65536K)], 0.1091860 secs] [Times: user=0.05 sys=0.06, real=0.11 secs]
       可以確認的是我們系統里的代碼絕對沒有調用System.gc()方法,但是不保證第三方代碼有調用,通過搜索代碼引用,后來定位到了mina的ByteBuffer創建上面。Mina 1.1封裝的ByteBuffer的allocate()方法默認創建的是Direct ByteBuffer,而DirectByteBuffer的構造函數里調用了
    Bits.reserveMemory(cap);

    這個方法強制調用了System.gc():
    static void reserveMemory(long size) {

        
    synchronized (Bits.class) {
            
    if (!memoryLimitSet && VM.isBooted()) {
            maxMemory 
    = VM.maxDirectMemory();
            memoryLimitSet 
    = true;
            }
            
    if (size <= maxMemory - reservedMemory) {
            reservedMemory 
    += size;
            
    return;
            }
        }

        System.gc();
        
    try {
            Thread.sleep(
    100);
        } 
    catch (InterruptedException x) {
            
    // Restore interrupt status
            Thread.currentThread().interrupt();
        }
        
    synchronized (Bits.class) {
            
    if (reservedMemory + size > maxMemory)
            
    throw new OutOfMemoryError("Direct buffer memory");
            reservedMemory 
    += size;
        }

        }
        調用這個方法是為了用戶對Direct ByteBuffer的內存可控。而在我們系統中使用的通訊層初始化Decoder的時候通過Mina 1.1創建了一個Direct ByteBuffer,導致了這一次強制的full gc。這個Buffer因為是長期持有的,因此創建Direct類型也還可以接受。

        但是在這次GC后,又發現每隔一個小時就有一次System.gc()引起的full gc,這就太難以忍受了,日志大概是這樣,注意間隔時間都是3600秒左右:
    10570.672: [Full GC (System) 10570.672: [CMS: 779199K->107679K(1310720K), 1.2957430 secs] 872163K->107679K(1546688K), [CMS Perm : 23993K->15595K(65536K)], 1.2959630 secs] [Times: user=1.27 sys=0.02, real=1.30 secs] 
    14171.971: [Full GC (System) 14171.971: [CMS: 680799K->83681K(1310720K), 1.0171580 secs] 836740K->83681K(1546688K), [CMS Perm : 20215K->15599K(65536K)], 1.0173850 secs] [Times: user=0.97 sys=0.01, real=1.02 secs] 
    17774.020: [Full GC (System) 17774.020: [CMS: 676201K->79331K(1310720K), 0.9652670 secs] 817596K->79331K(1546688K), [CMS Perm : 22808K->15619K(65536K)], 0.9655150 secs] [Times: user=0.93 sys=0.02, real=0.97 secs] 
    21374.989: [Full GC (System) 21374.989: [CMS: 677818K->78590K(1310720K), 0.9297080 secs] 822317K->78590K(1546688K), [CMS Perm : 16435K->15593K(65536K)], 0.9299620 secs] [Times: user=0.89 sys=0.01, real=0.93 secs] 
    24976.948: [Full GC (System) 24976.948: [CMS: 659511K->77608K(1310720K), 0.9255360 secs] 794004K->77608K(1546688K), [CMS Perm : 22359K->15594K(65536K)], 0.9257760 secs] [Times: user=0.88 sys=0.02, real=0.93 secs] 
    28578.892: [Full GC (System) 28578.892: [CMS: 562058K->77572K(1310720K), 0.8365500 secs] 735072K->77572K(1546688K), [CMS Perm : 15840K->15610K(65536K)], 0.8367990 secs] [Times: user=0.82 sys=0.00, real=0.84 secs] 
    32179.731: [Full GC (System) 32179.732: [CMS: 549874K->77224K(1310720K), 0.7864400 secs] 561803K->77224K(1546688K), [CMS Perm : 16016K->15597K(65536K)], 0.7866540 secs] [Times: user=0.75 sys=0.01, real=0.79 secs]

        搜遍了源碼和依賴庫,沒有再發現顯式的gc調用,問題只能出在運行時上,突然想起我們的系統使用RMI暴露JMX給監控程序,監控程序通過RMI連接JMX監控系統和告警等,會不會是RMI的分布式垃圾收集導致的?果然,一查資料,RMI的分布式收集會強制調用System.gc()來進行分布式GC,server端的間隔恰好是一個小時,這個參數可以通過:
    -Dsun.rmi.dgc.server.gcInterval=3600000
    來調整。調長時間是一個解決辦法,但是我們更希望能不出現顯式的GC調用,禁止顯式GC調用通過-XX:+DisableExplicitGC是一個辦法,但是禁止了分布式GC會導致什么問題卻是心理沒底,畢竟我們的JMX調用還是很頻繁的,幸運的是JDK6還提供了另一個選項-XX:+ExplicitGCInvokesConcurrent,允許System.gc()也并發運行,調整DGC時間間隔加上這個選項雙管齊下徹底解決了full gc的隱患。

        打開GC日志后發現的另一個問題是remark的時間過長,已經啟用了并行remark,但是時間還是經常超過200毫秒,這個可能的原因有兩個:我們的年老代太大或者觸發CMS的閥值太高了,CMS進行的時候年老代里的對象已經太多。初步的計劃是調小-XX:SurvivorRatio增大救助空間并且降低-XX:CMSInitiatingOccupancyFraction這個閥值。此外,還找到另一個可選參數-XX:+CMSScavengeBeforeRemark,啟用這個選項后,強制remark之前開始一次minor gc,減少remark的暫停時間,但是在remark之后也將立即開始又一次相對較長時間minor gc,如果你的minor gc很快的話可以考慮下這個選項,暫未實驗。


    posted @ 2009-09-22 20:58 dennis 閱讀(3491) | 評論 (0)編輯 收藏

        首先感謝阿寶同學的幫助,我才對這個gc算法的調整有了一定的認識,而不是停留在過去僅僅了解的階段。在讀過sun的文檔和跟阿寶討論之后,做個小小的總結。
        CMS,全稱Concurrent Low Pause Collector,是jdk1.4后期版本開始引入的新gc算法,在jdk5和jdk6中得到了進一步改進,它的主要適合場景是對響應時間的重要性需求大于對吞吐量的要求,能夠承受垃圾回收線程和應用線程共享處理器資源,并且應用中存在比較多的長生命周期的對象的應用。CMS是用于對tenured generation的回收,也就是年老代的回收,目標是盡量減少應用的暫停時間,減少full gc發生的幾率,利用和應用程序線程并發的垃圾回收線程來標記清除年老代。在我們的應用中,因為有緩存的存在,并且對于響應時間也有比較高的要求,因此希望能嘗試使用CMS來替代默認的server型JVM使用的并行收集器,以便獲得更短的垃圾回收的暫停時間,提高程序的響應性。
        CMS并非沒有暫停,而是用兩次短暫停來替代串行標記整理算法的長暫停,它的收集周期是這樣:
        初始標記(CMS-initial-mark) -> 并發標記(CMS-concurrent-mark) -> 重新標記(CMS-remark) -> 并發清除(CMS-concurrent-sweep) ->并發重設狀態等待下次CMS的觸發(CMS-concurrent-reset)。
        其中的1,3兩個步驟需要暫停所有的應用程序線程的。第一次暫停從root對象開始標記存活的對象,這個階段稱為初始標記;第二次暫停是在并發標記之后,暫停所有應用程序線程,重新標記并發標記階段遺漏的對象(在并發標記階段結束后對象狀態的更新導致)。第一次暫停會比較短,第二次暫停通常會比較長,并且remark這個階段可以并行標記。

        而并發標記、并發清除、并發重設階段的所謂并發,是指一個或者多個垃圾回收線程和應用程序線程并發地運行,垃圾回收線程不會暫停應用程序的執行,如果你有多于一個處理器,那么并發收集線程將與應用線程在不同的處理器上運行,顯然,這樣的開銷就是會降低應用的吞吐量。Remark階段的并行,是指暫停了所有應用程序后,啟動一定數目的垃圾回收進程進行并行標記,此時的應用線程是暫停的。

        CMS的young generation的回收采用的仍然是并行復制收集器,這個跟Paralle gc算法是一致的。

        下面是參數介紹和遇到的問題總結,

    1、啟用CMS:-XX:+UseConcMarkSweepGC。 咳咳,這里犯過一個低級錯誤,竟然將+號寫成了-號

    2。CMS默認啟動的回收線程數目是  (ParallelGCThreads + 3)/4) ,如果你需要明確設定,可以通過-XX:ParallelCMSThreads=20來設定,其中ParallelGCThreads是年輕代的并行收集線程數

    3、CMS是不會整理堆碎片的,因此為了防止堆碎片引起full gc,通過會開啟CMS階段進行合并碎片選項:-XX:+UseCMSCompactAtFullCollection,開啟這個選項一定程度上會影響性能,阿寶的blog里說也許可以通過配置適當的CMSFullGCsBeforeCompaction來調整性能,未實踐。

    4.為了減少第二次暫停的時間,開啟并行remark: -XX:+CMSParallelRemarkEnabled,如果remark還是過長的話,可以開啟-XX:+CMSScavengeBeforeRemark選項,強制remark之前開始一次minor gc,減少remark的暫停時間,但是在remark之后也將立即開始又一次minor gc。

    5.為了避免Perm區滿引起的full gc,建議開啟CMS回收Perm區選項:

    +CMSPermGenSweepingEnabled -XX:+CMSClassUnloadingEnabled


    6.默認CMS是在tenured generation沾滿68%的時候開始進行CMS收集,如果你的年老代增長不是那么快,并且希望降低CMS次數的話,可以適當調高此值:
    -XX:CMSInitiatingOccupancyFraction=80

    這里修改成80%沾滿的時候才開始CMS回收。

    7.年輕代的并行收集線程數默認是(ncpus <= 8) ? ncpus : 3 + ((ncpus * 5) / 8),如果你希望設定這個線程數,可以通過-XX:ParallelGCThreads= N 來調整。

    8.進入重點,在初步設置了一些參數后,例如:
    -server -Xms1536m -Xmx1536m -XX:NewSize=256m -XX:MaxNewSize=256m -XX:PermSize=64m
    -XX:MaxPermSize=64m -XX:-UseConcMarkSweepGC -XX:+UseCMSCompactAtFullCollection
    -XX:CMSInitiatingOccupancyFraction=80 -XX:+CMSParallelRemarkEnabled
    -XX:SoftRefLRUPolicyMSPerMB=0

    需要在生產環境或者壓測環境中測量這些參數下系統的表現,這時候需要打開GC日志查看具體的信息,因此加上參數:

    -verbose:gc -XX:+PrintGCTimeStamps -XX:+PrintGCDetails -Xloggc:/home/test/logs/gc.log

    在運行相當長一段時間內查看CMS的表現情況,CMS的日志輸出類似這樣:
    4391.322: [GC [1 CMS-initial-mark: 655374K(1310720K)] 662197K(1546688K), 0.0303050 secs] [Times: user=0.02 sys=0.02, real=0.03 secs]
    4391.352: [CMS-concurrent-mark-start]
    4391.779: [CMS-concurrent-mark: 0.427/0.427 secs] [Times: user=1.24 sys=0.31, real=0.42 secs]
    4391.779: [CMS-concurrent-preclean-start]
    4391.821: [CMS-concurrent-preclean: 0.040/0.042 secs] [Times: user=0.13 sys=0.03, real=0.05 secs]
    4391.821: [CMS-concurrent-abortable-preclean-start]
    4392.511: [CMS-concurrent-abortable-preclean: 0.349/0.690 secs] [Times: user=2.02 sys=0.51, real=0.69 secs]
    4392.516: [GC[YG occupancy: 111001 K (235968 K)]4392.516: [Rescan (parallel) , 0.0309960 secs]4392.547: [weak refs processing, 0.0417710 secs] [1 CMS-remark: 655734K(1310720K)] 766736K(1546688K), 0.0932010 secs] [Times: user=0.17 sys=0.00, real=0.09 secs]
    4392.609: [CMS-concurrent-sweep-start]
    4394.310: [CMS-concurrent-sweep: 1.595/1.701 secs] [Times: user=4.78 sys=1.05, real=1.70 secs]
    4394.310: [CMS-concurrent-reset-start]
    4394.364: [CMS-concurrent-reset: 0.054/0.054 secs] [Times: user=0.14 sys=0.06, real=0.06 secs]

    其中可以看到CMS-initial-mark階段暫停了0.0303050秒,而CMS-remark階段暫停了0.0932010秒,因此兩次暫停的總共時間是0.123506秒,也就是123毫秒左右。兩次短暫停的時間之和在200以下可以稱為正?,F象。

    但是你很可能遇到兩種fail引起full gc:Prommotion failed和Concurrent mode failed。

    Prommotion failed的日志輸出大概是這樣:
     [ParNew (promotion failed): 320138K->320138K(353920K), 0.2365970 secs]42576.951: [CMS: 1139969K->1120688K(
    2166784K), 
    9.2214860 secs] 1458785K->1120688K(2520704K), 9.4584090 secs]

    這個問題的產生是由于救助空間不夠,從而向年老代轉移對象,年老代沒有足夠的空間來容納這些對象,導致一次full gc的產生。解決這個問題的辦法有兩種完全相反的傾向:增大救助空間、增大年老代或者去掉救助空間。增大救助空間就是調整-XX:SurvivorRatio參數,這個參數是Eden區和Survivor區的大小比值,默認是32,也就是說Eden區是Survivor區的32倍大小,要注意Survivo是有兩個區的,因此Surivivor其實占整個young genertation的1/34。調小這個參數將增大survivor區,讓對象盡量在survitor區呆長一點,減少進入年老代的對象。去掉救助空間的想法是讓大部分不能馬上回收的數據盡快進入年老代,加快年老代的回收頻率,減少年老代暴漲的可能性,這個是通過將-XX:SurvivorRatio 設置成比較大的值(比如65536)來做到。在我們的應用中,將young generation設置成256M,這個值相對來說比較大了,而救助空間設置成默認大小(1/34),從壓測情況來看,沒有出現prommotion failed的現象,年輕代比較大,從GC日志來看,minor gc的時間也在5-20毫秒內,還可以接受,因此暫不調整。

    Concurrent mode failed的產生是由于CMS回收年老代的速度太慢,導致年老代在CMS完成前就被沾滿,引起full gc,避免這個現象的產生就是調小-XX:CMSInitiatingOccupancyFraction參數的值,讓CMS更早更頻繁的觸發,降低年老代被沾滿的可能。我們的應用暫時負載比較低,在生產環境上年老代的增長非常緩慢,因此暫時設置此參數為80。在壓測環境下,這個參數的表現還可以,沒有出現過Concurrent mode failed。


    參考資料:
    》 by 江南白衣
    《記一次Java GC調整經歷》
    1,2 by Arbow
    Java SE 6 HotSpot[tm] Virtual Machine Garbage Collection Tuning
    Tuning Garbage Collection with the 5.0 JavaTM Virtual Machine

      


    posted @ 2009-09-22 02:10 dennis 閱讀(18277) | 評論 (1)編輯 收藏

        按照《Unix網絡編程》的劃分,IO模型可以分為:阻塞IO、非阻塞IO、IO復用、信號驅動IO和異步IO,按照POSIX標準來劃分只分為兩類:同步IO和異步IO。如何區分呢?首先一個IO操作其實分成了兩個步驟:發起IO請求和實際的IO操作,同步IO和異步IO的區別就在于第二個步驟是否阻塞,如果實際的IO讀寫阻塞請求進程,那么就是同步IO,因此阻塞IO、非阻塞IO、IO服用、信號驅動IO都是同步IO,如果不阻塞,而是操作系統幫你做完IO操作再將結果返回給你,那么就是異步IO。阻塞IO和非阻塞IO的區別在于第一步,發起IO請求是否會被阻塞,如果阻塞直到完成那么就是傳統的阻塞IO,如果不阻塞,那么就是非阻塞IO。

       Java nio 2.0的主要改進就是引入了異步IO(包括文件和網絡),這里主要介紹下異步網絡IO API的使用以及框架的設計,以TCP服務端為例。首先看下為了支持AIO引入的新的類和接口:

     java.nio.channels.AsynchronousChannel
           標記一個channel支持異步IO操作。

     java.nio.channels.AsynchronousServerSocketChannel
           ServerSocket的aio版本,創建TCP服務端,綁定地址,監聽端口等。

     java.nio.channels.AsynchronousSocketChannel
           面向流的異步socket channel,表示一個連接。

     java.nio.channels.AsynchronousChannelGroup
           異步channel的分組管理,目的是為了資源共享。一個AsynchronousChannelGroup綁定一個線程池,這個線程池執行兩個任務:處理IO事件和派發CompletionHandler。AsynchronousServerSocketChannel創建的時候可以傳入一個AsynchronousChannelGroup,那么通過AsynchronousServerSocketChannel創建的AsynchronousSocketChannel將同屬于一個組,共享資源。

     java.nio.channels.CompletionHandler
           異步IO操作結果的回調接口,用于定義在IO操作完成后所作的回調工作。AIO的API允許兩種方式來處理異步操作的結果:返回的Future模式或者注冊CompletionHandler,我更推薦用CompletionHandler的方式,這些handler的調用是由AsynchronousChannelGroup的線程池派發的。顯然,線程池的大小是性能的關鍵因素。AsynchronousChannelGroup允許綁定不同的線程池,通過三個靜態方法來創建:
     public static AsynchronousChannelGroup withFixedThreadPool(int nThreads,
                                                                   ThreadFactory threadFactory)
            
    throws IOException

     
    public static AsynchronousChannelGroup withCachedThreadPool(ExecutorService executor,
                                                                    
    int initialSize)

     
    public static AsynchronousChannelGroup withThreadPool(ExecutorService executor)
            
    throws IOException

         需要根據具體應用相應調整,從框架角度出發,需要暴露這樣的配置選項給用戶。

         在介紹完了aio引入的TCP的主要接口和類之后,我們來設想下一個aio框架應該怎么設計。參考非阻塞nio框架的設計,一般都是采用Reactor模式,Reacot負責事件的注冊、select、事件的派發;相應地,異步IO有個Proactor模式,Proactor負責CompletionHandler的派發,查看一個典型的IO寫操作的流程來看兩者的區別:

         Reactor:  send(msg) -> 消息隊列是否為空,如果為空  -> 向Reactor注冊OP_WRITE,然后返回 -> Reactor select -> 觸發Writable,通知用戶線程去處理 ->先注銷Writable(很多人遇到的cpu 100%的問題就在于沒有注銷),處理Writeable,如果沒有完全寫入,繼續注冊OP_WRITE。注意到,寫入的工作還是用戶線程在處理。
         Proactor: send(msg) -> 消息隊列是否為空,如果為空,發起read異步調用,并注冊CompletionHandler,然后返回。 -> 操作系統負責將你的消息寫入,并返回結果(寫入的字節數)給Proactor -> Proactor派發CompletionHandler。可見,寫入的工作是操作系統在處理,無需用戶線程參與。事實上在aio的API中,AsynchronousChannelGroup就扮演了Proactor的角色。

        CompletionHandler有三個方法,分別對應于處理成功、失敗、被取消(通過返回的Future)情況下的回調處理:

    public interface CompletionHandler<V,A> {

         
    void completed(V result, A attachment);

        
    void failed(Throwable exc, A attachment);

       
        
    void cancelled(A attachment);
    }

        其中的泛型參數V表示IO調用的結果,而A是發起調用時傳入的attchment。

        在初步介紹完aio引入的類和接口后,我們看看一個典型的tcp服務端是怎么啟動的,怎么接受連接并處理讀和寫,這里引用的代碼都是yanf4j 的aio分支中的代碼,可以從svn checkout,svn地址: http://yanf4j.googlecode.com/svn/branches/yanf4j-aio

        第一步,創建一個AsynchronousServerSocketChannel,創建之前先創建一個AsynchronousChannelGroup,上文提到AsynchronousServerSocketChannel可以綁定一個AsynchronousChannelGroup,那么通過這個AsynchronousServerSocketChannel建立的連接都將同屬于一個AsynchronousChannelGroup并共享資源:
    this.asynchronousChannelGroup = AsynchronousChannelGroup
                        .withCachedThreadPool(Executors.newCachedThreadPool(),
                                
    this.threadPoolSize);

        然后初始化一個AsynchronousServerSocketChannel,通過open方法:
    this.serverSocketChannel = AsynchronousServerSocketChannel
                    .open(
    this.asynchronousChannelGroup);

        通過nio 2.0引入的SocketOption類設置一些TCP選項:
    this.serverSocketChannel
                        .setOption(
                                StandardSocketOption.SO_REUSEADDR,
    true);
    this.serverSocketChannel
                        .setOption(
                                StandardSocketOption.SO_RCVBUF,
    16*1024);

        綁定本地地址:

    this.serverSocketChannel
                        .bind(
    new InetSocketAddress("localhost",8080), 100);
       
        其中的100用于指定等待連接的隊列大小(backlog)。完了嗎?還沒有,最重要的監聽工作還沒開始,監聽端口是為了等待連接上來以便accept產生一個AsynchronousSocketChannel來表示一個新建立的連接,因此需要發起一個accept調用,調用是異步的,操作系統將在連接建立后,將最后的結果——AsynchronousSocketChannel返回給你:

    public void pendingAccept() {
            
    if (this.started && this.serverSocketChannel.isOpen()) {
                
    this.acceptFuture = this.serverSocketChannel.accept(null,
                        
    new AcceptCompletionHandler());

            } 
    else {
                
    throw new IllegalStateException("Controller has been closed");
            }
        }
       注意,重復的accept調用將會拋出PendingAcceptException,后文提到的read和write也是如此。accept方法的第一個參數是你想傳給CompletionHandler的attchment,第二個參數就是注冊的用于回調的CompletionHandler,最后返回結果Future<AsynchronousSocketChannel>。你可以對future做處理,這里采用更推薦的方式就是注冊一個CompletionHandler。那么accept的CompletionHandler中做些什么工作呢?顯然一個赤裸裸的AsynchronousSocketChannel是不夠的,我們需要將它封裝成session,一個session表示一個連接(mina里就叫IoSession了),里面帶了一個緩沖的消息隊列以及一些其他資源等。在連接建立后,除非你的服務器只準備接受一個連接,不然你需要在后面繼續調用pendingAccept來發起另一個accept請求
    private final class AcceptCompletionHandler implements
                CompletionHandler
    <AsynchronousSocketChannel, Object> {

            @Override
            
    public void cancelled(Object attachment) {
                logger.warn(
    "Accept operation was canceled");
            }

            @Override
            
    public void completed(AsynchronousSocketChannel socketChannel,
                    Object attachment) {
                
    try {
                    logger.debug(
    "Accept connection from "
                            
    + socketChannel.getRemoteAddress());
                    configureChannel(socketChannel);
                    AioSessionConfig sessionConfig 
    = buildSessionConfig(socketChannel);
                    Session session 
    = new AioTCPSession(sessionConfig,
                            AioTCPController.
    this.configuration
                                    .getSessionReadBufferSize(),
                            AioTCPController.
    this.sessionTimeout);
                    session.start();
                    registerSession(session);
                } 
    catch (Exception e) {
                    e.printStackTrace();
                    logger.error(
    "Accept error", e);
                    notifyException(e);
                } 
    finally {
                    pendingAccept();
                }
            }

            @Override
            
    public void failed(Throwable exc, Object attachment) {
                logger.error(
    "Accept error", exc);
                
    try {
                    notifyException(exc);
                } 
    finally {
                    pendingAccept();
                }
            }
        }
       
        注意到了吧,我們在failed和completed方法中在最后都調用了pendingAccept來繼續發起accept調用,等待新的連接上來。有的同學可能要說了,這樣搞是不是遞歸調用,會不會堆棧溢出?實際上不會,因為發起accept調用的線程與CompletionHandler回調的線程并非同一個,不是一個上下文中,兩者之間沒有耦合關系。要注意到,CompletionHandler的回調共用的是AsynchronousChannelGroup綁定的線程池,因此千萬別在回調方法中調用阻塞或者長時間的操作,例如sleep,回調方法最好能支持超時,防止線程池耗盡。

        連接建立后,怎么讀和寫呢?回憶下在nonblocking nio框架中,連接建立后的第一件事是干什么?注冊OP_READ事件等待socket可讀。異步IO也同樣如此,連接建立后馬上發起一個異步read調用,等待socket可讀,這個是Session.start方法中所做的事情:

    public class AioTCPSession {
        
    protected void start0() {
            pendingRead();
        }

        
    protected final void pendingRead() {
            
    if (!isClosed() && this.asynchronousSocketChannel.isOpen()) {
                
    if (!this.readBuffer.hasRemaining()) {
                    
    this.readBuffer = ByteBufferUtils
                            .increaseBufferCapatity(
    this.readBuffer);
                }
                
    this.readFuture = this.asynchronousSocketChannel.read(
                        
    this.readBuffer, thisthis.readCompletionHandler);
            } 
    else {
                
    throw new IllegalStateException(
                        
    "Session Or Channel has been closed");
            }
        }
       
    }

         AsynchronousSocketChannel的read調用與AsynchronousServerSocketChannel的accept調用類似,同樣是非阻塞的,返回結果也是一個Future,但是寫的結果是整數,表示寫入了多少字節,因此read調用返回的是Future<Integer>,方法的第一個參數是讀的緩沖區,操作系統將IO讀到數據拷貝到這個緩沖區,第二個參數是傳遞給CompletionHandler的attchment,第三個參數就是注冊的用于回調的CompletionHandler。這里保存了read的結果Future,這是為了在關閉連接的時候能夠主動取消調用,accept也是如此?,F在可以看看read的CompletionHandler的實現:
    public final class ReadCompletionHandler implements
            CompletionHandler
    <Integer, AbstractAioSession> {

        
    private static final Logger log = LoggerFactory
                .getLogger(ReadCompletionHandler.
    class);
        
    protected final AioTCPController controller;

        
    public ReadCompletionHandler(AioTCPController controller) {
            
    this.controller = controller;
        }

        @Override
        
    public void cancelled(AbstractAioSession session) {
            log.warn(
    "Session(" + session.getRemoteSocketAddress()
                    
    + ") read operation was canceled");
        }

        @Override
        
    public void completed(Integer result, AbstractAioSession session) {
            
    if (log.isDebugEnabled())
                log.debug(
    "Session(" + session.getRemoteSocketAddress()
                        
    + ") read +" + result + " bytes");
            
    if (result < 0) {
                session.close();
                
    return;
            }
            
    try {
                
    if (result > 0) {
                    session.updateTimeStamp();
                    session.getReadBuffer().flip();
                    session.decode();
                    session.getReadBuffer().compact();
                }
            } 
    finally {
                
    try {
                    session.pendingRead();
                } 
    catch (IOException e) {
                    session.onException(e);
                    session.close();
                }
            }
            controller.checkSessionTimeout();
        }

        @Override
        
    public void failed(Throwable exc, AbstractAioSession session) {
            log.error(
    "Session read error", exc);
            session.onException(exc);
            session.close();
        }

    }

       如果IO讀失敗,會返回失敗產生的異常,這種情況下我們就主動關閉連接,通過session.close()方法,這個方法干了兩件事情:關閉channel和取消read調用:
    if (null != this.readFuture) {
                
    this.readFuture.cancel(true);
            }
    this.asynchronousSocketChannel.close();
       在讀成功的情況下,我們還需要判斷結果result是否小于0,如果小于0就表示對端關閉了,這種情況下我們也主動關閉連接并返回。如果讀到一定字節,也就是result大于0的情況下,我們就嘗試從讀緩沖區中decode出消息,并派發給業務處理器的回調方法,最終通過pendingRead繼續發起read調用等待socket的下一次可讀??梢?,我們并不需要自己去調用channel來進行IO讀,而是操作系統幫你直接讀到了緩沖區,然后給你一個結果表示讀入了多少字節,你處理這個結果即可。而nonblocking IO框架中,是reactor通知用戶線程socket可讀了,然后用戶線程自己去調用read進行實際讀操作。這里還有個需要注意的地方,就是decode出來的消息的派發給業務處理器工作最好交給一個線程池來處理,避免阻塞group綁定的線程池。
      
       IO寫的操作與此類似,不過通常寫的話我們會在session中關聯一個緩沖隊列來處理,沒有完全寫入或者等待寫入的消息都存放在隊列中,隊列為空的情況下發起write調用:


        
    protected void write0(WriteMessage message) {
            
    boolean needWrite = false;
            
    synchronized (this.writeQueue) {
                needWrite 
    = this.writeQueue.isEmpty();
                
    this.writeQueue.offer(message);
            }
            
    if (needWrite) {
                pendingWrite(message);
            }
        }

        
    protected final void pendingWrite(WriteMessage message) {
            message 
    = preprocessWriteMessage(message);
            
    if (!isClosed() && this.asynchronousSocketChannel.isOpen()) {
                
    this.asynchronousSocketChannel.write(message.getWriteBuffer(),
                        
    thisthis.writeCompletionHandler);
            } 
    else {
                
    throw new IllegalStateException(
                        
    "Session Or Channel has been closed");
            }
        }


        write調用返回的結果與read一樣是一個Future<Integer>,而write的CompletionHandler處理的核心邏輯大概是這樣:
    @Override
        
    public void completed(Integer result, AbstractAioSession session) {
            
    if (log.isDebugEnabled())
                log.debug(
    "Session(" + session.getRemoteSocketAddress()
                        
    + ") writen " + result + " bytes");
                    
            WriteMessage writeMessage;
            Queue
    <WriteMessage> writeQueue = session.getWriteQueue();
            
    synchronized (writeQueue) {
                writeMessage 
    = writeQueue.peek();
                
    if (writeMessage.getWriteBuffer() == null
                        
    || !writeMessage.getWriteBuffer().hasRemaining()) {
                    writeQueue.remove();
                    
    if (writeMessage.getWriteFuture() != null) {
                        writeMessage.getWriteFuture().setResult(Boolean.TRUE);
                    }
                    
    try {
                        session.getHandler().onMessageSent(session,
                                writeMessage.getMessage());
                    } 
    catch (Exception e) {
                        session.onException(e);
                    }
                    writeMessage 
    = writeQueue.peek();
                }
            }
            
    if (writeMessage != null) {
                
    try {
                    session.pendingWrite(writeMessage);
                } 
    catch (IOException e) {
                    session.onException(e);
                    session.close();
                }
            }
        }

       compete方法中的result就是實際寫入的字節數,然后我們判斷消息的緩沖區是否還有剩余,如果沒有就將消息從隊列中移除,如果隊列中還有消息,那么繼續發起write調用。

       重復一下,這里引用的代碼都是yanf4j aio分支中的源碼,感興趣的朋友可以直接check out出來看看: http://yanf4j.googlecode.com/svn/branches/yanf4j-aio。
       在引入了aio之后,java對于網絡層的支持已經非常完善,該有的都有了,java也已經成為服務器開發的首選語言之一。java的弱項在于對內存的管理上,由于這一切都交給了GC,因此在高性能的網絡服務器上還是Cpp的天下。java這種單一堆模型比之erlang的進程內堆模型還是有差距,很難做到高效的垃圾回收和細粒度的內存管理。

       這里僅僅是介紹了aio開發的核心流程,對于一個網絡框架來說,還需要考慮超時的處理、緩沖buffer的處理、業務層和網絡層的切分、可擴展性、性能的可調性以及一定的通用性要求。

        

    posted @ 2009-09-20 14:02 dennis 閱讀(11817) | 評論 (10)編輯 收藏

        MQ在分布式系統中扮演著重要角色,異步的消息通信全要靠它,而異步通信正是提高系統伸縮性的不二良方。說說我認為的一個優秀的MQ產品需要具備的特征。

        首先顯然是高可用性,我們當然希望MQ能支撐7x24小時應用,而不是三天兩頭當機,我們要追求的是99.9%的可靠服務時間。要做到高可用性,顯然我們需要做MQ的集群,一臺當了,不影響整個集群的服務能力,這里涉及到告警、流控、消息的負載均衡、數據庫的使用、測試的完備程度等等。

        其次是消息存儲的高可靠性。我們要保證100%不丟消息。要做到消息存儲的高可靠性,不僅僅是MQ的責任,更涉及到硬件、操作系統、語言平臺和數據庫的一整套方案。許多號稱可靠存儲的MQ產品其實都不可靠,要知道,硬件錯誤是常態,如果在硬件錯誤的情況下還能保證消息的可靠存儲這才是難題。這里可能需要用到特殊的存儲硬件,特殊的數據庫,分布式的數據存儲,數據庫的分庫分表和同步等等。你要考慮消息存儲在哪里,是文件系統,還是數據庫,是本地文件,還是分布式文件,是搞主輔備份呢還是多主機寫入等等。
       
        第三是高可擴展性,MQ集群能很好地支持水平擴展,這就要求我們的節點之間最好不要有通信和數據同步。

        第四是性能,性能是實現高可用性的前提,很難想象單機性能極差的MQ組成的集群能在高負載下幸免于難。性能因素跟采用的平臺、語言、操作系統、代碼質量、數據庫、網絡息息相關。MQ產品的核心其實是消息的存儲,在保證存儲安全的前提下如何保證和提高消息入隊的效率是性能的關鍵因素。這里需要開發人員建立起性能觀念,不需要你對一行行代碼斤斤計較,但是你需要知道這樣做會造成什么后果,有沒有更好更快的方式,你怎么證明它更好更快。軟件實現的性能是一方面,另一方面就是平臺相關了,因為MQ本質上是IO密集型的系統,瓶頸在IO,如何優化網絡IO、文件IO這需要專門的知識。性能另一個相關因素是消息的調度上,引入消息順序和消息優先級,允許消息的延遲發送,都將增大消息發送調度的復雜性,如何保證高負載下的調度也是要特別注意的地方。

      
       第五,高可配置性和監控工具的完整,這是一個MQ產品容易忽略的地方。異步通信造成了查找問題的難度,不像同步調用那樣有相對明確的時序關系。因此查找異步通信的異常是很困難的,這就需要MQ提供方便的DEBUG工具,查找分析日志的工具,查看消息生命周期的工具,查看系統間依賴關系的工具等等。可定制也是MQ產品非常重要的一方面,可方便地配置各類參數并在集群中同步,并且可動態調整各類參數,這將大大降低維護難度。

       一些不成熟的想法,瞎侃。


       


    posted @ 2009-09-18 00:09 dennis 閱讀(2864) | 評論 (1)編輯 收藏


        XMemcached 1.2.0-RC2 released,main highlights:

    1、支持Kestrel。Kestrel是一個scala編寫的簡單高效的MQ,它是Twitter發布的開源產品,支持memcached協議,但并不完全兼容。更多信息看這里。Xmemcached提供了一個KestrelCommandFactory,用于對kestrel特性的支持。

    2、新增了基于Election Hash的SessionLocator。Election Hash的詳細解釋看這里。簡單來說就是每次查找key對應的節點的時候,都計算節點ip+key的MD5值,然后進行排序,取最大者為目標節點。這個算法解決的問題與Consistent Hash類似,但是因為每次都要計算,因此開銷會比較大,適合節點數比較少的情況,避免了consistent hash為了節點比較均勻需要引入虛擬節點的問題。測試表明,Election Hash的結果也是比較均勻的,并且在節點增刪的情況下能保持與一致性哈希相近的命中率。要使用election hash,請使用ElectionMemcachedSessionLocator

    3、從RC1版本以來的Bug fixed.

       歡迎試用并反饋任何意見和BUG。


    posted @ 2009-09-17 23:24 dennis 閱讀(1439) | 評論 (0)編輯 收藏

        Kestrel是一個scala寫的twitter開源的消息中間件,特點是高性能、小巧(2K行代碼)、持久存儲(記錄日志到journal)并且可靠(支持可靠獲取)。Kestrel的前身是Ruby寫的Starling項目,后來twitter的開發人員嘗試用scala重新實現。它的代碼非常簡潔并且優雅,推薦一讀。

        Kestrel采用的協議是memcached的文本協議,但是并不完全支持所有memcached協議,也不是完全兼容現有協議。標準的協議它僅支持GET、SET、FLUSH_ALL、STATS,擴展的協議有:
               
                  SHUTDOWN       關閉kestrel server     
                  RELOAD         動態重新加載配置文件
                  DUMP_CONFIG    dump配置文件
                  FLUSH queueName   flush某個隊列

        每個key對應都是一個隊列。標準memcached文本協議的支持上也沒有完全兼容,SET不支持flag,因此現有大多數基于flag做序列化的memcached client都無法存儲任意java類型到kestrel;FLUSH_ALL返回"Flushed all queues.\r\n"而不是"OK\r\n"。

        GET協議支持阻塞獲取和可靠獲取,都是在key上作文章,例如你要獲取queue1的消息,并且在沒有消息的時候等待一秒鐘,如果有消息馬上返回,超時時間后還沒有就返回空,kestrel允許你通過發送
                         "GET queue1/t=1000\r\n"

    來阻塞獲取。本來的key應該queue1,這里變成了"queue1/t=1000",因此如果你使用的client有對返回的key和發送的key做校驗,那么可能就認為kestrel返回錯誤。
        什么是可靠獲取呢?默認的GET是從隊列中獲取消息后,server端就將該消息從隊列中移除,客戶端需要自己保證不把這個消息丟失掉,也就是說這里是類似JMS規范中的自動應答(auto-acknowledge),如果客戶端在處理這個消息的時候異常崩潰或者在接收消息數據的時候連接斷開,那么可能導致這個消息永久丟失。Kestrel的可靠獲取就是類似JMS規范中的CLIENT_ACK mode,客戶端獲取消息后,server將這個消息從隊列移除并正常發送給客戶端,如果這時候客戶端崩潰或者連接斷開,那么server將不會確認該消息被消費并且"un-get"這個消息,重新放到隊列頭部,那么當client重新連接上來的時候還可以獲取這個消息;只有當server收到客戶端的明確確認消息成功的時候,才將消息移除。這個功能也是通過key做手腳,

    "GET queue1/open\r\n"   開始一次可靠獲取
    "GET queue1/close\r\n"  確認消費成功


    你要關閉前一次可靠獲取開啟新的一次,還可以這樣調用
    "GET queue1/close/open\r\n"


        要注意的是每個連接的client只能有一個正在執行的可靠獲取,關閉一個沒有開啟的reliable fetch或者在執行一次reliable fetch再次open一個新的獲取都將直接返回空。

        從kestrel的協議方面,我們可以學習到的一點就是在做一份協議的時候,如果有多種不同語言的client的話,應該盡量用通用協議,通用協議通常都已經有很多成熟的client可以使用,避免了為私有協議開發不同語言的client;并且我們可以在通用協議上作擴展,例如kestrel在key上面做的花樣,通過給key附加不同的屬性即可實現一些特殊功能。

        XMemcachedClient默認是無法支持kestrel對memcached的協議的擴展,也就是說無法支持阻塞獲取、可靠獲取和flush_all,這是因為xmemcached會對返回的key和發送的key做校驗,如果不相等就認為解碼錯誤;并且由于kestrel不支持flag,因此無法存儲java序列化類型;另外一個問題是,xmemcached(spymemcached)都會將連續的GET協議合并成一個bulk get協議,而kestrel也并不支持bulk get,所以需要關閉這個優化,這個可以通過下列代碼關閉:
    memcachedClient.setOptimizeGet(false);

    Spymemcached似乎不提供這個選項。為了解決序列化問題,我添加了一個新的KestrelCommandFactory,使用這個CommandFactory后,將默認關閉get優化,并且不對GET返回的key做校驗從而支持阻塞獲取和可靠獲取,并且將在存儲的數據之前加上4個字節的flag(整型),因此可以支持存儲任意可序列化類型。但是有一些應用只需要存儲字符串類型和原生類型,這是為了在不同語言的client之間保持可移植(如存儲json數據),那么就不希望在數據之前加上這個flag,關閉這個功能可以通過
    memcachedClient.setPrimitiveAsString(true);

    方法來設置,所有的原生類型都將調用toString轉成字符串來存儲,字符串前不再自動附加flag。

        KestrelCommandFactory已經提交到svn trunk,預計在xmemcached 1.2.0-RC2的時候發布。

        使用KestrelCommandFactory對kestrel做的性能測試,server和client都跑在linux上,jdk6,單線程單client連續push消息

                   消息個數       消息長度       是否啟用journal   時間       TPS(/s)
                    500000          256             否              123.0s     4065
                    500000          1024            否              126.3s     3959
                    500000          4096            否              120.6s     4145
                    500000          4096            是              122.1s     4095
                    500000          8192            是              121.2s     4125

    從數據上來看比官方數據好很多,可能機器配置不同。是否啟用journal帶來的影響似乎很小,寫文件都是append,還是比較高效的。

    kestrel的項目主頁  http://github.com/robey/kestrel
    kestrel的wiki頁    http://wiki.github.com/robey/kestrel
    xmemcached項目主頁  http://code.google.com/p/xmemcached/


    posted @ 2009-09-15 11:34 dennis 閱讀(15379) | 評論 (9)編輯 收藏

      XMemcached是一個基于java nio的Memcached Client,正式發布1.2.0-RC1版本。此版本又是一個里程碑版本,開始支持memcached的二進制協議,并添加了幾個更有價值的功能。此版本的主要改進如下:

    1、支持完整的memcached binary協議。XMemcached現在已經支持memcached的所有文本協議和二進制協議,成為一個比較完整的java client。Memcached的二進制協議帶來更好的性能以及更好的可擴展性。在XMemcached中使用二進制協議,你只要添加一行代碼:
     XMemcachedClientBuilder builder=.
      builder.setCommandFactory(
    new BinaryCommandFactory());//此行

    或者在Spring配置中增加一行配置:
    <bean name="memcachedClient2"
            class
    ="net.rubyeye.xmemcached.utils.XMemcachedClientFactoryBean" destroy-method="shutdown">
       
       
    <!--采用binary command -->
       
    <property name="commandFactory">
               
    <bean class="net.rubyeye.xmemcached.command.BinaryCommandFactory"></bean>

    </bean>

    2.支持與hibernate-memcached的集成。Hibernate-memcached是可以將memcached作為hibernate二級緩存的開源項目,它默認采用的是Spymemcached,XMemcached 1.2.0開始提供對它的集成,具體的配置信息參考這里

    3.兼容JDK5。XMemcached的1.x版本都僅能在jdk6上使用,從1.2.0-RC1開始,XMemcached開始兼容jdk5。當時考慮只支持jdk6是由于nio的Epoll Selector實現是在jdk6上成為默認,而jdk5需要設置環境變量。不過XMemcached 1.2.0-RC1將自動幫你判斷是否是linux平臺,并且判斷是否可以啟用epoll,如果可以,那么將在linux平臺采用EPollSelectorProvider,這一切對用戶來說是透明的。(注意,jdk5的低版本在linux平臺仍然是沒有epoll實現的)。

    4.日志從common-logging遷移到slf4j。XMemcached現在必須的兩個依賴包分別是slf4jyanf4j(1.0-SNAPSHOT).

    5.另一個關鍵性的改進是允許設置連接池。眾所周知,nio的client默認一般都是一個連接,傳統的阻塞io采用連接池的方式提高效率。但是在典型的高并發場景下,nio的單連接也將遇到瓶頸,此時允許設置連接池將是一個可選的調優手段。XMemcached 1.2.0-RC1支持設置連接池,允許對同一個memcached節點建立多個連接,啟用的代碼如下:
    MemcachedClient mc =.
    mc.setConnectionPoolSize(2);

     默認的pool size是1。設置這一數值不一定能提高性能,請依據你的項目的測試結果為準。初步的測試表明只有在大并發下才有提升。設置連接池的一個不良后果就是,同一個memcached的連接之間的數據更新并非同步的,因此你的應用需要自己保證數據更新的原子性(采用CAS或者數據之間毫無關聯)。

    6、簡化構建,移除ant構建,簡化maven構建,現在只采用maven構建了。借助于wagon-svn這個擴展,可以將svn作為maven倉庫,因此xmemcached的構建現在變的非常方便,下載源碼后敲入mvn package即可。

    7.升級yanf4j到1.0-SNAPSHOT版本,此版本引入了SocketOption類,方便設置socket選項,并為引入aio做了重構。

    8、從1.1.3和1.2.0-beta以來的bug fixed.


    歡迎使用和建議。

    下載地址:
    http://code.google.com/p/xmemcached/downloads/list


    posted @ 2009-09-09 09:50 dennis 閱讀(1738) | 評論 (2)編輯 收藏

        XMemcached的結構方面的文檔比較少,可能對有興趣了解它的基本結構,或者想讀源碼的朋友入手比較困難。畫了兩張UML圖,一張是主要的類圖,描述了主要的類和接口之間的關系和結構。一張是序列圖,一次典型的get操作需要經過什么步驟。

        首先看類圖,沒有什么需要特別說明的。


    再看一下get操作的序列圖,需要注意的是等待響應的過程是異步的。



    posted @ 2009-08-26 18:04 dennis 閱讀(2268) | 評論 (2)編輯 收藏

        推遲了半個月之后,發布xmemcached-1.2.0的beta測試版本,此版本又是一個里程碑版本,主要亮點如下:

    1、支持全部的二進制協議,包括noreply的二進制協議。memcached 1.4.0正式推出memcached的二進制協議,相比于文本協議,二進制協議更復雜,但是也更容易解析和編碼,并且可擴展性也比較強,比如原來文本協議只允許key為String類型,二進制協議允許key是任意類型,并且長度可以達到2^16-1,大大超過原有的255的限制。另一方面,文本協議的可讀性更好,在不同上平臺上實現也比較容易,而二進制協議就可能需要考慮可移植性的問題。 
       xmemcached支持全部二進制協議后才算是一個比較完整的memcached的java客戶端了。在實現上可能還有一些隱藏的BUG和問題,歡迎試用并反饋,注意,如果使用二進制協議,你的memcached版本是必須是最新的1.4.0。
       如果要使用二進制協議,你只需要添加一行代碼:
           
                 MemcachedClientBuilder builder = new XMemcachedClientBuilder(
                        AddrUtil.getAddresses(servers));
                
    //添加下面這行,采用BinaryCommandFactory即可使用二進制協議          
                builder.setCommandFactory(new BinaryCommandFactory());
                MemcachedClient mc 
    = builder.build();

    2、支持hibernate-memcached,在某用戶的要求下添加了此特性。hibernate-memcached允許你使用memcached作為hibernate的二級緩存,但是它默認使用的是Spymemcached,想替換成Xmemcached就需要做一些擴展,在1.2.0提供了這一支持。你需要做的是將memcacheClientFactory屬性設置為Xmemcached的即可:

    hibernate.memcached.memcacheClientFactory=net.rubyeye.xmemcached.utils.hibernate.XmemcachedClientFactory

    更多設置參考wiki page.

    3、1.1.3以來的一些bug fixed.

    項目主頁: http://code.google.com/p/xmemcached/
    下載地址:  http://code.google.com/p/xmemcached/downloads/list

    posted @ 2009-08-26 09:21 dennis 閱讀(2736) | 評論 (4)編輯 收藏

        在這里要推薦下《觀止-微軟創建NT和未來的奪命狂奔》,非常精彩,講述了windows NT開發過程中的人和事。這不僅僅是故事書,也可以看做一本項目管理方面的指南,可以看看這么巨大的項目(幾百萬行代碼)所遭遇到的難題和痛楚。我更愿意將這本書當做《人月神話》的故事版,同樣是創建劃時代的OS,同樣是管理眾多人參與的大型的項目,也同樣遭遇了種種困擾和痛苦,從這個角度也可以看出,人類的痛苦的相通的:)

       單純從軟件構建的角度去看這本書,可以說說我看到的東西,這些是我今天早上走在上班路上的時候想的,咳咳。

    1、開發OS是燒錢的事情,NT開發接近5年,每年的花費據說在5000萬美刀,那可是在90年代初期,換算成現在更是天文數字。從另一個側面也說明了linux系統的偉大。開發一個這么燒錢的玩意,如果沒有管理層的強力支持,那么不是被砍掉,就是遭遇流產的命運,幸運的是NT團隊得到了蓋茨的鼎力支持,大概也只有他能這么燒錢了。Dave Culter從DEC辭職的原因也是因為管理層砍掉了他的團隊。蓋茨另一個做法是不干涉NT團隊的開發工作,他只提出目標和期望,然后就偶爾過來看看,不對不知道的東西指手畫腳,這點可不容易。

    2、每日構建非常重要,NT團隊的構建實驗室一開始是每周構建,后來做到了每日構建。只有每日構建,持續集成,才能幫你掌控產品質量,及時發現潛在的問題。我們現在的項目使用了hudson,比CC容易配置一點,效果還不錯。

    3、測試極其重要,專業的測試團隊對于大型項目來說尤其重要。除了測試人員之外,開發人員需要做自測,需要對自己check-in的代碼負責,如果你簽入的代碼導致構建失敗,那么Dave culter可能沖破墻壁進來,拍著桌子沖你咆哮。對check in必須做嚴格控制和跟蹤,如果在項目的最后沖擊階段,除了showstopper級別的修正代碼允許簽入之外,其他的修改都不被接受。開發者和測試人員很容易存在對立,檢討自己,我對測試人員也存在偏見和某種程度上的輕視和厭煩,如果從就事論事和都是為一個目標努力的角度來說,測試和開發并不對立,兩者是相輔相成,甚至于測試人員更為至關重要。

    4、在一個長期而復雜的項目中,如何保持團隊成員的士氣也是個難事兒。軟件開發歸根到底是的因素是人,而非工具或者其他,關注人,其實就是在關注你的軟件。鼓勵士氣的常見做法就是設定里程碑,在這個里程碑上發布一個重要版本,讓大家看到希望,但是對于OS這樣的巨型項目來說,里程碑不是那么容易設定,這從書中項目的不斷延期可以看到。另外就是寬松的工作環境和假期,微軟的工作環境有目共睹,能做到每個員工獨立一個辦公室的國內企業還沒有吧。國外的開發者似乎很會玩,賽車、滑雪、空手道,其實不是我們不會玩,是我們玩不起,國內的待遇和生活壓力讓你想玩也玩不起。
       可是就算是再好的物質待遇,其實也換不來美好生活,書中充斥著開發者對家庭和婚姻的困惑和痛苦,為了NT,他們也失去了很多,對工作過度投入的后果就是失去平衡的家庭生活,再次驗證上帝是公平的,有得必有失,就看你看重的是什么。

    5、開發者的效率差異是驚人的,在《人月神話》里已經說明了這一點,開發者之間的效率差異可以達到驚人的10倍,在NT這樣的團隊里也再次驗證了這一結論。

    6、投入越多的人力,并不能帶來效率的提升,當NTFS文件系統的進度拖慢的時候,微軟的經理們考慮添加人手,但是經過慎重的考慮還是沒有加人,因為文件系統是技術活,新人很難馬上投入開發,而需要老手的帶領和培訓,引入了更多的溝通成本和培訓成本。

    7、優秀的代碼無法通過行數來衡量,軟件某種程度上還真是魔法的產物。

    8、NT的一個教訓是,應該及早設定你的性能目標,并在適當時候開始關注并優化系統。NT團隊后期的很大部分工作都是在優化系統性能,并縮小尺寸。

    9、設定Deadline常常是不靠譜的事情,對軟件開發的時間估計也常常是不靠譜的事情,這一點從NT的一次又一次的延期可以看出。延期失望的不僅僅是客戶,也會打擊你的團隊成員,遙遙無期的開發過程容易讓人崩潰。

    10、NT的開發貫穿了對市場的需求的考慮,有個牛X的產品經理還是相當重要的。當然,沒有開發者喜歡添加新功能,特別是在已經完成一個新功能的情況下,以至發展到NT的開發者看到產品經理就不由得拿起球棒擊墻的地步:)

       這本書花了我兩個晚上看完,還是看故事有趣呀,上面所說只是我的印象,書中還有許多八卦故事老少咸宜,如果有出入,請看原著:) 有空還得重讀下。




    posted @ 2009-08-13 12:44 dennis 閱讀(1005) | 評論 (2)編輯 收藏

    僅列出標題
    共56頁: First 上一頁 14 15 16 17 18 19 20 21 22 下一頁 Last 
    主站蜘蛛池模板: 亚洲美女又黄又爽在线观看| 新最免费影视大全在线播放| 亚洲一区无码精品色| 我要看免费的毛片| 99精品视频免费在线观看| 特a级免费高清黄色片| 久久精品国产亚洲av麻豆图片| 国产亚洲午夜高清国产拍精品| 精品熟女少妇AV免费观看| a级毛片黄免费a级毛片| 黄页网站在线观看免费| 亚洲色偷偷色噜噜狠狠99| 亚洲精品综合久久中文字幕 | 亚洲三级视频在线| 亚洲va久久久噜噜噜久久天堂| 亚洲av无码不卡私人影院| 天天天欲色欲色WWW免费| 国产h视频在线观看免费| 无码国产精品一区二区免费vr| 黄色网页在线免费观看| 牛牛在线精品观看免费正| 亚洲狠狠婷婷综合久久| 久久亚洲国产成人影院| 亚洲成a人片在线观看中文!!!| 亚洲国产精品一区二区久久| 亚洲国产精品无码AAA片| 亚洲国产精品无码专区| 亚洲精品乱码久久久久久中文字幕| 国产精品亚洲综合专区片高清久久久| 免费国产成人午夜电影| 真实乱视频国产免费观看| 国产白丝无码免费视频| 一本色道久久88—综合亚洲精品| 亚洲精品视频免费在线观看| 亚洲国产一区二区三区青草影视| 久久青青草原亚洲av无码| 又黄又爽一线毛片免费观看| 永久免费AV无码网站在线观看| 中文字幕在线观看免费视频| 久久免费精彩视频| 色www永久免费网站|