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


這是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,每天獲取最新文章!


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM