Docker部署ELK 5.5.0:Spring Boot日志JSON接入全链路实践
接手这套服务的时候最要紧的不是加新功能而是先把日志收拢起来。十几台机器上各自打印日志排一次问题基本靠人肉翻服务器效率实在低得离谱。我的方案是 Docker 部署 ELK 5.5.0再让 Spring Boot 项目把日志改成 JSON 格式通过 TCP 直接送进 Logstash最后落到 Elasticsearch统一在 Kibana 里检索。整套链路跑通之后排障时间从半小时压缩到五分钟以内这个变化是肉眼可见的。这套组合现在看确实不算新但对正在维护老 Spring Boot 项目的人来说它反而最稳当。文章不打算堆砌炫技内容核心是把这次从零到一的部署记录、Spring Boot 端配置以及中间踩过的坑完整写下来。适合下面几类人手上有老项目需要做日志集中化、因为依赖原因只能用 ELK 5.x 的团队以及想在本地用 Docker 快速搭一套日志平台验证效果的开发者。1. 为什么我会把这套 ELK 钉在 5.5.01.1 版本锁定背后的现实原因很多人一上来就问我既然要搭日志平台为什么不直接用最新版答案很现实不是不想是项目环境不允许。我当时接手的老服务基于 Spring Boot 1.5.x日志序列化依赖的是logstash-logback-encoder4.x 系列这个版本与 logback 1.2.x 配合得很好也正好匹配 ELK 5.5.0 时代的协议。如果强行把集群升到 7.xElasticsearch 的很多默认行为和配置项都变了老项目里现有的索引模板、查询语句、Kibana 图表全部要重新验证这个迁移成本在维护期项目里是不可能被批准的。这是 ELK 非常关键的一点Elasticsearch、Logstash、Kibana 三个组件的版本必须严格一致。比如 ES 用 5.5.0那 Logstash 也必须是 5.5.0Kibana 同样是 5.5.0。混用 5.5 的 ES 配 6.x 的 Logstash 或 Kibana轻则字段解析异常重则直接拒绝写入或者页面白屏。我见过有人只升级 Logstash 不升级 ES结果索引写入全部失败这个坑不值得再踩一遍。另外 5.5.0 版本还有一个特殊性它仍然支持document_type概念。ES 6.x 开始逐步取消多类型很多老配置都要改映射。我们这边已有的索引模板都是按单类型log写的继续留在 5.5.0 是最省事的选择。所以这里想给读者一个建议如果你也在维护老项目不用盲目追求新版本先看项目依赖能不能动再决定 ELK 版本。1.2 一次部署需要准备的东西正式动手之前先清点一下资源。我当时是在一台 8G 内存的 Linux 机器上做的演示部署实际分配给 ELK 的资源大致如下组件建议内存作用Elasticsearch 5.5.0512M - 1G索引存储与检索Logstash 5.5.0256M - 512M日志接收与解析Kibana 5.5.0默认即可可视化和检索入口加上机器本身的系统开销空闲内存至少留出 2G 比较稳妥。如果机器很紧张ES 的 JVM 堆甚至可以压到 256M但我不建议再低了否则索引合并会非常吃力。然后是镜像。ELK 5.5.0 建议直接从官方仓库拉对应 tag不要用latest否则版本一乱后面全是问题docker pull docker.elastic.co/elasticsearch/elasticsearch:5.5.0 docker pull docker.elastic.co/logstash/logstash:5.5.0 docker pull docker.elastic.co/kibana/kibana:5.5.0准备好之后我习惯先建一个独立的 Docker 网络让三个容器可以通过服务名互通。这样比--link老方式清爽也方便后续扩容docker network create elk-net接下来就可以按顺序折腾 Elasticsearch 了。2. Elasticsearch 5.5.0 容器能跑起来的第一个关卡2.1 网络与目录准备ES 是整条链路的地基它起不来后面全白搭。我先在自己机器上创建数据目录并且把属主改成容器里的用户这一步非常容易漏mkdir -p /opt/elk/es/data chown -R 1000:1000 /opt/elk/es/data为什么要改权限因为 ES 5.5.0 的官方镜像不是以 root 运行的容器内启动用户是elasticsearch在宿主机上对应的默认 UID 是 1000。如果目录属主是 root容器启动时会直接报AccessDeniedException日志里明明看到文件存在却写不进去。很多第一次用官方镜像的人都会在这里卡一下我后来都养成了条件反射挂数据卷之前先chown 1000:1000。2.2 elasticsearch.yml 与启动命令拆解ES 5.5.0 的容器配置我倾向于把核心参数写进elasticsearch.yml再挂载进去而不是全塞在docker run环境变量里。配置文件可读性好出了问题也能快速复核。cluster.name: elk-local node.name: es-node-1 path.data: /usr/share/elasticsearch/data network.host: 0.0.0.0 http.port: 9200 discovery.zen.minimum_master_nodes: 1 discovery.zen.ping.unicast.hosts: [127.0.0.1] xpack.security.enabled: false这里有几个点需要特别说明network.host必须设为0.0.0.0。官方镜像默认只绑定回环地址如果不改容器外部甚至其他容器都没法通过 9200 访问它Logstash 写不进来Kibana 也连不上。5.5.0 还没有 7.x 里的discovery.typesingle-node这个说法。单节点部署要写的是discovery.zen.minimum_master_nodes: 1。如果从网上抄了个新版本的命令直接贴给 5.5.0启动时大概率会抱怨不认识的配置项。xpack.security.enabled: false是 5.x 特有的需求。从 5.0 开始 ES 自带 X-Pack如果不显式关掉安全模块Kibana 访问时会要求登录单机调试非常麻烦。配置文件写好后用下面的命令启动容器docker run -d \ --name es \ --network elk-net \ -p 9200:9200 \ -p 9300:9300 \ -e ES_JAVA_OPTS-Xms512m -Xmx512m \ --ulimit nofile65535:65535 \ --ulimit memlock-1:-1 \ -v /opt/elk/es/config/elasticsearch.yml:/usr/share/elasticsearch/config/elasticsearch.yml:ro \ -v /opt/elk/es/data:/usr/share/elasticsearch/data \ docker.elastic.co/elasticsearch/elasticsearch:5.5.0ES_JAVA_OPTS里的-Xms和-Xmx我建议必须相等避免 JVM 堆动态伸缩引发停顿。开发机上 512M 足够生产环境再根据索引量往上调。--ulimit参数顺手把nofile和memlock拉高这是为了应付 ES 在生产模式下会执行的 bootstrap checks。启动完可以验证一下节点状态curl http://localhost:9200/_cluster/health?pretty看到status : green或者status : yellow都算正常。单节点没有副本分片时 yellow 是合理的不会影响日志写入。2.3 那个著名的 vm.max_map_countES 容器启动最容易遇到的翻车现场就是宿主机 Linux 内核参数不够。日志里会有一行非常明显的报错max virtual memory areas vm.max_map_count [65530] is too low, increase to at least [262144]原因很简单Elasticsearch 默认用mmap方式访问索引文件虚拟内存映射区域的数量上限不够节点就拒绝启动。这跟 ELK 版本无关7.x、8.x 也一样会遇到。解决方法是修改宿主机内核参数sysctl -w vm.max_map_count262144但这种修改重启机器就失效了。为了持久化我一般直接写入/etc/sysctl.d/99-elasticsearch.confecho vm.max_map_count262144 /etc/sysctl.d/99-elasticsearch.conf sysctl -p /etc/sysctl.d/99-elasticsearch.conf如果你用的是 Windows 或 macOS 的 Docker Desktop一般不会受这个参数影响但只要是生产环境 Linux 机器这一步必须提前处理。我后来部署到第二台机器时忘了检查又浪费了十分钟所以在这里提醒一句装完 ES 先看日志再看内核参数。3. Logstash 管道设计我为什么要配 json_lines3.1 input 用 TCP 而不是 beats 插件Logstash 是日志从业务系统进入 ES 的咽喉。最常见的架构是 Filebeat 采集日志文件再转发给 Logstash但我们的 Spring Boot 服务本身是容器化部署日志文件路径、挂载权限都非常折腾。既然应用端可以直接发 JSON那就让 Logstash 直接开启一个 TCP 端口等着收少一层 Filebeat 就少一个故障点。Logstash 5.5.0 的 tcp input 插件是内置的不需要额外安装。重点在于 codec 选择input { tcp { port 5000 codec json_lines } }这里我用的是json_lines不是json。很多新手在这里踩坑jsoncodec 要求一次性读入完整的 JSON 文档TCP 流是没有消息边界的多条 JSON 前后黏在一起它根本没法切分而json_lines是专门按换行符切分的每收到一行就尝试解析成一个 JSON 事件。正好 Spring Boot 端LogstashEncoder输出的时候每条日志后都带一个换行两者是天然匹配的。3.2 filter/output 与索引命名Logstash 管道配置我放在/opt/elk/logstash/logstash.confinput { tcp { port 5000 codec json_lines } } filter { mutate { add_field { receive_host %{host} } } } output { elasticsearch { hosts [http://es:9200] index springboot-%{YYYY.MM.dd} document_type log } stdout { codec rubydebug } }index springboot-%{YYYY.MM.dd}是 Logstash 的日期格式化语法会按事件里的timestamp自动生成按天分割的索引。这样日志量大了以后可以按索引做生命周期管理只保留最近 30 天的旧索引不需要一条一条删数据。document_type log这个是 5.5.0 专属写法ES 6.x 之后会被标记为 deprecation。如果你用的也是 5.x可以正常写如果你在给新项目做选型那请不要照抄这句。stdout输出我是专门留来做调试的。配置刚启动的一小段时间docker logs logstash里能看到每条日志的解析结果确认没有问题后再删掉它避免生产环境重复打屏。3.3 容器启动与验证Logstash 容器启动命令如下docker run -d \ --name logstash \ --network elk-net \ -p 5000:5000 \ -v /opt/elk/logstash/logstash.conf:/usr/share/logstash/pipeline/logstash.conf:ro \ -e LS_JAVA_OPTS-Xms256m -Xmx256m \ docker.elastic.co/logstash/logstash:5.5.0这里需要留意挂载路径。5.5.0 官方镜像的默认管道目录是/usr/share/logstash/pipeline里面默认可能有一个logstash.conf示例。直接把我们的配置文件挂载覆盖上去启动时就会自动加载。启动之后验证管道是否正常docker logs logstash 21 | grep -i Pipeline started如果看到类似Pipeline started的日志说明 TCP 监听已经就绪。如果一直没有输出多半是配置文件语法有问题再用docker logs logstash看具体报错。4. Kibana 侧从连接 ES 到建出第一个索引模式4.1 Kibana 启动参数解析Kibana 在这套链路里是最省心的环节。启动命令非常短docker run -d \ --name kibana \ --network elk-net \ -p 5601:5601 \ -e ELASTICSEARCH_URLhttp://es:9200 \ docker.elastic.co/kibana/kibana:5.5.0这里是 5.5.0 的一个版本细节5.5 镜像使用的环境变量名是ELASTICSEARCH_URL不是 6.6 之后的ELASTICSEARCH_HOSTS。如果你按新版本的写法去配旧版本Kibana 会一直报无法连接 Elasticsearch。我当时对照官方文档确认过这个变量名所以印象很深。启动之后可以用浏览器打开http://localhost:5601。如果页面一直转圈或者报 Unable to connect先别急着改配置多半是因为 ES 容器还没完全就绪Kibana 连接超时了。等在 ES 健康检查返回 green 之后再docker restart kibana一次基本都能恢复。4.2 创建索引模式与常见空白页Kibana 起来之后第一件事是创建索引模式。入口在左侧 Management 菜单选择 Index Patterns。点击 Create Index Pattern。输入springboot-*这个通配符会匹配所有按天生成的日志索引。时间过滤字段选择timestampKibana 的 Discover 页面才能按时间排序和筛选。点击 Create。这里有个常见的空白页问题如果 Elasticsearch 里还没有任何springboot-*索引Kibana 会提示找不到匹配的索引。这不是链路坏了而是数据还没进来。所以我的建议是先把后面 Spring Boot 端配置好或者手动往 Logstash 推一条测试数据确认 ES 里能看到索引了再回来建索引模式。还有一个小细节如果打开 Discover 页面发现时间轴是空的而明明已经写入了数据可以检查右上角时间范围是否选择正确。Kibana 默认只显示最近 15 分钟老日志会被过滤掉。5. Spring Boot 项目把日志打进这套管道5.1 依赖选择logstash-logback-encoderSpring Boot 端最核心的依赖是logstash-logback-encoder它的作用是把 logback 日志事件序列化成 JSON然后通过 TCP socket 发给 Logstash。dependency groupIdnet.logstash.logback/groupId artifactIdlogstash-logback-encoder/artifactId version4.11/version /dependency关于版本选择我给出一个非常实用的判断方法先看项目里 logback-classic 的版本。如果项目是 Spring Boot 1.5.x 时代logback 一般是 1.2.x那logstash-logback-encoder用 4.11 就很稳定如果项目里 logback 已经被升到 1.3.x 或更高那 encoder 也要跟着升到 6.x否则会出现方法签名不兼容的报错。检查依赖版本可以直接在项目根目录执行mvn dependency:tree -Dincludesch.qos.logback:logback-classic看到 logback 版本后再决定 encoder 版本这是最快的方案。5.2 logback-spring.xml 里的门道进入正题Spring Boot 项目在src/main/resources下放一个logback-spring.xml。下面这份是我当时用的完整配置?xml version1.0 encodingUTF-8? configuration include resourceorg/springframework/boot/logging/logback/defaults.xml/ springProperty scopecontext nameappName sourcespring.application.name defaultValueunknown-service/ appender nameLOGSTASH classnet.logstash.logback.appender.LogstashTcpSocketAppender destinationlogstash:5000/destination keepAliveDuration5 minutes/keepAliveDuration connectionTimeout3 seconds/connectionTimeout encoder classnet.logstash.logback.encoder.LogstashEncoder customFields{service:${appName}}/customFields includeMdctrue/includeMdc /encoder /appender appender nameASYNC_LOGSTASH classch.qos.logback.core.AsyncAppender appender-ref refLOGSTASH/ queueSize1024/queueSize neverBlocktrue/neverBlock discardingThreshold0/discardingThreshold /appender appender nameCONSOLE classch.qos.logback.core.ConsoleAppender encoder pattern${CONSOLE_LOG_PATTERN}/pattern charsetUTF-8/charset /encoder /appender logger nameorg.springframework levelINFO/ logger nameorg.apache levelWARN/ root levelINFO appender-ref refCONSOLE/ appender-ref refASYNC_LOGSTASH/ /root /configuration逐个解释几个容易被忽略的点。destination里写的是logstash:5000。这个logstash不是随便写的它是 Docker 网络里 Logstash 容器的名字。如果 Spring Boot 应用也跑在同一个elk-net网络里服务名就能直接解析到容器 IP如果应用跑在宿主机上或者不在 Docker 网络里那这里要改成宿主机 IP例如192.168.1.10:5000。不少人配置好后发现日志进不来排查到最后往往是这个 IP 写错了。customFields里我放了一个service字段内容来自spring.application.name。这样做的好处是多条服务的日志混在同一个 ES 集群时我们可以在 Kibana 里用service:order-service这种条件快速过滤不需要 Logstash 端再额外处理。includeMdc设为 true 之后代码里往 MDC 写入的上下文信息会自动带进 JSON 日志。比如在接口入口处设置traceId就能在 Kibana 里按traceId串起一次请求的全链路日志。5.3 异步与容错别让日志服务拖垮业务我最想强调的一点是日志发送绝不能阻塞业务线程。所以上面配置里我把 LOGSTASH appender 包了一层AsyncAppender。queueSize是异步队列长度当日志产生的速度超过网络发送速度时日志先排队。neverBlock设为 true 之后队列满了就直接丢弃新日志而不是让业务线程卡在队列写入上。在日志这个场景里丢几条日志远比重启一个核心服务划算。还有一个细节connectionTimeout我设置成 3 秒这是为了避免 Logstash 暂时不可达时业务启动阶段每打印一条日志都要傻等网络超时。加了超时时间和异步队列之后就算 Logstash 整个挂掉Spring Boot 业务本身也不会受到明显影响。至于 MDC 的使用我放一个极简示例MDC.put(traceId, UUID.randomUUID().toString()); log.info(订单创建成功: {}, orderId); MDC.remove(traceId);这样一来JSON 日志里就会多出trace_id字段排查跨服务调用的时候非常有用。但要注意MDC 是线程级变量处理完业务之后记得remove否则线程池复用的时候会把上一次的 traceId 带到下一次请求里反而干扰排查。6. 全链路验证清单与踩坑记忆6.1 按顺序排查的四步整套链路搭完后最忌讳的就是一边看 Kibana 页面一边瞎猜。我习惯按下面这个顺序逐段验证第一步确认 ES 本身是健康的curl -s http://localhost:9200/_cluster/health?pretty看到status : green或yellow就可以继续。第二步确认 Logstash 管道已经起来docker logs logstash 21 | grep -i Pipeline started如果这里没有输出TCP 监听可能根本没生效后面所有数据都会在门口堵住。第三步手动往 Logstash 推一条测试日志echo {\message\:\elk connectivity test\,\timestamp\:\$(date -u %Y-%m-%dT%H:%M:%S.000Z)\} | nc localhost 5000发送后马上检查 ES 里有没有生成索引curl -s http://localhost:9200/_cat/indices?v正常会看到一个形如springboot-2026.05.07的索引。如果没有索引说明 Logstash 到 ES 的写入链路有问题优先看docker logs logstash里的报错。第四步回 Kibana 的 Discover 页面刷新后确认能看到刚才那条测试日志。四步全过再回头看 Spring Boot 应用是否真的在产生日志。这样排查可以把问题范围一步步缩小而不是眉毛胡子一把抓。6.2 索引模板和时区这两个细节日志数据量变大之后字符串字段默认会被 ES 同时映射为 text 和 keyword但有些字段我希望显式控制。例如level、logger_name、service这类字段最好直接映射为 keyword方便做聚合统计和过滤。我给这套链路预置了一个索引模板PUT _template/springboot-log { template: springboot-*, order: 0, settings: { number_of_shards: 1, number_of_replicas: 0 }, mappings: { log: { properties: { service: {type: keyword}, level: {type: keyword}, logger_name: {type: keyword}, thread_name: {type: keyword}, message: {type: text, fields: {keyword: {type: keyword, ignore_above: 256}}}, timestamp: {type: date} } } } }执行这个模板创建之后后面所有按天生成的springboot-*索引都会自动套用该结构省去手工改 mapping 的麻烦。还有一个非常容易被忽的视频时区。Logstash 容器默认使用 UTC 时间如果不做任何处理索引名里的日期很容易在北京时间凌晨到早上 8 点之间显示成前一天。Kibana 页面里的时间也会感觉慢了 8 小时。我的处理方式是在启动 Logstash 时加上时区环境变量docker run -d \ --name logstash \ --network elk-net \ -p 5000:5000 \ -e TZAsia/Shanghai \ -e LS_JAVA_OPTS-Xms256m -Xmx256m \ -v /opt/elk/logstash/logstash.conf:/usr/share/logstash/pipeline/logstash.conf:ro \ docker.elastic.co/logstash/logstash:5.5.0ES 内部存储时间仍然建议保持 UTC这是标准做法显示层面的时区问题交给 Kibana 设置即可。6.3 再补几个我记下来的坑最后把我这次部署过程中记下来的常见问题整理成一张表方便大家对照现象原因处理方式ES 容器启动失败报 vm.max_map_count 过小Linux 内核参数不够修改/etc/sysctl.d/99-elasticsearch.conf并sysctl -pES 数据目录权限报错宿主机目录属主不是 UID 1000chown -R 1000:1000 /opt/elk/es/dataKibana 一直提示无法连接 ES环境变量名写错或 ES 未就绪确认使用ELASTICSEARCH_URL稍后重启 KibanaSpring Boot 日志能打印但 ES 没有索引destination写的地址应用访问不到检查应用与 Logstash 是否在同一网络或改用宿主机 IPLogstash 5000 端口被占本机开发工具占用换端口并同步修改 Spring Boot 端 destination整套链路真正跑通之后我最大的感受是Docker 部署 ELK 5.5.0 并不是一件多复杂的事难的是每一个环节都有属于自己的历史包袱。不管是 ES 的内核参数、Kibana 的变量名还是 Logstash 的 codec只要版本对了方案都是成熟的。最后再分享一个小习惯容器名固定用es、logstash、kibana这样在任意一台新机器重新部署时配置几乎不用改排查问题也能少对着 IP 列表来回看。