
最近半年我一直在跟一套日志系统较劲。项目本身不算复杂SpringBoot2 提供接口Vue3 做前端整套东西用 Docker Compose 部署在 Rockylinux 9.6 上。问题在于一旦线上出故障日常排查还停留在docker logs加肉眼翻文件的阶段后端日志在前端 Nginx 容器里翻一翻效率低不说还容易漏线索。这篇文章要分享的是我在这套环境下用 ELK 7.17.10 把 SpringBoot2 后端、Vue3 前端 Nginx 日志全部收进 Elasticsearch再通过 Kibana 统一检索的完整过程。版本、Docker Compose 编排、Filebeat 采集、Logstash 解析、Kibana 检索配置以及部署过程中踩到的坑都会写出来。如果你正在衡量要不要在单机或少量服务器上搭 ELK可以参考我这条已经跑通的链路。1. 为什么锁定 ELK 7.17.10 这个组合1.1 先确定需求再定选型我最初的诉求很简单把后端应用的业务日志、前端 Nginx 的访问日志和错误日志集中存到一个地方能按时间、按关键字、按日志级别快速过滤。团队之前也讨论过 Loki 或 ClickHouse 的方案但最终选 ELK有几个现实原因团队成员对 Kibana 的检索界面已经很熟不需要额外学习ELK 生态里的 Logstash 能做复杂的 grok 解析和字段清洗Elasticsearch 的全文检索能力对“不知道异常在哪先搜一搜”的场景非常合适。1.2 7.17.x 在 ELK 版本线里的特殊位置版本锁定在 7.17.10而不是直接上 8.x这一点我斟酌过。7.17.x 是 Elasticsearch 7.x 系列的最后一个大版本分支官方对它的兼容性维护是最晚结束的所以它既有 7.x 稳定的配置风格又补上了早期 7.x 版本的一堆 bug 修复。8.x 引入了新的安全配置方式、新的 API 组织方式和很多老项目里现成脚本、既有技能栈的兼容成本更高。对于只需要一个内部日志平台的场景来说7.17.10 足够稳资料也多遇到问题随便搜一下就能有答案。1.3 SpringBoot2 生态对 7.17 的兼容更省心标题里专门写了 SpringBoot2这里多说一句。我们现在的后端还是 SpringBoot 2.xSpring Data Elasticsearch 4.x 那一套对 ES 7.17 的支持匹配度很高。如果换成 8.x虽然日志采集链路本身不直接依赖 Spring Data Elasticsearch但后续如果要做业务侧对 ES 的读写操作2.x 项目里引入的客户端版本需要专门适配 8.x容易引入兼容性问题。既然是搭日志平台我倾向于把未来的变更面控制得小一点。1.4 组件版本统一的好处ELK 全家桶里我选的版本非常统一elasticsearch、logstash、kibana、filebeat 全部用7.17.10。配套镜像也直接从官方 Elastic 镜像仓库拉。Filebeat 和 Logstash 同版本、Logstash 和 Elasticsearch 同版本能够最大程度避开跨版本带来的 metadata 兼容问题。这里特别提醒一下如果你要混搭版本至少保证 Beats 的版本不高于 Elasticsearch 的主版本否则可能出现字段映射异常。综合下来7.17.10 是这套场景里最均衡的选择不算最新但足够安全资料丰富配置方式也成熟。2. 部署前的宿主机准备与目录规划2.1 确认基础环境Rockylinux 9.6 Docker Compose v2我这边宿主机是 Rockylinux 9.6内核自带的 Docker CE 环境和 Compose v2 插件。先做环境确认避免后续浪费排查时间cat /etc/rocky-release docker --version docker compose version之前有同事问过我docker compose和docker-compose的区别。简单说docker-compose是 Python 写的旧版独立命令现在很多新环境直接推荐用 Docker CLI 插件版的docker compose中间是空格不是短横线。Compose v2 的配置语法和对 docker compose 命令的兼容性都更好。我整篇的操作都用docker compose空格来说明。2.2 一个容易被忽略的内核参数Elasticsearch 底层依赖 mmap对虚拟内存映射区域有数量限制。如果不开这个内核参数ES 容器会直接启动失败报错信息类似max virtual memory areas vm.max_map_count [65530] is too low, increase to at least [262144]这不是容器配置能绕过去的必须在宿主机内核层修改sudo sysctl -w vm.max_map_count262144 echo vm.max_map_count262144 | sudo tee -a /etc/sysctl.conf sudo sysctl -p改完之后最好重新登录会话或者至少用sysctl vm.max_map_count确认一下新值已经生效。我在第一次部署时就是没改这个参数ES 容器反复重启白白浪费了十几分钟。2.3 内存和磁盘的规划单机跑一套 ELK 加业务容器内存分配要提前算好。我的服务器可用内存是 8G分配大致如下组件JVM/常驻内存说明Elasticsearch1G 堆 约 0.5G 系统开销堆内存不要超过物理内存一半Logstash512M 堆 约 0.3G 系统开销解析压力大时再往上加Kibana约 0.3G 常驻空闲时占用不高Filebeat约 0.1G 常驻很轻量业务容器按实际情况预留SpringBoot / Nginx / 数据库等这里有个经验Elasticsearch 的 JVM 堆不是越大越好超过物理内存的一半反而会增加 GC 压力甚至因为容器 cgroup 限制被系统 OOM 杀掉。另外如果凑巧 Compose 里看到deploy.resources这种配置要注意——deploy配置在非 Swarm 模式下默认是不生效的单机直接 Compose 跑资源限制用mem_limit更可靠。2.4 目录规划统一在 /opt/elk 下管理日志采集路径很关键必须在一开始就定好。我最终采用的目录结构如下/opt/elk/ ├── es-data/ # Elasticsearch 数据目录 ├── es-plugins/ # Elasticsearch 插件目录 ├── logstash/ │ └── pipeline/ │ └── logstash.conf # Logstash 主配置 ├── filebeat/ │ ├── config/ │ │ └── filebeat.yml # Filebeat 配置 │ └── data/ # Filebeat 内部状态目录持久化 registry ├── kibana/ │ └── config/ ├── logdata/ # 统一日志挂载层 │ ├── springboot/ # SpringBoot 应用日志落盘目录 │ └── nginx/ # Nginx access/error 日志目录ES 的数据目录必须持久化否则容器一删数据就没了Filebeat 的data目录也要持久化里面保存了类似“上次读到哪个位置”的 registry 状态。如果不挂载这个目录Filebeat 每次重启都可能把历史日志重新读一遍Kibana 里会凭空多出一堆重复记录。另外ES 容器内部是以 uid 1000 的用户运行的不是 root。所以宿主机上给es-data和es-plugins授权时要注意sudo mkdir -p /opt/elk/es-data /opt/elk/es-plugins sudo chown -R 1000:1000 /opt/elk/es-data /opt/elk/es-plugins否则 ES 容器启动后写不了数据目录直接报权限错误。3. 用 Docker Compose 把 ELK 四件套编排起来3.1 docker-compose.yml 完整内容把规划落到 Compose 文件我在/opt/elk下创建了docker-compose.yml。完整内容如下version: 3.8 services: elasticsearch: image: docker.elastic.co/elasticsearch/elasticsearch:7.17.10 container_name: es restart: always environment: - node.namees-node - cluster.namemy-es-cluster - discovery.typesingle-node - bootstrap.memory_locktrue - ES_JAVA_OPTS-Xms1g -Xmx1g - xpack.security.enabledfalse ulimits: memlock: soft: -1 hard: -1 nofile: soft: 65536 hard: 65536 volumes: - /opt/elk/es-data:/usr/share/elasticsearch/data - /opt/elk/es-plugins:/usr/share/elasticsearch/plugins ports: - 9200:9200 networks: - elk-net logstash: image: docker.elastic.co/logstash/logstash:7.17.10 container_name: logstash restart: always environment: - LS_JAVA_OPTS-Xms512m -Xmx512m volumes: - /opt/elk/logstash/pipeline:/usr/share/logstash/pipeline ports: - 5044:5044 depends_on: - elasticsearch networks: - elk-net kibana: image: docker.elastic.co/kibana/kibana:7.17.10 container_name: kibana restart: always environment: - ELASTICSEARCH_HOSTShttp://es:9200 - I18N_LOCALEzh-CN ports: - 5601:5601 depends_on: - elasticsearch networks: - elk-net filebeat: image: docker.elastic.co/beats/filebeat:7.17.10 container_name: filebeat restart: always user: root volumes: - /opt/elk/filebeat/config/filebeat.yml:/usr/share/filebeat/filebeat.yml:ro - /opt/elk/filebeat/data:/usr/share/filebeat/data - /opt/elk/logdata:/logs:ro depends_on: - logstash networks: - elk-net networks: elk-net: driver: bridge几个需要留意的点discovery.typesingle-node表示单节点部署省去集群发现配置。如果你后续要扩容成多节点这里要改。bootstrap.memory_locktrue配合ulimits的 memlock 配置让 ES 锁定内存避免内存交换导致的性能抖动。-1表示不限制。ES_JAVA_OPTS里-Xms和-Xmx要设成一样避免 JVM 动态扩容导致性能波动。Filebeat 用user: root因为采集的日志文件在宿主机挂载目录里普通用户可能没有读取权限。Filebeat 挂载/opt/elk/logdata:/logs:ro只读挂载就够因为 Filebeat 只是读取不会写业务日志目录。3.2 启动与首次验证在/opt/elk下执行docker compose up -d docker compose ps第一次启动建议看 ES 的启动日志确认它顺利过了 bootstrap 检查docker logs -f es看到类似下面的日志就说明 ES 起来了[INFO ][o.e.n.Node] [es-node] started再验证 HTTP 接口curl -s http://localhost:9200正常会返回类似cluster_name : my-es-cluster的 JSON。ES 起来了Kibana 和 Logstash 也会陆续起来。Kibana 第一次启动需要等几十秒因为要往 ES 里初始化自己的索引。这里还一个常见困惑docker compose up -d里的-d是什么含义它是--detach的缩写意思是后台运行不带-d的话Compose 会卡在前台不断输出日志。另外-f参数用来指定 Compose 文件路径-p参数用来指定 project 名。比如docker compose -f /opt/elk/docker-compose.yml -p elk up -d。project 名会影响容器的命名前缀和默认网络名多环境部署时特别有用。3.3 日志保留策略和容器清理ELK 组件多日志量大如果不控制宿主机/var/lib/docker/containers会越涨越大。我在每个业务服务的 Compose 配置里都加上了日志轮转logging: driver: json-file options: max-size: 100m max-file: 3包括 ELK 组件本身也建议加上这个限制。ES 的日志文件如果放任不管一天几个 GB 很常见。4. SpringBoot2 与 Vue3 日志采集的关键配置4.1 日志链路的设计思路在整套架构里日志的流动路径是一条直线SpringBoot 应用日志 - 宿主机挂载目录 - Filebeat 读取 - Logstash 解析 - Elasticsearch 索引 - Kibana 检索Vue3 前端本身不产生服务端日志能采集的是 Nginx 的访问日志和错误日志。Nginx 容器日志同样挂载到宿主机再交给 Filebeat。这个过程里最需要动脑的是两块日志如何落盘、落盘后如何被 Filebeat 识别并解析成结构化字段。4.2 SpringBoot 侧logback 输出、滚动与挂载SpringBoot 2.x 自带的日志框架是 Logback。默认的日志格式大致长这样2024-12-08T10:15:30.12308:00 INFO 12345 --- [http-nio-8080-exec-1] com.example.demo.HelloController : hello我把应用日志写到容器内/app/logs再挂载到宿主机的/opt/elk/logdata/springboot。在application.yml里可以配置logging: file: name: /app/logs/app.log logback: rollingpolicy: max-file-size: 100MB max-history: 7这样日志会在 100MB 时滚动保留最近 7 个历史文件。对应的 Compose 服务大致如下services: springboot-app: image: my-app:1.0.0 container_name: springboot-app volumes: - /opt/elk/logdata/springboot:/app/logs logging: driver: json-file options: max-size: 100m max-file: 34.3 Nginx 侧Vue3 前端项目日志挂载Vue3 构建出的静态资源最终由 Nginx 提供服务。Nginx 容器的标准配置会把访问日志写到/var/log/nginx/access.log错误日志写到/var/log/nginx/error.log。把整个日志目录挂载出来即可services: vue3-app: image: nginx:1.26-alpine container_name: vue3-app volumes: - /opt/elk/logdata/nginx:/var/log/nginx - ./dist:/usr/share/nginx/html:ro - ./nginx.conf:/etc/nginx/conf.d/default.conf:ro这里有一个容易混淆的地方Vue3 前端项目在浏览器端抛出的 JS 异常服务端是收不到的。ELK 这套链路采集的是 Nginx 访问日志能看到的只有“谁在什么时间请求了什么接口、返回了什么状态码”。如果想把 Vue3 里的 JS 错误也纳入日志平台需要在前端代码里埋点上报比如用 fetch 把错误信息 POST 到一个日志接口Logstash 的httpinput 可以直接接收这种 JSON 上报。作为扩展方向这篇文章不展开写但值得知道。如果 Vue3 项目使用 history 路由模式Nginx 需要额外处理前端路由的 fallback否则用户刷新页面时会出现 404location / { root /usr/share/nginx/html; try_files $uri $uri/ /index.html; }这个配置和日志采集本身没有直接关系但经常和 Nginx 日志一起被排查到顺便提一下。4.4 Filebeat 配置多来源采集、字段标识与多行合并Filebeat 是整个采集链路的入口。我创建的/opt/elk/filebeat/config/filebeat.yml完整内容如下filebeat.inputs: - type: log enabled: true paths: - /logs/springboot/*.log fields: log_type: springboot fields_under_root: true multiline.pattern: ^\d{4}-\d{2}-\d{2} multiline.negate: true multiline.match: after multiline.max_lines: 50 - type: log enabled: true paths: - /logs/nginx/access.log fields: log_type: nginx_access fields_under_root: true - type: log enabled: true paths: - /logs/nginx/error.log fields: log_type: nginx_error fields_under_root: true output.logstash: hosts: [logstash:5044] setup.template.enabled: false path.data: /usr/share/filebeat/data logging.level: info这里面multiline配置是最容易被忽略的。SpringBoot 应用抛出异常时堆栈信息往往是多行文本。如果不做合并Filebeat 会按行拆成好几条独立事件Logstash 的 grok 解析也会因为“这一行不是标准日志格式”而失败。multiline.pattern表示“以 ISO 时间格式开头的行是新的日志事件”negate: true和match: after组合的意思是不匹配该模式的行都合并到前一条日志后面。max_lines防止单条日志过长拖垮解析性能。fields_under_root: true的作用是把log_type这个字段放在文档根级别后面 Logstash 可以直接用[fields][log_type]来分流处理不同来源。4.5 Logstash 配置grok 解析、时间归一化与索引分类Logstash 的核心配置文件放在/opt/elk/logstash/pipeline/logstash.conf。我按日志来源把处理逻辑分成了三段input { beats { port 5044 } } filter { if [fields][log_type] springboot { grok { match { message %{TIMESTAMP_ISO8601:log_time} %{LOGLEVEL:log_level} %{NUMBER:pid} --- \\[%{NOTSPACE:thread}\\] %{NOTSPACE:logger} : %{GREEDYDATA:msg} } overwrite [ msg ] } date { match [ log_time, ISO8601 ] target timestamp } mutate { remove_field [ message ] } } if [fields][log_type] nginx_access { grok { match { message %{IPORHOST:clientip} - %{NOTSPACE:remote_user} \\[%{HTTPDATE:nginx_time}\\] \%{WORD:method} %{URIPATHPARM:request_uri} HTTP/%{NUMBER:http_version}\ %{NUMBER:status} %{NUMBER:bytes} \%{NOTSPACE:referrer}\ \%{NOTSPACE:user_agent}\ } } date { match [ nginx_time, dd/MMM/yyyy:HH:mm:ss Z ] target timestamp } mutate { remove_field [ message ] } } } output { if [fields][log_type] springboot { elasticsearch { hosts [ http://elasticsearch:9200 ] index springboot-app-%{yyyy.MM.dd} } } else if [fields][log_type] nginx_access { elasticsearch { hosts [ http://elasticsearch:9200 ] index nginx-access-%{yyyy.MM.dd} } } else { elasticsearch { hosts [ http://elasticsearch:9200 ] index logging-default-%{yyyy.MM.dd} } } }几个关键细节grok 表达式里SpringBoot 默认日志格式中间有个双空格那个不能省。我第一次配的时候漏了导致一整天日志全部解析失败进 ES 的字段都是原始 message。建议写完之后到 Kibana 的 Dev Tools 里用 Grok Debugger 调试拿一条真实日志行测试能省很多时间。date插件会覆盖timestamp让 ES 里的时间字段以日志本身的时间为准而不是以 Filebeat 接收时间为准。后面 Kibana 的时间排序依赖的就是这个字段。Logstash 解析完成后mutate.remove_field把原始message字段移除减少索引体积。如果你需要保留原始日志文本可以不要这一步。4.6 为什么 Filebeat 不直接输出到 Elasticsearch很多人会问Filebeat 本身就支持把数据直接写进 ES为什么还要中间绕一层 Logstash我的理由很简单Logstash 过滤阶段能做的事情比 Filebeat 多得多grok 解析、JSON 解析、日期归一化、字段拆分、脱敏这些用 Logstash 处理更顺手改了配置也方便热加载。如果在 Filebeat 里做每次调整都要重新读取、重新发送调试成本更高。对于当前场景Filebeat 只负责采集Logstash 负责解析职责边界很清楚。5. Kibana 索引配置与日常检索5.1 创建索引模式通配符、时间字段与格式化数据进入 ES 后Kibana 里要先建索引模式Index Pattern才能检索。打开 Management - Stack Management - 索引模式点创建索引模式索引名称填springboot-app-*再选择时间字段timestamp。同理再建一个nginx-access-*。注意如果没建索引模式Discover 页面会提示找不到数据。另外索引模式是支持通配符的*可以匹配后面的日期后缀。比如写