2026/10/17 13:17:03

SpringBoot 日志接入 ELK 实战:Filebeat + Logstash 采集与 Kibana 可视化

SpringBoot 日志接入 ELK 实战:Filebeat + Logstash 采集与 Kibana 可视化 1. 为什么我劝你别再靠 grep 翻日志了线上服务一出问题第一反应是什么登录服务器cd到日志目录tail -f盯着滚动的输出或者grep关键字一把梭。这套动作我干了不下几百次直到有一次线上订单接口大面积超时三个节点、六个日志文件、每秒上千行输出我盯着屏幕翻了二十分钟才定位到一条被淹没的异常堆栈。那一刻我就知道这套原始手段该退休了。这篇文章要聊的就是把SpringBoot应用的日志接入ELK这套组合拳——Elasticsearch负责存储和检索Logstash负责采集和加工Kibana负责可视化查询。说白了就是让日志从散落在各台机器上的文本文件变成一个可以秒级搜索、按字段过滤、能画图看趋势的集中式数据源。它解决的核心问题只有一个当你有多台机器、多个服务、海量日志时如何用几秒钟找到你想要的那一条。适合谁看如果你正在维护一个 SpringBoot 项目日志还停留在本地文件阶段团队里没人搞过日志收集那这篇就是写给你的。我会从零讲清楚每个环节为什么这么配、参数怎么算、坑在哪里代码可以直接抄。如果你已经用过 ELK 但总觉得配置别扭、查询慢、日志丢那也能从里面的调优细节里找到答案。先说清楚一个前提ELK 不是银弹。单机小项目、日志量每天几百兆以内老老实实写文件加个日志轮转就够了上 ELK 纯属给自己找运维负担。但只要你的服务超过两台机器或者你需要按 traceId 串联一次请求的全链路日志那 ELK 带来的效率提升是数量级的。判断标准很简单——你上一次排查线上问题花在找日志上的时间是否超过了分析问题的时间。如果是那就该上了。2. 整体架构设计与选型思路拆解2.1 三个组件到底各干什么活很多人一上来就背ELK 是 Elasticsearch Logstash Kibana但真到配置的时候一脸懵不知道哪段配置该写在哪。我用一个生活化的类比帮你理清楚把日志想象成快递包裹。Logstash是分拣中心。它负责从各个地方文件、网络端口、消息队列把包裹收进来拆开看看里面是什么解析日志格式贴上标准化的面单结构化字段再发往目的地。它的强项是加工能力强弱项是吃资源一个 Logstash 实例跑起来动辄几百兆到上 G 内存。Elasticsearch是仓库。包裹进来后按规则上架建立索引让你能快速找到。它是个分布式文档数据库天生支持水平扩展和海量数据检索核心能力是倒排索引——这也是为什么它能做到几亿条日志里毫秒级搜出关键字。Kibana是前台查询窗口。你在这里输入查询条件、拖拽图表、看日志详情。它自己不存数据所有查询都转发给 Elasticsearch。那为什么现在越来越多方案里Logstash 被换成了Filebeat因为 Filebeat 是轻量级的采集器用 Go 写的一个进程内存占用通常只有几十兆装在应用服务器上几乎无感。它的定位很纯粹盯着日志文件有新内容就转发出去。加工能力弱但胜在轻。所以现在主流的架构是Filebeat采集→ Logstash加工→ Elasticsearch存储→ Kibana展示或者更简单的Filebeat → Elasticsearch直连。2.2 为什么我推荐 Filebeat Logstash 组合而不是直连直连 Elasticsearch 最省事Filebeat 配置几行就能跑。但我踩过的坑告诉我中间加一层 Logstash 在多数生产场景下是值得的原因有三个。第一是格式解析。SpringBoot 默认输出的日志是带时间戳、线程名、日志级别、类名的一行文本直接进 Elasticsearch 就是一大坨字符串你没法按日志级别ERROR这种条件过滤。Logstash 的 grok 插件能把这行文本拆成结构化字段查询效率天差地别。第二是字段清洗和脱敏。手机号、身份证、用户 token 这些敏感信息不该原样进仓库。Logstash 的 mutate 插件可以在入库前做替换或删除这是合规的基本要求。第三是缓冲削峰。应用日志量是有波动的大促或者故障时可能瞬间暴涨。Logstash 可以配合消息队列做缓冲避免直接把压力打到 Elasticsearch 上导致集群抖动。当然如果你的日志本身就是 JSON 格式后面会讲怎么让 SpringBoot 直接输出 JSON且没有脱敏需求那 Filebeat 直连 Elasticsearch 完全够用架构越简单越不容易出问题。选型这事没有标准答案看你的实际约束。2.3 版本选择与资源规划版本上Elasticsearch、Logstash、Kibana、Filebeat 四个组件的大版本号必须一致这是硬性要求。7.x 和 8.x 之间差异很大8.x 默认开启了安全认证TLS 账号密码配置复杂度陡增。如果你是第一次搭我建议从7.17.x这个长期支持版本入手它对 SpringBoot 生态兼容性好网上资料也最全。等跑通了再考虑升 8.x。资源规划上给个参考值。假设你的应用每天产生 10GB 日志保留 30 天组件部署位置建议配置说明Filebeat每台应用服务器0.5 核 / 256MB几乎无感可忽略Logstash独立节点2 核 / 4GBJVM 堆设 2GB 左右Elasticsearch独立集群3 节点每节点 4 核 / 16GB堆内存不超过物理内存一半且不超过 31GBKibana独立节点1 核 / 2GB轻量Elasticsearch 的堆内存有个经典陷阱不要超过 31GB。因为 JVM 在堆小于 32GB 时能用指针压缩技术超过后指针膨胀反而更耗内存。所以哪怕你机器有 128GB 内存堆也建议设在 31GB 以内剩下的留给文件系统缓存——这对检索性能至关重要。3. SpringBoot 侧的日志改造实操3.1 日志格式标准化是第一步ELK 能不能用好一半取决于日志格式。默认的 SpringBoot 日志长这样2024-01-15 10:23:45.123 INFO 12345 --- [http-nio-8080-exec-1] c.e.demo.OrderController : 订单创建成功 orderId10086这行文本要解析成结构化字段grok 表达式得写一大串还容易因为线程名格式变化而匹配失败。更优雅的做法是让 SpringBoot 直接输出 JSON 格式日志Logstash 或 Filebeat 拿到就是现成的结构化数据解析成本几乎为零。在pom.xml里引入 logstash-logback-encoderdependency groupIdnet.logstash.logback/groupId artifactIdlogstash-logback-encoder/artifactId version7.4/version /dependency然后在src/main/resources下新建logback-spring.xml?xml version1.0 encodingUTF-8? configuration springProperty scopecontext nameappName sourcespring.application.name defaultValueunknown-app/ appender nameJSON_FILE classch.qos.logback.core.rolling.RollingFileAppender filelogs/${appName}.json/file rollingPolicy classch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy fileNamePatternlogs/${appName}-%d{yyyy-MM-dd}.%i.json/fileNamePattern maxFileSize100MB/maxFileSize maxHistory7/maxHistory totalSizeCap5GB/totalSizeCap /rollingPolicy encoder classnet.logstash.logback.encoder.LogstashEncoder customFields{app:${appName}}/customFields fieldNames timestamptimestamp/timestamp messagemsg/message loggerlogger_name/logger threadthread/thread levelValue[ignore]/levelValue /fieldNames /encoder /appender root levelINFO appender-ref refJSON_FILE/ /root /configuration这里有几个关键点值得展开说。customFields里塞了应用名这样多个服务的日志进同一个索引后你能按app字段区分来源。fieldNames把时间戳字段名改成timestamp这是 Elasticsearch 的约定字段Kibana 会自动识别为时间轴。levelValue设成[ignore]是为了去掉冗余的数值型级别字段只保留字符串级别省空间。3.2 让日志带上 traceId 实现全链路追踪光有日志还不够一次请求可能经过网关、订单服务、库存服务日志散在三个应用里。怎么把它们串起来答案是traceId。在请求入口生成一个唯一 ID通过 MDCMapped Diagnostic Context放进日志上下文后续所有日志都会自动带上这个 ID。写一个简单的过滤器Component public class TraceIdFilter implements Filter { private static final String TRACE_ID traceId; Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { String traceId UUID.randomUUID().toString().replace(-, ).substring(0, 16); MDC.put(TRACE_ID, traceId); try { chain.doFilter(request, response); } finally { MDC.remove(TRACE_ID); } } }然后在 logback 配置的 encoder 里加上encoder classnet.logstash.logback.encoder.LogstashEncoder includeMdcKeyNametraceId/includeMdcKeyName customFields{app:${appName}}/customFields /encoder这样每条日志都会带上traceId字段。排查问题时在 Kibana 里搜一个 traceId整条链路的日志按时间排好队全出来了。这个体验一旦用过就回不去了。注意MDC 是基于 ThreadLocal 的如果你用了线程池或者异步处理MDC 的值不会自动传递到子线程。需要手动包装线程池的 TaskDecorator或者用 TransmittableThreadLocal 这类方案。这是新手最容易踩的坑日志里 traceId 时有时无八成就是这个原因。3.3 日志文件命名与轮转策略Filebeat 是按文件采集的所以日志文件的命名和轮转策略直接影响采集效果。上面配置里用的是SizeAndTimeBasedRollingPolicy按天加按大小切分单文件 100MB保留 7 天总量上限 5GB。为什么单文件要限制在 100MB因为 Filebeat 是按行读取的文件太大时首次采集的延迟会变高而且文件被轮转删除时如果还没读完会丢日志。100MB 是个平衡点既不会切得太碎也不会让采集滞后太多。maxHistory和totalSizeCap是防止磁盘被日志撑爆的保险丝。生产环境一定要设我见过太多因为日志没清理导致磁盘满、服务挂掉的案例。7 天保留期对多数业务够用因为日志已经进 ELK 了本地文件只是采集的中间态。4. Filebeat 与 Logstash 的采集加工链路4.1 Filebeat 配置轻量采集的关键参数Filebeat 装在应用服务器上配置文件filebeat.yml核心就几段filebeat.inputs: - type: filestream enabled: true paths: - /app/logs/*.json fields: service: order-service fields_under_root: true parsers: - ndjson: target: overwrite_keys: true add_error_key: true processors: - drop_fields: fields: [agent, ecs, input, host, log] ignore_missing: true output.logstash: hosts: [logstash-host:5044] loadbalance: true worker: 2type: filestream是 7.16 之后推荐的输入类型替代了老的log类型它用文件指纹记录读取位置重启后不会重复采集。ndjson解析器专门处理 JSON 行日志target: 表示把 JSON 的字段平铺到顶层而不是嵌套在一个对象里。overwrite_keys: true保证日志里的字段能覆盖 Filebeat 自带的同名字段。drop_fields那段是精简字段。Filebeat 默认会加一堆元数据字段比如 agent 版本、主机信息、ecs 规范字段这些在多数场景下用不上删掉能省不少存储。但host字段建议保留它能告诉你日志来自哪台机器排查多节点问题时很有用——我这里删掉是因为在容器环境里 host 意义不大你可以根据自己的部署方式决定。output.logstash里的loadbalance: true和worker: 2是并发发送配置。当日志量大时单线程发送会成为瓶颈开多个 worker 能提升吞吐。但别开太多否则 Logstash 侧压力大。4.2 Logstash 管道配置解析、清洗、输出Logstash 的核心是管道配置文件通常放在/etc/logstash/conf.d/下一个文件搞定输入、过滤、输出三段input { beats { port 5044 codec json } } filter { # 时间戳字段标准化 date { match [timestamp, ISO8601] target timestamp } # 敏感信息脱敏手机号中间四位打码 mutate { gsub [ msg, 1[3-9]\d{9}, 1********** ] } # 根据日志级别打标签方便后续路由 if [level] ERROR { mutate { add_tag [error_log] } } # 丢弃健康检查等噪音日志 if [logger_name] ~ HealthCheck { drop {} } } output { elasticsearch { hosts [http://es-node1:9200, http://es-node2:9200] index app-log-%{app}-%{YYYY.MM.dd} user elastic password your_password manage_template true template_name app-log template_overwrite true } }input段用beats插件接收 Filebeat 发来的数据codec json表示按 JSON 解析。filter段是加工的核心date插件确保时间戳格式正确mutate的gsub做正则替换实现脱敏drop直接丢弃噪音日志。output段的index参数决定了索引命名规则。app-log-%{app}-%{YYYY.MM.dd}会生成类似app-log-order-service-2024.01.15的索引按应用和日期分片。这样做的好处是不同应用的日志物理隔离查询时只扫相关索引速度快按天分索引方便做生命周期管理比如 30 天前的索引自动删除。提示索引名里不要用大写字母Elasticsearch 会直接报错。另外%{app}这个字段必须存在如果某条日志没有 app 字段索引名会变成app-log--2024.01.15虽然不报错但很乱。所以前面 Filebeat 的fields和 SpringBoot 的customFields都要确保 app 字段存在。4.3 索引模板与分片策略索引模板决定了新建索引的默认配置比如分片数、副本数、字段类型。Logstash 的manage_template true会自动上传一个默认模板但默认配置往往不适合生产。分片数怎么定一个经验公式单个分片大小控制在 10GB 到 50GB 之间。假设你每天每应用产生 5GB 日志那单分片就够了设 1 个主分片。如果每天 50GB那就设 3 到 5 个主分片。分片太多会导致集群元数据膨胀、查询时合并开销大分片太少则无法利用多节点并行。副本数在单节点环境设 0否则集群状态会一直 yellow生产多节点环境设 1保证一个节点挂了数据不丢。自定义模板可以这样写通过 Kibana 的 Dev Tools 或者 curl 上传{ index_patterns: [app-log-*], template: { settings: { number_of_shards: 3, number_of_replicas: 1, refresh_interval: 30s }, mappings: { properties: { timestamp: { type: date }, level: { type: keyword }, app: { type: keyword }, traceId: { type: keyword }, logger_name: { type: keyword }, msg: { type: text } } } } }这里字段类型的选择有讲究。level、app、traceId这些用来精确过滤的字段设成keyword它们不分词查询快且支持聚合。msg是日志正文设成text会分词支持全文检索。如果把msg设成 keyword那你就只能精确匹配整条消息搜关键字就废了。refresh_interval从默认的 1s 改成 30s 是个重要优化。日志场景对实时性要求没那么高30 秒的延迟完全可以接受但写入吞吐能提升好几倍因为减少了 Lucene 段合并的频率。5. Kibana 查询与可视化实战5.1 索引模式创建与字段管理Kibana 第一次用先到 Stack Management 里的 Index Patterns 创建索引模式填app-log-*选择timestamp作为时间字段。创建后 Kibana 会扫描索引里的字段你可以在 Discover 页面看到所有可用字段。有个细节如果某个字段在部分索引里存在、部分不存在Kibana 可能识别不出类型。这时候可以在索引模式的字段列表里手动刷新或者用_field_capsAPI 查一下。字段类型冲突是常见问题比如level在一个索引里是 keyword另一个索引里被自动识别成 text聚合就会报错。解决办法就是前面说的用索引模板强制指定类型。5.2 高频查询语句速查Kibana 的查询有两种模式KQLKibana Query Language和 Lucene 语法。日常用 KQL 就够了语法直观。下面这些是我几乎每天都会用到的查询场景KQL 查询语句说明查某应用的所有错误app: order-service and level: ERROR最常用按 traceId 追全链路traceId: a1b2c3d4e5f6g7h8排查单次请求查某时间段异常level: ERROR and timestamp 2024-01-15T10:00:00配合时间选择器模糊搜关键字msg: *timeout*通配符查询慎用大范围排除健康检查not logger_name: HealthCheck过滤噪音查特定异常类msg: NullPointerException定位代码问题注意msg: *timeout*这种前置通配符查询性能很差因为它无法利用倒排索引会全表扫描。如果经常需要这样搜说明你的日志字段设计有问题应该把关键信息提取成独立字段。比如把超时时间、接口名单独解析出来查询时直接按字段过滤。5.3 用可视化面板盯住系统健康度光会搜还不够Kibana 的 Visualize 能把日志变成图表一眼看出趋势。我通常会做这么几个图放在一个 Dashboard 上第一个是错误日志趋势图用 Line ChartX 轴是时间Y 轴是 count筛选条件level: ERROR。这条线突然翘起来说明系统出问题了比盯着数字看直观得多。第二个是日志级别分布饼图按level字段聚合能看出 ERROR、WARN、INFO 的占比。如果 WARN 占比异常高可能是某些隐患在积累。第三个是Top 错误类型排行用 Data Table按msg的关键字聚合或者按logger_name聚合快速定位哪个模块问题最多。第四个是各应用日志量对比按app字段做柱状图能发现某个服务日志量突然暴涨可能是死循环或者异常刷屏。这些图配置起来都不复杂在 Visualize 里选对应的图表类型拖拽字段就行。关键是养成习惯——每天早上花两分钟扫一眼 Dashboard很多问题能在用户投诉之前发现。6. 踩坑实录与性能调优6.1 日志丢失的三种典型原因日志丢失是 ELK 最让人头疼的问题因为它是静默的你不主动核对根本发现不了。我遇到过三种典型情况。第一种是 Filebeat 的读取位置记录丢失。Filebeat 把读取进度存在 registry 文件里如果这个文件损坏或者被清理重启后会从头读或者跳过。用filestream类型能缓解这个问题因为它用文件 inode 和指纹双重标识。另外要确保 registry 文件所在目录有持久化容器环境里别放在临时层。第二种是 Logstash 背压导致 Filebeat 丢弃。当 Logstash 处理不过来时Filebeat 的发送队列会满满了之后默认行为是阻塞等待但如果配了drop策略就会丢数据。检查 Filebeat 日志里有没有 Harvester could not be started 或者 client is not connected 这类警告。解决办法是给 Logstash 加消息队列缓冲或者提升 Logstash 的pipeline.workers数量。第三种是 Elasticsearch 写入拒绝。当索引写入速度超过集群处理能力ES 会返回 429 状态码Logstash 如果没做重试就会丢。在 output 里加上重试配置output { elasticsearch { hosts [http://es-node1:9200] index app-log-%{app}-%{YYYY.MM.dd} retry_on_conflict 3 retry_initial_interval 2 retry_max_interval 60 } }同时给 ES 的索引设置写入限流保护避免雪崩。6.2 查询慢的排查思路Kibana 里搜一个关键字转半天圈通常有三个原因。索引分片过多。如果按天分索引且分片数设得大一个月就是上百个分片查询时要扫所有分片再合并。用_cat/indices?v看看分片数量如果单个索引分片数超过 5 个而数据量并不大就该合并了。字段类型不对。前面反复强调的 keyword 和 text 之分如果该用 keyword 的字段用了 text聚合和排序会非常慢。用_mappingAPI 检查一下关键字段的类型。查询语句本身有问题。前置通配符、大范围 range 查询、深分页from size 超过 10000都是性能杀手。深分页要用search_after替代通配符查询尽量改成字段精确匹配。6.3 磁盘水位与索引生命周期管理Elasticsearch 有个磁盘水位机制使用率超过 85% 时不再分配新分片超过 90% 时尝试迁移分片超过 95% 时索引变成只读。日志场景数据增长快很容易触发。所以索引生命周期管理ILM必须配。一个典型的日志 ILM 策略热阶段保留 7 天可写可查温阶段保留 23 天只读可降副本30 天后删除。在 Kibana 的 Index Lifecycle Policies 里配置然后关联到索引模板。这样日志自动滚动、自动清理不用人工干预。如果不想用 ILM也可以用定时任务删除老索引但 ILM 更优雅因为它能自动做 force merge 和 shrink减少分片数量提升查询效率。6.4 常见问题速查表现象可能原因排查方法解决方向Kibana 看不到日志索引模式没建 / 时间范围不对检查 Index Patterns 和时间选择器重建索引模式调整时间范围日志有但字段是乱的grok 或 JSON 解析失败看 Logstash 日志的 _grokparsefailure 标签修正解析规则用 JSON 格式traceId 时有时无MDC 跨线程丢失检查异步线程池配置加 TaskDecorator 传递 MDC查询超时分片过多 / 查询语句差_cat/indices看分片慢查询日志合并索引优化查询磁盘告警没配 ILM / 保留期太长_cat/allocation看磁盘使用配置 ILM缩短保留期日志重复Filebeat registry 异常检查 registry 文件清理 registry 重启ES 写入 429集群写入瓶颈看 ES 日志的 rejected 计数加节点调 refresh_interval这张表里的每一条我几乎都亲身踩过。最想强调的是 traceId 丢失那个当时排查了整整一个下午最后发现是业务代码里用了Async异步方法MDC 没传过去。后来统一在线程池配置里加了装饰器才解决。所以如果你用了异步一定要提前处理这个问题别等出事了才找。7. 我个人的几点实操体会搭 ELK 这件事配置本身不难难的是让它稳定跑下去。我最大的体会是日志格式的标准化比任何调优都重要。前期花半天时间把 JSON 格式和 traceId 搞定后期能省下无数排查解析问题的时间。很多人图省事用默认文本格式结果 grok 表达式写了一版又一版还是各种匹配失败这就是本末倒置。另一个体会是别追求一步到位。我见过有团队一上来就搞三节点 ES 集群、Kafka 缓冲、冷热分离结果运维复杂度太高没人维护最后荒废了。正确的做法是先单机跑通 Filebeat Logstash ES Kibana 全链路确认日志能正常采集查询再根据实际压力逐步扩展。日志量没到那个级别架构越简单越可靠。最后分享一个实用小技巧在 Kibana 的 Discover 页面把常用的查询条件保存成Saved Search比如最近1小时所有ERROR、订单服务慢查询下次直接点一下就能用不用每次重新敲查询语句。团队里可以共享这些 Saved Search新人上手时直接看现成的效率提升很明显。日志这东西采集只是开始让它真正被用起来、成为排查问题的第一入口才算没白搭。