2. 先想清楚再动手:EFK到底解决什么问题
聊到日志监控,很多团队的第一反应是“先上一套方案再说”,结果往往是ELK全家桶堆上去之后,发现排查问题依然靠猜,告警一片噪音,最后连日志平台本身都成了事故现场。我这次搭建EFK日志级别监控系统,最想解决的核心问题其实很简单:把散落在各台机器上的日志,变成统一、可检索、能告警的实时数据流。EFK里的E是Elasticsearch,F是Filebeat,K是Kibana,相比传统的ELK方案,把Logstash换成轻量级的Beats,接入成本低很多,资源占用小,适合中小规模集群的日志采集场景。
适合谁来参考?如果你手里有十几台到几十台服务器,每天产生几十GB的日志,想在不引入复杂MQ和流处理框架的前提下,快速搭建一套“能搜、能看、能报警”的日志平台,这篇内容基本就是照着抄的作业。如果你已经用过ELK,想搞清楚Filebeat和Logstash的分工差异,或者想在Kibana里做日志级别的告警规则,这里也会讲清楚几个关键配置背后“为什么要这样写”的逻辑。
我这次搭建的目标环境是这样的:三台CentOS 7.9的服务器,一台部署Elasticsearch负责存储和索引,一台部署Kibana提供可视化页面,另外几十台业务服务器统一通过Filebeat把日志推到Elasticsearch。整个链路不经过Logstash,因为日志格式是应用自己打的JSON,不需要复杂清洗,Filebeat自带的简单处理已经够用。
刚开始接触EFK的人容易陷入一个误区:拼命调Elasticsearch的堆内存、线程池,却忽略了最上游的Filebeat采集配置。实际上,日志监控系统的瓶颈往往出现在采集端——文件轮转没处理好、多行日志被拆散、字段类型映射错乱,这些问题在Kibana里都会变成“搜不到、看不了、统计不对”的怪现象。所以这篇分享里,我会把重点放在采集端的细节和告警规则的设计上,存储端只讲稳定可用的配置。
2. 总体架构和工具选型背后的考量
2.1 为什么是Filebeat而不是Logstash
ELK这个名字大家听得多了,但EFK的区别在于中间那层用的是Filebeat而不是Logstash。Logstash功能强大,支持各种输入输出插件、复杂的过滤语法、还有自己的队列缓冲机制,但随之而来的是JVM内存开销大、配置写法繁琐、管道排错困难。Filebeat是基于Go写的轻量级agent,安装包很小,部署时不需要依赖Java环境,c采集日志时能自动处理文件轮转、多行合并、读取位置记录这些头疼事。
我这次选择Filebeat还有一个直接原因:业务日志是JSON格式,字段结构清晰,不需要正则解析,不需要GeoIP查询,也不需要“if字段A等于xxx就拆分到字段B”这种动态逻辑。Filebeat自带的json.keys_under_root和add_fields配置足够满足需求。如果将来有需求要对接Kafka或者做复杂的ETL,再在Filebeat后面挂Logstash也来得及,Elasticsearch的索引结构不受影响。
下表是我在选型时做的简单对比,可以参考一下:
| 对比项 | Filebeat | Logstash |
|---|---|---|
| 资源占用 | 约30~50MB内存,CPU占用低 | 默认堆内存常在1GB以上,管道多时更高 |
| 部署依赖 | 无需Java,单个二进制文件 | 依赖JVM,需要额外安装配置 |
| 采集文件轮转 | 内置,自动跟踪inode | 需要额外配置input插件参数 |
| 多行日志合并 | 支持multiline插件,配置简单 | 支持,但语法相对复杂 |
| 数据处理能力 | 弱,适合轻量清洗 | 强,适合复杂ETL |
| 队列缓冲 | 内置内存队列,可配合kafka使用 | 自带持久化队列 |
这里把Filebeat定义为“采集器”而不是“处理器”,这个定位很重要。在监控场景下,我们更关心日志是否被可靠地送达Elasticsearch,而不是在中间环节做花哨的转换。把加工逻辑尽量前置到应用端,让采集链路保持简单,出了问题也容易定位。
2.2 Elasticsearch和Kibana的版本匹配问题
EFK最大的坑其实不在配置本身,而在版本之间互相不兼容。Elasticsearch、Kibana、Filebeat三者的版本号必须完全一致或保持同一大版本,否则可能出现Kibana连不上ES、Filebeat索引模板映射失败这类诡异问题。我这边的约定是全部采用7.10.2版本,当时选这个版本是因为它对硬件要求相对友好,API也足够成熟。
有人会问,为什么不用8.x?8.x默认开启安全认证,从配置安全性的角度来说是好事情,但对内网环境来说,为了图省事,关闭安全认证能减少不少证书配置的麻烦。7.10.2在新版本里已经不算新了,但在稳定性上经历了大量生产验证,相关问题的排查文章也最多,踩坑容易找解决方案。
具体到部署方式,我这里用的是RPM包而不是Docker容器。原因是在没有容器编排的情况下,RPM安装的系统服务管理更直接,systemctl restart命令可控性高,日志输出到journald也方便排查。如果你熟悉docker-compose,用容器方式部署Elasticsearch其实也完全可以,但注意挂载数据卷,否则容器重建后数据就丢了。
2.3 监控什么日志级别的信息
标题里提到“日志级别监控”,这是整个系统的灵魂。日志级别分开来讲有Debug、Info、Warn、Error、Fatal几类,但在监控场景下,我们需要做到的不只是把日志接入平台,更要形成“错误及时发现、警告趋势预警、访问统计可视化”三个层次。
我在设计时把采集对象分成两类:
- 业务日志:JSON格式,包含level、timestamp、service、message等字段,重点监控level=error和level=warn的日志。
- 访问日志:Nginx或网关的access.log,重点统计HTTP状态码、响应时间、请求频率。
这两类日志的监控指标不同。业务日志更关注“有没有报错”,访问日志更关注“请求量的波动”和“接口响应时间”。Filebeat可以同时配置多个输入,每个输入独立指定日志路径、字段标签和索引名称,互不干扰。
2. 核心细节解析与实操要点
2.1 Filebeat配置的完整示例与逐行讲解
Filebeat的核心配置写在/etc/filebeat/filebeat.yml里。直接贴一份我实际使用的精简配置,再逐行拆解。
filebeat.inputs: - type: log enabled: true paths: - /data/applogs/*/*.log json.keys_under_root: true json.add_error_key: true fields: log_type: business fields_under_root: true tags: ["app"] - type: log enabled: true paths: - /var/log/nginx/access.log fields: log_type: nginx_access fields_under_root: true output.elasticsearch: hosts: ["192.168.10.10:9200"] index: "filebeat-%{[log_type]}-%{+yyyy.MM.dd}" pipeline: geoip-info setup.template.enabled: true setup.template.name: "filebeat" setup.template.pattern: "filebeat-*"这段配置里有几个值得讲透的关键点。
第一个是json.keys_under_root。当初我拿到业务日志时才发现,如果不开这个选项,Filebeat会把整个JSON日志当成一个message字符串塞进去,Kibana里只能搜到一长串原文,内部字段根本没法做聚合分析。开了keys_under_root: true之后,JSON里的每个key才会提升为文档的顶层字段,比如log里的level字段就能直接用level: error来精确查询。
第二个是fields和fields_under_root的搭配。我在这里给不同来源的日志打上了不同的log_type标签,目的是在Elasticsearch里按来源区分索引。比如business日志写入filebeat-business-2024.05.01,Nginx日志写入filebeat-nginx_access-2024.05.01,在Kibana里创建索引模式时直接按filebeat-*通配符匹配,再通过log_type字段过滤不同来源的数据。
第三个容易踩坑的点是setup.template.enabled。如果不开模板管理,Elasticsearch会自动根据第一条日志的字段类型动态生成映射。日志里的时间戳字段很容易被映射成text类型,导致后面做时间范围聚合时报错。开启模板后,Filebeat会在首次连接时尝试创建索引模板,告诉ES“这些字段应该是什么类型”。
2.2 多行日志合并:Java异常栈是最大的坑
Java应用日志里经常出现多行异常栈,比如:
2025-04-01 10:00:01 ERROR [http-nio-8080-exec-1] - NullPointerException occurred. java.lang.NullPointerException: null at com.example.service.UserService.getUser(UserService.java:120) at com.example.controller.UserController.detail(UserController.java:50)如果Filebeat默认按行读取,这个异常栈就会被切成三四个独立的文档,搜一条报错只能搜到第一行,后面的堆栈信息全部丢失,对于排查问题几乎是致命的。
解决办法是配置Filebeat的multiline规则。在filebeat.yml对应的input里加上这样一段:
multiline.type: pattern multiline.pattern: '^[0-9]{4}-[0-9]{2}-[0-9]{2}' multiline.negate: true multiline.match: after这段的意思是:凡是匹配“以日期开头”的行,视为新日志的开始;不匹配的行(比如堆栈里的空格开头行),都归并到上一行后面。这样Kibana里一个异常栈就显示为一个完整的文档,搜索报错时堆栈信息完整保留。
注意这里有个小细节,正则里的^表示行首,一定要和实际日志的时间格式匹配。如果日志格式是[2025-04-01 10:00:01]这种带括号前缀的,pattern就要写成^\[[0-9]{4}。multiline配置错会导致所有日志被合并成一条,或者一条都合并不成功,排查时很迷惑。
2.3 Elasticsearch的重要配置参数与JVM调优
Elasticsearch部署时的配置主要在/etc/elasticsearch/elasticsearch.yml和/etc/elasticsearch/jvm.options里。对于单节点测试环境,我把ES配置为单节点模式,关闭了discovery的引导检查:
cluster.name: efk-cluster node.name: node-1 path.data: /data/elasticsearch/data path.logs: /data/elasticsearch/logs network.host: 192.168.10.10 http.port: 9200 discovery.type: single-node这里特别提一下path.data。ES的数据目录默认在/var/lib/elasticsearch,如果系统盘容量不大,建议挂载到独立数据盘。我遇到过多次因为日志量快速增长把系统盘撑满,导致ES集群全部变红的情况,这个教训很深刻。
JVM内存的设置遵循一个原则:不超过物理内存的一半,并且不要超过32GB。比如物理内存16GB,给ES分8GB比较合适;如果机器内存64GB以上,最多建议分配30~31GB,超过这个值对象指针压缩会失效,内存浪费反而严重。修改jvm.options里的-Xms8g和-Xmx8g,这两个值必须保持一致,避免JVM运行时动态扩容带来的性能抖动。
2.4 Kibana配置和使用前必做的几件事
Kibana安装完成后,修改/etc/kibana/kibana.yml:
server.port: 5601 server.host: "0.0.0.0" elasticsearch.hosts: ["http://192.168.10.10:9200"] i18n.locale: "zh-CN"i18n.locale设为zh-CN可以启用中文界面,对排查问题来说直观很多。启动后第一件事不是忙着看Discover,而是先到“Stack Management → Index Patterns”创建索引模式。我这边通配符填filebeat-*,时间筛选字段选择@timestamp。
这里有个细节,Kibana的Discover页面默认展示最近15分钟的数据,如果你刚采集完日志但页面上看不到,先检查右上角时间范围是不是正确,这不是配置错误,而是很多人会忽略的第一道坎。
3. 实操过程与核心环节实现
3.1 从零开始的安装部署全流程
下面以三台机器为例,把每一步的安装命令和验证方式梳理一遍。
假设规划如下:
| 组件 | 服务器IP | 说明 |
|---|---|---|
| Elasticsearch | 192.168.10.10 | 单节点存储 |
| Kibana | 192.168.10.11 | 可视化 |
| Filebeat | 多台业务机 | 采集日志 |
第一步,在192.168.10.10上安装Elasticsearch。使用RPM包安装:
yum install -y elasticsearch-7.10.2.rpm systemctl daemon-reload systemctl enable elasticsearch systemctl start elasticsearch安装完成后,用curl验证:
curl http://192.168.10.10:9200正常情况下会返回一个JSON对象,里面有cluster_name、tagline等字段。如果返回连接拒绝,先检查9200端口是否被防火墙拦截,或者ES进程是否真的起来了(journalctl -u elasticsearch查看日志)。
第二步,在192.168.10.11上安装Kibana:
yum install -y kibana-7.10.2.rpm systemctl enable kibana systemctl start kibana浏览器访问http://192.168.10.11:5601,能打开页面就算成功。
第三步,在业务服务器上安装Filebeat。Filebeat不需要部署在ES和Kibana所在的机器上,而是分布式部署在产生日志的每台业务机上,这也是它轻量化的意义:
rpm -ivh filebeat-7.10.2-x86_64.rpm systemctl enable filebeat修改配置后启动,并查看状态:
systemctl start filebeat systemctl status filebeat如果看到Errors reading file或Failed to connect之类的信息,大概率是路径写错或者网络不通,用journalctl -u filebeat或者直接前台运行filebeat -e -d "*"来调试。
3.2 索引生命周期管理:防止磁盘被日志撑爆
监控系统跑起来后,最现实的问题是磁盘消耗速度远超预期。业务日志一天产生10GB,保留30天就是300GB,即使ES压缩率高,也需要提前规划策略。
我用Index Lifecycle Management(ILM)来做冷热分层和自动删除。先创建一个生命周期策略,在Kibana的“Stack Management → Index Lifecycle Policies”里设置,或者直接通过API创建:
PUT _ilm/policy/logs_30days_policy { "policy": { "phases": { "hot": { "actions": { "rollover": { "max_size": "30GB", "max_age": "1d" } } }, "delete": { "min_age": "30d", "actions": { "delete": {} } } } } }这段策略的意思是:当索引超过30GB或者时间超过1天时,自动滚动生成新索引;当索引年龄超过30天后,自动删除。再用索引模板把策略绑定到filebeat-*开头的索引上:
PUT _template/logs_template { "index_patterns": ["filebeat-*"], "settings": { "number_of_shards": 1, "number_of_replicas": 0, "index.lifecycle.name": "logs_30days_policy" } }我这里把副本数设为0,因为单节点集群设副本没有意义,反而占空间。如果你的ES是多节点集群,建议副本至少设1,保证数据高可用。
3.3 Kibana告警:把日志级别变成实时通知
监控系统的最终目的是发现问题马上处理,而不是事后翻日志。Kibana的Alerting功能在7.10版本里已经比较成熟,我配置了两类告警。
第一类是“错误日志数超过阈值”告警。创建方式是:进入“Stack Management → Alerts and Insights → Rules”,新建规则,选择“Elasticsearch query”,设置查询条件为log_type: business AND level: error,时间窗口设为最近5分钟,阈值设为连续2个周期内数量大于等于5次。
这里要说明一个问题:Kibana告警依赖Elasticsearch的查询结果来评估是否触发。默认的情况下,它需要每隔一定时间(比如1分钟)执行一次查询,把结果和阈值比较。需要有一个“Connectors”来接收告警信息,我用的是Webhook方式,把告警消息发到企业微信机器人或者钉钉机器人。配置方法不复杂,在Connectors里添加Webhook,填上Webhook URL,消息模板里引用{{context.condition0.value}}之类的变量即可。
第二类是“访问日志5xx错误率”告警。查询条件改成log_type: nginx_access AND status: >=500,并且用聚合的方式统计百分比。这类告警对生产环境特别有用,一旦某个接口突然出现大量5xx,说明后端服务可能已经异常了。
3.4 在Kibana里查看日志和排查问题
配置好之后,日常操作基本集中在Discover和Visualize Library两个模块。Discover里输入level:error可以瞬间把所有业务服务的错误日志过滤出来,再按service字段分组,就能看出哪个服务报错最多。
在Visualize创建图表时,可以用terms聚合按service.keyword统计错误数量,用date_histogram聚合按时间展示错误趋势。如果你自定义了message字段,需要注意ES默认的字符串字段会被映射成text类型,无法直接做聚合,需要同时存在一个.keyword子字段。如果没这个子字段,就在索引模板里提前把字段映射成keyword,或者让应用输出时对字段做冗余处理。这是我在实操中遇到的比较典型的坑。
时间字段的聚合也要注意。ES里默认的@timestamp是UTC时间,Kibana在展示时会自动转换成本地时区,但如果你的业务日志里自带一个timestamp字段,务必确认该字段是date类型且格式正确,否则Kibana会提示Unable to parse date之类的问题。
4. 常见问题与排查技巧实录
4.1 Filebeat能采集到日志,但Kibana里搜不到数据
这个问题我遇到得最多,也是最容易误导人的。先确认Filebeat进程在跑,再确认ES索引已经创建:
curl http://192.168.10.10:9200/_cat/indices?v如果看到filebeat-business-2025.04.01之类索引存在,但Kibana里还是空的,多半是Kibana索引模式里没有包含这个索引,或者时间范围不对。还有种情况是索引模式创建在数据写入之前,导致索引模式的字段列表是空的,尤其在7.x版本里,Kibana会缓存字段列表,新索引写入后需要刷新索引模式字段。
另外,检查索引里的文档内容是否符合预期:
curl http://192.168.10.10:9200/filebeat-business-2025.04.01/_search?pretty如果文档里全是message,没有拆开的JSON字段,回去检查Filebeat配置里的json.keys_under_root是否开启。
4.2 Song不聊了,简单说说磁盘I/O和CPU占用
ES查询慢一般不是CPU瓶颈,更多是磁盘I/O。如果目录是机械硬盘,查询响应延迟会非常明显。解决办法是尽量用SSD,同时在ES配置里把index.refresh_interval改成30s(默认1s),减少刷新频率,明显降低I/O负载。对监控类场景,数据延迟30秒完全可接受,没必要追求实时显示到秒级。
Filebeat的CPU占用一般很低,但如果采集路径里包含通配符且文件数量特别多,Filebeat可能持续扫描目录,CPU占用会上去。我当时遇到的是/data/applogs/*/*.log这个通配符展开后匹配了几千个文件,Filebeat每次轮询都要重新遍历目录。优化方案是尽量把子目录结构调整成扁平式,减少通配符层数,或者按服务分别配置paths,而不是用一个广义path。
4.3 启动顺序和端口防火墙的注意事项
EFK各组件之间的启动顺序有讲究。Elasticsearch必须最先启动,等9200端口可访问后,再启动Kibana;Filebeat的启动顺序不影响,因为它可以反复重连ES。如果Kibana先启动而ES没起来,页面会一直报Unable to connect to Elasticsearch,但只要ES起来后刷新页面就能恢复,不用慌张。
防火墙方面,需要开放9200给Filebeat和Kibana访问,开放5601给浏览器访问。在实际生产环境里建议不要把所有端口暴露到公网,Kibana至少要在防火墙或反向代理后面加一层访问认证,避免日志数据泄露。
4.4 日志字段映射错乱:时间字段和数字字段
另一个高频问题是在日志里把HTTP状态码当成字符串处理,比如"status":"500"。ES默认动态映射会把它映射为text类型,导致后续做status >= 500范围查询时直接报错。解决办法是在索引模板里显式声明字段类型。
我通过一个自定义模板来覆盖默认行为:
{ "index_patterns": ["filebeat-*"], "mappings": { "properties": { "status": { "type": "long" }, "response_time": { "type": "float" }, "timestamp": { "type": "date" } } } }这个模板一定在索引创建前就生效,所以先把已经写坏的索引删掉,再更新模板,否则旧索引的映射不会自动改变。
5. 告警规则设计的一个实际经验
很多人把告警规则简单理解成“错误日志出现就告警”,这在低频场景下可以,但在高并发系统里,某几个偶发错误或者网络抖动引发的瞬时错误会被放大成告警风暴,值班人员很快就会对告警免疫。
我自己的经验是,告警要分两层:
- 第一层是“业务持久性错误”,用累计数量+时间窗口双重判断,比如5分钟内错误超过10次,且连续2个窗口持续触发,才告警。
- 第二层是“错误率突增”,不判断绝对值,而是和最近30分钟的平均值做对比,比如当前5分钟的错误率是过去30分钟平均值的3倍以上,说明有异常流量进来,此时才触发紧急告警。
这种做法需要在Kibana规则里配置两个不同的查询,或者在外部做计算。Kibana的“threshold”规则支持对聚合结果设置阈值,你可以基于count做简单阈值,也可以用average等聚合结果。如果要实现“同比对比”,Kibana原生能力有点弱,我最后采用了在业务侧输出聚合指标的方案,比如每个服务周期性地把错误数指标发到独立的监控索引,再对这个索引做趋势告警,效果比直接在Kibana里反复改查询稳定得多。
6. 实测后的几点总结和下一步扩展方向
跑了一段时间之后,整个EFK系统的状态基本稳定,日常使用中最值得庆幸的是当初把采集端的multiline和字段映射处理好了,否则后面排查问题的体验会非常痛苦。
如果要扩展,我建议思路依次是:接入Kafka做缓冲,让Filebeat不直接写ES,而是写到Kafka,由消费者写入ES,这样能有效抵抗ES短暂不可用导致的数据堆积;再进一步,可以把ES索引从一天一个改成按业务重要性分级别:核心交易日志保留180天,一般业务日志保留30天,调试日志保留7天,这样可以控制存储成本。还可以考虑在Filebeat侧做简单的drop事件处理,过滤掉大量无用的debug日志,减少ES的写入压力。
我个人在实际操作中的体会是,EFK这类系统的搭建难度并不高,真正拉开差距的是对日志格式和字段设计的控制力。如果每个应用都能规范输出结构化日志,日志平台的价值能翻好几倍。反过来说,如果生产环境里都是乱糟糟的非结构化文本,任凭EFK配置得再漂亮,也解决不了根本问题。
最后分享一个小技巧:给你的每一条日志都加上trace_id或者request_id字段,这样从网关发起的一次请求,无论经过多少个微服务,都能通过同一个ID把链路串联起来,排查问题时在Kibana里直接搜这个ID,就能看到完整调用链上的所有日志。这个改动对应用代码来说是最小成本的,但对整个日志监控系统的价值而言,是质的提升。