SpringBoot+Dubbo集成ELK實戰

前言

一直以來,日誌始終伴隨着咱們的開發和運維過程。當系統出現了Bug,每每就是經過Xshell鏈接到服務器,定位到日誌文件,一點點排查問題來源。前端

隨着互聯網的快速發展,咱們的系統愈來愈龐大。依賴肉眼分析日誌文件來排查問題的方式漸漸凸顯出一些問題:java

  • 分佈式集羣環境下,服務器數量可能達到成百上千,如何準肯定位?
  • 微服務架構中,如何根據異常信息,定位其餘各服務的上下文信息?
  • 隨着日誌文件的不斷增大,可能面臨在服務器上不能直接打開的尷尬。
  • 文本搜索太慢、沒法多維度查詢等

面臨這些問題,咱們須要集中化的日誌管理,將全部服務器節點上的日誌統一收集,管理,訪問。git

而今天,咱們的手段的就是使用Elastic Stack來解決它們。github

1、什麼是Elastic Stack ?

或許有人對Elastic感受有一點點陌生,它的前生正是ELK ,Elastic Stack 是ELK Stack的更新換代產品。正則表達式

Elastic Stack分別對應了四個開源項目。spring

  • Beats

Beats 平臺集合了多種單一用途數據採集器,它負責採集各類類型的數據。好比文件、系統監控、Windows事件日誌等。shell

  • Logstash

Logstash 是服務器端數據處理管道,可以同時從多個來源採集數據,轉換數據。沒錯,它既能夠採集數據,也能夠轉換數據。採集到了非結構化的數據,經過過濾器把他格式化成友好的類型。apache

  • Elasticsearch

Elasticsearch 是一個基於 JSON 的分佈式搜索和分析引擎。做爲 Elastic Stack 的核心,它負責集中存儲數據。咱們上面利用Beats採集數據,經過Logstash轉換以後,就能夠存儲到Elasticsearch。json

  • Kibana

最後,就能夠經過 Kibana,對本身的 Elasticsearch 中的數據進行可視化。後端

本文的實例是經過SpringBoot+Dubbo的微服務架構,結合Elastic Stack來整合日誌的。架構以下:

注意,閱讀本文須要瞭解ELK組件的基本概念和安裝。本文不涉及安裝和基本配置過程,重點是如何與項目集成,達成上面的需求。

2、採集、轉換

一、FileBeat

在SpringBoot項目中,咱們首先配置Logback,肯定日誌文件的位置。

<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
	<file>${user.dir}/logs/order.log</file>
	<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
	    <fileNamePattern>${user.dir}/logs/order.%d{yyyy-MM-dd}.log</fileNamePattern>
	    <maxHistory>7</maxHistory>
	</rollingPolicy>
	<encoder>
	    <pattern></pattern>
	</encoder>
</appender>
複製代碼

Filebeat提供了一種輕量型方法,用於轉發和彙總日誌與文件。

因此,咱們須要告訴FileBeat日誌文件的位置、以及向何處轉發內容。

以下所示,咱們配置了FileBeat讀取usr/local/logs路徑下的全部日誌文件。

- type: log
  # Change to true to enable this input configuration.
  enabled: true
  # Paths that should be crawled and fetched. Glob based paths.
  paths:
    - /usr/local/logs/*.log
複製代碼

而後,告訴FileBeat將採集到的數據轉發到Logstash

#----------------------------- Logstash output --------------------------------
output.logstash:
  # The Logstash hosts
  hosts: ["192.168.159.128:5044"]
複製代碼

另外,FileBeat採集文件數據時,是一行一行進行讀取的。可是FileBeat收集的文件可能包含跨越多行文本的消息。

例如,在開源框架中有意的換行:

2019-10-29 20:36:04.427  INFO  org.apache.dubbo.spring.boot.context.event.WelcomeLogoApplicationListener 
 :: Dubbo Spring Boot (v2.7.1) : https://github.com/apache/incubator-dubbo-spring-boot-project
 :: Dubbo (v2.7.1) : https://github.com/apache/incubator-dubbo
 :: Discuss group : dev@dubbo.apache.org
複製代碼

或者Java異常堆棧信息:

2019-10-29 21:30:59.849 INFO com.viewscenes.order.controller.OrderController http-nio-8011-exec-2 開始獲取數組內容...
java.lang.IndexOutOfBoundsException: Index: 3, Size: 0
	at java.util.ArrayList.rangeCheck(ArrayList.java:657)
	at java.util.ArrayList.get(ArrayList.java:433)
複製代碼

因此,咱們還須要配置multiline,以指定哪些行是單個事件的一部分。

multiline.pattern 指定要匹配的正則表達式模式。

multiline.negate 定義是否爲否認模式。

multiline.match 如何將匹配的行組合到事件中,設置爲after或before。

聽起來可能比較饒口,咱們來看一組配置:

# The regexp Pattern that has to be matched. The example pattern matches all lines starting with [
multiline.pattern: '^\<|^[[:space:]]|^[[:space:]]+(at|\.{3})\b|^java.'

# Defines if the pattern set under pattern should be negated or not. Default is false.
multiline.negate: false

# Match can be set to "after" or "before". It is used to define if lines should be append to a pattern
# that was (not) matched before or after or as long as a pattern is not matched based on negate.
# Note: After is the equivalent to previous and before is the equivalent to to next in Logstash
multiline.match: after
複製代碼

上面配置文件說的是,若是文本內容是以< 或 空格 或空格+at+包路徑 或 java.開頭,那麼就將此行內容當作上一行的後續,而不是當作新的行。

就上面的Java異常堆棧信息就符合這個正則。因此,FileBeat會將

java.lang.IndexOutOfBoundsException: Index: 3, Size: 0
	at java.util.ArrayList.rangeCheck(ArrayList.java:657)
	at java.util.ArrayList.get(ArrayList.java:433)
複製代碼

這些內容當作開始獲取數組內容...的一部分。

二、Logstash

Logback中,咱們打印日誌的時候,通常會帶上日誌等級、執行類路徑、線程名稱等信息。

有一個重要的信息是,咱們在ELK查看日誌的時候,是否但願將以上條件單獨拿出來作統計或者精確查詢?

若是是,那麼就須要用到Logstash過濾器,它可以解析各個事件,識別已命名的字段以構建結構,並將它們轉換成通用格式。

那麼,這時候就要先看咱們在項目中,配置了日誌以何種格式輸出。

好比,咱們最熟悉的JSON格式。先來看Logback配置:

<pattern>
    {"log_time":"%d{yyyy-MM-dd HH:mm:ss.SSS}","level":"%level","logger":"%logger","thread":"%thread","msg":"%m"}
</pattern>
複製代碼

沒錯,Logstash過濾器中正好也有一個JSON解析插件。咱們能夠這樣配置它:

input{ 
   stdin{}
}
filter{
   json {
      source => "message"
   }
}
output {
  stdout {}
}
複製代碼

這麼一段配置就是說利用JSON解析器格式化數據。咱們輸入這樣一行內容:

{
    "log_time":"2019-10-29 21:45:12.821",
    "level":"INFO",
    "logger":"com.viewscenes.order.controller.OrderController",
    "thread":"http-nio-8011-exec-1",
    "msg":"接收到訂單數據."
}
複製代碼

Logstash將會返回格式化後的內容:

可是JSON解析器並不太適用,由於咱們打印的日誌中msg字段自己可能就是JSON數據格式。

好比:

{
    "log_time":"2019-10-29 21:57:38.008",
    "level":"INFO",
    "logger":"com.viewscenes.order.controller.OrderController",
    "thread":"http-nio-8011-exec-1",
    "msg":"接收到訂單數據.{"amount":1000.0,"commodityCode":"MK66923","count":5,"id":1,"orderNo":"1001"}"
}
複製代碼

這時候JSON解析器就會報錯。那怎麼辦呢?

Logstash擁有豐富的過濾器插件庫,或者你對正則有信心,也能夠寫表達式去匹配。

正如咱們在Logback中配置的那樣,咱們的日誌內容格式是已經肯定的,不論是JSON格式仍是其餘格式。

因此,筆者今天推薦另一種:Dissect。

Dissect過濾器是一種拆分操做。與將一個定界符應用於整個字符串的常規拆分操做不一樣,此操做將一組定界符應用於字符串值。Dissect不使用正則表達式,而且速度很是快。

好比,筆者在這裏以 | 當作定界符。

input{ 
   stdin{}
}
filter{  
   dissect {
      mapping => {
	  "message" => "%{log_time}|%{level}|%{logger}|%{thread}|%{msg}"
     }
   }   
}
output {
  stdout {}
}
複製代碼

而後在Logback中這樣去配置日誌格式:

<pattern>
    %d{yyyy-MM-dd HH:mm:ss.SSS}|%level|%logger|%thread|%m%n
</pattern>
複製代碼

最後一樣能夠獲得正確的結果:

到此,關於數據採集和格式轉換都已經完成。固然,上面的配置都是控制檯輸入、輸出。

咱們來看一個正兒八經的配置,它從FileBeat中採集數據,經由dissect轉換格式,並將數據輸出到elasticsearch

input {
  beats {
    port => 5044
  }
}
filter{
   dissect {
      mapping => {
        "message" => "%{log_time}|%{level}|%{logger}|%{thread}|%{msg}"
     }
   }
   date{
      match => ["log_time", "yyyy-MM-dd HH:mm:ss.SSS"]
      target => "@timestamp"
   }
}
output {
  elasticsearch {
    hosts => ["192.168.216.128:9200"]
    index => "logs-%{+YYYY.MM.dd}"
  }
}
複製代碼

不出意外的話,打開瀏覽器咱們在Kibana中就能夠對日誌進行查看。好比咱們查看日誌等級爲DEBUG的條目:

3、追蹤

試想一下,咱們在前端發送了一個訂單請求。若是後端系統是微服務架構,可能會經由庫存系統、優惠券系統、帳戶系統、訂單系統等多個服務。如何追蹤這一個請求的調用鏈路呢?

一、MDC機制

首先,咱們要了解一下MDC機制。

MDC - Mapped Diagnostic Contexts ,實質上是由日誌記錄框架維護的映射。其中應用程序代碼提供鍵值對,而後能夠由日誌記錄框架將其插入到日誌消息中。

簡而言之,咱們使用了MDC.PUT(key,value) ,那麼Logback就能夠在日誌中自動打印這個value。

SpringBoot中,咱們就能夠先寫一個HandlerInterceptor,攔截全部的請求,來生成一個traceId

@Component
public class TraceIdInterceptor implements HandlerInterceptor {

    Snowflake snowflake = new Snowflake(1,0);

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler){
        MDC.put("traceId",snowflake.nextIdStr());
        return true;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView){
        MDC.remove("traceId");
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex){}
}
複製代碼

而後在Logback中配置一下,讓這個traceId出如今日誌消息中。

<pattern>
    %d{yyyy-MM-dd HH:mm:ss.SSS}|%level|%logger|%thread|%X{traceId}|%m%n
</pattern>
複製代碼

二、Dubbo Filter

另外還有一個問題,就是在微服務架構下咱們怎麼讓這個traceId來回透傳。

熟悉Dubbo的朋友可能就會想到隱式參數。是的,咱們就是利用它來完成traceId的傳遞。

@Activate(group = {Constants.PROVIDER, Constants.CONSUMER}, order = 99)
public class TraceIdFilter implements Filter {
    @Override
    public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {

        String tid = MDC.get("traceId");
        String rpcTid = RpcContext.getContext().getAttachment("traceId");

        boolean bind = false;
        if (tid != null) {
            RpcContext.getContext().setAttachment("traceId", tid);
        } else {
            if (rpcTid != null) {
                MDC.put("traceId",rpcTid);
                bind = true;
            }
        }
        try{
            return invoker.invoke(invocation);
        }finally {
            if (bind){
                MDC.remove("traceId");
            }
        }
    }
}
複製代碼

這樣寫完,咱們就能夠愉快的查看某一次請求全部的日誌信息啦。好比下面的請求,訂單服務和庫存服務兩個系統的日誌。

4、總結

本文介紹了Elastic Stack的基本概念。並經過一個SpringBoot+Dubbo項目,演示如何作到日誌的集中化管理、追蹤。

事實上,Kibana具備更多的分析和統計功能。因此它的做用不只限於記錄日誌。

另外Elastic Stack性能也很不錯。筆者在一臺虛擬機上,記錄了100+萬條用戶數據,index大小爲1.1G,查詢和統計速度也不遜色。

相關文章
相關標籤/搜索