国产一级a片免费看高清,亚洲熟女中文字幕在线视频,黄三级高清在线播放,免费黄色视频在线看

打開APP
userphoto
未登錄

開通VIP,暢享免費電子書等14項超值服

開通VIP
Log4j優(yōu)化(一)擴(kuò)展Log4j來實現(xiàn)性能優(yōu)化的異步日志收集器

日志收集在互聯(lián)網(wǎng)企業(yè)尤其是大數(shù)據(jù)時代是一件非常重要的事情,日志記錄著用戶行為和系統(tǒng)行為,是一種重要的數(shù)據(jù)來源。Log4j是Java應(yīng)用程序使用的最多的一種日志收集工作。目前大量的Java應(yīng)用程序都使用著Lo4j 1.2.X版本,Log4j 1.X版本飽受詬病的原因是使用了大量的鎖,在高并發(fā)的情況下影響了系統(tǒng)的性能。這篇簡單提供一種思路,說說如何擴(kuò)展一下Log4j,提升一下Log4j的性能。


網(wǎng)上有很多分析Log4j源碼的文章,這里不重復(fù)說了,只簡單分析一下最重要的幾個組件。

先看一下Log4j的配置文件log4j.xml

  1. <?xml version="1.0" encoding="UTF-8" ?>  
  2. <!DOCTYPE log4j:configuration SYSTEM "log4j.dtd">  
  3.   
  4. <log4j:configuration xmlns:log4j="http://jakarta.apache.org/log4j/">  
  5.     <appender name="rootAppender" class="org.apache.log4j.DailyRollingFileAppender">  
  6.         <param name="File" value="test.log" />  
  7.         <param name="DatePattern" value="'.'yyyy-MM-dd" />  
  8.         <layout class="org.apache.log4j.PatternLayout">  
  9.             <param name="ConversionPattern" value="%d{yyyy-MM-dd HH:mm:ss.SSS} %5p %c{1}:%L - %m%n" />  
  10.         </layout>  
  11.     </appender>  
  12.     <logger name="com.test" additivity="false">  
  13.         <level value="debug" />  
  14.         <appender-ref ref="rootAppender" />  
  15.     </logger>  
  16.     <root>  
  17.         <level value="debug" />  
  18.         <appender-ref ref="rootAppender" />  
  19.     </root>  
  20. </log4j:configuration>  

從log4j.xml里面我們就可以看到Log4j最主要的幾個組件

1. Logger,表示日志收集器,包含了各種Level下收集日志的方法,比如debug, info, error等。Logger的一個重要屬性是additivity,表示是否附加到父Logger。Logger被設(shè)置成單根的樹形結(jié)構(gòu),根就是Root,收集日志時,可以從葉子Logger一直往上直到Root。

Logger還包含了level表示級別,包含了一個appender列表,一個Logger可以對應(yīng)多個Appender

2. Appender,表示如何處理日志,可以寫本地文件,可以寫遠(yuǎn)程文件,也可以輸出到消息隊列,等等。最常見的場景就是寫本地文件。寫文件的抽象是WriterAppender,封裝了一個過濾器流QuietWriter。既然是過濾器流,那么可以包裝節(jié)點流,默認(rèn)的就是FileOutputStream流。Log4j也支持Buffer流,在WriterAppender的子類FileAppender里面有bufferedIO屬性,如果采用Buffer流,就會在FileOutputStream外包一層BufferedWriter,最后在包到QuietWriter中。

  1. public  
  2.  synchronized  
  3.  void setFile(String fileName, boolean append, boolean bufferedIO, int bufferSize)  
  4.                                                            throws IOException {  
  5.    LogLog.debug("setFile called: "+fileName+", "+append);  
  6.   
  7.    if(bufferedIO) {  
  8.      setImmediateFlush(false);  
  9.    }  
  10.   
  11.    reset();  
  12.    FileOutputStream ostream = null;  
  13.    try {  
  14.          ostream = new FileOutputStream(fileName, append);  
  15.    } catch(FileNotFoundException ex) {  
  16.          String parentName = new File(fileName).getParent();  
  17.          if (parentName != null) {  
  18.             File parentDir = new File(parentName);  
  19.             if(!parentDir.exists() && parentDir.mkdirs()) {  
  20.                ostream = new FileOutputStream(fileName, append);  
  21.             } else {  
  22.                throw ex;  
  23.             }  
  24.          } else {  
  25.             throw ex;  
  26.          }  
  27.    }  
  28.    Writer fw = createWriter(ostream);  
  29.    if(bufferedIO) {  
  30.      fw = new BufferedWriter(fw, bufferSize);  
  31.    }  
  32.    this.setQWForFiles(fw);  
  33.    this.fileName = fileName;  
  34.    this.fileAppend = append;  
  35.    this.bufferedIO = bufferedIO;  
  36.    this.bufferSize = bufferSize;  
  37.    writeHeader();  
  38.    LogLog.debug("setFile ended");  
  39.  }  

關(guān)于Appender,尤其是基于文件的FileAppender,一定要理解,一個FileAppender實例表示的是一個打開文件對象,在計算機(jī)底層知識拾遺(四)理解文件系統(tǒng) 中我們說了從Linux的角度來說,open調(diào)用就創(chuàng)建了一個打開文件對象,并返回了一個文件描述符,其他系統(tǒng)調(diào)用都用文件描述符來引用這個打開文件對象。打開文件對象包含了當(dāng)前的讀寫位置,文件長度等等。 而在Java中并沒有提供open操作,在Java中,new一個FileInputStream/FileOutputStream實例表示創(chuàng)建了一個打開文件對象。FileInputStream/FileOutputStream維護(hù)著當(dāng)前的讀寫位置,還維護(hù)了文件描述符FileDescriptor對象和FileChannel對象。隨便提一下,日志收集操作基本上都是對文件的Append,F(xiàn)ileOutputStream支持文件的append打開方式。

可以看到, new一個FileOutputStream會創(chuàng)建一個FileDescriptor對象,并支持open和openAppend方式。這和Linux的open系統(tǒng)調(diào)用完成的功能是一樣的。

  1. public  
  2. class FileOutputStream extends OutputStream  
  3. {  
  4.     /** 
  5.      * The system dependent file descriptor. The value is 
  6.      * 1 more than actual file descriptor. This means that 
  7.      * the default value 0 indicates that the file is not open. 
  8.      */  
  9.     private FileDescriptor fd;  
  10.   
  11.     private FileChannel channel= null;  
  12.   
  13.     private boolean append = false;  
  14.   
  15.    public FileOutputStream(File file, boolean append)  
  16.         throws FileNotFoundException  
  17.     {  
  18.         String name = (file != null ? file.getPath() : null);  
  19.     SecurityManager security = System.getSecurityManager();  
  20.     if (security != null) {  
  21.         security.checkWrite(name);  
  22.     }  
  23.         if (name == null) {  
  24.             throw new NullPointerException();  
  25.         }  
  26.     fd = new FileDescriptor();  
  27.         fd.incrementAndGetUseCount();  
  28.         this.append = append;  
  29.     if (append) {  
  30.         openAppend(name);  
  31.     } else {  
  32.         open(name);  
  33.     }  
  34.     }  
  35. }  


理解了一個FileOutputStream對象實例表示了一個打開文件之后,我們再來看一下操作系統(tǒng)內(nèi)部為進(jìn)程維護(hù)運行時文件數(shù)據(jù)結(jié)構(gòu)的圖。可以看到文件描述符指向了打開文件對象,打開文件對象又指向了inode對象,inode對象代表了一個文件在操作系統(tǒng)中的實例。

對于Java來說,JVM采用了單進(jìn)程多線程的方式來支持并發(fā),進(jìn)程打開的所有文件對象對所有線程都是共享的。既然是共享,就涉及到多線程并發(fā)安全問題。在這里涉及到到3個地方存在并發(fā)安全問題。

1. 一個new FileOutputStream對象表示一個打開文件對象。那么當(dāng)多個線程使用同一個FileOutputStream對象寫文件時,就需要進(jìn)行同步操作。

2. 當(dāng)多個線程new了多個FileOutputStream對象對同一個文件進(jìn)行寫操作時,會出現(xiàn)并發(fā)問題,寫入的順序和位置都是不確定的。這種情況比較難同步,因為多個FileOutputStream對象是分散的,難以加鎖。盡量避免這樣寫文件。當(dāng)然也有解決方案,比如采用集中的地方來對外提供FileOutputStream對象,使用文件鎖對inode對象加鎖等等。

3. 當(dāng)有另外的JVM進(jìn)程對同一個文件進(jìn)行操作時。這種情況只能通過加文件鎖的方式來解決


Java提供了文件鎖的支持,F(xiàn)ileChannel提供了lock方法,并返回一個FileLock對象。





對于Log4j的使用場景,肯定是多線程并發(fā)的情況。第3種情況不用考慮。第二種情況比較復(fù)雜,所以只有第一種情況是適合的。Log4j也是采用了這一種方式。

可以把一個FileAppender對象理解成維護(hù)了一個打開文件對象,當(dāng)Logger在多線程情況下把日志寫入文件時,需要對Appender進(jìn)行同步,也就是說對Logger加鎖,保證使用同一個Logger對象時,一次只有一個線程使用這個FileAppender來寫文件,避免了多線程情況下寫文件錯誤。

就是下面這兩段代碼進(jìn)行的同步操作,但是這也是Log4j被詬病最多的地方

  1. Hierarchy.java   
  2.   
  3. // 多個線程使用同一個Logger時,加鎖  
  4.   
  5. public  
  6.   void callAppenders(LoggingEvent event) {  
  7.     int writes = 0;  
  8.   
  9.     for(Category c = this; c != null; c=c.parent) {  
  10.       // Protected against simultaneous call to addAppender, removeAppender,...  
  11.       synchronized(c) {  
  12.     if(c.aai != null) {  
  13.       writes += c.aai.appendLoopOnAppenders(event);  
  14.     }  
  15.     if(!c.additive) {  
  16.       break;  
  17.     }  
  18.       }  
  19.     }  
  20.   
  21.     if(writes == 0) {  
  22.       repository.emitNoAppenderWarning(this);  
  23.     }  
  24.   }  
  25.   
  26.   
  27. AppenderSkeleton.java  
  28.   
  29. // 多個線程使用同一個Appender時,對Appender加鎖  
  30.   
  31.   public  
  32.   synchronized   
  33.   void doAppend(LoggingEvent event) {  
  34.     if(closed) {  
  35.       LogLog.error("Attempted to append to closed appender named ["+name+"].");  
  36.       return;  
  37.     }  
  38.       
  39.     if(!isAsSevereAsThreshold(event.getLevel())) {  
  40.       return;  
  41.     }  
  42.   
  43.     Filter f = this.headFilter;  
  44.       
  45.     FILTER_LOOP:  
  46.     while(f != null) {  
  47.       switch(f.decide(event)) {  
  48.       case Filter.DENY: return;  
  49.       case Filter.ACCEPT: break FILTER_LOOP;  
  50.       case Filter.NEUTRAL: f = f.getNext();  
  51.       }  
  52.     }  
  53.       
  54.     this.append(event);      
  55.   }  


這幾個鎖在高并發(fā)的情況下對系統(tǒng)的性能影響很大,會阻塞掉業(yè)務(wù)進(jìn)程。而日志收集工作應(yīng)該是作為一個輔助功能,不能影響主業(yè)務(wù)功能。

所以可以考慮從幾個方面對Log4j進(jìn)行擴(kuò)展

1. 同步操作改異步操作,將對業(yè)務(wù)線程的影響減到最小。比如使用一個緩沖隊列,Logger.debug操作只將LoggingEvent放入緩沖隊列就結(jié)束,然后用單獨的線程做消費者去消費緩沖隊列

2. 使用阻塞隊列來做緩沖,可以用基于數(shù)組的有界隊列的,內(nèi)存可控,但是在高并發(fā)下可能會阻塞生產(chǎn)者線程,也可以用基于鏈表的無界隊列,用內(nèi)存空間換性能

3. 消費這個緩沖隊列時,可以持續(xù)消費,也可以采用定時批量消費的方式。持續(xù)消費就是使用一個無限循環(huán)的線程,只要緩沖隊列不空,就從隊列中取LoggingEvent來寫文件。由于FileAppender維護(hù)了一個打開的FileOutputStream對象實例,不會重復(fù)執(zhí)行打開文件操作,也就是說只打開一次,然后一直寫,性能也可以接受。

定時批量消費就是采用一個定時的線程,一個時間段消費一次,一次消費一批數(shù)據(jù)。


Log4j本身就提供一個AsyncAppender類來異步收集日志。

1. 它采用阻塞式的有界緩沖隊列,但是沒有采用基于管城的BlockingQueue,而是使用了普通的ArrayList,并結(jié)合條件隊列操作wait, notifty來實現(xiàn)阻塞隊列。默認(rèn)容量是128個LoggingEvent

2. 包含了一個AppenderAttachableImpl,這是一個Appender的列表結(jié)構(gòu),可以包含多個Appender

3. 使用了Dispatcher線程來做消費者

4. 消費者采用持續(xù)消費的方式,只要緩沖隊列不為空,就喚醒消費者,先將buffer填充到一個數(shù)組中,然后單獨對這個數(shù)組消費,buffer可以繼續(xù)給生產(chǎn)者使用。

  1. public class AsyncAppender extends AppenderSkeleton  
  2.   implements AppenderAttachable {  
  3.     
  4.   public static final int DEFAULT_BUFFER_SIZE = 128;  
  5.   private final List buffer = new ArrayList();   
  6.   private final Map discardMap = new HashMap();  
  7.   private int bufferSize = DEFAULT_BUFFER_SIZE;  
  8.   /** Nested appenders. */  
  9.   AppenderAttachableImpl aai;  
  10.    
  11.   private final AppenderAttachableImpl appenders;  
  12.   
  13.   public AsyncAppender() {  
  14.     appenders = new AppenderAttachableImpl();  
  15.     aai = appenders;  
  16.   
  17.     dispatcher =  
  18.       new Thread(new Dispatcher(this, buffer, discardMap, appenders));  
  19.     dispatcher.setDaemon(true);  
  20.     dispatcher.setName("AsyncAppender-Dispatcher-" + dispatcher.getName());  
  21.     dispatcher.start();  
  22.   }  
  23.   
  24. private static class Dispatcher implements Runnable {  
  25.     private final AsyncAppender parent;  
  26.     private final List buffer;  
  27.     private final Map discardMap;  
  28.     private final AppenderAttachableImpl appenders;  
  29.   
  30. }  


AsyncAppender的代碼比較老,可以使用ArrayBlockingQueue對其進(jìn)行改寫,減少條件隊列的操作。也可以進(jìn)一步優(yōu)化,采用無界的LinkedBlockingQueue,采用空間換性能的方式,這樣不會阻塞生產(chǎn)者線程,也就是不阻塞業(yè)務(wù)線程。只有當(dāng)LinedBlockingQueue為空時才會阻塞消費線程。這是可以接受的,因為消費線程不是業(yè)務(wù)線程,也防止了消費線程的空轉(zhuǎn)。

還可以使用定時批量處理的生產(chǎn)者消費者模式來異步處理線程,下面是一個簡單的基于定時線程池的定時批量異步日志收集器實現(xiàn)

1. 使用ScheduledExecutorService 定時線程池

2. 使用LinkedBlockingQueue無界阻塞隊列做緩沖,不會阻塞生產(chǎn)者線程

3. 定時執(zhí)行Dispatcher線程,每次把緩沖隊列填充到一個局部的列表,只對局部列表中的數(shù)據(jù)處理,緩沖隊列可以繼續(xù)被生產(chǎn)者使用

4. appenders上的鎖是防止添加刪除appender時出并發(fā)問題,實際上Logger一旦創(chuàng)建,很少會在運行時取修改appenders,所以這個鎖基本沒有影響


  1. import java.util.ArrayList;  
  2. import java.util.Enumeration;  
  3. import java.util.List;  
  4. import java.util.concurrent.Executors;  
  5. import java.util.concurrent.LinkedBlockingQueue;  
  6. import java.util.concurrent.ScheduledExecutorService;  
  7. import java.util.concurrent.TimeUnit;  
  8.   
  9. import org.apache.log4j.Appender;  
  10. import org.apache.log4j.AppenderSkeleton;  
  11. import org.apache.log4j.helpers.AppenderAttachableImpl;  
  12. import org.apache.log4j.spi.AppenderAttachable;  
  13. import org.apache.log4j.spi.LoggingEvent;  
  14.   
  15. public class ScheduledAsyncAppender extends AppenderSkeleton implements  
  16.         AppenderAttachable {  
  17.   
  18.     private final AppenderAttachableImpl appenders;  
  19.   
  20.     protected final ScheduledExecutorService executorService;  
  21.   
  22.     protected final LinkedBlockingQueue<LoggingEvent> queue;  
  23.   
  24.     protected long initialDelay;  
  25.   
  26.     protected long period;  
  27.   
  28.     protected TimeUnit unit;  
  29.   
  30.     public ScheduledAsyncAppender(long initialDelay, long period, TimeUnit unit) {  
  31.         this.initialDelay = initialDelay;  
  32.         this.period = period;  
  33.         this.unit = unit;  
  34.         appenders = new AppenderAttachableImpl();  
  35.         // 初始化線程池  
  36.         executorService = Executors.newScheduledThreadPool(1);  
  37.         queue = new LinkedBlockingQueue<LoggingEvent>();  
  38.         executorService.scheduleWithFixedDelay(new Dispatcher(), initialDelay,  
  39.                 period, unit);  
  40.     }  
  41.   
  42.     /** 
  43.      * 生產(chǎn)者線程只把LoggingEvent加入阻塞隊列就返回,由于是無界隊列,所以生產(chǎn)者線程不會阻塞,不影響業(yè)務(wù) 
  44.      * */  
  45.     @Override  
  46.     protected void append(LoggingEvent event) {  
  47.         try {  
  48.             queue.put(event);  
  49.         } catch (InterruptedException e) {  
  50.             // if producer interrupted, do nothing  
  51.         }  
  52.     }  
  53.   
  54.     @Override  
  55.     public void close() {  
  56.         executorService.shutdown();  
  57.   
  58.         synchronized (appenders) {  
  59.             Enumeration iter = appenders.getAllAppenders();  
  60.             if (iter != null) {  
  61.                 while (iter.hasMoreElements()) {  
  62.                     Object next = iter.nextElement();  
  63.   
  64.                     if (next instanceof Appender) {  
  65.                         ((Appender) next).close();  
  66.                     }  
  67.                 }  
  68.             }  
  69.         }  
  70.     }  
  71.   
  72.     private class Dispatcher implements Runnable {  
  73.   
  74.         @Override  
  75.         public void run() {  
  76.             List<LoggingEvent> logCache = new ArrayList<LoggingEvent>();  
  77.             queue.drainTo(logCache);  
  78.             for (LoggingEvent event : logCache) {  
  79.                 synchronized (appenders) {  
  80.                     appenders.appendLoopOnAppenders(event);  
  81.                 }  
  82.             }  
  83.         }  
  84.   
  85.     }  
  86.   
  87.     @Override  
  88.     public void addAppender(final Appender newAppender) {  
  89.         synchronized (appenders) {  
  90.             appenders.addAppender(newAppender);  
  91.         }  
  92.     }  
  93.   
  94.     @Override  
  95.     public Enumeration getAllAppenders() {  
  96.         synchronized (appenders) {  
  97.             return appenders.getAllAppenders();  
  98.         }  
  99.     }  
  100.   
  101.     @Override  
  102.     public Appender getAppender(final String name) {  
  103.         synchronized (appenders) {  
  104.             return appenders.getAppender(name);  
  105.         }  
  106.     }  
  107.   
  108.     @Override  
  109.     public boolean isAttached(final Appender appender) {  
  110.         synchronized (appenders) {  
  111.             return appenders.isAttached(appender);  
  112.         }  
  113.     }  
  114.   
  115.     @Override  
  116.     public void removeAllAppenders() {  
  117.         synchronized (appenders) {  
  118.             appenders.removeAllAppenders();  
  119.         }  
  120.     }  
  121.   
  122.     @Override  
  123.     public void removeAppender(final Appender appender) {  
  124.         synchronized (appenders) {  
  125.             appenders.removeAppender(appender);  
  126.         }  
  127.     }  
  128.   
  129.     @Override  
  130.     public void removeAppender(final String name) {  
  131.         synchronized (appenders) {  
  132.             appenders.removeAppender(name);  
  133.         }  
  134.     }  
  135.   
  136.     @Override  
  137.     public boolean requiresLayout() {  
  138.         return false;  
  139.     }  
  140.   
  141. }  

具體采用持續(xù)消費,還是定時批量消費,可以根據(jù)實際情況進(jìn)行選擇。定時批量消費時還可以優(yōu)化,比如先把批量的日志日志變成一個大日志,一次只寫一次文件文件,減少寫文件的次數(shù)。


最后看一下如何運行時創(chuàng)建Logger,而不是使用log4j.xml配置文件的方式

  1. static{  
  2.     Layout patternLayout = new PatternLayout();  
  3.     Appender appender = null;  
  4.     String fileName = Configuration.getInstance().getLogFileName();  
  5.     try {  
  6.         appender = new DailyRollingFileAppender(patternLayout, fileName, "'.'yyyy-MM-dd");  
  7.     } catch (IOException e) {  
  8.         e.printStackTrace();  
  9.     }  
  10.     log.addAppender(rtracerAppender);  
  11.     log.setLevel(Level.DEBUG);  
  12.     log.setAdditivity(false);  
  13. }  


本站僅提供存儲服務(wù),所有內(nèi)容均由用戶發(fā)布,如發(fā)現(xiàn)有害或侵權(quán)內(nèi)容,請點擊舉報。
打開APP,閱讀全文并永久保存 查看更多類似文章
猜你喜歡
類似文章
日志框架的體系結(jié)構(gòu)及l(fā)ogback的使用
ibatis 打印SQL語句
日志Log4j使用
log4j基本使用方法
log4j MDC用戶操作日志追蹤配置
Log4j自定義Appender介紹
更多類似文章 >>
生活服務(wù)
分享 收藏 導(dǎo)長圖 關(guān)注 下載文章
綁定賬號成功
后續(xù)可登錄賬號暢享VIP特權(quán)!
如果VIP功能使用有故障,
可點擊這里聯(lián)系客服!

聯(lián)系客服