java並發系列 – 第28天:實戰篇,微服務日誌的傷痛,一併幫你解決掉

  • 2019 年 10 月 3 日
  • 筆記

這是java高並發系列第28篇文章。

環境:jdk1.8。

本文內容

  1. 日誌有什麼用?
  2. 日誌存在的痛點?
  3. 構建日誌系統

日誌有什麼用?

  1. 系統出現故障的時候,可以通過日誌資訊快速定位問題,修復bug,恢復業務
  2. 提取有用數據,做數據分析使用

本文主要討論通過日誌來快速定位並解決問題。

日誌存在的痛點

先介紹一下多數公司採用的方式:目前比較流行的是採用springcloud(或者dubbo)做微服務,按照業拆分為多個獨立的服務,服務採用集群的方式部署在不同的機器上,當一個請求過來的時候,可能會調用到很多服務進行處理,springcloud一般採用logback(或者log4j)輸出日誌到文件中。當系統出問題的時候,按照系統故障的嚴重程度,嚴重的會回退版本,然後排查bug,輕的,找運維去線上拉日誌,然後排查問題。

這個過程中存在一些問題:

  1. 日誌文件太大太多,不方便查找
  2. 日誌分散在不同的機器上,也不方便查找
  3. 一個請求可能會調用多個服務,完整的日誌難以追蹤
  4. 系統出現了問題,只能等到用戶發現了,自己才知道

本文要解決上面的幾個痛點,構建我們的日誌系統,達到以下要求:

  1. 方便追蹤一個請求完整的日誌
  2. 方便快速檢索日誌
  3. 系統出現問題自動報警,通知相關人員

構建日誌系統

按照上面我們定的要求,一個個解決。

方便追蹤一個請求完整的日誌

當一個請求過來的時候,可能會調用多個服務,多個服務內部可能又會產生子執行緒處理業務,所以這裡面有兩個問題需要解決:

  1. 多個服務之間日誌的追蹤
  2. 服務內部子執行緒和主執行緒日誌的追蹤,這個地方舉個例子,比如一個請求內部需要給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找到一個請求的完整日誌了。

方便快速檢索日誌

日誌分散在不同的機器上,如果要快速檢索,需要將所有服務產生的日誌彙集到一個地方。

關於檢索日誌的,列一下需求:

  1. 我們將收集日誌發送到消息中間件中(可以是kafka、rocketmq),消息中間件這塊不介紹,選擇玩的比較溜的就可以了
  2. 系統產生日誌盡量不要影響介面的效率
  3. 頻寬有限的情況下,發送日誌也盡量不要去影響業務
  4. 日誌盡量低延次,產生的日誌,盡量在生成之後1分鐘後可以檢索到
  5. 檢索日誌功能要能夠快速響應

關於上面幾點,我們需要做的:日誌發送的地方進行改造,引入消息中間件,將日誌非同步發送到消息中間件中,查詢的地方採用elasticsearch,日誌系統需要訂閱消息中間件中的日誌,然後丟給elasticsearch建索引,方便快速檢索,咱們來一點點的介紹。

日誌發送端的改造

日誌是有業務系統產生的,一個請求過來的時候會產生很多日誌,日誌產生時,我們盡量減少日誌輸出對業務耗時的影響,我們的過程如下:

  1. 業務系統內部引用一個執行緒池來非同步處理日誌,執行緒池內部可以使用一個容量稍微大一點的阻塞隊列
  2. 業務系統將日誌丟給執行緒池進行處理
  3. 執行緒池中將需要處理的日誌先壓縮一下,然後發送至mq

執行緒池的使用可以參考:JAVA執行緒池,這一篇就夠了

引入mq存儲日誌

業務系統將日誌先發送到mq中,後面由其他消費者訂閱進行消費。日誌量比較大的,對mq的要求也比較高,可以選擇kafka,業務量小的,也可以選取activemq。

使用elasticsearch來檢索日誌

elasticsearch(以下簡稱es)是一個全文檢索工具,具體詳情可以參考其官網相關文檔。使用它來檢索數據效率非常高。日誌系統中需要我們開發一個消費端來拉取mq中的消息,將其存儲到es中方便快速檢索,關於這塊有幾點說一下:

  1. 建議按天在es中建立資料庫,日品質非常大的,也可以按小時建立資料庫。查詢的時候,時間就是必選條件了,這樣可以快速讓es定位到日誌庫進行檢索,提升檢索效率
  2. 日誌常見的需要收集的資訊:trace_id、時間、日誌級別、類、方法、url、調用的介面開始時間、調用介面的結束時間、介面耗時、介面狀態碼、異常資訊、日誌資訊等等,可以按照這些在es中建立索引,方便檢索。

日誌監控報警

日誌監控報警是非常重要的,這個必須要有,日誌系統中需要開發監控報警功能,這塊我們可以做成通過頁面配置的方式,支援報警規則的配置,如日誌中產生了某些異常、介面響應時間大於多少、介面返回狀態碼404等異常資訊的時候能夠報警,具體的報警可以是語音電話、簡訊通知、釘釘機器人報警等等,這些也做成可以配置的。

日誌監控模組從mq中拉取日誌,然後去匹配我們啟用的一些規則進行報警。

結構圖如下

關於搭建日誌中遇到的一些痛點,可以加我微信itsoku交流。

構建日誌系統需要用到的知識點

  1. java中執行緒池的使用
  2. ThreadLocal、InheritableThreadLocal(通俗易懂)
  3. elasticsearch,可以參考其官方文檔
  4. mq

java高並發系列目錄

  1. 第1天:必須知道的幾個概念
  2. 第2天:並發級別
  3. 第3天:有關並行的兩個重要定律
  4. 第4天:JMM相關的一些概念
  5. 第5天:深入理解進程和執行緒
  6. 第6天:執行緒的基本操作
  7. 第7天:volatile與Java記憶體模型
  8. 第8天:執行緒組
  9. 第9天:用戶執行緒和守護執行緒
  10. 第10天:執行緒安全和synchronized關鍵字
  11. 第11天:執行緒中斷的幾種方式
  12. 第12天JUC:ReentrantLock重入鎖
  13. 第13天:JUC中的Condition對象
  14. 第14天:JUC中的LockSupport工具類,必備技能
  15. 第15天:JUC中的Semaphore(訊號量)
  16. 第16天:JUC中等待多執行緒完成的工具類CountDownLatch,必備技能
  17. 第17天:JUC中的循環柵欄CyclicBarrier的6種使用場景
  18. 第18天:JAVA執行緒池,這一篇就夠了
  19. 第19天:JUC中的Executor框架詳解1
  20. 第20天:JUC中的Executor框架詳解2
  21. 第21天:java中的CAS,你需要知道的東西
  22. 第22天:JUC底層工具類Unsafe,高手必須要了解
  23. 第23天:JUC中原子類,一篇就夠了
  24. 第24天:ThreadLocal、InheritableThreadLocal(通俗易懂)
  25. 第25天:掌握JUC中的阻塞隊列
  26. 第26篇:學會使用JUC中常見的集合,常看看!
  27. 第27天:實戰篇,介面性能提升幾倍原來這麼簡單

java高並發系列連載中,總計估計會有四五十篇文章。

阿里p7一起學並發,公眾號:路人甲java,每天獲取最新文章!