1. 程式人生 > >Slf4j MDC 使用和 基於 Logback 的實現分析

Slf4j MDC 使用和 基於 Logback 的實現分析

前言

如今,在 Java 開發中,日誌的列印輸出是必不可少的,Slf4j + LogBack 的組合是最通用的方式。

有了日誌之後,我們就可以追蹤各種線上問題。但是,在分散式系統中,各種無關日誌穿行其中,導致我們可能無法直接定位整個操作流程。因此,我們可能需要對一個使用者的操作流程進行歸類標記,比如使用執行緒+時間戳,或者使用者身份標識等;如此,我們可以從大量日誌資訊中grep出某個使用者的操作流程,或者某個時間的流轉記錄。

因此,這就有了 Slf4j MDC 方法。

MDC ( Mapped Diagnostic Contexts ),顧名思義,其目的是為了便於我們診斷線上問題而出現的方法工具類。雖然,Slf4j 是用來適配其他的日誌具體實現包的,但是針對 MDC功能,目前只有logback 以及 log4j 支援,或者說由於該功能的重要性,slf4j 專門為logback系列包裝介面提供外部呼叫(玩笑~:))。

logback 和 log4j 的作者為同一人,所以這裡統稱logback系列。

先來看看 MDC 對外提高的介面:

public class MDC {
  //Put a context value as identified by key
  //into the current thread's context map.
  public static void put(String key, String val);

  //Get the context identified by the key parameter.
  public static String
get(String key); //Remove the context identified by the key parameter. public static void remove(String key); //Clear all entries in the MDC. public static void clear(); }

介面定義非常簡單,此外,其使用也非常簡單。

如上程式碼所示,一般,我們在程式碼中,只需要將指定的值put到執行緒上下文的Map中,然後,在對應的地方使用 get方法獲取對應的值。此外,對於一些執行緒池使用的應用場景,可能我們在最後使用結束時,需要呼叫clear方法來清洗將要丟棄的資料。

然後,看看一個MDC使用的簡單示例。

public class LogTest {
    private static final Logger logger = LoggerFactory.getLogger(LogTest.class);

    public static void main(String[] args) {

        MDC.put("THREAD_ID", String.valueOf(Thread.currentThread().getId()));

        logger.info("純字串資訊的info級別日誌");

    }

}

然後看看logback的輸出模板配置:

<?xml version="1.0" encoding="UTF-8"?>
<configuration>

    <property name="log.base" value="${catalina.base}/logs" />

    <contextListener class="ch.qos.logback.classic.jul.LevelChangePropagator">
        <resetJUL>true</resetJUL>
    </contextListener>

    <appender name="console" class="ch.qos.logback.core.ConsoleAppender">
        <encoder charset="UTF-8">
            <pattern>[%d{yyyy-MM-dd HH:mm:ss} %highlight(%-5p) %logger.%M\(%F:%L\)] %X{THREAD_ID} %msg%n</pattern>
        </encoder>
    </appender>
    <root level="INFO">
        <appender-ref ref="console" />
    </root>
</configuration>

於是,就有了輸出:

[2015-04-30 15:34:35 INFO  io.github.ketao1989.log4j.LogTest.main(LogTest.java:29)] 1 純字串資訊的info級別日誌

當我們在web應用中,對服務的所有請求前進行filter攔截,然後加上自定義的唯一標識到MDC中,就可以在所有日誌輸出中,清楚看到某使用者的操作流程。關於web MDC,會單獨一遍部落格介紹。

此外,關於logback 是如何將模板中的變數替換成具體的值,會在下一節分析。

在日誌模板logback.xml 中,使用 %X{ }來佔位,替換到對應的 MDC 中 key 的值。

InheritableThreadLocal 介紹

在程式碼開發中,經常使用 ThreadLocal來保證在同一個執行緒中共享變數。在 ThreadLocal 中,每個執行緒都擁有了自己獨立的一個變數,執行緒間不存在共享競爭發生,並且它們也能最大限度的由CPU排程,併發執行。顯然這是一種以空間來換取執行緒安全性的策略。

但是,ThreadLocal有一個問題,就是它只保證在同一個執行緒間共享變數,也就是說如果這個執行緒起了一個新執行緒,那麼新執行緒是不會得到父執行緒的變數資訊的。因此,為了保證子執行緒可以擁有父執行緒的某些變數檢視,JDK提供了一個數據結構,InheritableThreadLocal

javadoc 文件對 InheritableThreadLocal 說明:

該類擴充套件了 ThreadLocal,為子執行緒提供從父執行緒那裡繼承的值:在建立子執行緒時,子執行緒會接收所有可繼承的執行緒區域性變數的初始值,以獲得父執行緒所具有的值。通常,子執行緒的值與父執行緒的值是一致的;但是,通過重寫這個類中的 childValue 方法,子執行緒的值可以作為父執行緒值的一個任意函式。

當必須將變數(如使用者 ID 和 事務 ID)中維護的每執行緒屬性(per-thread-attribute)自動傳送給建立的所有子執行緒時,應儘可能地採用可繼承的執行緒區域性變數,而不是採用普通的執行緒區域性變數。

程式碼對比可以看出兩者區別:

ThreadLocal:

public class ThreadLocal<T> {

    /**
     * Method childValue is visibly defined in subclass
     * InheritableThreadLocal, but is internally defined here for the
     * sake of providing createInheritedMap factory method without
     * needing to subclass the map class in InheritableThreadLocal.
     * This technique is preferable to the alternative of embedding
     * instanceof tests in methods.
     */
    T childValue(T parentValue) {
        throw new UnsupportedOperationException();
    }

}

InheritableThreadLocal:

public class InheritableThreadLocal<T> extends ThreadLocal<T> {
    /**
     * Computes the child's initial value for this inheritable thread-local
     * variable as a function of the parent's value at the time the child
     * thread is created.  This method is called from within the parent
     * thread before the child is started.
     * <p>
     * This method merely returns its input argument, and should be overridden
     * if a different behavior is desired.
     *
     * @param parentValue the parent thread's value
     * @return the child thread's initial value
     */
    protected T childValue(T parentValue) {
        return parentValue;
    }

    /**
     * Get the map associated with a ThreadLocal.
     *
     * @param t the current thread
     */
    ThreadLocalMap getMap(Thread t) {
       return t.inheritableThreadLocals;
    }

    /**
     * Create the map associated with a ThreadLocal.
     *
     * @param t the current thread
     * @param firstValue value for the initial entry of the table.
     * @param map the map to store.
     */
    void createMap(Thread t, T firstValue) {
        t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
    }
}

// 這個是開發時一般使用的類,直接copy父執行緒的變數
public class CopyOnInheritThreadLocal extends
    InheritableThreadLocal<HashMap<String, String>> {

  /**
   * Child threads should get a copy of the parent's hashmap.
   */
  @Override
  protected HashMap<String, String> childValue(
      HashMap<String, String> parentValue) {
    if (parentValue == null) {
      return null;
    } else {
      return new HashMap<String, String>(parentValue);
    }
  }

}

為了支援InheritableThreadLocal的父子執行緒傳遞變數,JDK在Thread中,定義了ThreadLocal.ThreadLocalMap inheritableThreadLocals 屬性。該屬性變數線上程初始化的時候,如果父執行緒的該變數不為null,則會把其值複製到ThreadLocal。

從上面的程式碼實現,還可以看到,如果我們使用原生的 InheritableThreadLocal類則在子執行緒中修改變數,可能會影響到父執行緒的變數值,及其他子執行緒的值。因此,一般我們推薦沒有特殊情況,最好使用CopyOnInheritThreadLocal類,該實現是新建一個map來保持值,而不是直接使用父執行緒的引用。

Slf4j MDC 實現分析

Slf4j 的實現原則就是呼叫底層具體實現類,比如logback,logging等包;而不會去實現具體的輸出列印等操作。因此,除了前文中介紹的門面(Facade)模式外,提供這種功能的還有介面卡(Adapter)模式和裝飾(Decorator)模式。

MDC 使用的就是Decorator模式,雖然,其類命名為M MDCAdapter

Slf4j MDC 內部實現很簡單。實現一個單例對應例項,獲取具體的MDC實現類,然後其對外介面,就是對引數進行校驗,然後呼叫 MDCAdapter 的方法實現。

實現原始碼如下:

public class MDC {

  static MDCAdapter mdcAdapter;

  private MDC() {
  }

  static {
    try {
      mdcAdapter = StaticMDCBinder.SINGLETON.getMDCA();
    } catch (NoClassDefFoundError ncde) {
      //......
  }


  public static void put(String key, String val)
      throws IllegalArgumentException {
    if (key == null) {
      throw new IllegalArgumentException("key parameter cannot be null");
    }
    if (mdcAdapter == null) {
      throw new IllegalStateException("MDCAdapter cannot be null. See also "
          + NULL_MDCA_URL);
    }
    mdcAdapter.put(key, val);
  }

  public static String get(String key) throws IllegalArgumentException {
    if (key == null) {
      throw new IllegalArgumentException("key parameter cannot be null");
    }

    if (mdcAdapter == null) {
      throw new IllegalStateException("MDCAdapter cannot be null. See also "
          + NULL_MDCA_URL);
    }
    return mdcAdapter.get(key);
  }
}

對於Slf4j的MDC 部分非常簡單,MDC的核心實現是在logback方法中的。

在 logback 中,提供了 LogbackMDCAdapter類,其實現了MDCAdapter介面。基於效能的考慮,logback 對於InheritableThreadLocal的使用做了一些優化工作。

Logback MDC 實現分析

Logback 中基於 MDC 實現了LogbackMDCAdapter 類,其 get 方法實現很簡單,但是 put 方法會做一些優化操作。

關於 put 方法,主要有:

  • 使用原始的InheritableThreadLocal<Map<String, String>>類,而不是使用子執行緒複製類 CopyOnInheritThreadLocal。這樣,執行時可以大量避免不必要的copy操作,節省CPU消耗,畢竟在大量log操作中,子執行緒會很少去修改父執行緒中的key-value值。

  • 由於上一條的優化,所以程式碼實現上實現了一個寫時複製版本的 InheritableThreadLocal。實現會根據上一次操作來確定是否需要copy一份新的引用map,而不是去修改老的父執行緒的map引用。

  • 此外,和 log4j 不同,其map中的val可以為null。

下面給出,get 和 put 的程式碼實現:

public final class LogbackMDCAdapter implements MDCAdapter {

  final InheritableThreadLocal<Map<String, String>> copyOnInheritThreadLocal = new InheritableThreadLocal<Map<String, String>>();

  private static final int WRITE_OPERATION = 1;
  private static final int READ_OPERATION = 2;

  // keeps track of the last operation performed
  final ThreadLocal<Integer> lastOperation = new ThreadLocal<Integer>();

  private Integer getAndSetLastOperation(int op) {
    Integer lastOp = lastOperation.get();
    lastOperation.set(op);
    return lastOp;
  }

  private boolean wasLastOpReadOrNull(Integer lastOp) {
    return lastOp == null || lastOp.intValue() == READ_OPERATION;
  }

  private Map<String, String> duplicateAndInsertNewMap(Map<String, String> oldMap) {
    Map<String, String> newMap = Collections.synchronizedMap(new HashMap<String, String>());
    if (oldMap != null) {
        // we don't want the parent thread modifying oldMap while we are
        // iterating over it
        synchronized (oldMap) {
          newMap.putAll(oldMap);
        }
    }

    copyOnInheritThreadLocal.set(newMap);
    return newMap;
  }


  public void put(String key, String val) throws IllegalArgumentException {
    if (key == null) {
      throw new IllegalArgumentException("key cannot be null");
    }

    Map<String, String> oldMap = copyOnInheritThreadLocal.get();
    Integer lastOp = getAndSetLastOperation(WRITE_OPERATION);

    if (wasLastOpReadOrNull(lastOp) || oldMap == null) {
      // 當上一次操作是read時,這次write,則需要new
      Map<String, String> newMap = duplicateAndInsertNewMap(oldMap);
      newMap.put(key, val);
    } else {
      // 寫的話,已經new了就不需要再new
      oldMap.put(key, val);
    }
  }

  /**
   * Get the context identified by the <code>key</code> parameter.
   * <p/>
   */
  public String get(String key) {
    Map<String, String> map = getPropertyMap();
    if ((map != null) && (key != null)) {
      return map.get(key);
    } else {
      return null;
    }
  }
}

需要注意,在上面的程式碼中,write操作即put會去修改 lastOperation ,而get操作則不會。這樣就保證了,只會在第一次寫時複製。

MDC clear 操作

Notes:對於涉及到ThreadLocal相關使用的介面,都需要去考慮在使用完上下文物件時,清除掉對應的資料,以避免記憶體洩露問題。

因此,下面來分析下在MDC中如何清除掉不在需要的物件。

在MDC中提供了clear方法,該方法完成物件的清除工作,使用logback時,則呼叫的是LogbackMDCAdapter#clear()方法,繼而呼叫copyOnInheritThreadLocal.remove()

在ThreadLocal中,實現remove()方法:

public void remove() {
         ThreadLocalMap m = getMap(Thread.currentThread());
         if (m != null)
             m.remove(this);
     }

這裡,就是呼叫ThreadLocal#remove方法完成物件清理工作。

所有執行緒的ThreadLocal都是以ThreadLocalMap來維護的,也就是,我們獲取threadLocal物件時,實際上是根據當前執行緒去該Map中獲取之前的設定。在清除的時候,從這個Map中獲取對應的物件,然後移除map.

MDC 的功能實現很簡單,就是線上程上下文中,維護一個 Map<String,String> 屬性來支援日誌輸出的時候,當我們在配置檔案logback.xml 中配置了%X{key},則後臺日誌打印出對應的 key 的值。

同樣,logback.xml配置檔案支援了多種格式的日誌輸出,比如%highlight%d等等,這些標誌,在

相關推薦

Slf4j MDC 使用 基於 Logback實現分析

前言 如今,在 Java 開發中,日誌的列印輸出是必不可少的,Slf4j + LogBack 的組合是最通用的方式。 有了日誌之後,我們就可以追蹤各種線上問題。但是,在分散式系統中,各種無關日誌穿行其中,導致我們可能無法直接定位整個操作流程。因此,我們

SpringBatch中的retryskip機制實現分析

pub 限制次數 else boolean exceptio 2個 let move vat 本文主要分析SpringBatch中的retry和skip機制的實現。先簡單說明下SpringBatch在SpringBoot中的使用。如果要在springboot中使用batch

Django學習【第27篇】:ModelForm 基於Form元件實現的增刪改基於ModelForm實現的增刪改

基於Form元件實現的增刪改和基於ModelForm實現的增刪改 一、ModelForm的介紹 M

ReactiveCocoa 中 集合類RACSequence RACTuple底層實現分析

前言 在OOP的世界裡使用FRP的思想來程式設計,光有函式這種一等公民,還是無法滿足我們一些需求的。因此還是需要引用變數來完成各式各樣的類的操作行為。 在前幾篇文章中詳細的分析了RACStream中RACSignal的底層實現。RACStream還有另外一個子類,RACS

Redis分布式鎖,基於StringRedisTemplate基於Lettuce實現setNx

timeout out light 代碼 efault enum img 時間 comm 使用redis分布式鎖,來確保多個服務對共享數據操作的唯一性一般來說有StringRedisTemplate和RedisTemplate兩種redis操作模板。 根據key-valu

Array ListLinked List實現分析

一,前言 ​ 先來一張Collection集合圖。 ​ 今天分享一些關於Collection集合中的List,講真的集合這東西在網上真是老生常談了。說實話連本人都覺得膩了(哈哈),但是話又說回來,整個集合體系對於我們實際開發來說是非常重要的,所以還是有必要系統總結下。 ​ 不過在此之前先說說兩種資料結構,

SLF4J + logback 實現日誌輸出記錄

-- .com 保持 不存在 default stat 我們 fix jar包 一、SLF4J   SLF4J,即簡單日誌門面(Simple Logging Facade for Java),不是具體的日誌解決方案,它只服務於各種各樣的日誌系統。在使用SLF4J的時候,不

Slf4j.MDC原始碼分析:以及利用MDCAOP進行日誌追蹤

在 Java 開發中,日誌的列印輸出是必不可少的,Slf4j + LogBack 的組合是最通用的方式。但是,在分散式系統中,各種無關日誌穿行其中,導致我們可能無法直接定位整個操作流程。因此,我們可能需要對一個使用者的操作流程進行歸類標記,既在其日誌資訊上新增一

聚類分析(K-means 層次聚類基於密度DBSCAN演算法三種實現方式)

之前也做過聚類,只不過是用經典資料集,這次是拿的實際資料跑的結果,效果還可以,記錄一下實驗過程。 首先: 確保自己資料集是否都完整,不能有空值,最好也不要出現為0的值,會影響聚類的效果。 其次: 想好要用什麼演算法去做,K-means,層次聚類還是基於密

基於SLF4J MDC機制實現日誌的鏈路追蹤

問題描述 最近經常做線上問題的排查,而排查問題用得最多的方式是檢視日誌,但是在現有系統中,各種無關日誌穿行其中,導致我沒辦法快速的找出使用者在一次請求中所有的日誌。 問題分析 我們沒辦法快速定位使用者在一次請求中對應的所有日誌,或者說是定位某個使用者操

基於openswan klips的IPsec VPN實現分析(五)應用層核心通訊(2)

基於openswan klips的IPsec VPN實現分析(五)應用層和核心通訊——核心操作 轉載請註明出處:http://blog.csdn.net/rosetta          在資料傳送一節講過,載入模組時會執行pfkey_init()初始化與使用者層通訊的P

ThreadPoolExecutor的應用實現分析(中)—— 任務處理相關源碼分析

stateless 自身 tran als row exce 繼承 break attribute 轉自:http://www.tuicool.com/articles/rmqYjq 前面一篇文章從Executors中的工廠方法入手,已經對ThreadPoolExecuto

基於iscroll實現下拉上拉刷新

com wheel fresh ble 朋友 掃描 add 基本上 操作 http://www.zhangyunling.com/359.html 重要提示 本插件已經經過更新,查看更新的插件代碼,以及介紹請查看:基於iscroll實現下拉和上拉刷新(優化); 在原生A

基於kickstart實現網絡共享以及制作光盤U盤實現半自動安裝centos6系統

centos 一、使用kickstart實現網絡共享半自動化安裝。 ①在centos6上安裝system-config-kickstart、ftpd包。 ②使用system-config-kickstart命令,編輯裏面的內容,該文件生成ks.cfg文件。 修改完之後在File菜單中選擇Sa

基於corosyncpacemaker+drbd實現mfs高可用

mfs高可用 drbd pacemaker corosync moosefs 基於corosync和pacemaker+drbd實現mfs高可用一、MooseFS簡介1、介紹 MooseFS是一個具備冗余容錯功能的分布式網絡文件系統,它將數據分別存

基於CentOS實現LVS的nat模式DR模式

linux lvs nat dr關於LVS的錯誤總結見以下:nat模式:http://amelie.blog.51cto.com/12850951/1979172DR模式:http://amelie.blog.51cto.com/12850951/1979437來自於某國內名企架構師的說法——LVS學好了,網

實驗:基於keepalived實現兩臺realserver服務器中的nginxphp-fpm服務互為主從

基於keepalived實現nginx和php-fpm互為主從 基於keepalived實現兩臺realserver服務器中的nginx和php-fpm服務互為主從 思路:利用兩個VIP,一個定位nginx,一個定位php-fpm步驟:1、準備兩臺基於LNMP架構的服務器(能夠提供正常的web服務)2、在ng

基於std::mutex std::lock_guard std::condition_variable std::async實現的簡單同步隊列

有關 com urn list 占用空間 當前 條件變量 size 多線程 C++多線程編程中通常會對共享的數據進行寫保護,以防止多線程在對共享數據成員進行讀寫時造成資源爭搶導致程序出現未定義的行為。通常的做法是在修改共享數據成員的時候進行加鎖--mutex。在使用鎖的時

基於arcpy實現工作中需要實現功能的經驗代碼總結:

命名 parameter gem 叠代器 aps .... pri 工具箱 文件夾 不知道從哪兒總結起,就按時間順序整理吧。 關鍵詞:arcgis,python,批量,字段,地圖發布 1.給不同的要素添加不同的批量字段(例如:給“閥門”要素添加“本點號”、“點類型”、“狀

基於Express實現Passport用戶名密碼登陸認證

on() highlight npm return message false als 項目 expressjs Passport項目是一個基於Nodejs的認證中間件。 Passport可以根據應用程序的特點,配置不同的認證機制。本文將介紹,用戶名和密碼的認證登陸。