ELK vs EFK 日志系统选型、搭建与调优实战指南
日志系统这玩意儿没出事的时候谁都不在意一出事就是救命的稻草。我见过太多团队平时日志打得稀里哗啦真到线上故障要定位的时候翻服务器翻到手抽筋最后发现关键日志早就被滚动覆盖了。ELK 和 EFK 这两套方案本质上就是来解决这个问题的——把散落在几十上百台机器上的日志集中收上来存好、索引好让你能在几秒钟内从海量日志里捞出你要的那一条。这篇文章我会把这两套栈从选型、搭建、调优到踩坑完整地讲一遍不管你是刚接触日志系统的新手还是已经用过但总觉得哪里不对劲的老手应该都能从里面找到点有用的东西。1. 先把选型这件事聊透ELK 和 EFK 到底差在哪1.1 两套栈的组成与核心差异很多人一开始就纠结选哪个其实这个问题的答案取决于你的日志采集端是什么形态。先把两套栈的组成摆清楚对比维度ELKEFK采集端LogstashFluentd / Fluent Bit存储与索引ElasticsearchElasticsearch可视化KibanaKibana资源占用较高JVM较低Fluent Bit 是 C 写的插件生态丰富但配置较重丰富且配置轻量典型场景传统虚拟机、固定日志路径容器化、Kubernetes 环境核心差异就在采集端。Logstash 是 Elastic 自家出的功能全、插件多但它是 JVM 应用光启动就要吃掉几百 MB 内存跑起来对 CPU 也有一定要求。Fluentd 是 CNCF 旗下的项目用 Ruby 写的资源占用比 Logstash 好一些而 Fluent Bit 更是用 C 写的内存占用可以压到几 MB 级别特别适合在容器里作为 DaemonSet 跑。我个人的经验是如果你的日志源是传统的物理机或虚拟机日志文件路径固定团队又熟悉 Logstash 的配置语法那 ELK 上手最快。如果你是在 Kubernetes 里跑业务Pod 随时创建销毁日志走 stdout那 EFK尤其是 Fluent Bit 替代 Fluentd几乎是默认答案。1.2 为什么采集端的选择比你想的重要采集端决定了日志从产生到进入 Elasticsearch 之间的整条链路质量。这里有几个关键考量背压处理能力。当日志量突然暴涨Elasticsearch 写入跟不上时采集端能不能扛住Logstash 有持久化队列PQ机制Fluentd 有 buffer 配置Fluent Bit 也有内存和文件两种缓冲模式。如果采集端没有缓冲日志就会丢而且是悄无声息地丢。解析能力的位置。日志的解析比如把一行 Nginx 日志拆成字段可以在采集端做也可以在 Elasticsearch 的 Ingest Pipeline 里做。放在采集端做的好处是减轻 ES 压力坏处是采集端配置会变得复杂而且一旦解析规则要改得重启所有采集端。放在 ES 端做的好处是规则集中管理坏处是 ES 的 CPU 会吃紧。资源隔离。采集端和业务进程跑在同一台机器上如果采集端吃太多资源会影响业务。这就是为什么 Fluent Bit 在容器场景下这么受欢迎——它足够轻不会跟业务抢资源。1.3 一个容易被忽略的选型因素团队技能栈技术选型从来不只是技术问题。我见过一个团队运维全是搞 Java 出身对 JVM 调优门儿清那 Logstash 对他们来说就是舒适区。另一个团队全是 Go 和 Python 背景对 Ruby 生态不熟Fluentd 的插件调试起来就很痛苦。所以选型的时候除了看技术指标也要看团队能不能 hold 住。一套你调不动的系统再先进也是白搭。2. 搭建前的环境规划与资源估算2.1 集群规模怎么定在动手之前先算一笔账。假设你每天产生 100GB 的原始日志保留 30 天副本数为 1那么需要的存储空间大约是100GB × 30天 × (1 1) 6000GB 6TB这还没算索引开销。Elasticsearch 的索引本身有 overhead实际占用通常是原始日志的 1.1 到 1.3 倍。所以保守估计要准备 7TB 左右的磁盘。节点数量方面我一般建议至少 3 个节点起步原因有两个一是副本分片需要分配到不同节点上单节点集群副本无法分配集群状态会一直是 yellow二是 3 个节点可以容忍 1 个节点故障而不丢失数据。内存方面Elasticsearch 的堆内存建议设置为物理内存的一半但不要超过 31GB这是 JVM 压缩指针的阈值超过这个值指针压缩失效反而浪费内存。比如一台 64GB 的机器堆内存设 31GB剩下的留给文件系统缓存这对 ES 的查询性能至关重要。2.2 操作系统层面的调优这一步很多人会跳过然后后面遇到各种莫名其妙的性能问题。以下是我每次部署都会做的系统调优# 1. 关闭 swapES 极度依赖内存swap 会导致性能断崖式下跌 swapoff -a # 永久关闭注释掉 /etc/fstab 里的 swap 行 # 2. 调整虚拟内存映射数ES 会创建大量 mmap sysctl -w vm.max_map_count262144 # 写入 /etc/sysctl.conf 永久生效 # 3. 调整文件描述符限制 ulimit -n 65536 # 写入 /etc/security/limits.conf # 4. 调整线程数限制 ulimit -u 4096注意vm.max_map_count这个参数如果没调ES 启动时会直接报错退出错误信息是 max virtual memory areas vm.max_map_count [65530] is too low。这个坑我踩过不止一次每次在新机器上部署都要检查一遍。2.3 网络与时钟同步集群节点之间的时钟必须同步否则日志的时间戳会乱排查问题时能把人逼疯。用 NTP 或者 chrony 都行确保所有节点时间偏差在毫秒级以内。网络方面ES 节点之间的通信对延迟比较敏感尽量部署在同一个局域网内。如果跨机房部署要考虑专线或者高质量的内网连接否则集群状态同步会很慢容易出现脑裂。3. ELK 栈的完整搭建过程3.1 Elasticsearch 集群部署我以 3 节点集群为例操作系统是常见的 Linux 发行版。先下载和解压# 下载版本号根据实际情况选择 wget https://artifacts.elastic.co/downloads/elasticsearch/elasticsearch-8.x.x-linux-x86_64.tar.gz tar -xzf elasticsearch-8.x.x-linux-x86_64.tar.gz mv elasticsearch-8.x.x /usr/local/elasticsearch然后修改配置文件config/elasticsearch.ymlcluster.name: my-log-cluster node.name: node-1 path.data: /data/elasticsearch/data path.logs: /data/elasticsearch/logs network.host: 0.0.0.0 http.port: 9200 transport.port: 9300 discovery.seed_hosts: [10.0.0.1:9300, 10.0.0.2:9300, 10.0.0.3:9300] cluster.initial_master_nodes: [node-1, node-2, node-3]几个关键点解释一下path.data一定要指向独立的磁盘不要和系统盘混用。ES 的写入是 IO 密集型的系统盘被写满会导致整个节点挂掉。network.host设为0.0.0.0让 ES 监听所有网卡生产环境要配合防火墙规则限制访问来源。cluster.initial_master_nodes只在集群首次启动时使用集群形成后要删掉这行配置否则重启时可能出问题。JVM 配置在config/jvm.options里我一般单独建一个jvm.options.d/heap.options文件-Xms31g -Xmx31gXms 和 Xmx 设成一样的值避免运行时堆伸缩带来的性能抖动。启动 ES 不能用 root 用户需要先创建专用用户useradd elasticsearch chown -R elasticsearch:elasticsearch /usr/local/elasticsearch chown -R elasticsearch:elasticsearch /data/elasticsearch su - elasticsearch -c /usr/local/elasticsearch/bin/elasticsearch -d三个节点都启动后验证集群状态curl -X GET localhost:9200/_cluster/health?pretty看到status: green就说明集群正常了。如果是 yellow检查副本分片是否因为节点数不够而无法分配。3.2 Logstash 的配置与管道设计Logstash 的配置文件分三部分input、filter、output。我以一个采集 Nginx 访问日志的场景为例input { file { path /var/log/nginx/access.log start_position beginning sincedb_path /dev/null codec plain { charset UTF-8 } } } filter { grok { match { message %{IPORHOST:client_ip} - - \[%{HTTPDATE:timestamp}\] \%{WORD:method} %{DATA:request} HTTP/%{NUMBER:http_version}\ %{NUMBER:status} %{NUMBER:body_bytes_sent} \%{DATA:referrer}\ \%{DATA:user_agent}\ } } date { match [timestamp, dd/MMM/yyyy:HH:mm:ss Z] target timestamp } mutate { convert { status integer body_bytes_sent integer } remove_field [message, timestamp] } } output { elasticsearch { hosts [http://10.0.0.1:9200, http://10.0.0.2:9200, http://10.0.0.3:9200] index nginx-access-%{YYYY.MM.dd} user elastic password your_password } }这里有几个实操要点sincedb_path /dev/null只在测试时用生产环境不要这么配否则每次重启都会从头读文件导致日志重复。grok 正则的调试很痛苦建议用 Kibana 自带的 Grok Debugger 先调好再放到配置文件里。我一般会把复杂的 grok 拆成多个 match用break_on_match false让它们依次匹配这样比写一个巨大的正则好维护。datefilter 的作用是把日志里的时间戳替换掉timestamp否则 ES 里记录的是 Logstash 收到日志的时间而不是日志实际产生的时间。这个区别在排查历史问题时非常关键。3.3 Kibana 的部署与基础配置Kibana 的部署相对简单解压后改config/kibana.ymlserver.port: 5601 server.host: 0.0.0.0 elasticsearch.hosts: [http://10.0.0.1:9200, http://10.0.0.2:9200, http://10.0.0.3:9200] kibana.index: .kibana i18n.locale: zh-CNi18n.locale设为zh-CN可以启用中文界面对团队里英文不太好的同学很友好。启动后访问http://your-host:5601第一次进去要创建 Index Pattern比如nginx-access-*然后就可以在 Discover 里查日志了。提示Kibana 本身不存储数据它只是 ES 的前端。所以 Kibana 挂了不影响日志采集和存储重启即可恢复。4. EFK 栈在容器环境下的落地实践4.1 Fluent Bit 作为 DaemonSet 的部署在 Kubernetes 环境下Fluent Bit 通常以 DaemonSet 的形式部署每个节点跑一个 Pod采集该节点上所有容器的日志。核心配置通过 ConfigMap 管理apiVersion: v1 kind: ConfigMap metadata: name: fluent-bit-config namespace: logging data: fluent-bit.conf: | [SERVICE] Flush 5 Daemon Off Log_Level info Parsers_File parsers.conf [INPUT] Name tail Path /var/log/containers/*.log Parser docker Tag kube.* Refresh_Interval 5 Mem_Buf_Limit 50MB Skip_Long_Lines On [FILTER] Name kubernetes Match kube.* Kube_URL https://kubernetes.default.svc:443 Kube_CA_File /var/run/secrets/kubernetes.io/serviceaccount/ca.crt Kube_Token_File /var/run/secrets/kubernetes.io/serviceaccount/token Merge_Log On Keep_Log Off K8S-Logging.Parser On K8S-Logging.Exclude On [OUTPUT] Name es Match * Host 10.0.0.1 Port 9200 Index k8s-logs Type _doc Logstash_Format On Logstash_Prefix k8s-logs Retry_Limit 5Mem_Buf_Limit这个参数很关键它限制了 Fluent Bit 的内存缓冲上限。当日志量暴涨时超过这个限制的日志会被丢弃但至少不会把节点的内存吃爆。这个值要根据节点的内存情况来定我一般设 50MB 到 100MB。Merge_Log On会把容器 stdout 里的 JSON 日志自动解析成字段省去了在 ES 端做解析的麻烦。4.2 多行日志的处理Java 应用的异常堆栈是多行的如果不做处理每一行都会变成一条独立的日志查起来非常痛苦。Fluent Bit 支持多行解析[PARSER] Name java_multiline Format regex Regex ^(?time\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}.\d{3}) Time_Key time Time_Format %Y-%m-%d %H:%M:%S.%L [INPUT] Name tail Path /var/log/containers/*.log Parser java_multiline Tag kube.* Multiline.Parser java_multiline多行解析的原理是如果一行匹配了指定的正则比如以时间戳开头就认为是一条新日志的开始否则就追加到上一条日志后面。这个配置对 Java 应用的日志采集几乎是必须的。4.3 与 ELK 方案的性能对比实测我在一个测试环境里做过对比同样的日志量大约每秒 5000 条同样的硬件配置指标LogstashFluent Bit内存占用约 1.2GB约 15MBCPU 占用约 40%约 8%日志延迟2-3 秒1-2 秒配置复杂度中等较低这个数据不是绝对的跟具体配置和日志内容有关但趋势很明显Fluent Bit 在资源效率上有压倒性优势。当然Logstash 的插件生态和解析能力更强如果你的日志格式特别复杂需要大量 grok 解析Logstash 可能更合适。5. 索引管理与性能调优5.1 索引生命周期管理ILM日志数据的特点是热数据少冷数据多。最近几天的日志查得最频繁一个月前的日志基本没人看。ILM 就是根据这个规律自动管理索引的{ policy: { phases: { hot: { actions: { rollover: { max_size: 50gb, max_age: 1d } } }, warm: { min_age: 7d, actions: { forcemerge: { max_num_segments: 1 }, shrink: { number_of_shards: 1 } } }, delete: { min_age: 30d, actions: { delete: {} } } } } }这个策略的意思是索引达到 50GB 或 1 天就滚动新建7 天后进入 warm 阶段做 force merge 和 shrink 减少分片数30 天后自动删除。force merge 这个操作很重会消耗大量 IO建议放在业务低峰期执行。shrink 可以减少分片数量降低集群的元数据压力。5.2 分片数量的计算分片数量不是越多越好。每个分片都是一个 Lucene 索引有固定的内存和文件句柄开销。经验法则是单个分片的大小控制在 10GB 到 50GB 之间。假设你每天产生 100GB 日志保留 30 天那么总分片数 100GB × 30天 / 30GB每分片 ≈ 100 个分片3 个节点的话每个节点大约 33 个分片。这个数量是合理的。如果分片太多比如几千个集群的 master 节点会不堪重负集群状态更新变慢。5.3 写入性能调优的几个关键参数# elasticsearch.yml 中的关键参数 indices.memory.index_buffer_size: 20% thread_pool.write.queue_size: 1000index_buffer_size默认是 10%对于写入密集型的日志集群可以调到 20%。这个缓冲区越大ES 在写入时合并段的频率越低写入吞吐越高。thread_pool.write.queue_size控制写入队列的长度。队列满了之后写入请求会被拒绝。适当调大可以吸收突发流量但也不能太大否则请求在队列里等太久客户端会超时。还有一个重要的参数是refresh_interval。默认是 1 秒意味着每秒都会生成一个新的 segment这对写入性能影响很大。对于日志场景可以设为 30 秒甚至更长curl -X PUT localhost:9200/k8s-logs-*/_settings -H Content-Type: application/json -d { index: { refresh_interval: 30s } }代价是日志写入后最多 30 秒才能被搜索到。对于大部分日志场景这个延迟是可以接受的。6. 常见问题与排查技巧实录6.1 日志丢失的排查思路日志丢失是最让人头疼的问题因为它是静默失败。排查的时候按链路逐段检查排查环节检查方法常见原因采集端查看采集器日志和缓冲队列缓冲满、文件句柄不足传输链路检查网络连通性和延迟网络抖动、防火墙拦截ES 写入查看 ES 的 rejected 指标写入队列满、磁盘水位高索引状态检查索引是否只读磁盘超过水位线ES 有一个磁盘水位线机制当磁盘使用率超过 85% 时ES 会将索引设为只读不再接受写入。这个阈值可以通过cluster.routing.allocation.disk.watermark.low/high/flood_stage调整但根本解决办法还是加磁盘或者缩短日志保留时间。6.2 查询慢的优化手段Kibana 里查日志转圈圈通常有几个原因时间范围太大。查最近 30 天的日志和查最近 1 小时的日志性能差几个数量级。养成先缩小时间范围再查的习惯。通配符查询。message:*error*这种查询会扫描所有文档非常慢。尽量用结构化字段查询比如status:500。分片过多。前面说过分片太多会导致查询时需要合并的结果集变大。通过 ILM 的 shrink 操作可以减少历史索引的分片数。字段数据过大。如果对message这种高基数字段做聚合会消耗大量内存。可以在 mapping 里设置fielddata: false禁止对特定字段做聚合。6.3 集群变红的紧急处理集群状态变红意味着有主分片未分配数据可能丢失。紧急处理步骤# 1. 查看未分配的分片 curl -X GET localhost:9200/_cat/shards?vhindex,shard,prirep,state,unassigned.reason | grep UNASSIGNED # 2. 查看分配失败的具体原因 curl -X GET localhost:9200/_cluster/allocation/explain?pretty # 3. 如果是磁盘水位问题先清理磁盘或调整水位线 curl -X PUT localhost:9200/_cluster/settings -H Content-Type: application/json -d { transient: { cluster.routing.allocation.disk.watermark.low: 90%, cluster.routing.allocation.disk.watermark.high: 95% } } # 4. 尝试重新分配 curl -X POST localhost:9200/_cluster/reroute?retry_failedtrue注意调整水位线只是临时手段根本解决还是要扩容或者清理数据。长期把水位线设得很高磁盘写满后 ES 会直接挂掉后果更严重。6.4 我踩过的几个印象深刻的坑坑一时区问题导致日志时间错乱。Logstash 的 date filter 解析时间戳时如果日志里没有时区信息Logstash 会按 UTC 解析导致 ES 里的时间比实际时间差 8 小时。解决办法是在 date filter 里明确指定时区或者在 grok 里把时区信息也解析出来。坑二grok 正则性能问题。一个写得不好的 grok 正则能让 Logstash 的 CPU 直接跑满。特别是用了.*这种贪婪匹配遇到长日志行会回溯到天荒地老。尽量用%{DATA}代替.*用%{GREEDYDATA}代替.*的多行匹配。坑三ES 的 mapping 爆炸。如果日志里有动态字段比如 JSON 日志里的自定义 keyES 会自动为每个新字段创建 mapping。字段太多会导致集群元数据膨胀严重时 master 节点会 OOM。解决办法是开启 dynamic mapping 的限制或者用 index template 预先定义好字段类型。坑四Kibana 的 saved objects 冲突。多人同时编辑同一个 dashboard 时后保存的会覆盖先保存的。建议用 Git 管理 Kibana 的 saved objects通过 API 导入导出避免手动编辑。7. 安全加固与日常运维7.1 开启认证与授权默认情况下 ES 是不需要密码的这在生产环境是绝对不能接受的。开启 X-Pack 的基础认证xpack.security.enabled: true xpack.security.transport.ssl.enabled: true然后设置内置用户的密码bin/elasticsearch-setup-passwords interactive对于更细粒度的权限控制可以创建角色和用户限制只能访问特定的索引。比如给开发团队创建一个只能读nginx-access-*索引的角色。7.2 监控告警的配置ES 自身提供了监控 API可以采集集群的健康指标# 集群健康 curl -X GET localhost:9200/_cluster/health?pretty # 节点统计 curl -X GET localhost:9200/_nodes/stats?pretty # 索引统计 curl -X GET localhost:9200/_stats?pretty关键监控指标包括集群状态green/yellow/red、JVM 堆使用率、磁盘使用率、写入拒绝数、查询延迟。这些指标可以接入现有的监控系统设置阈值告警。我一般会设置这几条告警规则集群状态非 green 持续 5 分钟、JVM 堆使用率超过 85%、磁盘使用率超过 80%、写入拒绝数在 5 分钟内超过 100。7.3 备份与恢复策略日志数据虽然不像业务数据那么关键但有时候也需要恢复历史数据。ES 提供了 snapshot API# 注册快照仓库以共享文件系统为例 curl -X PUT localhost:9200/_snapshot/my_backup -H Content-Type: application/json -d { type: fs, settings: { location: /mnt/backups/elasticsearch } } # 创建快照 curl -X PUT localhost:9200/_snapshot/my_backup/snapshot_20240101?wait_for_completiontrue快照是增量备份第一次全量后续只备份变化的部分。建议每天凌晨执行一次保留最近 7 天的快照。8. 从单集群到跨机房架构演进的一些思考当业务规模继续增长单集群可能不够用了。这时候有几个方向可以考虑冷热分离架构。用高配机器做热节点SSD 存储承载最近的日志用低配大容量机器做冷节点HDD 存储承载历史日志。通过 ILM 自动在冷热节点之间迁移数据。这个方案能显著降低成本因为大部分日志查询都集中在最近几天。跨集群搜索。如果日志按业务线拆成了多个集群可以用 Elasticsearch 的跨集群搜索功能在一个 Kibana 里查询多个集群的数据。配置方式是在 Kibana 所在的集群里注册远程集群cluster.remote.remote_cluster_1.seeds: [10.0.1.1:9300, 10.0.1.2:9300]然后在查询时指定remote_cluster_1:index_name即可。采集端的水平扩展。当单台采集机扛不住时可以部署多个采集实例用消息队列比如 Kafka做缓冲。采集端只负责把日志写入 Kafka另一组消费者从 Kafka 读取并写入 ES。这个架构的好处是采集和写入解耦任何一端出问题都不会导致日志丢失。这套架构的复杂度明显上升运维成本也更高。我的建议是除非单集群真的扛不住了否则不要过早引入 Kafka。很多团队日志量其实没那么大单集群完全够用上 Kafka 反而增加了故障点。日志系统的建设是一个持续迭代的过程没有一步到位的方案。先把基础链路跑通确保日志不丢、能查然后再逐步优化性能、完善告警、控制成本。我在实际运维中发现大部分问题都不是架构层面的而是配置细节没做到位——一个参数没调对可能就会导致性能差好几倍。所以每次调整配置后都要用真实的查询场景验证效果别只看监控面板上的数字。