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

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

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

    莊周夢蝶

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

    找bug記(1)

    Posted on 2011-07-10 23:29 dennis 閱讀(9281) 評論(16)  編輯  收藏 所屬分類: java

        轉載請注明出處 http://www.tkk7.com/killme2008/archive/2011/07/10/354062.html

        上周在線上系統發現了兩個bug,值得記錄下查找的過程和原因。以后如果還有查找bug比較有價值的經歷,我也會繼續分享。
        第一個bug的起始,是在線上日志發現一個頻繁打印的異常——java.lang.ArrayIndexOutOfBoundsException。但是卻沒有堆棧,只有一行一行的ArrayIndexOutOfBoundsException。沒有堆棧,不知道異常是從什么地方拋出來的,也就不能找到問題的根源,更談不上解決。題外,工程師在用log4j記錄錯誤異常的時候,我看到很多人這樣用(假設e是異常對象):
    log.error("發生錯誤:"+e);
    或者:
    log.error("發生錯誤:"+e.getMessage());
        這樣的寫法是不對,只記錄了異常的信息,而沒有將堆棧輸出到日志,正確的寫法是利用error的重載方法:
    log.error("xxx發生錯誤",e);

        這樣才能在日志中完整地輸出異常堆棧來。如何寫好日志是另一個話題,這里不展開。繼續我們的找bug經歷。剛才提到,我們線上日志一直出現一行錯誤信息ArrayIndexOutOfBoundsException卻沒有堆棧,是我們沒有正確地寫日志嗎?檢查代碼不是的,這個問題其實是跟JDK5引入的一個新特性有關,對于一些頻繁拋出的異常,JDK為了性能會做一個優化,在JIT重新編譯后會拋出沒有堆棧的異常。在使用server模式的時候,這個優化是開啟的,我們的服務器跑在server模式下并且jdk版本是6,因此在頻繁拋出ArrayIndexOutOfBoundsException異常一段時間后優化開始起作用,只拋出沒有堆棧的異常信息了。

        那么怎么解決這個問題呢?因為這個優化是在JIT重新編譯后才起作用,因此一開始拋出異常的時候還是有堆棧的,所以可以查看較舊的日志,尋找有完整堆棧的異常信息。但是由于我們的日志太大,會定期刪除,我們的服務器也啟動了很長時間,因此查找日志不是很靠譜的方法。
        另一個解決辦法是暫時禁用這個優化,強制要求每次都要拋出有堆棧的異常。幸好JDK提供了選項來關閉這個優化,配置JVM參數
    -XX:-OmitStackTraceInFastThrow
        就可以禁止這個優化(注意選項中的減號,加號是啟用)。

        我們找了臺機器,配置了這個參數并重啟一下。過了一會就找到問題所在,堆棧類似這樣
    Caused by: java.lang.ArrayIndexOutOfBoundsException: -1831238
        at sun.util.calendar.BaseCalendar.getCalendarDateFromFixedDate(BaseCalendar.java:
    436)
        at java.util.GregorianCalendar.computeFields(GregorianCalendar.java:
    2081)
        at java.util.GregorianCalendar.computeFields(GregorianCalendar.java:
    1996)
        at java.util.Calendar.setTimeInMillis(Calendar.java:
    1109)
        at java.util.Calendar.setTime(Calendar.java:
    1075)
        at java.text.SimpleDateFormat.format(SimpleDateFormat.java:
    876)
        at java.text.SimpleDateFormat.format(SimpleDateFormat.java:
    869)
        at java.text.DateFormat.format(DateFormat.java:
    316)

        讀者肯定猜到了,這個問題是由于SimpleDateFormat的誤用引起的。SimpleDateFormatjavadoc中有這么句話:
    Date formats are not synchronized. It is recommended to create separate format instances for each thread.
    If multiple threads access a format concurrently, it must be 
    synchronized externally.
        但是很悲劇的是這句話是放在整個doc的最后面,在我看來,這句話應該放在最前面才對。簡單來說就是SimpleDateFormat不是線程安全的,你要么每次都new一個來用,要么做加鎖來同步使用。

        出問題的代碼就是由于工程師認為SimpleDateFormat的創建代價很高,然后搞了個map做緩存,所有線程共用這個instance做format,同時沒有做同步。悲劇就誕生了。
        這里就引出我想提到的第二點問題,在使用一個類或者方法的時候,最好能詳細地看下該類的javadoc,JDK的javadoc是做的非常好的,javadoc除了做說明之外,通常還會給示例,并且會點出一些關鍵問題,如線程安全性和平臺移植性。

        最后,我將做個測試,到底在使用SimpleDateFormat怎么做才是最好的方式?假設我們要實現一個formatDate方法將日期格式化成"yyyy-MM-dd"的格式。
        第一個方法是每次使用都創建一個instance,并調用format方法:
       public static String formatDate1(Date date) {
            SimpleDateFormat format 
    = new SimpleDateFormat("yyyy-MM-dd");
            
    return format.format(date);
        }

        第二個方法是只創建一個instance,但是在調用方法的時候做同步:
       private static final SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd");

        
    public static synchronized String formatDate2(Date date) {
            
    return formatter.format(date);
        }

        第三個方法比較特殊,我們為每個線程都緩存一個instance,存放在ThreadLocal里,使用的時候從ThreadLocal里取就可以了:
       private static ThreadLocal<SimpleDateFormat> formatCache = new ThreadLocal<SimpleDateFormat>() {

            @Override
            
    protected SimpleDateFormat initialValue() {
                
    return new SimpleDateFormat("yyyy-MM-dd");
            }

        };

       
    public static String formatDate3(Date date) {
            SimpleDateFormat format 
    = formatCache.get();
            
    return format.format(date);
        }

        然后我們測試下三個方法并發調用下的性能并做一個比較,并發100個線程循環調用1000萬次,記錄耗時。我們設置了JVM參數:
    -Xmx512m -XX:CompileThreshold=10000
        設置堆最大為512M,設置當一個方法被調用1萬次的時候就被JIT編譯。測試的結果如下:
     
    第1次測試
    第2次測試
    第3次測試
    formatDate1
    50545
    49365
    53532
    formatDate2
    10895
    10761
    10673
    formatDate3 10386 9919 9527
    (單位:毫秒)
       
        從結果來看,方法1最慢,方法3最快,但是就算是最慢的方法1也可以達到每秒鐘200 20萬次的調用量,很少有系統能達到這個量級。這個點很難成為你系統的瓶頸所在。從我的角度出發,我會建議你用方法1或者方法2,如果你追求那么一點性能提升的話,可以考慮用方法3,也就是用ThreadLocal做緩存。

        總結下本文找bug經歷想表達的幾點想法:
    (1)正確地打印錯誤日志
    (2)在server模式下,最好都設置-XX:-OmitStackTraceInFastThrow
    (3)使用類或者方法的時候,最好能詳細閱讀下javadoc,很多問題都能找到答案
    (4)使用SimpleDateFormat的時候要注意線程安全性,要么每次new,要么做同步,兩者的性能有差距,但是這個差距很難成為你的性能瓶頸。

        下篇文章我再分享另一個bug的查找經歷,也是比較有趣,可以看到一些工具的使用。


    評論

    # re: 找bug記(1)  回復  更多評論   

    2011-07-11 09:56 by fuyou001
    程序員不正確的優化,并期待下篇

    # re: 找bug記(1)  回復  更多評論   

    2011-07-11 10:38 by hz_hz
    你找的這臺機器是急群中的產品線上的server嗎

    如果慎重起見或者不允許產品線機器隨便重啟 恐怕只能在開發機上做這個測試了

    # re: 找bug記(1)  回復  更多評論   

    2011-07-11 10:43 by hz_hz
    但是有時開發server又很難重現 產品線上的問題 這個時候就比較麻煩
    解決問題得選在 空閑時間

    # re: 找bug記(1)  回復  更多評論   

    2011-07-11 15:00 by xiekz
    內容很好,期待下一篇文章

    # re: 找bug記(1)[未登錄]  回復  更多評論   

    2011-07-11 18:04 by xiaoyu
    感覺方法1的測試結果很奇怪, 估計是不是產生了大量的GC?

    # re: 找bug記(1)  回復  更多評論   

    2011-07-11 18:10 by dennis
    @hz_hz

    是線上機器,我們都是集群部署的應用。


    @xiaoyu

    這里gc有影響,new對象的開銷也有影響。如果你用jstat觀察測試程序的gc情況就能發現。

    # re: 找bug記(1)  回復  更多評論   

    2011-07-12 11:39 by imlyj
    不好意思的問題一句:我們用的是tomcat,所以我就知道設置內存大小可以在catalina.sh里。不知道您設置jvm參數在哪設置的呀。我網上找一圈也沒找到答案。網上全告訴你怎么調。就沒告訴你在哪調。你別見笑。還有你這篇文章看完我有疑問:出現沒有堆棧的信息,你怎么就知道是“JDK5引入的一個新特性有關".而且要調整-XX:-OmitStackTraceInFastThrow參數。我總覺得你沒有把這個來龍去脈講清楚。感覺解決問題的時候全是跳躍性的。能否給解答一下?我相信任何的問題及其解決方法,肯定有其來龍去脈。你說呢?

    # re: 找bug記(1)[未登錄]  回復  更多評論   

    2011-07-12 12:58 by steven
    (2)在server模式下,最好都設置-XX:-OmitStackTraceInFastThrow

    這個最好不要這么做,因為這個會比較影響性能,為了定位問題,重啟一下JVM,這個異常信息會被完整打印出來,也很方便定位問題。

    # re: 找bug記(1)  回復  更多評論   

    2011-07-12 15:40 by MrKiller
    不錯的文章,很有啟發。

    # re: 找bug記(1)  回復  更多評論   

    2011-07-12 20:59 by dennis
    @imlyj

    這是我沒有講清楚。我們過去遇到過這個問題,在發現日志記錄沒有問題的時候,自然而然就想到這一點。如果第一次遇到這個問題,我的解決辦法可能是使用搜索引擎來查找問題。

    怎么設置jvm參數?java -Xmx512m HelloWorld即可。如果你沒用過javac和java命令,那很奇怪。在eclipse里可以在run里的vm參數設置。


    至于跳躍性,在解決一個沒有思路的問題的時候,需要你發散性地思維,考慮各種可能,充分發揮自己的經驗和知識。

    # re: 找bug記(1)  回復  更多評論   

    2011-07-12 21:00 by dennis
    @steven
    有數據證明這一點嗎?一個應用如果經常性地頻繁拋異常,我覺的只有兩種可能:
    (1)應用本身有問題,寫的不夠健壯
    (2)將異常當成控制流使用。

    # re: 找bug記(1)  回復  更多評論   

    2011-07-16 00:00 by imlyj
      謝謝@dennis給予我的解答。繼續我的疑問。
    javac和java命令我在借助IDE的時候就基本不用了,呵呵。至于java -Xmx512m HelloWorld,是編譯HelloWorld這個應用,并且調置JVM參數是吧,那這次編譯完,下次重新JVM,參數還要重新指定嗎?這個有點疑問。我理解是是不是下次重啟JVM時,還得再調一下java -Xmx512m HelloWorld吧,那啟不是很煩!!
    還有你eclipse的調優我記住了。我們在生產環境中,即在centos中,我們只是直接調用tomcat.sh命令啟動應用,順便jvm被起來,這個時候,我要調整參數比如你說的-XX:-OmitStackTraceInFastThrow。應該怎么弄呢。別笑話我。我真沒接觸過這個,還請麻煩一下。
      我問這么細,一個我是真不清楚,二個網上沒搜到,最重要是上上星期去sohu面試時,PM問我調優啥的。我被鄙視了。所以既然你講了。我就多問一下了。呵呵!!

    # re: 找bug記(1)[未登錄]  回復  更多評論   

    2011-07-20 09:21 by steven
    @dennis
    雖然我不能給你確切的數據,但是從理論上來說這是毋庸置疑的,否則jdk不會多搞出這個參數來做優化,再說出現這個問題一定是程序有問題,所以我們正確的做法應該是避免這種不合適的使用就可以了。。。至于你說的異常當作控制流的問題我倒覺得應該盡量避免,實在沒有辦法拋出一個stack比較淺的exception就可以了。。。

    # re: 找bug記(1)  回復  更多評論   

    2011-07-20 09:46 by dennis
    @steven
    恩,我相信對性能是有影響的。這里是一個權衡問題,我是傾向于配置這個參數來發現任何異常隱患,因為異常通常對應一個問題,而性能通常是最后一個考慮因素。

    # re: 找bug記(1)  回復  更多評論   

    2011-07-21 12:26 by imlyj
    @dennis

    麻煩回答我的問題吧。

    # re: 找bug記(1)  回復  更多評論   

    2011-07-24 11:24 by jameswxx
    我看到很多同事確實有些小地方沒有注意到:
    SimpleDateFormat不是線程安全的,被很多人誤用了。
    打日志是吃掉了異常堆棧。
    主站蜘蛛池模板: 免费国产午夜高清在线视频| 免费一级特黄特色大片| 久久国产精品免费专区| 黄网站色视频免费看无下截| 女同免费毛片在线播放| 亚洲精品蜜桃久久久久久| 巨胸喷奶水www永久免费| 亚洲深深色噜噜狠狠爱网站| 手机看片国产免费永久| 无码欧精品亚洲日韩一区| 一级毛片免费观看不卡的| 亚洲国产精品午夜电影| 国产成人免费高清激情视频| 亚洲精品无码专区在线播放| 免费黄色大片网站| 日韩在线观看免费| 亚洲精品午夜无码电影网| 免费无码成人AV在线播放不卡| 亚洲高清美女一区二区三区| 老司机在线免费视频| 久久人午夜亚洲精品无码区 | 亚洲中文字幕乱码AV波多JI| 成人黄页网站免费观看大全| 99亚洲精品卡2卡三卡4卡2卡| 精品久久久久成人码免费动漫 | a视频在线免费观看| 亚洲狠狠狠一区二区三区| 成年午夜视频免费观看视频| 老司机午夜精品视频在线观看免费 | 亚洲国产综合精品| 免费日本黄色网址| 欧洲人免费视频网站在线| 亚洲13又紧又嫩又水多| 亚洲国产高清在线一区二区三区| 久久久久久毛片免费看| 亚洲国产一区在线观看| 亚洲免费视频一区二区三区| 国产h视频在线观看网站免费| 朝桐光亚洲专区在线中文字幕| 久久被窝电影亚洲爽爽爽| 久久国内免费视频|