一直以來,日志始終伴随着我們的開發和運維過程。當系統出現了Bug,往往就是通過Xshell連接配接到伺服器,定位到日志檔案,一點點排查問題來源。
随着網際網路的快速發展,我們的系統越來越龐大。依賴肉眼分析日志檔案來排查問題的方式漸漸凸顯出一些問題:
分布式叢集環境下,伺服器數量可能達到成百上千,如何準确定位?
微服務架構中,如何根據異常資訊,定位其他各服務的上下文資訊?
随着日志檔案的不斷增大,可能面臨在伺服器上不能直接打開的尴尬。
文本搜尋太慢、無法多元度查詢等
面臨這些問題,我們需要集中化的日志管理,将所有伺服器節點上的日志統一收集,管理,通路。
而今天,我們的手段的就是使用 <code>ElasticStack</code>來解決它們。
或許有人對Elastic感覺有一點點陌生,它的前生正是ELK ,Elastic Stack 是ELK Stack的更新換代産品。
Elastic Stack分别對應了四個開源項目。
Beats
Beats 平台集合了多種單一用途資料采集器,它負責采集各種類型的資料。比如檔案、系統監控、Windows事件日志等。
Logstash
Logstash 是伺服器端資料處理管道,能夠同時從多個來源采集資料,轉換資料。沒錯,它既可以采集資料,也可以轉換資料。采集到了非結構化的資料,通過過濾器把他格式化成友好的類型。
Elasticsearch
Elasticsearch 是一個基于 JSON 的分布式搜尋和分析引擎。作為 Elastic Stack 的核心,它負責集中存儲資料。我們上面利用Beats采集資料,通過Logstash轉換之後,就可以存儲到Elasticsearch。
Kibana
最後,就可以通過 Kibana,對自己的 Elasticsearch 中的資料進行可視化。
本文的執行個體是通過 <code>SpringBoot+Dubbo</code>的微服務架構,結合 <code>ElasticStack</code>來整合日志的。架構如下:

<code>注意,閱讀本文需要了解ELK元件的基本概念和安裝。本文不涉及安裝和基本配置過程,重點是如何與項目內建,達成上面的需求。</code>
在SpringBoot項目中,我們首先配置Logback,确定日志檔案的位置。
<code>Filebeat</code>提供了一種輕量型方法,用于轉發和彙總日志與檔案。
是以,我們需要告訴 <code>FileBeat</code>日志檔案的位置、以及向何處轉發内容。
如下所示,我們配置了 <code>FileBeat</code>讀取 <code>usr/local/logs</code>路徑下的所有日志檔案。
然後,告訴 <code>FileBeat</code>将采集到的資料轉發到 <code>Logstash</code>。
另外, <code>FileBeat</code>采集檔案資料時,是一行一行進行讀取的。但是 <code>FileBeat</code>收集的檔案可能包含跨越多行文本的消息。
例如,在開源架構中有意的換行:
或者Java異常堆棧資訊:
是以,我們還需要配置 <code>multiline</code>,以指定哪些行是單個事件的一部分。
<code>multiline.pattern</code> 指定要比對的正規表達式模式。
<code>multiline.negate</code> 定義是否為否定模式。
<code>multiline.match</code> 如何将比對的行組合到事件中,設定為after或before。
聽起來可能比較饒口,我們來看一組配置:
<code># The regexp Pattern that has to be matched. The example pattern matches all lines starting with [</code>
<code>multiline.pattern: '^\<|^[[:space:]]|^[[:space:]]+(at|\.{3})\b|^java.'</code>
<code># Defines if the pattern set under pattern should be negated or not. Default is false.</code>
<code>multiline.negate: false</code>
<code># Match can be set to "after" or "before". It is used to define if lines should be append to a pattern</code>
<code># that was (not) matched before or after or as long as a pattern is not matched based on negate.</code>
<code># Note: After is the equivalent to previous and before is the equivalent to to next in Logstash</code>
<code>multiline.match: after</code>
上面配置檔案說的是,如果文本内容是以 <code>< 或 空格 或空格+at+包路徑 或 java.</code>開頭,那麼就将此行内容當做上一行的後續,而不是當做新的行。
就上面的Java異常堆棧資訊就符合這個正則。是以, <code>FileBeat</code>會将
這些内容當做 <code>開始擷取數組内容...</code>的一部分。
在 <code>Logback</code>中,我們列印日志的時候,一般會帶上日志等級、執行類路徑、線程名稱等資訊。
有一個重要的資訊是,我們在 <code>ELK</code>檢視日志的時候,是否希望将以上條件單獨拿出來做統計或者精确查詢?
如果是,那麼就需要用到 <code>Logstash</code>過濾器,它能夠解析各個事件,識别已命名的字段以建構結構,并将它們轉換成通用格式。
那麼,這時候就要先看我們在項目中,配置了日志以何種格式輸出。
比如,我們最熟悉的JSON格式。先來看 <code>Logback</code>配置:
沒錯, <code>Logstash</code>過濾器中正好也有一個JSON解析插件。我們可以這樣配置它:
這麼一段配置就是說利用JSON解析器格式化資料。我們輸入這樣一行内容:
<code>Logstash</code>将會傳回格式化後的内容:
但是JSON解析器并不太适用,因為我們列印的日志中msg字段本身可能就是JSON資料格式。
比如:
這時候JSON解析器就會報錯。那怎麼辦呢?
<code>Logstash</code>擁有豐富的過濾器插件庫,或者你對正則有信心,也可以寫表達式去比對。
正如我們在 <code>Logback</code>中配置的那樣,我們的日志内容格式是已經确定的,不管是JSON格式還是其他格式。
是以,筆者今天推薦另外一種:Dissect。
Dissect過濾器是一種拆分操作。與将一個定界符應用于整個字元串的正常拆分操作不同,此操作将一組定界符應用于字元串值。Dissect不使用正規表達式,并且速度非常快。
比如,筆者在這裡以 <code>|</code> 當做定界符。
然後在 <code>Logback</code>中這樣去配置日志格式:
最後同樣可以得到正确的結果:
到此,關于資料采集和格式轉換都已經完成。當然,上面的配置都是控制台輸入、輸出。
我們來看一個正兒八經的配置,它從 <code>FileBeat</code>中采集資料,經由 <code>dissect</code>轉換格式,并将資料輸出到 <code>elasticsearch</code>。
不出意外的話,打開浏覽器我們在Kibana中就可以對日志進行檢視。比如我們檢視日志等級為 <code>DEBUG</code>的條目:
試想一下,我們在前端發送了一個訂單請求。如果後端系統是微服務架構,可能會經由庫存系統、優惠券系統、賬戶系統、訂單系統等多個服務。如何追蹤這一個請求的調用鍊路呢?
首先,我們要了解一下MDC機制。
MDC - Mapped Diagnostic Contexts ,實質上是由日志記錄架構維護的映射。其中應用程式代碼提供鍵值對,然後可以由日志記錄架構将其插入到日志消息中。
簡而言之,我們使用了 <code>MDC.PUT(key,value)</code> ,那麼 <code>Logback</code>就可以在日志中自動列印這個value。
在 <code>SpringBoot</code>中,我們就可以先寫一個 <code>HandlerInterceptor</code>,攔截所有的請求,來生成一個 <code>traceId</code>。
<code>@Component</code>
<code>public class TraceIdInterceptor implements HandlerInterceptor {</code>
<code> Snowflake snowflake = new Snowflake(1,0);</code>
<code> @Override</code>
<code> public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler){</code>
<code> MDC.put("traceId",snowflake.nextIdStr());</code>
<code> return true;</code>
<code> }</code>
<code> public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView){</code>
<code> MDC.remove("traceId");</code>
<code> public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex){}</code>
<code>}</code>
然後在 <code>Logback</code>中配置一下,讓這個 <code>traceId</code>出現在日志消息中。
另外還有一個問題,就是在微服務架構下我們怎麼讓這個 <code>traceId</code>來回透傳。
熟悉 <code>Dubbo</code>的朋友可能就會想到隐式參數。是的,我們就是利用它來完成 <code>traceId</code>的傳遞。
<code>@Activate(group = {Constants.PROVIDER, Constants.CONSUMER}, order = 99)</code>
<code>public class TraceIdFilter implements Filter {</code>
<code> public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {</code>
<code> String tid = MDC.get("traceId");</code>
<code> String rpcTid = RpcContext.getContext().getAttachment("traceId");</code>
<code> boolean bind = false;</code>
<code> if (tid != null) {</code>
<code> RpcContext.getContext().setAttachment("traceId", tid);</code>
<code> } else {</code>
<code> if (rpcTid != null) {</code>
<code> MDC.put("traceId",rpcTid);</code>
<code> bind = true;</code>
<code> }</code>
<code> }</code>
<code> try{</code>
<code> return invoker.invoke(invocation);</code>
<code> }finally {</code>
<code> if (bind){</code>
<code> MDC.remove("traceId");</code>
這樣寫完,我們就可以愉快的檢視某一次請求所有的日志資訊啦。比如下面的請求,訂單服務和庫存服務兩個系統的日志。
本文介紹了 <code>ElasticStack</code>的基本概念。并通過一個 <code>SpringBoot+Dubbo</code>項目,示範如何做到日志的集中化管理、追蹤。
事實上, <code>Kibana</code>具有更多的分析和統計功能。是以它的作用不僅限于記錄日志。
另外 <code>ElasticStack</code>性能也很不錯。筆者在一台虛拟機上,記錄了100+萬條使用者資料,index大小為1.1G,查詢和統計速度毫不遜色。