首先区岗,當我們?nèi)绻鳛榧軜?gòu)師的角度去處理一件事情的時候,必須要有一些大局觀。
也就是要求我們對個 Logging 的生態(tài)有完整的認識酸茴,從而來考慮分布式日志如何處理。
我們先來理解一些概念:
劃分清楚 Logging 兢交、Metrics薪捍、 Tracing
身邊有很多同事會把這三件可能認識不太徹底,其實這是三件分別側(cè)重點不同的事情,每件事都有各自的深度酪穿、邊界和重疊部分凳干。
Logging:
Logging 更加偏重的是一條一條的記錄,而記錄本身是離散事件被济,沒有任何直接關(guān)系救赐。Logging 可以在 Console、ElasticSearch只磷、Kafka净响、File 等各種媒介中顯示。而 Logging 的格式又可以通過各種 Logging 的實現(xiàn)去定義 Logging 的格式喳瓣。
Tracing:
Tracing 的重心是整個處理鏈條的間接關(guān)系馋贤,而是需要把各種離散的 Logging 產(chǎn)生聯(lián)系,讓各種處理事件產(chǎn)生一定范圍畏陕。
Metrics:
Metrics 度量的定義特征是它們是可聚合的配乓。它們是在一段時間內(nèi)構(gòu)成單個邏輯度量,計數(shù)或直方圖的原子數(shù)據(jù)惠毁,偏重于度量犹芹。
而三者的邊界和重疊部分需要我們在整個分布式系統(tǒng)中要非常清楚,而本 chat 就圍繞 Logging 和 Tracing 這兩件事情展開一下。
技術(shù) Tracing 鏈路跟蹤鞠绰、生態(tài)圈現(xiàn)狀
Google Dapper:Dapper——Google 生產(chǎn)環(huán)境下的分布式跟蹤系統(tǒng)腰埂,而緊接著就發(fā)表了論文 Google Dapper paper 。
然后就變成了所有分布式日志 Tracing 的鼻祖了蜈膨,后來發(fā)展起來的 Zipkin屿笼、OpenTracing、sleuth 都是在 Google 的這篇論文作為理論基礎(chǔ)上翁巍,不斷優(yōu)化發(fā)展出來的驴一。
Zipkin:Zipkin 是分布式日志鏈路跟蹤系統(tǒng),最早由 Twitter 創(chuàng)造和開源灶壶,現(xiàn)在交由 OpenZipkin 社區(qū)管理肝断。它可以幫助收集時間數(shù)據(jù)在 Microservice 架構(gòu)需要解決延遲問題。
它管理這些數(shù)據(jù)的收集和查找驰凛。Zipkin 的設計是基于 Dapper胸懈。它也是一個完整的生態(tài)解決方案包括:collector(收集)、 storage(儲存)恰响、 search(搜索)趣钱、 Zipkin Web UI(頁面控制臺)。
而其也對個各種語言做了支持渔隶,我們重點關(guān)注了一下 java 的 client羔挡,看后面的表格洁奈。github : https://github.com/openzipkin;官方地址:http://zipkin.io/
OpenTracing:OpenTracinghttp://opentracing.io/通過提供平臺無關(guān)绞灼、廠商無關(guān)的 API利术,使得開發(fā)人員能夠方便的添加(或更換)追蹤系統(tǒng)的實現(xiàn)。
OpenTracing 正在為全球的分布式追蹤低矮,提供統(tǒng)一的概念和數(shù)據(jù)標準印叁。而 OpenTracing 來自大名鼎鼎的 CNCF(Cloud Native Computing Foundation, https://www.cncf.io/)。
雖然 OpenTracing 晚于 Zipkin 但是更大大佬军掂、更大抽象轮蜕,這使其很快成為業(yè)內(nèi)首選。而開源團隊:Zipkin蝗锥、TRACER跃洛、JAEGER(Uber 出品)等等逐漸實現(xiàn)了 OpenTracing 的標準。
Sleuth:這個熱鬧的事情终议,怎么能少了 Spring 開源社區(qū)呢汇竭?Spring-Cloud-Sleuth 是 Spring Cloud 的組成部分之一,為 SpringCloud 應用實現(xiàn)了一種分布式追蹤解決方案穴张,其兼容了Zipkin细燎、HTrace、OpenTracing皂甘。
而底層是基于 Zipkin 的 brave 做了實現(xiàn)玻驻。設計思路也是參考 Dapper(他們之間的關(guān)系,作者認為應該是這樣的 sleuth 通過 brave 默認輸出到 Zipkin偿枕,由 Zipkin 決定是否是 OpenTracing 格式的輸出璧瞬,PS:這個歡迎大家一起討論)。
官方地址:http://cloud.spring.io/spring-cloud-static/spring-cloud-sleuth/2.0.0.RC1/single/spring-cloud-sleuth.html
Jaeger 分布式監(jiān)控系統(tǒng)由 Uber 設計并實現(xiàn)益老,現(xiàn)已捐贈給 CNCF 基金會彪蓬,作為分布式環(huán)境下推薦的監(jiān)控系統(tǒng)。其實主要是作為 Tracing 的 server 端捺萌,前期先支持了 Zipkin 后來又實現(xiàn)了 OpenTracing 的標準。有 UI 和儲存(ElasticSearch膘茎、cassandra)桃纯。和 Zipkin 的 ui 服務器端有點像。官方地址:https://www.jaegertracing.io/
重點關(guān)注一下 Zipkin 的開源庫
Tracing 整體負責干的事情有:
生成 trackId, spanId披坏。
負責 MDC 給 log态坦。
發(fā)送給 tracingserver 端,server 負責儲存和搜索棒拂。
Tracing UI 負責展示伞梯。
技術(shù) Logging 本身玫氢,生態(tài)圈現(xiàn)狀
上面我們了解整個 Tracing 的技術(shù)棧,我們再來看下關(guān)于 Logging 的技術(shù)棧谜诫。
Spring Logging:Java Util Logging漾峡、SLF4J、Log4J喻旷、Log4J2和Logback 這些都是老生常談的問題了生逸,默認 Spring Logging 內(nèi)部采用的是 Commons Logging。
但是當我們引用相應的其它 Logging 的實現(xiàn)和相應的 Logging 文件的時候就會自動切換 Logging 的實現(xiàn)且预,并做到兼容槽袄。我們唯一需要注意的是:SpringProfile 的支持,如下:
<springProfile name="staging">
? ?<!-- configuration to be enabled when the "staging" profile is active -->
</springProfile>
<springProfile name="dev, staging">
? ?<!-- configuration to be enabled when the "dev" or "staging" profiles are active -->
</springProfile>
<springProfile name="!production">
? ?<!-- configuration to be enabled when the "production" profile is not active -->
</springProfile>
自定義日志實現(xiàn):
ELK:Spring Logging 緊緊是負責單機日志輸出锋谐,而分布式不得不請出 ELK遍尺。
ElasticSearch 負責作為我們的 logs 的儲存和查詢,其數(shù)據(jù)可以提供給 Jaeger 使用可以給 Kibana 使用涮拗。
而 Kibana 負責做各種基于 logs 的 chat 圖和查看詳細的 Logging 的日志記錄的詳情乾戏。Logstash 不用多說了,負責給我們收集日志多搀,包括網(wǎng)關(guān)層歧蕉,業(yè)務層等。
Sentry:也是一個重量級選手康铭。負責解決我們系統(tǒng)中的 error 日志和 error 日志警告惯退。
Sentry 就是來幫我們解決這個問題的,它是一款精致的 Django 應用从藤,目的在于幫助開發(fā)人員從散落在多個不同服務器上毫無頭緒的日志文件里發(fā)掘活躍的異常催跪,繼而找到潛在的臭蟲。
Sentry 是一個日志平臺, 它分為客戶端和服務端夷野,客戶端(目前客戶端有 Python懊蒸、PHP、C#悯搔、Ruby 等多種語言)就嵌入在你的應用程序中間骑丸,程序出現(xiàn)異常就向服務端發(fā)送消息,服務端將消息記錄到數(shù)據(jù)庫中并提供一個 Web 界面方便查看妒貌。
Sentry 還有有很多亮點通危,比如敏感信息過濾, release 版本跟蹤灌曙,關(guān)鍵字查找菊碟,受影響用戶統(tǒng)計,權(quán)限管理等(部分可能需要我們通過代碼提供內(nèi)容)可以通過 Sentry 進行問題分配與跟蹤在刺。
Sentry 的 plugin 模塊還可以集成大量的第三方工具如: slack 逆害, jira 头镊。
對我們來說最大的便利就是利用日志進行錯誤發(fā)現(xiàn)和排查的效率變高了。
重要的有一下三點:
及時提醒
報警的及時性:不需要自己再去額外集成報警系統(tǒng)魄幕,一旦產(chǎn)生了 issue 便以郵件通知到項目組的每個成員相艇。
問題關(guān)聯(lián)信息的聚合
每個問題不僅有一個整體直觀的描繪,聚合的日志信息省略了人工從海量日志中尋找線索梅垄,免除大量無關(guān)信息的干擾厂捞。
豐富的上下文
Sentry 不僅豐富還規(guī)范了上下文的內(nèi)容,也讓我們意識到更多的有效內(nèi)容队丝,提高日志的質(zhì)量靡馁。
技術(shù)選型 VS
當我們了解了我們需要知道的技術(shù)點之后,接下去就是針對我們公司具體業(yè)務現(xiàn)狀進行選型机久,以我們公司為例臭墨,可能不止一個 Java 團隊,還有 Ruby膘盖,node.js 等其它語言的開發(fā)團隊胧弛。
好多其它技術(shù)選型都是基于 cncf 的,如:k8s侠畔、docker结缚、permissions 等,所以我們就一如既往的還選擇了 CNCF 的技術(shù)體系及 OpenTracing软棺。
其實如果要去真實比較的話红竭,差別也不是特別大,并且都做到了相互的兼容喘落。而 Jaeger VS Zipkin server 選擇了 Jaeger茵宪,因其啟動簡單與 Java 解耦。
Java 語言體系采用 Spring 的 Sleuth瘦棋,這樣我們可以省很多事情稀火,并且也是很成熟的解決方案,而 Spring Cloud 生態(tài)也非常成熟赌朋。
實戰(zhàn)
生產(chǎn)的日志要求
每個請求的參數(shù)是什么凰狞,輸出結(jié)果是什么,debug 可以選擇自由開啟沛慢。
每個請求的鏈路要串起來服球。
error 獨立收集上下文是什么,及時警告颠焦,各個環(huán)境分開。
生產(chǎn)的日志實現(xiàn)
第一個問題:所有請求的日志明細
1. 我們利用
importorg.springframework.web.filter.CommonsRequestLoggingFilter;
來打印我們的所有的請求的日志配置如下:
//我們只需要將此類在配置文件中加載即可往枣。里面可以設置Logging里面是否打印header 伐庭、request payload粉渠、query String 、client信息等圾另。唯一的缺點就是沒有辦法打印responseBody霸株。@Bean@ConditionalOnMissingBeanpublicCommonsRequestLoggingFilterrequestLoggingFilter(){ ? ? ? ?CommonsRequestLoggingFilter loggingFilter =newCommonsRequestLoggingFilter(); ? ? ? ?loggingFilter.setIncludeClientInfo(true); ? ? ? ?loggingFilter.setIncludeQueryString(true); ? ? ? ?loggingFilter.setIncludePayload(true); ? ? ? ?loggingFilter.setIncludeHeaders(true);returnloggingFilter; ? ?}
//源碼和原理其實非常簡單,做個filter做logging debug即可集乔。publicclassCommonsRequestLoggingFilterextendsAbstractRequestLoggingFilter{@OverrideprotectedbooleanshouldLog(HttpServletRequest request){returnlogger.isDebugEnabled(); ? ?}/**
? ? * Writes a log message before the request is processed.
? ? */@OverrideprotectedvoidbeforeRequest(HttpServletRequest request, String message){ ? ? ? ?logger.debug(message); ? ?}/**
? ? * Writes a log message after the request is processed.
? ? */@OverrideprotectedvoidafterRequest(HttpServletRequest request, String message){ ? ? ? ?logger.debug(message); ? ?}}
日志輸出的格式如下:
[36667] 2018-05-19 20:22:06.185 - [notification-api,93bb291ab411e41a,93bb291ab411e41a,false] - DEBUG [http-nio-8080-exec-1] org.springframework.web.filter.CommonsRequestLoggingFilter.log - Before request [uri=/hello;client=127.0.0.1;headers={host=[127.0.0.1:8080], connection=[keep-alive], accept=[*/*], user-agent=[Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.139 Safari/537.36], referer=[http://127.0.0.1:8080/swagger-ui.html], accept-encoding=[gzip, deflate, br], accept-language=[en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7], cookie=[OUTFOX_SEARCH_USER_ID_NCOO=1602949848.9012377; gsScrollPos-73=]}]
[36667] 2018-05-19 20:22:06.434 - [notification-api,93bb291ab411e41a,93bb291ab411e41a,false] - DEBUG [http-nio-8080-exec-1] org.springframework.web.filter.CommonsRequestLoggingFilter.log - After request [uri=/hello;client=127.0.0.1;headers={host=[127.0.0.1:8080], connection=[keep-alive], accept=[*/*], user-agent=[Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.139 Safari/537.36], referer=[http://127.0.0.1:8080/swagger-ui.html], accept-encoding=[gzip, deflate, br], accept-language=[en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7], cookie=[OUTFOX_SEARCH_USER_ID_NCOO=1602949848.9012377; gsScrollPos-73=]}]
2. 針對沒有 responseBody 的問題去件,我們可以自定義一個攔截器,和 CommonsRequestLoggingFilter 做差不多的事情即可扰路。這里需要注意的是需要用到:
importorg.springframework.web.util.ContentCachingRequestWrapper;
importorg.springframework.web.util.ContentCachingResponseWrapper;
來做參數(shù)的輸出和 response 的 io 的輸出尤溜。但是切記很多東西不需要重復寫給大家看一個關(guān)鍵代碼:
第二個問題: 將 Logging 收集到 ELK
此處我們采用的是 Docker 容器,直接將日志輸出到控制臺汗唱,用 logstash 直接收集 Docker 的日志給 ElasticSearch 在 kibana 顯示宫莱。如下圖所示:
我們只需要 search trackID 即可。
或者以 logback 為例哩罪,添加 logstash appender授霸。關(guān)鍵代碼如下:
? ? ? ? ? ? <!-- Appender to log to file in a JSON format -->
? ? ? ? ? ? <appender name="logstash" class="ch.qos.logback.core.rolling.RollingFileAppender">
? ? ? ? ? ? <file>${LOG_FILE}.json</file>
? ? ? ? ? ? <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
? ? ? ? ? ? <fileNamePattern>${LOG_FILE}.json.%d{yyyy-MM-dd}.gz</fileNamePattern>
? ? ? ? ? ? <maxHistory>7</maxHistory>
? ? ? ? ? ?</rollingPolicy>
? ? ? ? ? <encoder class="net.logstash.logback.encoder.LoggingEventCompositeJsonEncoder">
? ? ? ? ? ? <providers>
? ? ? ? ? ?<timestamp>
? ? ? ? ? ? ? ?<timeZone>UTC</timeZone>
? ? ? ? ? ?</timestamp>
? ? ? ? ? ? ? <pattern>
? ? ? ? ? ? ? ?<pattern>
? ? ? ? ? ? ? ? ? ?{
? ? ? ? ? ? ? ? ? ?"severity": "%level",
? ? ? ? ? ? ? ? ? ?"service": "${springAppName:-}",
? ? ? ? ? ? ? ? ? ?"trace": "%X{X-B3-TraceId:-}",
? ? ? ? ? ? ? ? ? ?"span": "%X{X-B3-SpanId:-}",
? ? ? ? ? ? ? ? ? ?"parent": "%X{X-B3-ParentSpanId:-}",
? ? ? ? ? ? ? ? ? ?"exportable": "%X{X-Span-Export:-}",
? ? ? ? ? ? ? ? ? ?"pid": "${PID:-}",
? ? ? ? ? ? ? ? ? ?"thread": "%thread",
? ? ? ? ? ? ? ? ? ?"class": "%logger{40}",
? ? ? ? ? ? ? ? ? ?"rest": "%message"
? ? ? ? ? ? ? ? ? ?}
? ? ? ? ? ? ? ?</pattern>
? ? ? ? ? ?</pattern>
? ? ? ?</providers>
? ?</encoder>
</appender>
第三個問題:我們在我們的每個請求 Header 上加上 traceId
//從上下文中取到traceId,然后丟到返回的header里面@OverridepublicvoiddoFilter(ServletRequest request, ServletResponse response, FilterChain chain)throwsIOException, ServletException{ ? ? ? ?String traceId = ThreadContext.get("traceId"); ? ? ? ? ? ?chain.doFilter(request, response); ? ? ? ?((HttpServletResponse)response).setHeader("TraceId", traceId); ? ?}
第四個問題:Tracing 處理
1. 有了上面的理論基礎(chǔ)际插,就是就看看 spring cloud sleuth 怎么支持 OpenTracing 和生成 tracId 和 span碘耳,及其將 log 吐給 jaeger。