戰(zhàn):Spring Boot日志收集方案與踩坑記錄)
凌晨兩點(diǎn)被值班電話叫醒說線上訂單成功率驟降。我登上跳板機(jī)打開終端面對的是三臺應(yīng)用服務(wù)器上的幾十個(gè)日志文件。一行一行地grep關(guān)鍵字一會兒懷疑是數(shù)據(jù)庫連接池爆了一會兒又覺得像是下游接口超時(shí)來來回回折騰了一個(gè)多小時(shí)最后才發(fā)現(xiàn)是某臺實(shí)例里一條被截?cái)嗟亩褩H罩局赶蛄藘?nèi)存溢出。那一刻我下定決心不能再靠肉眼翻文件排查問題了必須把日志統(tǒng)一收起來在一個(gè)地方就能搜、能看、能分析。這就是我搭建ELK收集Spring Boot日志的背景。ELK指的是Elasticsearch、Logstash、Kibana這套組合專門用來做日志的采集、存儲、檢索和可視化。如果你也在維護(hù)Spring Boot應(yīng)用而且手頭有超過兩臺服務(wù)器日志排查已經(jīng)讓你感到吃力那這篇文章寫的整套方案和踩坑記錄應(yīng)該能幫你少走不少彎路。下面不講什么架構(gòu)圖和高大上的原理只講我從零開始搭、上線跑了半年之后沉淀下來的實(shí)操內(nèi)容和教訓(xùn)。1. 為什么非要用ELK這套組合來收集Spring Boot日志1.1 沒有日志平臺之前排查問題到底是什么狀態(tài)在動手搭建之前我先梳理了一下原來的日志處理方式。Spring Boot默認(rèn)的日志輸出就是往本地文件里寫一臺服務(wù)器一個(gè)文件甚至一個(gè)應(yīng)用多個(gè)實(shí)例就是多個(gè)文件。我們當(dāng)時(shí)的架構(gòu)是Nginx后面掛了三個(gè)應(yīng)用實(shí)例日志分別落在各自的服務(wù)器上排查問題只能按下面這套流程走先看告警猜一個(gè)大概的時(shí)間點(diǎn)。依次登錄三臺服務(wù)器用tail、grep配合時(shí)間戳去翻日志。如果問題跨多個(gè)服務(wù)還要一邊看應(yīng)用日志一邊看下游服務(wù)日志人工對時(shí)間線。遇到堆棧異常被截?cái)噙€得回服務(wù)器重新找原始文件。這招在小流量的單機(jī)場景下勉強(qiáng)能忍但只要請求量上來日志一滾動舊文件被壓縮成.gz包排查效率會進(jìn)一步下降。更頭疼的是沒有全局聚合能力——你根本無法快速回答“過去半小時(shí)內(nèi)所有ERROR級別的日志有多少條、分布在哪些實(shí)例”這類問題。而這類問題恰恰是定位線上故障最常用的切入點(diǎn)。1.2 ELK三個(gè)組件在鏈路里的分工ELK這個(gè)名字看著像是一個(gè)整體但每個(gè)組件的職責(zé)非常清楚我后來跟同事解釋的時(shí)候經(jīng)常打一個(gè)比方Logstash是搬運(yùn)工Elasticsearch是倉庫Kibana是倉庫的展示窗口。Logstash負(fù)責(zé)從日志文件或者其他數(shù)據(jù)源采集數(shù)據(jù)做過濾、解析、轉(zhuǎn)換然后把干凈的數(shù)據(jù)送給Elasticsearch。它插件生態(tài)豐富支持input、filter、output各種插件自由組合。Elasticsearch負(fù)責(zé)存儲和檢索。它基于倒排索引能對日志內(nèi)容做全文檢索也能對字段做聚合統(tǒng)計(jì)比如按日志級別、按應(yīng)用實(shí)例、按接口路徑等維度做count。Kibana負(fù)責(zé)可視化。它把Elasticsearch里的數(shù)據(jù)變成可查詢的界面可以寫KQL語法做搜索也可以拖拽生成柱狀圖、折線圖、餅圖甚至做Dashboard。對Spring Boot項(xiàng)目而言這套組合最大的優(yōu)勢在于應(yīng)用只需要做很小的改動——把日志格式從純文本改成JSON——剩下的采集、清洗、存儲、查詢?nèi)溌范际乾F(xiàn)成的解決方案。不需要從零開發(fā)也不需要維護(hù)復(fù)雜的自研組件社區(qū)里的踩坑資料也足夠豐富遇到問題搜一下一般都有答案。我當(dāng)時(shí)也考慮過是否直接用云廠商的日志服務(wù)但最終選擇自建ELK一是因?yàn)槲覀冇鞋F(xiàn)成的服務(wù)器資源二是因?yàn)槿罩緮?shù)據(jù)的控制權(quán)在自己手里索引策略、保留周期、字段映射都能靈活調(diào)。1.3 什么規(guī)模的上應(yīng)用才適合上ELK這里我必須說點(diǎn)實(shí)話。ELK不是銀彈如果你的Spring Boot應(yīng)用只是單機(jī)部署每天的日志量只有幾百M(fèi)B那完全沒必要搞這套東西直接在服務(wù)器上用grep和tail就夠用Kibana帶來的查詢便利遠(yuǎn)不如運(yùn)維成本大。適合上ELK的場景我總結(jié)為三條應(yīng)用實(shí)例數(shù)大于等于3日志已經(jīng)分散在多臺機(jī)器上。排查線上問題需要對多實(shí)例日志做時(shí)間線合并或全局檢索。團(tuán)隊(duì)有基本的Linux操作能力和意愿去維護(hù)Elasticsearch集群。如果你符合上面兩三條再動手。否則不要為了技術(shù)而技術(shù)。2. Spring Boot日志端改造先把日志變成機(jī)器能讀懂的結(jié)構(gòu)2.1 默認(rèn)文本日志為什么不適合被采集網(wǎng)上很多ELK教程上來就直接教你怎么配Logstash但我個(gè)人強(qiáng)烈建議先改應(yīng)用日志輸出格式。Spring Boot默認(rèn)的文本日志長這樣2025-01-05 10:00:00.123 INFO 12345 --- [http-nio-8080-exec-1] com.example.service.OrderService : 訂單創(chuàng)建成功orderId10086這種日志人眼讀起來沒問題但對Logstash來說不夠友好。想在Logstash里把時(shí)間、級別、線程名、類名、消息拆成獨(dú)立字段你得寫一堆grok正則而且Java異常堆棧是多行的處理起來更是麻煩——一條堆棧信息會被拆成好幾個(gè)事件后續(xù)檢索時(shí)根本沒法把異常堆棧當(dāng)成一個(gè)整體來看。我一開始也想用純文本加grok的方案后來發(fā)現(xiàn)隨著日志里字段越來越多正則只會越來越復(fù)雜維護(hù)成本實(shí)在太高。最后換成了JSON結(jié)構(gòu)化輸出效果立竿見影Logstash里一句json插件就能解析所有字段自動拆分多行堆棧在JSON里就是一個(gè)字符串不用再處理multiline問題。2.2 用logstash-logback-encoder輸出JSON日志Spring Boot 2.x默認(rèn)使用Logback作為日志框架我們只需要引入一個(gè)編碼器依賴并調(diào)整logback-spring.xml里的輸出格式即可。這個(gè)依賴在Maven里的坐標(biāo)是dependency groupIdnet.logstash.logback/groupId artifactIdlogstash-logback-encoder/artifactId version7.3/version /dependency然后新建logback-spring.xml放在src/main/resources下。下面是我實(shí)際在用的配置刪減了一些內(nèi)部業(yè)務(wù)字段保留了核心結(jié)構(gòu)?xml version1.0 encodingUTF-8? configuration springProperty scopecontext nameappName sourcespring.application.name/ springProperty scopecontext nameprofile sourcespring.profiles.active/ appender nameJSON_FILE classch.qos.logback.core.rolling.RollingFileAppender file/data/logs/${appName}/${appName}.json/file rollingPolicy classch.qos.logback.core.rolling.TimeBasedRollingPolicy fileNamePattern/data/logs/${appName}/${appName}.%d{yyyy-MM-dd}.%i.json/fileNamePattern timeBasedFileNamingAndTriggeringPolicy classch.qos.logback.core.rolling.SizeAndTimeBasedFNATP maxFileSize500MB/maxFileSize /timeBasedFileNamingAndTriggeringPolicy maxHistory7/maxHistory /rollingPolicy encoder classnet.logstash.logback.encoder.LogstashEncoder customFields{app_name:${appName},profile:${profile}}/customFields /encoder /appender root levelINFO appender-ref refJSON_FILE/ /root /configuration這個(gè)配置做了幾件重要的事日志統(tǒng)一寫到/data/logs/{appName}/{appName}.json按天滾動單個(gè)文件超過500MB繼續(xù)滾動保留7天。每條日志以JSON格式輸出LogstashEncoder會自動帶上timestamp、version、logger_name、thread_name、level、message等標(biāo)準(zhǔn)字段。通過customFields給每條日志額外加上app_name和profile兩個(gè)字段。這個(gè)太重要了后續(xù)在Kibana里按應(yīng)用篩選就靠它。有一點(diǎn)提一下LogstashEncoder默認(rèn)會輸出level_value等字段生產(chǎn)環(huán)境建議看下實(shí)際生成的結(jié)果按需精簡避免每一行日志攜帶過多無關(guān)字段。2.3 MDC里塞traceId鏈路日志才能串起來光有JSON還不夠。Spring Boot應(yīng)用在微服務(wù)調(diào)用鏈里一個(gè)請求會經(jīng)過多個(gè)方法甚至多個(gè)服務(wù)如果每條日志之間沒有關(guān)聯(lián)標(biāo)識在ELK里你只能按時(shí)間猜沒法快速拉出整條調(diào)用鏈路。我用的是在日志過濾器里往MDC塞traceId的方案。具體做法是在logback-spring.xml里增加一個(gè)MDCInsertingServletFilter或自定義Filter在請求進(jìn)入時(shí)生成UUID寫入MDC然后在JSON輸出里帶上MDC字段。LogstashEncoder默認(rèn)會把MDC里的鍵值也輸出成JSON字段所以無需額外配置。有了trace_id字段之后在Kibana里搜一個(gè)請求直接按trace_id篩選整條鏈路的所有日志就都出來了排查效率完全不在一個(gè)量級。2.4 同步打印還是異步打印日志輸出本身有IO開銷在業(yè)務(wù)QPS較高時(shí)每條日志都同步寫磁盤會影響接口響應(yīng)時(shí)間。線上數(shù)據(jù)說話我們某個(gè)核心服務(wù)在日志量高峰期同步輸出JSON時(shí)接口RT上升了約20%。后來我把日志輸出改成了異步問題立刻緩解。Logback的異步配置也簡單在JSON_FILE外面包一層AsyncAppenderappender nameASYNC_JSON classch.qos.logback.core.AsyncAppender queueSize8192/queueSize discardingThreshold0/discardingThreshold neverBlocktrue/neverBlock appender-ref refJSON_FILE/ /appender通過上面配置日志寫入成為異步操作業(yè)務(wù)線程不會被磁盤IO卡住。如果隊(duì)列滿了discardingThreshold0意味著丟棄部分日志而不是阻塞業(yè)務(wù)線程這在日志不敏感業(yè)務(wù)里是可以接受的如果日志需要百分百采集可以調(diào)大queueSize但也要接受內(nèi)存占用上升的現(xiàn)實(shí)具體取舍看業(yè)務(wù)。3. Logstash采集鏈路從日志文件到Elasticsearch的這段路3.1 用Filebeat采集還是直接讓Logstash讀文件第一個(gè)要做的決定是采集端選型。Logstash可以直接用file插件讀文件但它的JVM進(jìn)程比較重如果每臺應(yīng)用服務(wù)器都部署一個(gè)Logstash內(nèi)存占用會非常難看。我們應(yīng)用服務(wù)器是4C8G的配置再塞一個(gè)默認(rèn)分配1-2G堆內(nèi)存的Logstash業(yè)務(wù)壓力大的時(shí)候會很危險(xiǎn)。所以我最終選了Filebeat Logstash的經(jīng)典組合Filebeat部署在每臺Spring Boot應(yīng)用服務(wù)器上用Go寫的輕量、資源占用小負(fù)責(zé)讀日志文件并轉(zhuǎn)發(fā)給中心Logstash。Logstash單獨(dú)部署在日志服務(wù)器上集中處理所有應(yīng)用的日志流做解析、清洗、寫ES。這種架構(gòu)的好處是采集端足夠輕解析邏輯只在中心Logstash維護(hù)一份后續(xù)修改解析規(guī)則不用去每臺應(yīng)用服務(wù)器上動配置管理成本直接下降。3.2 Filebeat配置要采集哪些文件Filebeat的配置在filebeat.yml里核心部分如下filebeat.inputs: - type: log enabled: true paths: - /data/logs/*/*.json json.keys_under_root: true json.add_error_key: true fields: log_source: app fields_under_root: true output.logstash: hosts: [10.0.0.x:5044]這里我用的是/data/logs/*/*.json這樣的通配路徑因?yàn)槊颗_服務(wù)器上可能部署多個(gè)應(yīng)用各自在/data/logs下有獨(dú)立子目錄Filebeat會自動遞歸匹配。有個(gè)細(xì)節(jié)要說明json.keys_under_root: true表示JSON日志里的字段直接放到日志事件的根層級這樣后續(xù)在Logstash里解析起來省事很多。fields里加的log_source: app是為了標(biāo)識日志來源未來如果還想采集Nginx、系統(tǒng)日志等其他數(shù)據(jù)源就能做區(qū)分了。3.3 Logstash Pipeline配置一個(gè)能直接用的示例中心Logstash的Pipeline配置文件我放在/etc/logstash/conf.d/springboot.conf內(nèi)容如下input { beats { port 5044 } } filter { if [log_source] app { json { source message target json_body skip_on_invalid_json true } if [json_body] { mutate { replace { message %{[json_body][message]} } add_field { logger_name %{[json_body][logger_name]} thread_name %{[json_body][thread_name]} level %{[json_body][level]} app_name %{[json_body][app_name]} } remove_field [ json_body ] } date { match [ [timestamp], ISO8601 ] target timestamp } mutate { remove_field [ host, log ] } } } } output { elasticsearch { hosts [http://10.0.0.x:9200] index springboot-logs-%{yyyy.MM.dd} } }這套Pipeline做了這么幾件事接收來自5044端口的Filebeat數(shù)據(jù)。用json插件把message字段里的JSON字符串解析出來放到j(luò)son_body里。用mutate把json_body里我們關(guān)心的字段提到頂層然后把原始json_body刪掉。這一步是為了讓最終寫入ES的文檔干凈避免嵌套層太深導(dǎo)致Kibana里查詢麻煩。用date插件把日志原始時(shí)間覆蓋為timestamp保證后續(xù)Kibana里展示的時(shí)間是日志真實(shí)產(chǎn)生時(shí)間而不是Logstash接收時(shí)間。索引名按天滾動形如springboot-logs-2025.01.05。這樣做的好處后面會細(xì)說。3.4 關(guān)于杜絕堆棧日志被拆散的一個(gè)認(rèn)知很多人問我Java異常堆棧是多行的Filebeat采集的時(shí)候會不會拆成多個(gè)事件這個(gè)問題在純文本日志場景下確實(shí)存在解決辦法是配置Filebeat的multiline規(guī)則把堆棧行合并到前一條日志里。但如果你按我第2節(jié)的做法把日志輸出改成了JSON格式那么這個(gè)坑在源頭就消失了。LogstashEncoder輸出的JSON是單行的異常堆棧作為字符串的一部分在JSON里自動轉(zhuǎn)義存儲Logstash解析后message字段里天然就是完整的堆棧文本。所以“多行堆棧問題”在我看來本質(zhì)上是個(gè)上游格式化問題而不是采集端問題。把日志端改成結(jié)構(gòu)化輸出比在采集端做各種multiline配置更省心、更可靠這也是我實(shí)際用了半年之后的體會。3.5 Logstash性能參數(shù)與持久化隊(duì)列Logstash默認(rèn)Pipeline配置在日志量比較大的時(shí)候可能成為瓶頸我調(diào)過幾個(gè)關(guān)鍵參數(shù)放在logstash.yml里pipeline.workers: 4 pipeline.batch.size: 1250 pipeline.batch.delay: 5 queue.type: persisted queue.max_bytes: 1gbpipeline.workers控制filter和output階段的并發(fā)線程數(shù)一般跟CPU核數(shù)掛鉤pipeline.batch.size和batch.delay控制批量攢批的大小適當(dāng)提升能減少IO次數(shù)提高吞吐。重點(diǎn)說下queue.type: persisted。Logstash默認(rèn)用內(nèi)存隊(duì)列如果寫ES時(shí)ES恰好重啟或者網(wǎng)絡(luò)抖動內(nèi)存里的日志可能直接丟。用持久化隊(duì)列后日志先落盤保存等ES恢復(fù)后再消費(fèi)發(fā)送可靠性好很多。代價(jià)是需要額外的磁盤空間我給了1GB實(shí)際運(yùn)行中完全夠用畢竟消費(fèi)速度很快。4. Elasticsearch索引設(shè)計(jì)與Kibana查詢配置4.1 按天滾動索引寫ES的索引名設(shè)計(jì)成springboot-logs-2025.01.05這種按天切分的格式這個(gè)決定在我后來維護(hù)時(shí)幫了很大忙。按天索引的好處是刪除舊數(shù)據(jù)簡單直接刪整個(gè)索引就行。索引體積可控單個(gè)索引的數(shù)據(jù)量不至于膨脹成巨無霸。排查問題時(shí)可以只查指定幾天甚至一天的數(shù)據(jù)響應(yīng)速度更快。配合索引模板可以讓每天新建的索引自動套用統(tǒng)一的字段映射和分片設(shè)置不用每天手動建索引。模板配置大致如下{ index_patterns: [springboot-logs-*], settings: { number_of_shards: 3, number_of_replicas: 1, refresh_interval: 5s }, mappings: { properties: { timestamp: { type: date }, level: { type: keyword }, logger_name: { type: keyword }, thread_name: { type: keyword }, app_name: { type: keyword }, message: { type: text }, trace_id: { type: keyword }, profile: { type: keyword } } } }分片數(shù)3不是拍腦袋定的我們每天的日志量大概在2GB左右算上副本單個(gè)索引6GB。按單個(gè)分片20-30GB的常規(guī)規(guī)劃標(biāo)準(zhǔn)3個(gè)分片既能支撐當(dāng)前的日增量也有余量應(yīng)對未來的日志增長。如果你日志量更大分片數(shù)可以相應(yīng)提高但別貪多——分片過多會導(dǎo)致ES打開文件數(shù)暴漲集群狀態(tài)反而容易黃。4.2 索引生命周期管理防止磁盤被日志打爆按天滾動索引如果不做清理日志會無限堆積一年下來磁盤遲早被寫滿。ES自帶ILMIndex Lifecycle Management機(jī)制我用它把索引分成hot、warm、delete三個(gè)階段hot階段保存最近1天的數(shù)據(jù)使用SSD盤上的節(jié)點(diǎn)refresh_interval設(shè)置小一點(diǎn)保證查詢實(shí)時(shí)性。warm階段超過1天后進(jìn)入索引只讀refresh_interval調(diào)大減少資源占用。delete階段保留30天后刪除索引釋放磁盤空間。策略配置好后在Kibana的Stack Management里創(chuàng)建ILM Policy再把策略綁定到索引模板新索引會自動套用。這個(gè)動作我建議在接入日志的第一天就做好否則后期磁盤告警再補(bǔ)中間那幾天可能已經(jīng)積累了沒法挽回的數(shù)據(jù)。4.3 Kibana里怎么高效查日志日志進(jìn)了ES之后Kibana里先創(chuàng)建Index Pattern匹配springboot-logs-*時(shí)間字段選timestamp就能在Discover里檢索了。我實(shí)際用得最多的查詢語法分這么幾種按級別篩錯誤level: ERROR按應(yīng)用篩app_name: order-service按關(guān)鍵字全文檢索message: NullPointerException按鏈路ID查全鏈路trace_id: abc123...組合查詢level: ERROR AND app_name: order-service AND message: timeoutKQL寫法很直觀布爾操作符用AND、OR、NOT。注意一點(diǎn)message字段類型是text全文檢索會自動分詞像trace_id、app_name這類字段是keyword查詢時(shí)必須用精確匹配不能寫模糊搜索。如果寫對了字段名但搜不到數(shù)據(jù)先懷疑字段類型是不是不對。除了Discover檢索我還給運(yùn)維同事做了幾個(gè)常用面板按小時(shí)統(tǒng)計(jì)ERROR數(shù)量變化的折線圖、錯誤日志Top10接口的排名、應(yīng)用各實(shí)例日志量對比。Kibana的Dashboard拖拽完成不需要額外寫代碼但前提是日志里有維度字段比如我加過app_name、trace_id、profile這些字段在排障時(shí)確實(shí)救了命。5. 實(shí)測中遇到的坑與完整排查過程5.1 時(shí)區(qū)偏了8小時(shí)Kibana時(shí)間永遠(yuǎn)對不上這是上線第一天就碰到的問題。日志在應(yīng)用服務(wù)器上顯示的時(shí)間是正確的北京時(shí)間但在Kibana里查詢時(shí)總比實(shí)際時(shí)間慢8個(gè)小時(shí)看日志時(shí)感覺像是日志延遲了。當(dāng)時(shí)我檢查Kibana配置顯示默認(rèn)時(shí)區(qū)是瀏覽器當(dāng)?shù)貢r(shí)區(qū)看起來沒有問題。接著我又去ES里查原始文檔用curl直接請求ES接口看寫入的數(shù)據(jù)發(fā)現(xiàn)文檔里的timestamp字段存的確實(shí)比實(shí)際時(shí)間少了8小時(shí)。問題根因出在應(yīng)用日志生成時(shí)間上。Spring Boot的JSON日志中timestamp攜帶UTC時(shí)區(qū)Logstash的date插件按ISO8601解析后寫入ES沒有做時(shí)區(qū)轉(zhuǎn)換ES內(nèi)部統(tǒng)一按UTC存儲。而Kibana在展示時(shí)會按瀏覽器時(shí)區(qū)換算這本身沒錯錯在我Filebeat和Logstash之間的鏈路沒有把時(shí)區(qū)對齊到北京時(shí)間。排查鏈路是這樣的先確認(rèn)timestamp原始值是否為UTC時(shí)間。用ES里某個(gè)文檔的原始JSON確認(rèn)。再對比Logstash日志里采集時(shí)的日志時(shí)間確認(rèn)Logstash解析沒問題。最后確認(rèn)Kibana的數(shù)據(jù)源和時(shí)區(qū)設(shè)置沒問題。最后的解決辦法很簡單在Kibana里把時(shí)間顯示時(shí)區(qū)改為Asia/Shanghai同時(shí)在Logstash的date插件里不依賴時(shí)區(qū)轉(zhuǎn)換邏輯而是改用timezone參數(shù)指定Asia/Shanghai確保timestamp正確處理北京時(shí)間。這里也提醒一句排查時(shí)區(qū)問題不要一上來就懷疑ES先在數(shù)據(jù)源頭把時(shí)間戳的時(shí)區(qū)含義搞清楚再逐步往后端鏈路排查你會少走很多彎路。5.2 Filebeat斷點(diǎn)續(xù)傳失效重啟后日志丟失有一次給應(yīng)用服務(wù)器發(fā)布新版本重啟了Spring Boot進(jìn)程然后發(fā)現(xiàn)重啟前最后幾條日志沒有進(jìn)ES。一開始以為是應(yīng)用沒打印出來后來去服務(wù)器上看了原始日志文件內(nèi)容明明在Filebeat卻沒有采到。排查過程是這樣的確認(rèn)Filebeat進(jìn)程是否正常運(yùn)行systemctl status filebeat顯示正常。查看Filebeat日志發(fā)現(xiàn)沒有報(bào)錯。檢查Filebeat的registry文件發(fā)現(xiàn)里面記錄的文件offset可能因?yàn)槿罩疚募嗈D(zhuǎn)錯亂了。問題出在應(yīng)用重啟后日志文件路徑發(fā)生了變化。因?yàn)長ogback滾動策略是SizeAndTimeBasedFNATP當(dāng)進(jìn)程重啟且當(dāng)天已有多個(gè)滾動文件時(shí)文件名后綴可能變化。Filebeat靠文件名和文件inode來記住讀取位置如果輪轉(zhuǎn)把舊文件刪了或者改了名它的registry里記錄的offset就丟了。我做的改進(jìn)有兩點(diǎn)在Filebeat配置中加入clean_inactive和ignore_older參數(shù)確保不讀取太舊的文件同時(shí)及時(shí)清理registry里的過期狀態(tài)。更重要的是優(yōu)化日志滾動策略把滾動周期和進(jìn)程重啟對不上號的問題從源頭規(guī)避。我的實(shí)際配置是讓當(dāng)前日志始終寫入固定的app.json滾動文件才帶日期后綴。這樣Filebeat始終針對同一路徑追讀很少再出現(xiàn)斷點(diǎn)續(xù)傳問題。5.3 Elasticsearch堆內(nèi)存配置不對集群頻繁GC告警日志上線一個(gè)月后收到ES集群GC時(shí)間過長的告警。登錄服務(wù)器一查JVM堆內(nèi)存被用滿頻繁Full GC查詢時(shí)Kibana卡得幾乎沒法用。排查后發(fā)現(xiàn)根因是我在啟動ES時(shí)沒有顯式設(shè)置ES_JAVA_OPTS默認(rèn)堆內(nèi)存是1GB而日志量增長后索引和查詢的壓力早就超過這個(gè)水位。確認(rèn)問題很像補(bǔ)救方案是調(diào)大堆內(nèi)存但調(diào)大前必須看服務(wù)器物理內(nèi)存剩余情況避免堆設(shè)置太大導(dǎo)致系統(tǒng)本身內(nèi)存不足。ES官方建議堆內(nèi)存不超過物理內(nèi)存的50%但不能超過32GB。我們?nèi)罩痉?wù)器是8C16G給ES分配了8G堆內(nèi)存。調(diào)整后GC告警消失查詢響應(yīng)時(shí)間從秒級降到幾百毫秒。這個(gè)坑的教訓(xùn)是ES的堆內(nèi)存一定要在部署時(shí)就根據(jù)物理資源規(guī)劃好不要用默認(rèn)值。默認(rèn)堆1G只能說能啟動根本跑不了生產(chǎn)。類似的還有bootstrap.memory_lock建議開啟鎖住物理內(nèi)存防止堆內(nèi)存被系統(tǒng)Swap到磁盤導(dǎo)致性能斷崖式下跌。5.4 同一字段在不同日志中出現(xiàn)類型沖突導(dǎo)致索引mapping報(bào)錯還有一個(gè)容易踩的坑是字段類型沖突。某天發(fā)現(xiàn)有個(gè)應(yīng)用的日志索引創(chuàng)建失敗查看ES日志提示某個(gè)字段在已有mapping中是keyword類型新日志想寫入long類型沖突了。這個(gè)問題的場景很典型應(yīng)用A日志里有個(gè)字段叫request_time輸出的是字符串應(yīng)用B日志里同名字段是數(shù)字ES按天索引本來各建各的沒問題但因?yàn)槲覀冇昧送粋€(gè)索引模板mapping固化后就無法再寫入不同類型的數(shù)據(jù)。排查鏈路其實(shí)比較簡單ES日志里已經(jīng)明確指出沖突字段名和類型但關(guān)鍵是怎么規(guī)避方案一是不同應(yīng)用用不同索引前綴從根上隔離比如order-logs-*、pay-logs-*但這樣Kibana里檢索要切換多個(gè)索引模式不夠方便。方案二是在索引模板里對常用字段顯式聲明類型消除字段映射歧義把字段類型控制權(quán)掌握在自己手里。我最終用的是方案二在索引模板里把所有可能產(chǎn)生沖突的字段類型顯式定義好新數(shù)據(jù)只接受匹配的類型?,F(xiàn)在這個(gè)坑基本不會再出現(xiàn)。6. 如果讓我再搭一次ELK這幾個(gè)建議我會放在最前面從決定搭建ELK到整套鏈路跑順前后花了大概兩周時(shí)間中間踩過的坑不少。如果讓我重新來一次我會把下面這幾條作為起步階段就貫徹的原則第一日志端的結(jié)構(gòu)化改造優(yōu)先級高于一切采集配置。先把Spring Boot的日志變成JSON后面所有解析、檢索、面板都會省力不要跳過這一步直接去配Logstash和Kibana。第二從第一天就規(guī)劃好索引模板和ILM策略不要等日志積累了幾天、幾十G之后再補(bǔ)那時(shí)插補(bǔ)策略的成本和風(fēng)險(xiǎn)都會大很多。第三給每條日志都帶上trace_id和應(yīng)用標(biāo)識。ES的全文檢索再強(qiáng)大沒有關(guān)聯(lián)標(biāo)識你也很難在奔涌的日志洪流里把一次請求的完整生命周期撈出來。鏈路日志的價(jià)值在使用ELK半年之后我才算真正體會到。第四不要追求把所有日志都收進(jìn)來。有些Debug日志、健康檢查的探活日志、批量任務(wù)的空轉(zhuǎn)日志不僅占存儲還在檢索時(shí)制造大量噪音。我在Logstash的filter里用drop插件把明顯無用的日志在源頭丟棄效果很不錯索引體積直接降了30%以上Kibana里的檢索結(jié)果也干凈了很多。日志中臺這件事做起來并不神秘難的是把鏈路里的每個(gè)環(huán)節(jié)都按生產(chǎn)標(biāo)準(zhǔn)配置好并且堅(jiān)持治理。希望這篇基于實(shí)際經(jīng)驗(yàn)寫下的東西能幫正在搭建或者準(zhǔn)備搭建Spring Boot日志收集體系的你避掉一些坑。如果你在搭建過程中也有自己的踩坑記錄歡迎一起交流。