一:MDC介紹
MDC(Mapped Diagnostic Context,映射調試上下文)是 log4j 和 logback 提供的一種方便在多線程條件下記錄日志的功能。某些應用程序采用多線程的方式來處理多個用戶的請求。在一個用戶的使用過程中,可能有多個不同的線程來進行處理。典型的例子是 Web 應用服務器。當用戶訪問某個頁面時,應用服務器可能會創建一個新的線程來處理該請求,也可能從線程池中復用已有的線程。在一個用戶的會話存續期間,可能有多個線程處理過該用戶的請求。這使得比較難以區分不同用戶所對應的日志。當需要追蹤某個用戶在系統中的相關日志記錄時,就會變得很麻煩。
一種解決的辦法是采用自定義的日志格式,把用戶的信息采用某種方式編碼在日志記錄中。這種方式的問題在於要求在每個使用日志記錄器的類中,都可以訪問到用戶相關的信息。這樣才可能在記錄日志時使用。這樣的條件通常是比較難以滿足的。MDC 的作用是解決這個問題。
MDC 可以看成是一個與當前線程綁定的哈希表,可以往其中添加鍵值對。MDC 中包含的內容可以被同一線程中執行的代碼所訪問。當前線程的子線程會繼承其父線程中的 MDC 的內容。當需要記錄日志時,只需要從 MDC 中獲取所需的信息即可。MDC 的內容則由程序在適當的時候保存進去。對於一個 Web 應用來說,通常是在請求被處理的最開始保存這些數據。
為了驗證MDC的正確性,我寫了個簡單的多線程程序,代碼如下:
import org.apache.log4j.MDC; public class ThreadTest extends Thread { private int i ; public ThreadTest(){ } public ThreadTest(int i){ this.i = i; } public void run(){ System.out.println(++i); MDC.put("username", i); for (int j = 0; j < 100; j++) { System.out.println("aaa" + i); if(j==10){ try { this.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } } System.out.println("run: " + i + " " + MDC.get("username")); } public static void main(String args[]) throws InterruptedException{ ThreadTest t1 = new ThreadTest(1); t1.start(); ThreadTest t2 = new ThreadTest(2); t2.start(); } }
運行結果如下:
2 3 aaa3 aaa3 aaa2 aaa3 aaa2 aaa3 aaa2 aaa3 aaa2 aaa3 aaa2 aaa3 aaa2 aaa2 aaa2 aaa2 aaa2 run: 2 2 aaa3 aaa3 aaa3 run: 3 3
從結果中可以看出:進程t1與t2在MDC中的值是沒有相互影響的,確保了多進程下進程之間在MDC存放的值是沒有相互的影響的或者說是無關的(進程t1在MDC中的username的鍵值為2;進程t2在MDC中的username的鍵值為3)。
分析:
MDC類put方法:
public static void put(String key, Object o) { mdc.put0(key, o); } private void put0(String key, Object o) { if (this.java1) { return; } Hashtable ht = (Hashtable)((ThreadLocalMap)this.tlm).get(); if (ht == null) { ht = new Hashtable(7); ((ThreadLocalMap)this.tlm).set(ht); } ht.put(key, o); }
結合類java.lang.ThreadLocal<T>及Thread類可以知道,MDC中的put方法其實就是講鍵值對放入一個Hashtable對象中,然后賦值給當前線程的ThreadLocal.ThreadLocalMap對象,即threadLocals,這保證了各個線程的在MDC鍵值對的獨立性。
下邊為java.lang.ThreadLocal<T>的部分代碼:
public class ThreadLocal<T> { public void set(T value) { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) map.set(this, value); else createMap(t, value); } public T get() { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) { ThreadLocalMap.Entry e = map.getEntry(this); if (e != null) return (T)e.value; } return setInitialValue(); } ThreadLocalMap getMap(Thread t) { return t.threadLocals; } }
Thread類的部分代碼:
public class Thread implements Runnable { ThreadLocal.ThreadLocalMap threadLocals = null; ...................... ......................... static class ThreadLocalMap { //ThreadLocalMap為Thread類的內部類 } }
二:日志聚合與分析(摘自:http://www.ibm.com/developerworks/cn/java/j-lo-practicelog/index.html) -- 此部分是為了方便自己查閱,從參考資料中摘出來放到這里的
在程序中正確的地方輸出合適的日志消息,只是合理使用日志的第一步。日志記錄的真正作用在於當有問題發生時,能夠幫助開發人員很快的定位問題所在。不過一個實用的系統通常由很多個不同的部分組成。這其中包括所開發的程序本身,也包括所依賴的第三方應用程序。以一個典型的電子商務網站為例,除了程序本身,還包括所依賴的底層操作系統、應用服務器、數據庫、HTTP 服務器和代理服務器和緩存等。當一個問題發生時,真正的原因可能來自程序本身,也可能來自所依賴的第三方程序。這就意味着開發人員可能需要檢查不同服務器上不同應用程序的日志來確定真正的原因。
日志聚合的作用就在於可以把來自不同服務器上不同應用程序產生的日志聚合起來,存放在單一的服務器上,方便進行搜索和分析。在日志聚合方面,已經有不少成熟的開源軟件可以很好的滿足需求。本文中要介紹的是 logstash,一個流行的事件和日志管理開源軟件。logstash 采用了一種簡單的處理模式:輸入 -> 過濾器 -> 輸出。logstash 可以作為代理程序安裝到每台需要收集日志的機器上。logstash 提供了非常多的插件來處理不同類型的數據輸入。典型的包括控制台、文件和 syslog 等;對於輸入的數據,可以使用過濾器來進行處理。典型的處理方式是把日志消息轉換成結構化的字段;過濾之后的結果可以被輸出到不同的目的地,比如 ElasticSearch、文件、電子郵件和數據庫等。
Logstash 在使用起來很簡單。從官方網站下載 jar 包並運行即可。在運行時需要指定一個配置文件。配置文件中定義了輸入、過濾器和輸出的相關配置。清單 9 給出了一個簡單的 logstash 配置文件的示例。
清單 9. logstash 配置文件示例
input { file { path => [ "/var/log/*.log", "/var/log/messages", "/var/log/syslog" ] type => 'syslog' } } output { stdout { debug => true debug_format => "json" } }
清單 9 中定義了 logstash 收集日志時的輸入(input)和輸出(output)的相關配置。輸入類型是文件(file)。每種類型輸入都有相應的配置。對於文件來說,需要配置的是文件的路徑。對每種類型的輸入,都需要指定一個類型(type)。該類型用來區分來自不同輸入的記錄。代碼中使用的輸出是控制台。配置文件完成之后,通過“java -jar logstash-1.1.13-flatjar.jar agent -f logstash-simple.conf”就可以啟動 logstash。
在日志分析中,比較重要的是結構化的信息。而日志信息通常只是一段文本,其中的不同字段表示不同的含義。不同的應用程序產生的日志的格式並不相同。在分析時需要關注的是其中包含的不同字段。比如 Apache 服務器會產生與用戶訪問請求相關的日志。在日志中包含了訪問者的各種信息,包括 IP 地址、時間、HTTP 狀態碼、響應內容的長度和 User Agent 字符串等信息。在 logstash 收集到日志信息之后,可以根據一定的規則把日志信息中包含的數據提取出來並命名。logstash 提供了 grok 插件可以完成這樣的功能。grok 基於正則表達式來工作,同時提供了非常多的常用類型數據的提取模式,如清單 10 所示。
清單 10. 使用 grok 提取日志記錄中的內容
在經過上面 grok 插件的提取之后,Apache 訪問日志被轉換成包含字段 client、method、request、status、bytes 和 useragent 的格式化數據。可以根據這些字段來進行搜索。這對於分析問題和進行統計都是很有幫助的。
當日志記錄通過 logstash 進行收集和處理之后,通常會把這些日志記錄保存到數據庫中進行分析和處理。目前比較流行的方式是保存到 ElasticSearch 中,從而可以利用 ElasticSearch 提供的索引和搜索能力來分析日志。已經有不少的開源軟件在 ElasticSearch 基礎之上開發出相應的日志管理功能,可以很方便的進行搜索和分析。本文中介紹的是 Graylog2。
Graylog2 由服務器和 Web 界面兩部分組成。服務器負責接收日志記錄並保存到 ElasticSearch 之中。Web 界面則可以查看和搜索日志,並提供其他的輔助功能。logstash 提供了插件 gelf,可以把 logstash 收集和處理過的日志記錄發送到 Graylog2 的服務器。這樣就可以利用 Graylog2 的 Web 界面來進行查詢和分析。只需要把清單 9 中的 logstash 的配置文件中的 output 部分改成清單 11 中所示即可。
清單 11. 配置 logstash 輸出到 Graylog2
output { gelf { host => '127.0.0.1' } }
在安裝 Graylog2 時需要注意,一定要安裝與 Graylog2 的版本相對應的版本的 ElasticSearch,否則會出現日志記錄無法保存到 ElasticSearch 的問題。本文中使用的是 Graylog2 服務器 0.11.0 版本和 ElasticSearch 0.20.4 版本。
除了 Graylog2 之外,另外一個開源軟件 Kibana 也比較流行。Kibana 可以看成是 logstash 和 ElasticSearch 的 Web 界面。Kibana 提供了更加豐富的功能來顯示和分析日志記錄。與代碼清單中的 logstash 的配置相似,只需要把輸出改為 elasticsearch 就可以了。Kibana 可以自動讀取 ElasticSearch 中包含的日志記錄並顯示。
參考資料:《Java 日志管理最佳實踐》http://www.ibm.com/developerworks/cn/java/j-lo-practicelog/index.html