☰
ELK与FELK日志平台架构选型与全链路排障实践
2026/11/2 12:15:42 网站建设 项目流程

凌晨两点被电话叫醒,说某个核心接口大面积超时。爬起来打开电脑,结果发现日志平台自己先趴了——采集端堆着十几个 G 没发出去的文件,处理端磁盘写满直接退出,检索端查一条报错要转二十秒。故障排查最怕的不是没日志,而是你需要日志的那一刻它刚好不工作。这件事之后我把整套日志链路推倒重来,ELK 和 FELK 这两套架构的取舍也是那段时间被反复验证出来的。

ELK 是 Elasticsearch、Logstash、Kibana 三个组件的组合,负责日志收集、存储检索和可视化分析;FELK 是在前面加一个 Filebeat 做轻量采集,把 Logstash 后移成处理层。它们要解决的核心问题就一个:把散落在几十上百台机器上的、格式各异的、每天几十上百 G 的文本,变成能秒级检索、能画图、能报警的结构化数据。这套东西适合谁?运维和 SRE 要做故障定位,后端开发想追一次请求的全链路,安全同学要留痕审计,甚至业务方想看接口耗时分布——只要你手上超过五台机器、日志还在靠tail -f和grep,那就该认真考虑这套方案了。下面我按采集、传输、处理、存储、可视化、排障的顺序,把每个环节的真实做法和踩过的坑掰开讲。

1. 先把架构讲透:ELK 与 FELK 差在哪一个字母

1.1 四个组件各自的职责边界

很多人上手就把四个组件全装上,配置文件抄一份,跑通了就完事。但真出问题的时候,你会发现根本不知道该去哪个环节看。所以第一步必须把职责切清楚,边界模糊的架构迟早会在半夜给你惊喜。

Elasticsearch是存储和检索引擎,本质是一个分布式的倒排索引数据库。它负责把文本切成词条建索引、按分片存储、提供近实时的查询能力。它的强项是检索,弱项是写入放大——每写一条数据要建索引、要刷盘、要同步副本,所以它天然不适合当"消息队列"用。

Logstash是数据处理管道,输入端接各种数据源,中间做解析、清洗、富化,输出端写往各种目的地。它真正的价值在中间的 filter 段:把一个2024-05-11 03:22:11 ERROR [order-service] traceId=abc123 ...的原始行,拆成 timestamp、level、service、traceId、message 五个独立字段。它的弱项是资源占用,JVM 起底就是几百兆到上 G 内存,装在业务机器上会被开发和运维一起骂。

Kibana是可视化前端,负责查询界面、图表、仪表盘和告警。它本身不存数据,所有查询都转发给 Elasticsearch,所以 Kibana 慢通常是 Elasticsearch 慢。

Filebeat是轻量采集器,Go 写的,常驻内存几十兆。它只干一件事:盯着日志文件,把新增的行读出来,加上元数据,发出去。它支持背压感知——下游不通就自己慢下来,不会把业务机器拖死。

理解了这四个东西的性格,架构选择就变成了一道简单的匹配题:谁重谁轻,谁该放在离数据源近的地方,谁该放在离存储近的地方。

1.2 为什么把 Logstash 往后挪一位

传统 ELK 的做法是把 Logstash 直接装在业务机器上,Filebeat 出现之后,主流做法变成了 FELK:业务机上只留 Filebeat,Logstash 集中部署在中间层。

这个调整的理由很实在。第一是资源隔离,Logstash 的 JVM 一旦 GC 抖动,业务进程跟着遭殃,我见过一次因为 Logstash 堆内存不足反复 Full GC,把同机的服务响应时间从 20ms 拖到 800ms。第二是配置管理,采集配置散在几十台机器上,改一个字段要全量推一遍;集中到 Logstash 之后,改一处就生效。第三是解耦,业务机器不需要知道日志最终去哪,它只负责发出去,后面接 Kafka 也好、直接进 Logstash 也好,对采集端是透明的。

代价也有:多了一跳网络传输,端到端延迟从秒级变成几秒级;Filebeat 的解析能力弱,复杂的多行合并、字段提取只能往后放。但对绝大多数场景来说,这点延迟换来的是稳定性和可维护性,非常划算。

需要说明的是,业内对 FELK 的字母展开并不完全统一,有的场景指 Filebeat 打头的这套链路,有的场景指的是 Fluentd 系的轻量采集方案,还有的把 Kafka 也算进去叫 FELK 的变体。你看到别人说 FELK 时,先问清楚他指的是采集端用谁,别急着对号入座。

1.3 架构选型的三个判断维度

不要迷信"业界最佳实践",选型的依据只能是你的实际约束。我一般从三个维度来判断。

判断维度小型场景(<20 台机器,<20GB/天)中型场景(20~200 台,20~200GB/天)大型场景(>200 台,>200GB/天)
采集端Filebeat 直发 LogstashFilebeat 发 KafkaFilebeat 发 Kafka,分区按业务划分
处理层单机 Logstash,2~4 核2~3 节点 Logstash 集群独立 Logstash 集群,按业务拆管道
缓冲层不需要,或磁盘队列兜底Kafka 3 节点起Kafka 集群 + 副本数 2~3
存储层单节点 ES,1 副本可省3 节点 ES,1 副本5 节点以上,冷热分层
典型痛點磁盘容易写满Logstash 成为瓶颈分片数失控、集群状态频繁抖动

小场景硬上 Kafka 是典型的过度设计,运维成本翻倍,收益几乎为零。大场景图省事直连,那 Kafka 存在的意义就是在流量峰值和 ES 抖动之间当缓冲垫,省掉它等于把风险全压给存储层。判断标准其实就一条:你的日志峰值写入速率和 ES 的稳定写入能力之间,差距有多大。差距小于两倍,磁盘队列能扛;差距大于五倍,老老实实上缓冲层。

2. 采集层设计:Filebeat 不是装上就完事

2.1 输入配置与多行日志合并的关键细节

Filebeat 的配置文件看着简单,但几个参数配错就会导致丢数据或者数据错乱。下面这份是我在生产环境跑了很久的模板,做了删减,只留关键项。

filebeat.inputs: - type: filestream id: app-java-logs paths: - /data/logs/*/app.log parsers: - ndjson: target: "" add_error_key: true - multiline: type: pattern pattern: '^\d{4}-\d{2}-\d{2}' negate: true match: after max_lines: 500 timeout: 5s fields: env: prod app: order-service log_type: application fields_under_root: true close.on_state_change.removed: true clean_removed: true processors: - add_host_metadata: when.not.contains.tags: forwarded - drop_fields: fields: ["agent", "ecs", "input", "host.architecture", "host.os"] ignore_missing: true output.kafka: hosts: ["10.0.0.11:9092", "10.0.0.12:9092", "10.0.0.13:9092"] topic: "logs-app" partition.hash: hash: ["app", "host.name"] required_acks: 1 compression: gzip max_message_bytes: 10485760

几个值得展开讲的点:

多行合并的negate: true+match: after组合。这条配置的意思是"不匹配^\d{4}-\d{2}-\d{2}这个正则的行,合并到上一行后面"。Java 的堆栈信息、Python 的 traceback 都是这种形态——首行有时间戳,后续行是缩进的异常详情。如果写反了变成negate: false,整个日志会被切得七零八落,你在 Kibana 里搜到的全是半截堆栈。

max_lines和timeout必须显式设置。默认值在某些版本里是 500 行,但如果你有超长堆栈,超出的部分会被当作新事件发出去。timeout更重要,它决定了"多久没等到新行就认为这条日志结束了",设太小会截断慢速输出的日志,设太大则最后一条日志迟迟不发出。5 秒是个经验值,日志密集的服务可以调到 2 秒。

clean_removed这个参数容易被忽略。日志轮转后旧文件被删,Filebeat 的注册表里还留着记录,时间长了注册表文件会膨胀到几百兆,启动变慢。打开它能自动清理。

提示:不要用type: log,那是老版本配置。filestream是新版推荐方式,对文件轮转和 inode 复用处理得更稳,特别是容器环境下同一个路径反复创建文件的情况。

2.2 字段设计与元数据规范

日志平台建设初期最容易犯的错,是把所有信息都塞进message一个字段里。等到要按服务维度聚合、按环境过滤、按 traceId 串联请求的时候,你才发现自己在用grep的思维做检索。

我的做法是在采集端就强制约定一套基础字段,作为所有日志的"身份证":

字段名类型说明是否必填
envkeyword环境标识,prod/staging/dev是
appkeyword应用名,用中划线命名是
host.namekeyword主机名或容器名是
log_typekeywordapplication/access/audit/gc是
levelkeyword日志级别,统一大写否
traceIdkeyword链路追踪 ID否
@timestampdate事件发生时间是

为什么这些字段都用keyword而不是text?因为text会被分词器切开,order-service可能被切成order和service两个词条,聚合的时候你按应用名分组会得到一堆碎片。keyword是整串存储,精确匹配和聚合都快。这条规则看似基础,但我见过太多人在 mapping 定型之后才发现聚合结果不对,只能重建索引。

还有一点:@timestamp必须用日志本身的时间,不能用采集时间。Filebeat 默认会用读到的时刻打时间戳,如果日志里自带时间,一定要用dateprocessor 或者 Logstash 的datefilter 覆盖掉。否则你补采历史日志的时候,所有数据的时间戳都是"现在",时序图完全错乱。

2.3 采集层的资源与背压控制

Filebeat 虽然轻,但在高吞吐场景下也需要调优。三个参数最关键:

queue.mem: events: 8192 flush.min_events: 512 flush.timeout: 1s output.kafka: bulk_max_size: 2048 worker: 4 max_procs: 2

queue.mem.events是内存队列长度,决定了下游卡顿时能缓多少事件。设太小,下游一抖就丢数据(filestream会停读,但如果注册表在队列清空前更新,还是有丢失风险);设太大,内存占用上去了,业务机器压力增加。8192 到 16384 是常见区间。

max_procs限制 Filebeat 能用几个 CPU 核,业务机器上一定要设,默认它会用满所有核抢 CPU。

关于背压,需要理解一个机制:Filebeat 是"至少一次"投递语义,它先把事件发出去,收到确认才更新注册表位置。所以下游不可用时它不会丢数据,而是停止读取文件。这意味着磁盘上的日志会持续堆积——这不是 Filebeat 的问题,而是下游容量不足的暴露。真正该做的是加监控,盯住filebeat_pipeline_events_retry_total和文件读取偏移量的差值,超过阈值就告警。

3. 传输与缓冲层:Kafka 到底要不要上

3.1 直连模式与缓冲模式的取舍

直连模式下,Filebeat 直接输出到 Logstash 的 beats 端口,链路短、延迟低、部署简单。适用条件是:日志量稳定、Logstash 有足够冗余、ES 集群健康。

但真实世界里日志量从来不稳定。一次批量任务、一次大促、一次容器集体重启,写入量瞬间翻十倍。直连模式下这个尖峰直接砸到 Logstash,Logstash 的管道队列满了之后开始拒绝连接,Filebeat 只能干等。而此时如果 Logstash 在做一次全量重启或者配置热加载,中间那几分钟的数据就全靠 Filebeat 的内存队列兜着,稍有不慎就是数据丢失。

Kafka 在这里扮演的角色不是"必须有的高端组件",而是一个削峰填谷的蓄水池。它有三个实打实的好处:解耦生产消费速度、支持多消费方(同一份日志既能进 ES 也能进离线数仓)、支持回溯(消费位点可以重置,重新处理历史数据)。

代价也明确:多一套集群要运维,多一层端到端延迟,多一个可能出故障的组件。如果团队里没人熟悉 Kafka 的调优和故障处理,上了它反而是增加了不确定性。

3.2 Kafka 主题与分区规划

决定上 Kafka 之后,主题设计会直接影响后续的扩展性。我推荐按日志类型分主题,而不是按应用分:

主题名分区数副本数保留时长说明
logs-app623 天应用日志
logs-access623 天网关/接入日志
logs-audit337 天审计日志,保留更久
logs-gc311 天GC 日志,量大价值低

按类型分的原因:不同类型日志的保留策略、处理复杂度、消费速度完全不同。审计日志要长期留存且不能丢,GC 日志量大但可以快速丢弃,混在一个主题里只能按最严格的标准来,浪费存储。

分区数的估算逻辑是:单分区消费吞吐上限大约 10MB/s(保守值),你的峰值写入如果是 60MB/s,那至少要 6 个分区。分区数只能增不能减,所以宁可从宽。但也别盲目开 50 个,分区多了会导致同时打开的文件句柄数暴增,而且分区数一旦超过消费端并行度就是浪费。

分区键的选择有个小技巧:用app+host.name做 hash。这样同一个应用同一台机器的日志有序,既保证了对同一份文件的内容不乱序,又能让消费端多实例并行。

3.3 容量估算的实际算法

很多人做容量规划靠拍脑袋,结果要么存储不够要么买多了浪费。给一套可以直接套用的算法。

假设条件:200 台业务机,每台每天产生 1GB 原始日志,副本数 1,保留 30 天。

第一步算日增量:200 × 1GB = 200GB/天。

第二步算压缩后体积。纯文本日志的压缩比通常在 0.15 到 0.25 之间,取 0.2:200 × 0.2 = 40GB/天。这个压缩比指的是 ES 的索引压缩(index.codec: best_compression),如果用默认的 LZ4 大概是 0.35 到 0.45。

第三步加副本:40 × 2 = 80GB/天(1 主 1 副)。

第四步算总容量:80 × 30 = 2400GB,约 2.4TB。

第五步留冗余,一般按 1.3 倍算:2400 × 1.3 ≈ 3.1TB。这是磁盘的裸容量需求,考虑到 ES 建议单节点磁盘使用率不超过 75%,实际采购要 4TB 以上。

分片数则按"单分片 30~50GB"来推算:2400 / 50 = 48个主分片。如果按天建索引保留 30 天,每天 2 个主分片就够了。这个数字看起来很保守,但分片过多是 ES 集群性能下降的头号原因——每个分片都是一个 Lucene 实例,都要占堆内存和文件句柄,1000 个分片的集群光维持状态同步就很吃力。

4. 处理层:Logstash 管道怎么写才不拖后腿

4.1 三段式管道的结构设计

Logstash 的配置文件分input、filter、output三段,看起来直白,但结构组织得好不好,直接决定了后期维护成本。我的习惯是拆文件:inputs.conf、filters-app.conf、filters-common.conf、outputs.conf,用path.config指向目录,Logstash 会按文件名顺序合并。

input { kafka { bootstrap_servers => "10.0.0.11:9092,10.0.0.12:9092" topics => ["logs-app"] group_id => "logstash-app" codec => json consumer_threads => 4 decorate_events => basic } } filter { # 时间解析:把日志里的时间字符串转成 @timestamp date { match => ["log_time", "yyyy-MM-dd HH:mm:ss.SSS", "ISO8601"] target => "@timestamp" tag_on_failure => ["_dateparsefailed"] } # 结构化解析 grok { match => { "message" => "%{TIMESTAMP_ISO8601:ts}\s+%{LOGLEVEL:level}\s+\[%{DATA:thread}\]\s+%{DATA:logger}\s+-\s+%{GREEDYDATA:msg}" } tag_on_failure => ["_grokparsefailure"] break_on_match => true } # 字段规范化 mutate { uppercase => { "level" => "" } remove_field => ["ts", "thread", "host", "agent", "ecs"] convert => { "cost_ms" => "integer" } } # 丢弃低价值日志,降低存储压力 if [level] == "DEBUG" and [env] == "prod" { drop {} } } output { elasticsearch { hosts => ["http://10.0.1.21:9200", "http://10.0.1.22:9200"] index => "logs-app-%{+YYYY.MM.dd}" user => "${ES_USER}" password => "${ES_PASSWORD}" manage_template => false pool_max => 1000 flush_size => 2000 idle_flush_time => 1 } }

specific细节里有两个关键点值得单独说。

一是consumer_threads要和 Kafka 分区数匹配。6 个分区配 4 个线程,会有 2 个分区分不到消费线程,出现"消费端看起来很闲但 lag 一直在涨"的假象。正确做法是线程数等于或略小于分区数,如果不够就扩 Logstash 实例,多个实例同属一个group_id会自动分摊分区。

二是flush_size和idle_flush_time的配合。flush_size: 2000表示攒够 2000 条批量写一次 ES,idle_flush_time: 1表示就算没攒够,1 秒也要写一次。前者影响吞吐(批量大效率高),后者影响延迟(批量大但流量低的时候数据会卡住)。低峰期日志量小,如果只设flush_size不设idle_flush_time,可能出现日志几分钟才出现在 Kibana 里。

4.2 Grok 解析的性能陷阱

Grok 是 Logstash 里最强大也最容易出问题的部分。它的原理是把%{PATTERN:field}展开成正则,GREEDYDATA会展开成.*,而.*在正则引擎里是贪婪匹配,遇到长字符串时回溯次数会爆炸。

一个典型反例:

grok { match => { "message" => "%{GREEDYDATA:prefix} %{GREEDYDATA:suffix}" } }

这条规则在遇到几 KB 的日志行时,正则引擎会尝试所有可能的分割点,CPU 直接打满。我见过一次因为改了这么一行,Logstash 的 CPU 从 30% 飙到 400%,Kafka 堆积到几百万条。

正确的做法是三条:

第一,尽量用具体模式代替DATA和GREEDYDATA。%{IP:client_ip}、%{NUMBER:cost:int}、%{WORD:method}这些都有明确的字符集约束,回溯空间小得多。

第二,把最常用的格式写在最前面,并用break_on_match => true。默认 Logstash 会把所有 match 都尝试一遍,加上这个参数后一旦匹配成功就停。

第三,用自定义正则精确定位。比如固定格式的日志,直接写锚定正则:

grok { match => { "message" => "^%{TIMESTAMP_ISO8601:ts}\s+%{LOGLEVEL:level}\s+\[%{DATA:thread}\]\s+%{NOTSPACE:logger}\s+-\s+%{GREEDYDATA:msg}$" } }

如果日志格式完全是自己控制的,最省事的方案是直接输出 JSON,codec => json一行解析完,比任何 grok 都快。这条建议看着朴素,但效果立竿见影——我让一个团队把日志改成 JSON 输出后,Logstash 节点从 3 台减到 1 台,CPU 还降了一半。

4.3 落库前的字段瘦身与索引模板

数据进 ES 之前还有两道处理要做,很多人会漏掉。

第一道是字段裁剪。Filebeat 默认会加一堆元数据:agent.ephemeral_id、agent.id、ecs.version、host.os.*、input.type……这些字段在检索时基本没用,但每个都要建索引、占存储。用mutate { remove_field => [...] }干掉,能省 10% 到 20% 的存储空间。

第二道是索引模板。不要在 Logstash 里用默认的动态映射,提前定义好模板:

{ "index_patterns": ["logs-app-*"], "template": { "settings": { "number_of_shards": 2, "number_of_replicas": 1, "index.refresh_interval": "30s", "index.codec": "best_compression", "index.mapping.total_fields.limit": 1000 }, "mappings": { "dynamic": "strict", "properties": { "@timestamp": { "type": "date" }, "message": { "type": "text", "fields": { "raw": { "type": "keyword", "ignore_above": 1024 } } }, "level": { "type": "keyword" }, "app": { "type": "keyword" }, "env": { "type": "keyword" }, "traceId": { "type": "keyword" }, "cost_ms": { "type": "long" } } } } }

dynamic: strict是最反直觉但最有价值的设置。它意味着任何不在 mapping 里的字段写入时直接报错,而不是自动猜测类型。这看起来很不友好,但它能逼着你在上线前把字段定义清楚。一旦开了dynamic: true(默认值),某个服务偶尔往日志里塞一个userId=12345字符串、另一个服务塞userId=12345数字,ES 就会因为类型冲突拒绝整批写入,而这个错误在 Logstash 日志里只是一行 warning,很容易被忽略。

index.refresh_interval: 30s也是必调项。默认 1 秒刷新一次,意味着每秒都要生成一个新的 segment,写入压力大的时候段合并根本跟不上。日志场景对实时性要求没那么高,30 秒完全够,写入吞吐能提升 30% 以上。

5. 存储与检索:索引设计决定集群寿命

5.1 分片规划与节点角色划分

分片数怎么定,前面给了估算方法,这里补充几个实操中必须注意的约束。

单分片大小控制在 10GB 到 50GB 之间。小于 10GB 说明分片太碎,元数据开销占比高;大于 50GB 会导致恢复慢、段合并压力大。日志场景如果是按天建索引,日增量小于 20GB 的应用,完全可以用周索引或者月索引,减少分片总数。

每个节点的分片数不要超过 20 × CPU 核数。这个经验公式的来源是 Lucene 的并发能力,超过之后查询会排长队。一台 16 核的节点,分片数上限大概 300 个。

节点分工要明确。中小规模至少分三类角色:master 节点(3 个,只负责集群状态,不做数据)、data 节点(存数据)、coordinating 节点(接收查询请求并汇总结果)。如果只有一个角色全混在一起,一次大查询就能把 master 拖到集群失去响应。

冷热分层是另一个值得早点做的设计。热节点用 SSD,存最近 7 天的索引,负责写入和高频查询;温节点用大容量 HDD,存 7 到 30 天的索引,只读不写;超过 30 天的直接删除或者归档到对象存储。一套 ILM 策略就能自动完成这件事:

{ "policy": { "phases": { "hot": { "min_age": "0ms", "actions": { "rollover": { "max_primary_shard_size": "40gb", "max_age": "1d" }, "set_priority": { "priority": 100 } } }, "warm": { "min_age": "7d", "actions": { "forcemerge": { "max_num_segments": 1 }, "shrink": { "number_of_shards": 1 }, "set_priority": { "priority": 50 } } }, "delete": { "min_age": "30d", "actions": { "delete": {} } } } } }

rollover配合max_primary_shard_size比按天滚动更科学,因为它根据实际大小决定何时切分,日志量忽大忽小时不会出现某些索引只有 1GB、某些索引 100GB 的畸形。forcemerge在进入温阶段时把段数合并到 1,能把存储压下来 20% 到 30%,查询也更快。注意forcemerge很耗 IO,一定要放在业务低峰期或者用限速。

5.2 索引命名的坑

索引命名看起来是小事,但踩过坑的都知道疼。

不要用%{+YYYY.MM.dd}这种纯日期加应用名的方式,因为同一应用的不同日志类型(应用日志、访问日志、GC 日志)会被写进同一个索引,字段类型冲突的概率大增。推荐格式是logs-{类型}-{应用}-{日期},例如logs-app-order-service-2024.05.11。

索引名不能包含大写字母,不能包含\ / * ? " < > |空格和逗号。这条规则看着无关紧要,但如果你用%{[app]}做索引名,恰好某个应用的app字段配置成了OrderService,整批数据就会写入失败。保险做法是索引名里只用固定前缀加日期,应用维度用字段过滤来区分。

还有一个容易忽略的点:索引名长度别超过 255 字节,而且过长的索引名会让某些 API 调用失败。曾经有人用完整的主机名加路径做索引名,结果集群的_cat/indices直接报错。

6. 可视化层:别把 Kibana 做成柱状图展览

6.1 从"能看"到"能定位"的仪表盘设计

我见过太多日志平台,仪表盘做得花里胡哨,饼图柱状图折线图应有尽有,但真正出故障的时候没人看。原因很简单:这些图回答不了"哪儿出了问题"。

一个能用的仪表盘应该按照排查路径来组织,我把它叫做"三级下钻"结构:

第一屏是全局健康度,回答"现在有没有异常"。只放四个东西:最近 24 小时各服务错误率排行(表格)、错误数量时间趋势(折线图)、当前告警数(数字)、日志写入速率(面积图)。判断标准是能否在 5 秒内看出"哪个服务在出错"。

第二屏是服务详情,从第一屏点击某个服务进来,回答"错在什么地方"。展示该服务的错误类型分布(按 message 聚合)、错误随时间的分布、涉及的接口或模块 Top 10。这层用Saved Search配合过滤条件是最灵活的。

第三屏是原始日志,从第二屏点击某个错误类型进来,展示具体日志行,只保留时间、level、traceId、message 四列,其余藏起来。这一屏的目标是让人能直接复制 traceId 去查全链路。

这套结构的关键是每一屏都只回答一个问题,不贪多。图表的数量控制在 6 到 8 个以内,超过之后人的注意力就散了。

字段设置上有个小技巧:把message字段的Discover展示高度调大,把level用颜色标记(ERROR 红色、WARN 黄色),把常用字段固定在左侧。这些设置在 Kibana 的 Index Pattern 里配,配一次全组受益。

6.2 该配哪些告警

告警不在多,在于准。日志平台的告警我分成三类:

告警类型触发条件通知渠道处理时限
链路自身健康采集延迟 > 5 分钟、Kafka 消费 lag > 100 万值班群15 分钟
业务错误突增5 分钟内 ERROR 数量 > 平峰 3 倍值班群 + 短信5 分钟
存储容量单节点磁盘使用率 > 80%、分片数 > 800值班群1 小时

第一类最容易被忽略但最重要。日志链路自己坏了没人发现,等到要查故障时才抓瞎。建议加一个独立的"探针"服务,每分钟往日志文件写一条带固定标记的记录,然后从 Kibana 查是否存在。这条记录端到端走完全链路,缺了就是链路断了,比任何内部指标都直观。

第二类告警要防止误报。直接按"ERROR 数量"做阈值会在发布、重启、批量任务时疯狂刷屏。我的做法是配置一个基线,取过去 7 天同一时段的中位数,超过中位数的 3 倍才触发,并且同一告警 10 分钟内只发一次。

7. 常见问题排查实录:踩过的坑才是真经验

7.1 数据不落库的排查链路

"日志在文件里明明有,Kibana 就是搜不到"——这是被问得最多的问题。我整理了一条固定的排查路径,从下游往上倒着查,最快能定位到 90% 的问题。

第一步,查 ES 索引。执行GET _cat/indices/logs-app-*?v&s=index:desc,看最新索引的docs.count是不是在涨。不涨说明数据根本没进来,往上游继续;在涨说明数据进来了,那可能是查询条件、时间范围或者 mapping 的问题——重点检查是不是message字段用了keyword类型导致搜不到分词内容。

第二步,查 Logstash 输出。GET _cat/thread_pool/write?v看write队列的 rejected 数,或者直接看 Logstash 日志里有没有Could not index event to Elasticsearch。常见的报错有两类:mapping 冲突(前面提到的字段类型打架)和illegal_argument_exception: Limit of total fields(字段数超限)。

第三步,查 Kafka 消费延迟。kafka-consumer-groups.sh --describe --group logstash-app看 LAG 列。lag 持续增长说明消费跟不上,要么加实例,要么检查是不是某条日志触发了 grok 性能问题。

第四步,查采集端。GET /_stats看 Filebeat 的output.events.acked和pipeline.events.published是否同步增长。看服务本身的监控页面或者日志,确认 Filebeat 进程还在。曾经有一次,Filebeat 因为所在主机磁盘 inode 耗尽,配置文件都读不出来,进程活着但什么也不做。

第五步,查日志文件本身。确认路径通配符有没有匹配到、文件权限对不对、有没有做日志轮转导致 Filebeat 跟丢了文件(表现为harvester数量异常)。

这条链路的关键是从下游往上游倒着走,因为每一层都能告诉你"我收到没有"。如果从上游往下查,你得逐层确认"发了"和"收到"两个动作,效率低一倍。

7.2 常见问题速查表

现象大概率原因处理方式
Kibana 时间范围选"最近 15 分钟"查不到数据,选"最近 24 小时"能查到@timestamp用的是采集时间而非日志时间,或者时区没处理检查 Logstash 的datefilter,确认时区配置
数据量突然翻倍日志被重复采集,常见于容器重启后 inode 复用检查 Filebeat 注册表,确认filebeat.registry.path是否持久化
搜索一个 ID 搜不到,但明明日志里有字段类型是text,被分词器切碎了改用keyword类型,或用field.raw子字段查询
ES 写入变慢、CPU 高refresh 太频繁、分片太多、字段太多调refresh_interval,合并小索引,清理无用字段
集群状态变黄且一直不恢复有副本分片未分配,通常是节点数少于副本数 + 1增加数据节点,或临时调低副本数
Logstash 内存持续上涨直到 OOM管道队列积压,或者某个 filter 有内存泄漏检查pipeline.batch.size,排查正则和translatefilter
Kibana 打开很慢默认时间范围太大,或者在跨大量分片做聚合缩小默认时间范围,避免对高基数字段做 terms 聚合

7.3 几条用血换来的避坑心得

不要在日志里打敏感信息。密码、令牌、完整的手机号和身份证号,一律脱敏。这件事的重要性不用强调,但很多人是在数据已经躺在索引里几个月之后才发现。在 Logstash 里加一条mutate { gsub => ["message", "(?i)password=\S+", "password=***"] },成本极低。

日志平台也要做容量演练。我做过一次"日志洪峰"演练,故意让某个服务循环打日志,结果两小时内就把 ES 磁盘写到 90%,触发了只读锁。磁盘写满后的 ES 会进入read_only_allow_delete状态,需要手动解除,慌乱之下很容易操作失误。所以磁盘水位告警一定要提前,80% 就该动手了。

保留策略别一刀切。我见过有人所有索引都保留 90 天,结果 GC 日志占了 60% 的存储。不同类型日志的价值密度差别巨大,审计日志留一年都不多,DEBUG 日志留三天都嫌久。按类型配置不同的 ILM 策略,能省出一半硬件成本。

升级前先在沙箱跑一遍全链路。ES、Logstash、Kibana 的版本兼容性有严格约束,客户端版本和集群版本差太多会报各种诡异的错。而且升级 ES 大版本往往涉及索引格式变更,旧索引可能读不了。沙箱验证的内容包括:现有索引模板能否导入、现有管道能否启动、现有查询能否返回正确结果、仪表盘是否正常渲染。这套检查清单我列过一版,四十分钟能跑完,比在生产上发现问题再回滚强太多。

我个人在实际操作中的体会是,日志平台的价值不在于组件装了多少、图做得有多漂亮,而在于出问题时你有多信任它。如果每次故障你都怀疑"是不是日志又漏了",那这套系统就没起到作用。把链路的每一段都加上独立的健康检查,让数据从采集到展示全程可验证,比任何花哨的功能都值钱。另外补充一个小技巧:给每条日志的采集端固定写入一个带实例编号的source_id字段,排查时能一眼看出是哪台机器哪条链路产生的数据,比翻注册表快得多。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询