這是java高並發系列第28篇文章。
環境:jdk1.8。
本文內容
- 日志有什么用?
- 日志存在的痛點?
- 構建日志系統
日志有什么用?
- 系統出現故障的時候,可以通過日志信息快速定位問題,修復bug,恢復業務
- 提取有用數據,做數據分析使用
本文主要討論通過日志來快速定位並解決問題。
日志存在的痛點
先介紹一下多數公司采用的方式:目前比較流行的是采用springcloud(或者dubbo)做微服務,按照業拆分為多個獨立的服務,服務采用集群的方式部署在不同的機器上,當一個請求過來的時候,可能會調用到很多服務進行處理,springcloud一般采用logback(或者log4j)輸出日志到文件中。當系統出問題的時候,按照系統故障的嚴重程度,嚴重的會回退版本,然后排查bug,輕的,找運維去線上拉日志,然后排查問題。
這個過程中存在一些問題:
- 日志文件太大太多,不方便查找
- 日志分散在不同的機器上,也不方便查找
- 一個請求可能會調用多個服務,完整的日志難以追蹤
- 系統出現了問題,只能等到用戶發現了,自己才知道
本文要解決上面的幾個痛點,構建我們的日志系統,達到以下要求:
- 方便追蹤一個請求完整的日志
- 方便快速檢索日志
- 系統出現問題自動報警,通知相關人員
構建日志系統
按照上面我們定的要求,一個個解決。
方便追蹤一個請求完整的日志
當一個請求過來的時候,可能會調用多個服務,多個服務內部可能又會產生子線程處理業務,所以這里面有兩個問題需要解決:
- 多個服務之間日志的追蹤
- 服務內部子線程和主線程日志的追蹤,這個地方舉個例子,比如一個請求內部需要給10000人發送推送,內部開啟10個線程並行處理,處理完畢之后響應操作者,這里面有父子線程,我們要能夠找到這個里面所有的日志
需要追蹤一個請求完整日志,我們需要給每個請求設置一個全局唯一編號,可以使用UUID或者其他方式也行。
多個服務之間日志追蹤的問題:當一個請求過來的時候,在入口處生成一個trace_id,然后放在ThreadLocal中,如果內部設計到多個服務之間相互調用,調用其他服務的時,將trace_id順便攜帶過去。
父子線程日志追蹤的問題:可以采用InheritableThreadLocal來存放trace_id,這樣可以在線程中獲取到父線程中的trace_id。
所以此處我們需要使用InheritableThreadLocal
來存儲trace_id。
關於ThreadLocal和InheritableThreadLocal可以參考:ThreadLocal、InheritableThreadLocal(通俗易懂)
如果自己使用了線程池處理請求的,由於線程池中的線程采用的是復用的方式,所以需要對執行的任務Runable做一些改造,如代碼:
public class TraceRunnable implements Runnable {
private String tranceId;
private Runnable target;
public TraceRunnable(Runnable target) {
this.tranceId = TraceUtil.get();
this.target = target;
}
@Override
public void run() {
try {
TraceUtil.set(this.tranceId);
MDC.put(TraceUtil.MDC_TRACE_ID, TraceUtil.get());
this.target.run();
} finally {
MDC.remove(TraceUtil.MDC_TRACE_ID);
TraceUtil.remove();
}
}
public static Runnable trace(Runnable target) {
return new TraceRunnable(target);
}
}
需要用線程池執行的任務使用TraceRunnable
封裝一下就可以了。
TraceUtil代碼:
public class TraceUtil {
public static final String REQUEST_HEADER_TRACE_ID = "com.ms.header.trace.id";
public static final String MDC_TRACE_ID = "trace_id";
private static InheritableThreadLocal<String> inheritableThreadLocal = new InheritableThreadLocal<>();
/**
* 獲取traceid
*
* @return
*/
public static String get() {
String traceId = inheritableThreadLocal.get();
if (traceId == null) {
traceId = IDUtil.getId();
inheritableThreadLocal.set(traceId);
}
return traceId;
}
public static void set(String trace_id) {
inheritableThreadLocal.set(trace_id);
}
public static void remove() {
inheritableThreadLocal.remove();
}
}
日志輸出中攜帶上trace_id,這樣最終我們就可以通過trace_id找到一個請求的完整日志了。
方便快速檢索日志
日志分散在不同的機器上,如果要快速檢索,需要將所有服務產生的日志匯集到一個地方。
關於檢索日志的,列一下需求:
- 我們將收集日志發送到消息中間件中(可以是kafka、rocketmq),消息中間件這塊不介紹,選擇玩的比較溜的就可以了
- 系統產生日志盡量不要影響接口的效率
- 帶寬有限的情況下,發送日志也盡量不要去影響業務
- 日志盡量低延次,產生的日志,盡量在生成之后1分鍾后可以檢索到
- 檢索日志功能要能夠快速響應
關於上面幾點,我們需要做的:日志發送的地方進行改造,引入消息中間件,將日志異步發送到消息中間件中,查詢的地方采用elasticsearch,日志系統需要訂閱消息中間件中的日志,然后丟給elasticsearch建索引,方便快速檢索,咱們來一點點的介紹。
日志發送端的改造
日志是有業務系統產生的,一個請求過來的時候會產生很多日志,日志產生時,我們盡量減少日志輸出對業務耗時的影響,我們的過程如下:
- 業務系統內部引用一個線程池來異步處理日志,線程池內部可以使用一個容量稍微大一點的阻塞隊列
- 業務系統將日志丟給線程池進行處理
- 線程池中將需要處理的日志先壓縮一下,然后發送至mq
線程池的使用可以參考:JAVA線程池,這一篇就夠了
引入mq存儲日志
業務系統將日志先發送到mq中,后面由其他消費者訂閱進行消費。日志量比較大的,對mq的要求也比較高,可以選擇kafka,業務量小的,也可以選取activemq。
使用elasticsearch來檢索日志
elasticsearch(以下簡稱es)是一個全文檢索工具,具體詳情可以參考其官網相關文檔。使用它來檢索數據效率非常高。日志系統中需要我們開發一個消費端來拉取mq中的消息,將其存儲到es中方便快速檢索,關於這塊有幾點說一下:
- 建議按天在es中建立數據庫,日質量非常大的,也可以按小時建立數據庫。查詢的時候,時間就是必選條件了,這樣可以快速讓es定位到日志庫進行檢索,提升檢索效率
- 日志常見的需要收集的信息:trace_id、時間、日志級別、類、方法、url、調用的接口開始時間、調用接口的結束時間、接口耗時、接口狀態碼、異常信息、日志信息等等,可以按照這些在es中建立索引,方便檢索。
日志監控報警
日志監控報警是非常重要的,這個必須要有,日志系統中需要開發監控報警功能,這塊我們可以做成通過頁面配置的方式,支持報警規則的配置,如日志中產生了某些異常、接口響應時間大於多少、接口返回狀態碼404等異常信息的時候能夠報警,具體的報警可以是語音電話、短信通知、釘釘機器人報警等等,這些也做成可以配置的。
日志監控模塊從mq中拉取日志,然后去匹配我們啟用的一些規則進行報警。
結構圖如下
關於搭建日志中遇到的一些痛點,可以加我微信itsoku交流。
構建日志系統需要用到的知識點
- java中線程池的使用
- ThreadLocal、InheritableThreadLocal(通俗易懂)
- elasticsearch,可以參考其官方文檔
- mq
java高並發系列目錄
- 第1天:必須知道的幾個概念
- 第2天:並發級別
- 第3天:有關並行的兩個重要定律
- 第4天:JMM相關的一些概念
- 第5天:深入理解進程和線程
- 第6天:線程的基本操作
- 第7天:volatile與Java內存模型
- 第8天:線程組
- 第9天:用戶線程和守護線程
- 第10天:線程安全和synchronized關鍵字
- 第11天:線程中斷的幾種方式
- 第12天JUC:ReentrantLock重入鎖
- 第13天:JUC中的Condition對象
- 第14天:JUC中的LockSupport工具類,必備技能
- 第15天:JUC中的Semaphore(信號量)
- 第16天:JUC中等待多線程完成的工具類CountDownLatch,必備技能
- 第17天:JUC中的循環柵欄CyclicBarrier的6種使用場景
- 第18天:JAVA線程池,這一篇就夠了
- 第19天:JUC中的Executor框架詳解1
- 第20天:JUC中的Executor框架詳解2
- 第21天:java中的CAS,你需要知道的東西
- 第22天:JUC底層工具類Unsafe,高手必須要了解
- 第23天:JUC中原子類,一篇就夠了
- 第24天:ThreadLocal、InheritableThreadLocal(通俗易懂)
- 第25天:掌握JUC中的阻塞隊列
- 第26篇:學會使用JUC中常見的集合,常看看!
- 第27天:實戰篇,接口性能提升幾倍原來這么簡單
java高並發系列連載中,總計估計會有四五十篇文章。
阿里p7一起學並發,公眾號:路人甲java,每天獲取最新文章!