1. 程式人生 > 程式設計 >詳解SpringBoot+Dubbo整合ELK實戰

詳解SpringBoot+Dubbo整合ELK實戰

前言

一直以來,日誌始終伴隨著我們的開發和運維過程。當系統出現了Bug,往往就是通過Xshell連線到伺服器,定位到日誌檔案,一點點排查問題來源。

隨著網際網路的快速發展,我們的系統越來越龐大。依賴肉眼分析日誌檔案來排查問題的方式漸漸凸顯出一些問題:

  • 分散式叢集環境下,伺服器數量可能達到成百上千,如何準確定位?
  • 微服務架構中,如何根據異常資訊,定位其他各服務的上下文資訊?
  • 隨著日誌檔案的不斷增大,可能面臨在伺服器上不能直接開啟的尷尬。
  • 文字搜尋太慢、無法多維度查詢等

面臨這些問題,我們需要集中化的日誌管理,將所有伺服器節點上的日誌統一收集,管理,訪問。

而今天,我們的手段的就是使用 Elastic Stack

來解決它們。

一、什麼是Elastic Stack ?

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

Elastic Stack分別對應了四個開源專案。

Beats

Beats 平臺集合了多種單一用途資料採集器,它負責採集各種型別的資料。比如檔案、系統監控、Windows事件日誌等。

Logstash

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

Elasticsearch

Elasticsearch 是一個基於 JSON 的分散式搜尋和分析引擎。作為 Elastic Stack 的核心,它負責集中儲存資料。我們上面利用Beats採集資料,通過Logstash轉換之後,就可以儲存到Elasticsearch。

Kibana

最後,就可以通過 Kibana,對自己的 Elasticsearch 中的資料進行視覺化。

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

注意,閱讀本文需要了解ELK元件的基本概念和安裝。本文不涉及安裝和基本配置過程,重點是如何與專案整合,達成上面的需求。

二、採集、轉換

1、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 : [email protected]

或者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)

這些內容當做 開始獲取陣列內容... 的一部分。

2、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","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 的條目:

三、追蹤

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

1、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,Object handler,ModelAndView modelAndView){
  MDC.remove("traceId");
 }

 @Override
 public void afterCompletion(HttpServletRequest request,Exception ex){}
}

然後在 Logback 中配置一下,讓這個 traceId 出現在日誌訊息中。

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

2、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");
   }
  }
 }
}

這樣寫完,我們就可以愉快的檢視某一次請求所有的日誌資訊啦。比如下面的請求,訂單服務和庫存服務兩個系統的日誌。

四、總結

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

事實上, Kibana 具有更多的分析和統計功能。所以它的作用不僅限於記錄日誌。

另外 Elastic Stack 效能也很不錯。筆者在一臺虛擬機器上,記錄了100+萬條使用者資料,index大小為1.1G,查詢和統計速度也不遜色。

以上就是本文的全部內容,希望對大家的學習有所幫助,也希望大家多多支援我們。