詳解SpringBoot+Dubbo集成ELK實(shí)戰(zhàn)
前言
一直以來,日志始終伴隨著我們的開發(fā)和運(yùn)維過程。當(dāng)系統(tǒng)出現(xiàn)了Bug,往往就是通過Xshell連接到服務(wù)器,定位到日志文件,一點(diǎn)點(diǎn)排查問題來源。
隨著互聯(lián)網(wǎng)的快速發(fā)展,我們的系統(tǒng)越來越龐大。依賴肉眼分析日志文件來排查問題的方式漸漸凸顯出一些問題:
- 分布式集群環(huán)境下,服務(wù)器數(shù)量可能達(dá)到成百上千,如何準(zhǔn)確定位?
- 微服務(wù)架構(gòu)中,如何根據(jù)異常信息,定位其他各服務(wù)的上下文信息?
- 隨著日志文件的不斷增大,可能面臨在服務(wù)器上不能直接打開的尷尬。
- 文本搜索太慢、無法多維度查詢等
面臨這些問題,我們需要集中化的日志管理,將所有服務(wù)器節(jié)點(diǎn)上的日志統(tǒng)一收集,管理,訪問。
而今天,我們的手段的就是使用 Elastic Stack 來解決它們。
一、什么是Elastic Stack ?
或許有人對(duì)Elastic感覺有一點(diǎn)點(diǎn)陌生,它的前生正是ELK ,Elastic Stack 是ELK Stack的更新?lián)Q代產(chǎn)品。
Elastic Stack分別對(duì)應(yīng)了四個(gè)開源項(xiàng)目。
Beats
Beats 平臺(tái)集合了多種單一用途數(shù)據(jù)采集器,它負(fù)責(zé)采集各種類型的數(shù)據(jù)。比如文件、系統(tǒng)監(jiān)控、Windows事件日志等。
Logstash
Logstash 是服務(wù)器端數(shù)據(jù)處理管道,能夠同時(shí)從多個(gè)來源采集數(shù)據(jù),轉(zhuǎn)換數(shù)據(jù)。沒錯(cuò),它既可以采集數(shù)據(jù),也可以轉(zhuǎn)換數(shù)據(jù)。采集到了非結(jié)構(gòu)化的數(shù)據(jù),通過過濾器把他格式化成友好的類型。
Elasticsearch
Elasticsearch 是一個(gè)基于 JSON 的分布式搜索和分析引擎。作為 Elastic Stack 的核心,它負(fù)責(zé)集中存儲(chǔ)數(shù)據(jù)。我們上面利用Beats采集數(shù)據(jù),通過Logstash轉(zhuǎn)換之后,就可以存儲(chǔ)到Elasticsearch。
Kibana
最后,就可以通過 Kibana,對(duì)自己的 Elasticsearch 中的數(shù)據(jù)進(jìn)行可視化。
本文的實(shí)例是通過 SpringBoot+Dubbo 的微服務(wù)架構(gòu),結(jié)合 Elastic Stack 來整合日志的。架構(gòu)如下:

注意,閱讀本文需要了解ELK組件的基本概念和安裝。本文不涉及安裝和基本配置過程,重點(diǎn)是如何與項(xiàng)目集成,達(dá)成上面的需求。
二、采集、轉(zhuǎn)換
1、FileBeat
在SpringBoot項(xiàng)目中,我們首先配置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 提供了一種輕量型方法,用于轉(zhuǎn)發(fā)和匯總?cè)罩九c文件。
所以,我們需要告訴 FileBeat 日志文件的位置、以及向何處轉(zhuǎn)發(fā)內(nèi)容。
如下所示,我們配置了 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 將采集到的數(shù)據(jù)轉(zhuǎn)發(fā)到 Logstash 。
#----------------------------- Logstash output -------------------------------- output.logstash: # The Logstash hosts hosts: ["192.168.159.128:5044"]
另外, FileBeat 采集文件數(shù)據(jù)時(shí),是一行一行進(jìn)行讀取的。但是 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 開始獲取數(shù)組內(nèi)容... java.lang.IndexOutOfBoundsException: Index: 3, Size: 0 at java.util.ArrayList.rangeCheck(ArrayList.java:657) at java.util.ArrayList.get(ArrayList.java:433)
所以,我們還需要配置 multiline ,以指定哪些行是單個(gè)事件的一部分。
multiline.pattern 指定要匹配的正則表達(dá)式模式。
multiline.negate 定義是否為否定模式。
multiline.match 如何將匹配的行組合到事件中,設(shè)置為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
上面配置文件說的是,如果文本內(nèi)容是以 < 或 空格 或空格+at+包路徑 或 java. 開頭,那么就將此行內(nèi)容當(dāng)做上一行的后續(xù),而不是當(dāng)做新的行。
就上面的Java異常堆棧信息就符合這個(gè)正則。所以, FileBeat 會(huì)將
java.lang.IndexOutOfBoundsException: Index: 3, Size: 0 at java.util.ArrayList.rangeCheck(ArrayList.java:657) at java.util.ArrayList.get(ArrayList.java:433)
這些內(nèi)容當(dāng)做 開始獲取數(shù)組內(nèi)容... 的一部分。
2、Logstash
在 Logback 中,我們打印日志的時(shí)候,一般會(huì)帶上日志等級(jí)、執(zhí)行類路徑、線程名稱等信息。
有一個(gè)重要的信息是,我們?cè)?ELK 查看日志的時(shí)候,是否希望將以上條件單獨(dú)拿出來做統(tǒng)計(jì)或者精確查詢?
如果是,那么就需要用到 Logstash 過濾器,它能夠解析各個(gè)事件,識(shí)別已命名的字段以構(gòu)建結(jié)構(gòu),并將它們轉(zhuǎn)換成通用格式。
那么,這時(shí)候就要先看我們?cè)陧?xiàng)目中,配置了日志以何種格式輸出。
比如,我們最熟悉的JSON格式。先來看 Logback 配置:
<pattern>
{"log_time":"%d{yyyy-MM-dd HH:mm:ss.SSS}","level":"%level","logger":"%logger","thread":"%thread","msg":"%m"}
</pattern>
沒錯(cuò), Logstash 過濾器中正好也有一個(gè)JSON解析插件。我們可以這樣配置它:
input{
stdin{}
}
filter{
json {
source => "message"
}
}
output {
stdout {}
}
這么一段配置就是說利用JSON解析器格式化數(shù)據(jù)。我們輸入這樣一行內(nèi)容:
{
"log_time":"2019-10-29 21:45:12.821",
"level":"INFO",
"logger":"com.viewscenes.order.controller.OrderController",
"thread":"http-nio-8011-exec-1",
"msg":"接收到訂單數(shù)據(jù)."
}
Logstash 將會(huì)返回格式化后的內(nèi)容:

但是JSON解析器并不太適用,因?yàn)槲覀兇蛴〉娜罩局衜sg字段本身可能就是JSON數(shù)據(jù)格式。
比如:
{
"log_time":"2019-10-29 21:57:38.008",
"level":"INFO",
"logger":"com.viewscenes.order.controller.OrderController",
"thread":"http-nio-8011-exec-1",
"msg":"接收到訂單數(shù)據(jù).{"amount":1000.0,"commodityCode":"MK66923","count":5,"id":1,"orderNo":"1001"}"
}
這時(shí)候JSON解析器就會(huì)報(bào)錯(cuò)。那怎么辦呢?
Logstash 擁有豐富的過濾器插件庫,或者你對(duì)正則有信心,也可以寫表達(dá)式去匹配。
正如我們?cè)?Logback 中配置的那樣,我們的日志內(nèi)容格式是已經(jīng)確定的,不管是JSON格式還是其他格式。
所以,筆者今天推薦另外一種:Dissect。
Dissect過濾器是一種拆分操作。與將一個(gè)定界符應(yīng)用于整個(gè)字符串的常規(guī)拆分操作不同,此操作將一組定界符應(yīng)用于字符串值。Dissect不使用正則表達(dá)式,并且速度非???。
比如,筆者在這里以 | 當(dāng)做定界符。
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>
最后同樣可以得到正確的結(jié)果:

到此,關(guān)于數(shù)據(jù)采集和格式轉(zhuǎn)換都已經(jīng)完成。當(dāng)然,上面的配置都是控制臺(tái)輸入、輸出。
我們來看一個(gè)正兒八經(jīng)的配置,它從 FileBeat 中采集數(shù)據(jù),經(jīng)由 dissect 轉(zhuǎn)換格式,并將數(shù)據(jù)輸出到 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}"
}
}
不出意外的話,打開瀏覽器我們?cè)贙ibana中就可以對(duì)日志進(jìn)行查看。比如我們查看日志等級(jí)為 DEBUG 的條目:

三、追蹤
試想一下,我們?cè)谇岸税l(fā)送了一個(gè)訂單請(qǐng)求。如果后端系統(tǒng)是微服務(wù)架構(gòu),可能會(huì)經(jīng)由庫存系統(tǒng)、優(yōu)惠券系統(tǒng)、賬戶系統(tǒng)、訂單系統(tǒng)等多個(gè)服務(wù)。如何追蹤這一個(gè)請(qǐng)求的調(diào)用鏈路呢?
1、MDC機(jī)制
首先,我們要了解一下MDC機(jī)制。
MDC - Mapped Diagnostic Contexts ,實(shí)質(zhì)上是由日志記錄框架維護(hù)的映射。其中應(yīng)用程序代碼提供鍵值對(duì),然后可以由日志記錄框架將其插入到日志消息中。
簡(jiǎn)而言之,我們使用了 MDC.PUT(key,value) ,那么 Logback 就可以在日志中自動(dòng)打印這個(gè)value。
在 SpringBoot 中,我們就可以先寫一個(gè) HandlerInterceptor ,攔截所有的請(qǐng)求,來生成一個(gè) 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 中配置一下,讓這個(gè) traceId 出現(xiàn)在日志消息中。
<pattern>
%d{yyyy-MM-dd HH:mm:ss.SSS}|%level|%logger|%thread|%X{traceId}|%m%n
</pattern>
2、Dubbo Filter
另外還有一個(gè)問題,就是在微服務(wù)架構(gòu)下我們?cè)趺醋屵@個(gè) traceId 來回透?jìng)鳌?/p>
熟悉 Dubbo 的朋友可能就會(huì)想到隱式參數(shù)。是的,我們就是利用它來完成 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");
}
}
}
}
這樣寫完,我們就可以愉快的查看某一次請(qǐng)求所有的日志信息啦。比如下面的請(qǐng)求,訂單服務(wù)和庫存服務(wù)兩個(gè)系統(tǒng)的日志。
四、總結(jié)
本文介紹了 Elastic Stack 的基本概念。并通過一個(gè) SpringBoot+Dubbo 項(xiàng)目,演示如何做到日志的集中化管理、追蹤。
事實(shí)上, Kibana 具有更多的分析和統(tǒng)計(jì)功能。所以它的作用不僅限于記錄日志。
另外 Elastic Stack 性能也很不錯(cuò)。筆者在一臺(tái)虛擬機(jī)上,記錄了100+萬條用戶數(shù)據(jù),index大小為1.1G,查詢和統(tǒng)計(jì)速度也不遜色。

以上就是本文的全部?jī)?nèi)容,希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
Java新手學(xué)習(xí)之IO流的簡(jiǎn)單使用
IO主要用于設(shè)備之間的數(shù)據(jù)傳輸,Java將操作數(shù)據(jù)流的功能封裝到了IO包中,這篇文章主要給大家介紹了關(guān)于Java新手學(xué)習(xí)之IO流簡(jiǎn)單使用的相關(guān)資料,需要的朋友可以參考下2021-10-10
SpringBoot中MockMVC單元測(cè)試的實(shí)現(xiàn)
Mock是一種用于模擬和替換類的對(duì)象的方法,以便在單元測(cè)試中獨(dú)立于外部資源進(jìn)行測(cè)試,本文主要介紹了SpringBoot中MockMVC單元測(cè)試的實(shí)現(xiàn),具有應(yīng)該的參考價(jià)值,感興趣的可以了解一下2024-02-02
Java網(wǎng)絡(luò)編程基礎(chǔ)教程之Socket入門實(shí)例
這篇文章主要介紹了Java網(wǎng)絡(luò)編程基礎(chǔ)教程之Socket入門實(shí)例,本文講解了創(chuàng)建Socket、Socket發(fā)送數(shù)據(jù)、Socket讀取數(shù)據(jù)、關(guān)閉Socket等內(nèi)容,都是最基礎(chǔ)的知識(shí)點(diǎn),需要的朋友可以參考下2014-09-09
Mybatis中攔截器的簡(jiǎn)單實(shí)現(xiàn)方法
這篇文章主要給大家介紹了關(guān)于Mybatis中攔截器的簡(jiǎn)單實(shí)現(xiàn)方法,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家學(xué)習(xí)或者使用Mybatis具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面來一起學(xué)習(xí)學(xué)習(xí)吧2019-08-08
Spring 動(dòng)態(tài)代理實(shí)現(xiàn)代碼實(shí)例
這篇文章主要介紹了Spring 動(dòng)態(tài)代理實(shí)現(xiàn)代碼實(shí)例,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2019-09-09
EventBus與Spring Event區(qū)別詳解(EventBus 事件機(jī)制,Spring Event事件機(jī)制)
這篇文章主要介紹了EventBus與Spring Event區(qū)別,需要的朋友可以參考下2020-02-02

