高并發(fā)調(diào)優(yōu)案例之logback

書接上回哀卫,在優(yōu)化完了httpClient線程池之后尸变,我們繼續(xù)增加吞吐量對(duì)該接口進(jìn)行施壓,看看是否仍有優(yōu)化的空間。同樣還是獲取ServiceB的堆棧日志并丟到gceasy(fastThread)中分析拄显,有了一個(gè)驚人的發(fā)現(xiàn),竟然有一個(gè)線程阻塞了其他61個(gè)線程案站!

線程阻塞關(guān)系

點(diǎn)開罪魁禍?zhǔn)?em>http-nio-8080-exec-86線程的堆棧信息躬审,看看它到底干了什么傷天害理之事搞得其他61個(gè)同僚只能擱那兒干瞪眼:

http-nio-8080-exec-86
Stack Trace is:
java.lang.Thread.State: RUNNABLE
at java.io.FileOutputStream.writeBytes(Native Method)
at java.io.FileOutputStream.write(FileOutputStream.java:326)
at java.io.BufferedOutputStream.flushBuffer(BufferedOutputStream.java:82)
at java.io.BufferedOutputStream.flush(BufferedOutputStream.java:140)
- locked <0x00000006c0bbef40> (a java.io.BufferedOutputStream)
at ch.qos.logback.core.recovery.ResilientOutputStreamBase.flush(ResilientOutputStreamBase.java:79)
at ch.qos.logback.core.encoder.LayoutWrappingEncoder.doEncode(LayoutWrappingEncoder.java:133)
at ch.qos.logback.core.OutputStreamAppender.writeOut(OutputStreamAppender.java:187)
at ch.qos.logback.core.FileAppender.writeOut(FileAppender.java:279)
at ch.qos.logback.core.OutputStreamAppender.subAppend(OutputStreamAppender.java:212)
at ch.qos.logback.core.rolling.RollingFileAppender.subAppend(RollingFileAppender.java:235)
at ch.qos.logback.core.OutputStreamAppender.append(OutputStreamAppender.java:100)
at ch.qos.logback.core.UnsynchronizedAppenderBase.doAppend(UnsynchronizedAppenderBase.java:84)
at ch.qos.logback.core.spi.AppenderAttachableImpl.appendLoopOnAppenders(AppenderAttachableImpl.java:51)
at ch.qos.logback.classic.Logger.appendLoopOnAppenders(Logger.java:270)
at ch.qos.logback.classic.Logger.callAppenders(Logger.java:257)
at ch.qos.logback.classic.Logger.buildLoggingEventAndAppend(Logger.java:421)
at ch.qos.logback.classic.Logger.filterAndLog_2(Logger.java:414)
at ch.qos.logback.classic.Logger.info(Logger.java:587)
at com.sawyer.mall.service.impl.CommodityServiceImp.queryStock(CommodityServiceImp.java:225)
...
中略
...
at java.lang.Thread.run(Thread.java:748)
Locked ownable synchronizers:
- <0x00000006c0bc1db8> (a java.util.concurrent.locks.ReentrantLock$NonfairSync)
- <0x00000006c6564120> (a java.util.concurrent.ThreadPoolExecutor$Worker)

通過分析堆棧信息可以看出,這個(gè)線程正在使用logback打印日志蟆盐,對(duì)照代碼里也只是很簡(jiǎn)單的一句:

log.info("業(yè)務(wù)日志");

在堆棧信息的最后承边,記錄了該線程持有的同步鎖信息Locked ownable synchronizers,可以看到該線程持有了一個(gè)很可疑的可重入鎖,鎖定對(duì)象為<0x00000006c0bc1db8>石挂,那其他線程一定是在等待這個(gè)對(duì)象的釋放博助,我們點(diǎn)開被阻塞的線程之一的http-nio-8080-exec-89

http-nio-8080-exec-89
Stack Trace is:
java.lang.Thread.State: WAITING (parking)
at sun.misc.Unsafe.park(Native Method)
- parking to wait for <0x00000006c0bc1db8> (a java.util.concurrent.locks.ReentrantLock$NonfairSync)
at java.util.concurrent.locks.LockSupport.park(LockSupport.java:175)
at java.util.concurrent.locks.AbstractQueuedSynchronizer.parkAndCheckInterrupt(AbstractQueuedSynchronizer.java:836)
at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireQueued(AbstractQueuedSynchronizer.java:870)
at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquire(AbstractQueuedSynchronizer.java:1199)
at java.util.concurrent.locks.ReentrantLock$NonfairSync.lock(ReentrantLock.java:209)
at java.util.concurrent.locks.ReentrantLock.lock(ReentrantLock.java:285)
at ch.qos.logback.core.OutputStreamAppender.subAppend(OutputStreamAppender.java:210)
at ch.qos.logback.core.rolling.RollingFileAppender.subAppend(RollingFileAppender.java:235)
at ch.qos.logback.core.OutputStreamAppender.append(OutputStreamAppender.java:100)
at ch.qos.logback.core.UnsynchronizedAppenderBase.doAppend(UnsynchronizedAppenderBase.java:84)
at ch.qos.logback.core.spi.AppenderAttachableImpl.appendLoopOnAppenders(AppenderAttachableImpl.java:51)
at ch.qos.logback.classic.Logger.appendLoopOnAppenders(Logger.java:270)
at ch.qos.logback.classic.Logger.callAppenders(Logger.java:257)
at ch.qos.logback.classic.Logger.buildLoggingEventAndAppend(Logger.java:421)
at ch.qos.logback.classic.Logger.filterAndLog_1(Logger.java:398)
at ch.qos.logback.classic.Logger.info(Logger.java:583)
at com.sawyer.mall.commodity.impl.biz.CommodityBiz.queryByIdFromCache(CommodityBiz.java:253)
下略
...

發(fā)現(xiàn)里面有一句parking to wait for <0x00000006c0bc1db8>,表示該線程的確是因?yàn)榈却?code><0x00000006c0bc1db8>對(duì)象的釋放而被阻塞痹愚。那么為什么線程http-nio-8080-exec-86在寫日志的時(shí)候會(huì)持有一個(gè)可重入鎖呢富岳?難道logback寫入日志是同步寫入的嗎蛔糯?為了探究這個(gè)問題的答案,我們查看logback的源碼窖式,發(fā)現(xiàn)常規(guī)的appender(比如我們代碼里使用的RollingFileAppender)最終都會(huì)通過OutputStreamAppender類的append寫入日志:

    /**
     * All synchronization in this class is done via the lock object.
     */
    protected final ReentrantLock lock = new ReentrantLock(false);

    /**
     * 中間代碼省略
     */

     /**
     * Actual writing occurs here.
     * <p>
     * Most subclasses of <code>WriterAppender</code> will need to override this
     * method.
     * 
     * @since 0.9.0
     */
    protected void subAppend(E event) {
        if (!isStarted()) {
            return;
        }
        try {
            // this step avoids LBCLASSIC-139
            if (event instanceof DeferredProcessingAware) {
                ((DeferredProcessingAware) event).prepareForDeferredProcessing();
            }
            // the synchronization prevents the OutputStream from being closed while we
            // are writing. It also prevents multiple threads from entering the same
            // converter. Converters assume that they are in a synchronized block.
            lock.lock();
            try {
                writeOut(event);
            } finally {
                lock.unlock();
            }
        } catch (IOException ioe) {
            // as soon as an exception occurs, move to non-started state
            // and add a single ErrorStatus to the SM.
            this.started = false;
            addStatus(new ErrorStatus("IO failure in appender", this, ioe));
        }
    }

而這個(gè)方法里蚁飒,在真正寫入日志writeOut(event)前,使用了lock.lock()進(jìn)入了同步萝喘。這里大家可以思考下為什么這里要加鎖淮逻。

其實(shí)答案很簡(jiǎn)單,就是為了防止多線程的情況下阁簸,使用同一個(gè)appender的線程互相影響爬早,比如:

  1. 線程1寫了一半,線程2就把OutputStream關(guān)閉了强窖;
  2. 多個(gè)線程同時(shí)寫入導(dǎo)致日志內(nèi)容錯(cuò)亂凸椿。

那怎么才能優(yōu)化多線程高并發(fā)的場(chǎng)景,使logback不至于成為瓶頸呢翅溺?畢竟它畢竟只是個(gè)寫日志的工具脑漫,如果因?yàn)樗拗屏送掏铝浚瑢?shí)在是撿了芝麻丟了西瓜咙崎。

通過查看源碼和官方文檔發(fā)現(xiàn)优幸,除了常規(guī)的UnsynchronizedAppender外,還有一個(gè)異步的AsyncAppender褪猛,官方文檔對(duì)它的介紹如下:

AsyncAppender buffers events in a BlockingQueue. A worker thread created by AsyncAppender takes events from the head of the queue, and dispatches them to the single appender attached to AsyncAppender. Note that by default, AsyncAppender will drop events of level TRACE, DEBUG and INFO if its queue is 80% full. This strategy has an amazingly favorable effect on performance at the cost of event loss.

簡(jiǎn)而言之网杆,就是這個(gè)AsyncAppender創(chuàng)建了一個(gè)阻塞隊(duì)列,當(dāng)有日志需要寫入的時(shí)候伊滋,先放到這個(gè)隊(duì)列中碳却,并通過一個(gè)工作線程不停地將隊(duì)列中的日志寫入事件取出來執(zhí)行涂滴,其中很重要的一點(diǎn)是捂龄,這個(gè)隊(duì)列本身是有長度限制的纵东,一旦到達(dá)了80%的水位線肮疗,則TRACE, DEBUGINFO級(jí)別的日志會(huì)被拋棄,最后官方還評(píng)價(jià)了一句尽狠,這個(gè)策略在損失一部分日志的代價(jià)下拿穴,會(huì)有顯著的性能提升蜕提。

閑話少敘我們還是來看關(guān)鍵源碼:

    @Override
    public void start() {
        if (isStarted())
            return;
        if (appenderCount == 0) {
            addError("No attached appenders found.");
            return;
        }
        if (queueSize < 1) {
            addError("Invalid queue size [" + queueSize + "]");
            return;
        }
        blockingQueue = new ArrayBlockingQueue<E>(queueSize);

        if (discardingThreshold == UNDEFINED)
            discardingThreshold = queueSize / 5;
        addInfo("Setting discardingThreshold to " + discardingThreshold);
        worker.setDaemon(true);
        worker.setName("AsyncAppender-Worker-" + getName());
        // make sure this instance is marked as "started" before staring the worker Thread
        super.start();
        worker.start();
    }

    @Override
    protected void append(E eventObject) {
        //當(dāng)超過水位線且符合TRACE/DEBUG/INFO時(shí)乌妙,直接return使兔,不打日志
        if (isQueueBelowDiscardingThreshold() && isDiscardable(eventObject)) {
            return;
        }
        preprocess(eventObject);
        put(eventObject);
    }

    private boolean isQueueBelowDiscardingThreshold() {
        return (blockingQueue.remainingCapacity() < discardingThreshold);
    }

    private void put(E eventObject) {
        if (neverBlock) {
            blockingQueue.offer(eventObject);
        } else {
            try {
                blockingQueue.put(eventObject);
            } catch (InterruptedException e) {
                // Interruption of current thread when in doAppend method should not be consumed
                // by AsyncAppender
                Thread.currentThread().interrupt();
            }
        }
    }

work線程如下:

    class Worker extends Thread {

        public void run() {
            AsyncAppenderBase<E> parent = AsyncAppenderBase.this;
            AppenderAttachableImpl<E> aai = parent.aai;

            // loop while the parent is started
            while (parent.isStarted()) {
                try {
                    E e = parent.blockingQueue.take();
                    aai.appendLoopOnAppenders(e);
                } catch (InterruptedException ie) {
                    break;
                }
            }

            addInfo("Worker thread will flush remaining events before exiting. ");

            for (E e : parent.blockingQueue) {
                aai.appendLoopOnAppenders(e);
                parent.blockingQueue.remove(e);
            }

            aai.detachAndStopAllAppenders();
        }
    }

其實(shí)說白了,這就是跟我們用MQ同步轉(zhuǎn)異步一個(gè)道理藤韵,把本來同步寫入的日志先放到隊(duì)列里虐沥,然后一個(gè)個(gè)消費(fèi)。

要配置也很簡(jiǎn)單泽艘,在logback.xml中置蜀,將需要異步寫入的appender和AsyncAppender關(guān)聯(lián)即可:

    <appender name="ASYNC" class="ch.qos.logback.classic.AsyncAppender">
        <discardingThreshold>0</discardingThreshold>
        <queueSize>500</queueSize>
        <neverBlock>true</neverBlock>
        <appender-ref ref="bizAppender"/>
    </appender>

    <root>
        <level value="INFO"/>
        <appender-ref ref="ASYNC"/>
        ...
    </root>

這里有三個(gè)重要的參數(shù):

  • discardingThreshold用來指定拋棄的水位線奈搜,默認(rèn)80%悉盆;
  • queueSize用來指定隊(duì)列大小盯荤,默認(rèn)256;
  • neverBlock 用來指定當(dāng)隊(duì)列已滿時(shí)放入新的日志事件是否拋出異常焕盟,查看上面put方法即可發(fā)現(xiàn)秋秤,如果為true則使用offer,否則使用put脚翘,區(qū)別為前者非阻塞后者阻塞灼卢。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市来农,隨后出現(xiàn)的幾起案子鞋真,更是在濱河造成了極大的恐慌,老刑警劉巖沃于,帶你破解...
    沈念sama閱讀 219,188評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件涩咖,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡繁莹,警方通過查閱死者的電腦和手機(jī)檩互,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,464評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來咨演,“玉大人闸昨,你說我怎么就攤上這事”》纾” “怎么了饵较?”我有些...
    開封第一講書人閱讀 165,562評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長遭赂。 經(jīng)常有香客問我循诉,道長,這世上最難降的妖魔是什么嵌牺? 我笑而不...
    開封第一講書人閱讀 58,893評(píng)論 1 295
  • 正文 為了忘掉前任打洼,我火速辦了婚禮,結(jié)果婚禮上逆粹,老公的妹妹穿的比我還像新娘募疮。我一直安慰自己,他們只是感情好僻弹,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,917評(píng)論 6 392
  • 文/花漫 我一把揭開白布阿浓。 她就那樣靜靜地躺著,像睡著了一般蹋绽。 火紅的嫁衣襯著肌膚如雪芭毙。 梳的紋絲不亂的頭發(fā)上筋蓖,一...
    開封第一講書人閱讀 51,708評(píng)論 1 305
  • 那天,我揣著相機(jī)與錄音退敦,去河邊找鬼粘咖。 笑死,一個(gè)胖子當(dāng)著我的面吹牛侈百,可吹牛的內(nèi)容都是我干的瓮下。 我是一名探鬼主播,決...
    沈念sama閱讀 40,430評(píng)論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼钝域,長吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼讽坏!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起例证,我...
    開封第一講書人閱讀 39,342評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤路呜,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后织咧,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體胀葱,經(jīng)...
    沈念sama閱讀 45,801評(píng)論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,976評(píng)論 3 337
  • 正文 我和宋清朗相戀三年烦感,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了巡社。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,115評(píng)論 1 351
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡手趣,死狀恐怖晌该,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情绿渣,我是刑警寧澤朝群,帶...
    沈念sama閱讀 35,804評(píng)論 5 346
  • 正文 年R本政府宣布,位于F島的核電站中符,受9級(jí)特大地震影響姜胖,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜淀散,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,458評(píng)論 3 331
  • 文/蒙蒙 一右莱、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧档插,春花似錦慢蜓、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,008評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春耘柱,著一層夾襖步出監(jiān)牢的瞬間如捅,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,135評(píng)論 1 272
  • 我被黑心中介騙來泰國打工调煎, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留镜遣,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,365評(píng)論 3 373
  • 正文 我出身青樓汛蝙,卻偏偏與公主長得像烈涮,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子窖剑,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,055評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容