1. 项目概述:为什么ELK不是“配个环境就完事”的玩具,而是一套需要亲手拧紧每颗螺丝的日志生命线
你有没有遇到过这样的场景:线上服务突然响应变慢,用户投诉涌进来,运维在告警群里刷屏,开发在日志文件里用grep翻到凌晨三点,最后发现是某个微服务悄悄抛出了一个被吞掉的空指针——而这个异常,早在两小时前就写进了某台容器的标准输出,只是没人看见?这不是个别现象,而是绝大多数中小团队在日志管理上正在经历的真实困境。手把手教你使用docker一键搭建elk日志系统(含源码)这个标题里的“一键”,绝不是让你双击运行后就坐等大屏自动亮起的魔法按钮;它指的是:在理解Logstash管道如何过滤字段、Elasticsearch分片为何不能设为1、Kibana索引模式怎样绑定时间戳字段的前提下,用Docker Compose把三者粘合成一个可复现、可审计、可伸缩的日志处理闭环。我带过的几个项目组,最初都信了“一键”的字面意思,结果在生产环境跑了一周后,发现Logstash内存暴涨、ES集群黄标不退、Kibana查不到昨天的数据——问题全出在那几个被跳过的“手把手”环节:比如没给Logstash配置pipeline.workers: 2导致单核打满,比如ES的discovery.type=single-node没加在生产配置里引发脑裂误判,比如Kibana的server.host: "0.0.0.0"漏写了引号导致启动失败。这些细节,恰恰是ELK从“能跑”走向“稳跑”的分水岭。本文面向的是已经写过Spring Boot应用、能看懂YAML缩进、知道docker ps和docker logs区别的一线开发者与SRE,不讲Elasticsearch底层Lucene倒排索引原理,但会告诉你为什么index.refresh_interval: "30s"在日志场景下比默认的1秒更合理;不堆砌Kibana所有可视化图表类型,但会手把手带你配置一个能自动识别Nginx access log中$status和$request_time字段的Ingest Pipeline。你不需要成为搜索专家,但必须清楚:日志系统不是锦上添花的装饰品,而是故障定位的呼吸机、性能优化的CT机、安全审计的监控摄像头。接下来的内容,每一行命令、每一个参数、每一处缩进,都是我在某电商中台项目里,和运维同事蹲在IDC机房调试通宵后记下的真实操作痕迹。
2. 整体架构设计与核心组件选型逻辑:为什么不用Elastic Stack 8.x最新版,为什么Logstash不换Fluentd
2.1 架构图不是画出来的,是踩坑踩出来的拓扑关系
很多人一上来就画个三层架构图:应用层 → Logstash → Elasticsearch → Kibana。这没错,但漏掉了三个致命细节:数据流向不是单向的,资源边界不是模糊的,故障传播不是静默的。我们最终落地的架构,实际是这样的:
- 应用容器通过
logging driver直接将stdout/stderr发送至本地fluentd(轻量级),再由fluentd转发至Logstash(做复杂ETL); - Logstash不做持久化,只做字段解析、敏感信息脱敏、错误级别归类,然后批量写入ES;
- Elasticsearch采用3节点伪集群(单机多实例),主分片数设为3,副本数为1,禁用
_source字段对非关键索引(如debug日志); - Kibana与ES同机部署,但通过反向代理暴露HTTPS端口,强制启用Space隔离不同业务线日志视图;
- 所有组件通过Docker Network桥接,但Logstash与ES之间额外配置
network.host: _site_确保绑定物理网卡而非Docker虚拟IP。
这个结构不是凭空设计的。最早我们试过让Spring Boot应用直接用logstash-logback-encoder发日志到Logstash,结果发现当Logstash重启时,应用TCP连接重试风暴直接打垮了服务;后来换成Filebeat,又遇到Filebeat采集器在容器销毁时丢失最后几条日志的问题。最终选择fluentd+Logstash组合,是因为fluentd的buffer机制天然支持断点续传,而Logstash的JRuby生态对Java应用日志的Grok模式支持最成熟——比如解析2024-05-20T14:23:17.123Z [ERROR] com.example.service.UserService - User not found: id=1001这种混合格式,Logstash的grok { match => { "message" => "%{TIMESTAMP_ISO8601:timestamp} \[%{LOGLEVEL:level}\] %{JAVACLASS:class} - %{GREEDYDATA:msg}" } }一行就能搞定,而Filebeat的processors写起来要绕三道弯。
2.2 版本锁定:为什么坚持用Elasticsearch 7.17.21而不是8.12.0
搜索社区总在鼓吹新版本的向量检索、RAG集成能力,但日志系统的核心诉求永远是:写入吞吐稳定、查询延迟可控、升级路径平滑。我们对比测试过ES 7.17.21与8.12.0在相同硬件上的表现:
| 测试项 | ES 7.17.21 | ES 8.12.0 | 差异说明 |
|---|---|---|---|
| 10万条/秒日志写入P99延迟 | 42ms | 68ms | 8.x引入的Security模块增加TLS握手开销 |
GET /_cat/indices?v响应时间 | <50ms | 210ms | 8.x默认开启Index Lifecycle Management,元数据查询变重 |
| Logstash批量写入成功率 | 99.998% | 99.921% | 8.x对bulk API的refresh参数校验更严格,旧Logstash插件易触发400错误 |
| 升级回滚耗时 | 3分钟(停服) | 22分钟(需reindex) | 7.x到8.x的mapping变更不可逆 |
最关键的是,7.17.21是Elastic官方声明的最后一个无强制安全认证的GA版本。这意味着你可以用curl -XGET "localhost:9200/_cat/indices"直接调试,而不用先折腾PKI证书、角色权限、API Key。对于刚搭建日志系统的团队,少一个SSL证书链验证失败的排查小时,就能早一小时看到第一条日志入库。我们不是拒绝新版本,而是在日志系统这个基础设施上,把“确定性”放在“先进性”前面。所以整个ELK栈的版本锁定如下:
- Elasticsearch:
docker.elastic.co/elasticsearch/elasticsearch:7.17.21 - Logstash:
docker.elastic.co/logstash/logstash:7.17.21 - Kibana:
docker.elastic.co/kibana/kibana:7.17.21 - fluentd:
fluent/fluentd:v1.16-1
提示:不要用
latest标签!某次CI流水线自动拉取了Logstash 8.x镜像,导致所有Grok filter报undefined method 'gsub' for nil:NilClass错误,排查了6小时才发现是Ruby版本不兼容。
2.3 为什么Logstash不换Fluentd或Vector:当“轻量”变成“残缺”
Fluentd常被宣传为“比Logstash更轻量”,Vector则号称“Rust写的高性能替代品”。但在真实日志处理场景中,“轻量”不等于“够用”。我们做过对照实验:用同一份10GB的Nginx access log(含$upstream_response_time、$http_x_forwarded_for等23个字段),分别用三种工具做以下ETL:
- 提取
$status为数字字段(非字符串) - 将
$http_user_agent解析为os.name、browser.name、device.type - 对
$request_uri做URL decode并截取前128字符 - 过滤掉
$status为200且$request_time<0.01秒的健康探针日志
结果如下:
| 工具 | CPU占用峰值 | 内存占用峰值 | 配置代码行数 | 能否原生解析User-Agent |
|---|---|---|---|---|
| Logstash (v7.17) | 3.2核 | 1.8GB | 47行 | 是(useragentfilter) |
| Fluentd (v1.16) | 1.4核 | 850MB | 89行(含大量ruby脚本) | 否(需自定义plugin) |
| Vector (v0.35) | 0.9核 | 420MB | 63行 | 否(需调用外部Python函数) |
Logstash看似“重”,但它把最痛的日志解析能力做成了开箱即用的filter插件。Fluentd的轻量是以牺牲开箱即用性为代价的——你得自己写Ruby代码处理嵌套JSON;Vector的极致性能则要求你用TOML写复杂的条件分支,一旦parse_json失败,整条日志就丢弃。而Logstash的if [status] == "200" { drop {} }这种语法,开发人员一眼就能懂。所以我们的结论很务实:在日志解析复杂度中等以上(字段>10个、需正则/Grok/JSON解析)的场景,Logstash仍是不可替代的“瑞士军刀”。它的“重”是功能重量,不是资源负担——只要给足2GB内存和2核CPU,它稳如磐石。
3. 核心组件配置详解与实操要点:从docker-compose.yml到每行grok表达式
3.1 docker-compose.yml:不是复制粘贴,而是理解每个字段的生存意义
下面这份docker-compose.yml是我们在线上稳定运行14个月的配置,删减了所有注释,但每一行都值得你逐字读:
version: '3.8' services: elasticsearch: image: docker.elastic.co/elasticsearch/elasticsearch:7.17.21 container_name: es01 environment: - node.name=es01 - cluster.name=es-docker-cluster - discovery.seed_hosts=es02,es03 - cluster.initial_master_nodes=es01,es02,es03 - bootstrap.memory_lock=true - "ES_JAVA_OPTS=-Xms1g -Xmx1g" - xpack.security.enabled=false - xpack.monitoring.collection.enabled=true ulimits: memlock: soft: -1 hard: -1 volumes: - es_data01:/usr/share/elasticsearch/data ports: - 9200:9200 networks: - elk elasticsearch2: image: docker.elastic.co/elasticsearch/elasticsearch:7.17.21 container_name: es02 environment: - node.name=es02 - cluster.name=es-docker-cluster - discovery.seed_hosts=es01,es03 - cluster.initial_master_nodes=es01,es02,es03 - bootstrap.memory_lock=true - "ES_JAVA_OPTS=-Xms1g -Xmx1g" - xpack.security.enabled=false - xpack.monitoring.collection.enabled=true ulimits: memlock: soft: -1 hard: -1 volumes: - es_data02:/usr/share/elasticsearch/data networks: - elk elasticsearch3: image: docker.elastic.co/elasticsearch/elasticsearch:7.17.21 container_name: es03 environment: - node.name=es03 - cluster.name=es-docker-cluster - discovery.seed_hosts=es01,es02 - cluster.initial_master_nodes=es01,es02,es03 - bootstrap.memory_lock=true - "ES_JAVA_OPTS=-Xms1g -Xmx1g" - xpack.security.enabled=false - xpack.monitoring.collection.enabled=true ulimits: memlock: soft: -1 hard: -1 volumes: - es_data03:/usr/share/elasticsearch/data networks: - elk logstash: image: docker.elastic.co/logstash/logstash:7.17.21 container_name: logstash environment: - xpack.monitoring.enabled=false - LS_JAVA_OPTS=-Xms512m -Xmx512m volumes: - ./logstash/pipeline:/usr/share/logstash/pipeline:ro - ./logstash/config:/usr/share/logstash/config:ro depends_on: - elasticsearch - elasticsearch2 - elasticsearch3 networks: - elk kibana: image: docker.elastic.co/kibana/kibana:7.17.21 container_name: kibana environment: - ELASTICSEARCH_HOSTS=http://elasticsearch:9200,http://elasticsearch2:9200,http://elasticsearch3:9200 - SERVER_NAME=kibana - SERVER_HOST=0.0.0.0 - ELASTICSEARCH_REQUEST_TIMEOUT=30000 ports: - 5601:5601 depends_on: - elasticsearch - elasticsearch2 - elasticsearch3 networks: - elk volumes: es_data01: driver: local es_data02: driver: local es_data03: driver: local networks: elk: driver: bridge现在,我们逐行拆解那些容易被忽略却决定成败的细节:
discovery.seed_hosts:这是ES节点发现的“根DNS”。必须写成其他节点的容器名(es02, es03),而不是localhost或127.0.0.1。因为Docker容器间通信走的是bridge网络,localhost在es01容器内指向自己,根本连不到es02。cluster.initial_master_nodes:仅在首次启动集群时生效,列出所有有资格成为master的节点名。如果后续扩容加节点,这里不能改,否则新节点无法加入。我们曾因误删了es03导致集群状态变为red,恢复花了40分钟。bootstrap.memory_lock=true:强制ES使用mlockall锁住内存,避免JVM堆被OS交换到磁盘。配合ulimits.memlock设置为-1(无限制),否则ES启动会报max virtual memory areas vm.max_map_count [65530] is too low错误。xpack.security.enabled=false:在内部可信网络中关闭安全模块,省去证书管理和RBAC配置。但注意:绝不能将9200端口暴露到公网,必须用Nginx反向代理+Basic Auth做第一道门禁。LS_JAVA_OPTS=-Xms512m -Xmx512m:Logstash的JVM堆必须设为固定值(Xms=Xmx),否则GC时堆动态伸缩会导致CPU毛刺。512MB是处理1000TPS日志的底线,低于此值会出现Pipeline aborted due to error。volumes挂载:./logstash/pipeline必须是只读(:ro),否则Logstash热重载配置时可能因文件锁导致崩溃。我们吃过亏:某次误写成读写,Logstash反复重启,日志堆积在内存队列里,最终OOM Killed。
注意:ES的
discovery.type=single-node只适用于单节点开发环境。生产必须用discovery.seed_hosts,否则节点间无法形成集群,副本分片永远是UNASSIGNED状态。
3.2 Logstash pipeline配置:grok不是猜谜游戏,而是结构化手术刀
Logstash的核心是pipeline.conf,它定义了日志从输入→过滤→输出的完整血路。我们以处理Spring Boot应用的console日志为例,这是最典型的Java日志场景:
input { beats { port => 5044 } } filter { # 第一步:用grok提取基础时间、级别、类名、消息体 grok { match => { "message" => "%{TIMESTAMP_ISO8601:timestamp} \[%{LOGLEVEL:level}\] %{JAVACLASS:class} - %{GREEDYDATA:msg}" } overwrite => [ "message" ] } # 第二步:将timestamp字符串转为ES可识别的date类型 date { match => [ "timestamp", "ISO8601" ] target => "@timestamp" } # 第三步:解析msg中的JSON结构(如Feign调用返回的error body) if [msg] =~ /\{.*\}/ { json { source => "msg" target => "json_body" remove_field => [ "msg" ] } } # 第四步:提取异常堆栈的首行作为error.summary if [msg] =~ /java\.lang\./ { grok { match => { "msg" => "(?<error_summary>java\.lang\.[^\n]+)" } tag_on_failure => [] } } # 第五步:标准化level字段,统一为大写 mutate { uppercase => [ "level" ] } } output { elasticsearch { hosts => ["http://elasticsearch:9200","http://elasticsearch2:9200","http://elasticsearch3:9200"] index => "springboot-%{+YYYY.MM.dd}" user => "elastic" password => "changeme" } }这段配置背后,藏着五个必须掌握的实操要点:
grok的match顺序就是执行顺序:必须先匹配外层时间戳,再处理内层JSON。如果把
json{}放在grok{}前面,msg还是原始字符串,json{}会因解析失败而丢弃整条日志。我们曾因此丢失了所有带JSON响应体的错误日志。overwrite => [ "message" ]是关键开关:它让grok提取的字段(timestamp,level,class)直接覆盖原始message字段。否则message里还是完整的原始行,而新字段是平行存在的,Kibana里会看到重复数据。date{}的target => "@timestamp"不能省略:ES默认用@timestamp字段做时间序列索引。如果这里不指定,ES会用Logstash接收日志的时间(即@timestamp的默认值),而不是日志里记录的2024-05-20T14:23:17.123Z,导致按时间筛选时错位。if [msg] =~ /\{.*\}/的正则要克制:.*是贪婪匹配,但/\{.*\}/会匹配从第一个{到最后一个}之间的所有内容,包括中间的}。更安全的写法是/\{[^}]*\}/,限定只匹配单层JSON。不过对于Spring Boot的{"code":500,"msg":"xxx"}这种扁平结构,前者足够。index => "springboot-%{+YYYY.MM.dd}"的日期格式必须用+开头:这是Logstash的日期格式语法,%{+YYYY.MM.dd}表示用当前时间(即日志到达Logstash的时间)生成索引名。如果写成%{YYYY.MM.dd},它会尝试从@timestamp字段里提取,而此时@timestamp还没被date{}插件赋值,结果索引名变成springboot--,ES拒绝写入。
实操心得:调试grok时,别在生产环境硬刚。用Logstash自带的
--config.test_and_exit参数验证语法,再用--log.level=debug启动看字段提取效果。最高效的方法是:写好grok后,在Kibana Dev Tools里执行POST /_analyze?pretty,把原始日志粘贴进去,看分词结果是否符合预期。
3.3 Elasticsearch索引模板:让日志字段“生下来就懂事”
ES不会自动为新字段创建mapping,如果第一条日志里有user_id:123,第二条突然出现user_id:"abc",ES会因类型冲突拒绝写入。解决方案是预定义索引模板(Index Template),在日志首次写入前就告诉ES:“所有springboot-*索引,user_id必须是keyword类型,response_time必须是float”。
我们在./elasticsearch/templates/springboot.json中定义:
{ "index_patterns": ["springboot-*"], "settings": { "number_of_shards": 3, "number_of_replicas": 1, "refresh_interval": "30s", "analysis": { "analyzer": { "url_analyzer": { "type": "custom", "tokenizer": "uax_url_email" } } } }, "mappings": { "properties": { "@timestamp": { "type": "date" }, "timestamp": { "type": "date" }, "level": { "type": "keyword" }, "class": { "type": "keyword" }, "msg": { "type": "text", "analyzer": "standard" }, "error_summary": { "type": "keyword" }, "json_body": { "type": "object", "enabled": true }, "response_time": { "type": "float" }, "user_id": { "type": "keyword" }, "request_uri": { "type": "text", "analyzer": "url_analyzer" } } } }这个模板的关键设计点:
"number_of_shards": 3:与ES集群节点数一致,确保分片均匀分布。如果设为1,所有数据都在一个节点上,失去高可用。"refresh_interval": "30s":日志场景不需要实时可见,30秒刷新一次可降低ES的I/O压力。默认1秒会导致每秒产生大量segment文件。"analyzer": "url_analyzer":为request_uri字段定制分词器,用uax_url_email能正确切分/api/v1/users/123?sort=name中的路径和参数,避免被/或?错误截断。"error_summary": {"type": "keyword"}:错误摘要必须是keyword,才能在Kibana里做terms聚合(如统计TOP10错误类型)。如果设为text,会被分词成java、lang、NullPointerException,无法聚合。
模板加载方式(在ES启动后执行):
curl -XPUT "http://localhost:9200/_template/springboot-template" \ -H 'Content-Type: application/json' \ -d @./elasticsearch/templates/springboot.json提示:模板名称
sprinboot-template必须全局唯一。如果重复执行,ES会报resource_already_exists_exception,但不影响功能。建议在CI脚本中加|| true忽略该错误。
4. 完整实操流程与关键环节实现:从零开始,每一步都附带验证命令
4.1 环境准备:检查不是形式主义,而是排除90%的启动失败
在运行docker-compose up -d之前,必须完成三项硬性检查。这三步做完,90%的“启动失败”问题已提前规避:
第一步:确认Docker资源上限
# 检查当前Docker守护进程的内存限制(ES需要至少4GB可用内存) docker info | grep "Total Memory" # 检查vm.max_map_count(ES强制要求≥262144) sysctl vm.max_map_count # 如果小于262144,临时生效: sudo sysctl -w vm.max_map_count=262144 # 永久生效(写入/etc/sysctl.conf): echo "vm.max_map_count=262144" | sudo tee -a /etc/sysctl.conf第二步:验证Docker Network连通性
# 创建专用网络(避免与默认bridge冲突) docker network create elk # 启动一个临时busybox容器,测试能否ping通ES节点名 docker run --rm --network elk busybox ping -c 2 elasticsearch # 正确输出应为: # PING elasticsearch (172.20.0.2): 56 data bytes # 64 bytes from 172.20.0.2: seq=0 ttl=64 time=0.123 ms第三步:检查目录权限与SELinux
# ES数据目录必须是1001:1001(ES容器内用户ID) sudo chown -R 1001:1001 ./es_data # 如果系统启用了SELinux(如CentOS/RHEL),必须添加context sudo semanage fcontext -a -t container_file_t "/path/to/es_data(/.*)?" sudo restorecon -R /path/to/es_data # 验证:ls -Z ./es_data 应显示 context=system_u:object_r:container_file_t:s0注意:
chown 1001:1001是ES镜像的硬性要求。ES容器以UID 1001运行,如果宿主机目录属主是root,ES会因权限不足拒绝写入data目录,日志里只显示java.io.IOException: Permission denied,极其难排查。
4.2 一键启动与状态验证:用5条命令确认系统真正“活了”
执行docker-compose up -d后,不要立刻打开Kibana。按顺序执行以下5条命令,每一条都是生死线:
# 1. 确认所有容器状态为Up,且无Restarting docker-compose ps # 输出应为: # Name Command State Ports # ----------------------------------------------------------------- # elasticsearch /usr/local/bin/docker-entr ... Up 0.0.0.0:9200->9200/tcp # elasticsearch2 /usr/local/bin/docker-entr ... Up 9200/tcp # elasticsearch3 /usr/local/bin/docker-entr ... Up 9200/tcp # kibana /usr/local/bin/kibana-docker Up 0.0.0.0:5601->5601/tcp # logstash /usr/local/bin/docker-entryp ... Up 5044/tcp, 9600/tcp # 2. 检查ES集群健康状态(必须是green,yellow都不行) curl -s "http://localhost:9200/_cat/health?v" | awk '{print $4}' # 输出必须是 green # 3. 检查ES节点列表(必须显示3个节点,且status为U) curl -s "http://localhost:9200/_cat/nodes?v" | wc -l # 输出必须是 4(表头+3行数据) # 4. 检查Logstash是否监听5044端口(Beats输入端口) docker exec logstash netstat -tlnp | grep :5044 # 输出应为:tcp6 0 0 :::5044 :::* LISTEN 1/java # 5. 检查Kibana是否能连通ES(关键!很多Kibana白屏源于此) docker exec kibana curl -s "http://elasticsearch:9200/_cat/indices?v" | head -3 # 输出应为类似: # health status index uuid pri rep docs.count docs.deleted store.size pri.store.size # yellow open .kibana_1 xxxxx 1 1 2 0 47.2kb 47.2kb这5条命令,每一条失败都对应一个经典故障:
docker-compose ps显示Restarting → ES内存不足或vm.max_map_count未调_cat/health返回red → 分片未分配,检查_cat/shards看UNASSIGNED原因_cat/nodes只有1行 →discovery.seed_hosts配置错误,节点无法发现彼此netstat查不到5044 → Logstash配置文件语法错误,容器启动失败(查docker logs logstash)- Kibana连不通ES →
ELASTICSEARCH_HOSTS环境变量写错,或ES未启用HTTP端口(检查ES日志是否有bound_addresses {0.0.0.0:9200})
4.3 日志注入与Kibana验证:让第一条日志“破土而出”
现在,我们模拟一个真实应用日志注入过程。不用写Java代码,用curl发送一条标准日志:
# 向Logstash的Beats端口发送一条JSON日志(模拟Filebeat) curl -XPOST "http://localhost:5044" \ -H 'Content-Type: application/json' \ -d '{ "timestamp": "2024-05-20T14:23:17.123Z", "level": "ERROR", "class": "com.example.service.PaymentService", "msg": "Payment failed: timeout, order_id=ORD-7890" }'等待10秒后,在Kibana中执行:
- 访问
http://localhost:5601,首次进入会提示配置Index Pattern; - 输入
springboot-*,点击Next step; - 在Time Filter field name中选择
@timestamp,点击Create index pattern; - 进入Discover页,点击左上角Auto-refresh,设为10秒;
- 在搜索框输入
level: "ERROR",回车。
如果看到一条日志,且字段@timestamp、level、class、msg都正确显示,恭喜,你的ELK系统已成功打通任督二脉。
实操心得:第一次看不到日志?别急着重装。90%的情况是:① Logstash的
index => "springboot-%{+YYYY.MM.dd}"生成的索引名与Kibana配置的springboot-*不匹配(检查curl http://localhost:9200/_cat/indices?v看实际索引名);② Kibana的Time Filter范围太窄(默认只查最近15分钟,而你的日志时间戳是昨天);③ ES的refresh_interval设为30秒,需等待30秒才可见。
5. 常见问题与排查技巧实录:那些文档里不会写的“血泪教训”
5.1 问题速查表:按症状反推根因
| 症状 | 可能根因 | 快速验证命令 | 解决方案 |
|---|---|---|---|
docker-compose ps显示logstash Restarting | Logstash配置语法错误 | docker logs logstash | head -20 | 检查pipeline.conf末尾是否有遗漏的},或output{}里hosts写成host |
ES集群状态为yellow,unassigned_shards>0 | 副本分片无法分配(磁盘水位超85%或节点数不足) | curl "http://localhost:9200/_cat/allocation?v" | 清理旧索引:curl -XDELETE "http://localhost:9200/springboot-2024.05.15";或临时调低水位:curl -XPUT "http://localhost:9200/_cluster/settings" -H 'Content-Type: application/json' -d '{"persistent":{"cluster.routing.allocation.disk.watermark.low":"90%"}}' |
Kibana页面空白,Console报ERR_CONNECTION_REFUSED | Kibana容器未监听5601端口 | docker exec kibana netstat -tlnp | grep :5601 | 检查docker-compose.yml中kibana的ports是否写成- "5601"(缺少0.0.0.0:前缀) |
Discover页查不到任何日志,但_cat/indices显示索引存在 | Kibana Index Pattern未关联到正确字段 | 进入Stack Management → Index Patterns → 选中springboot-*→ 查看Time Field设置 | 确保Time Field是@timestamp而非timestamp,且该字段mapping类型为date(查curl "http://localhost:9200/springboot-*/_mapping?pretty") |
Logstash CPU持续100%,docker stats显示内存缓慢上涨 | Grok表达式存在灾难性回溯(如.*嵌套) | docker exec logstash jstack 1 | grep -A 10 "RUNNABLE" | 用https://regex101.com/测试grok正则,将.*替换为[^}]*等原子组 |
5.2 经典故障深度复盘:一次因时区引发的“幽灵日志”
故障现象:某天凌晨3点,运维收到告警:springboot-2024.05.19索引突然涌入10万条日志,全是level: "INFO",但应用日志文件里并无对应记录。更诡异的是,这些日志的@timestamp显示为2024-05-19T22:00:00.000Z,而服务器本地时间是`2024-05-20T03:00:00