服务器日志分散在各处,排查问题时需要登录多台机器翻找文件,效率低到令人发指。更麻烦的是,一旦遇到分布式系统调用链报错,单台机器的日志根本看不出上下游的因果关系。ELK 这套组合拳就是专门来解决这个痛点的,它把日志采集、存储、检索、可视化串成一条流水线,让运维和开发人员能在几秒钟内从海量日志中捞出关键信息。
ELK 到底是什么以及为什么是它ELK 是 Elasticsearch、Logstash、Kibana 三个开源工具的缩写。实际生产环境中,这个栈通常还会加上 Filebeat,变成 EFK 或者依然习惯性叫 ELK。Elasticsearch 负责分布式存储和全文检索,Logstash 负责日志的清洗和转换,Kibana 负责图表展示和交互式查询,Filebeat 则作为轻量级采集端部署在每台需要收集日志的服务器上。
选 ELK 而不是自己写脚本去 grep,核心原因有三个。第一是集中化,所有服务器的日志汇聚到一个平台,不用再挨个登录机器。第二是结构化,原始日志是非结构化文本,ELK 可以在采集阶段把半结构化数据解析成字段,比如把 Nginx 访问日志拆成状态码、响应时间、请求 URL 等独立维度。第三是关联分析能力,一条业务请求可能经过网关、应用服务、数据库,ELK 能通过 trace id 把整条链路串起来看。
日志采集层的选型与部署细节Filebeat 是目前最主流的采集端方案,它用 Go 语言编写,内存占用极低,单个实例通常只消耗十几 MB 内存。部署时把它安装到每一台需要采集日志的服务器上,通过 YAML 配置文件指定日志路径。一个典型的 filebeat.yml 配置如下:
filebeat.inputs:
- type: log
enabled: true
paths:
- /var/log/nginx/access.log
- /var/log/nginx/error.log
fields:
app: nginx
env: production
fields_under_root: true
multiline.pattern: '^[0-9]{4}-[0-9]{2}-[0-9]{2}'
multiline.negate: true
multiline.match: after
output.logstash:
hosts: ["logstash-server:5044"]
loadbalance: true
这里有几个容易踩坑的点需要特别注意。多行日志合并必须配置 multiline,否则 Java 异常堆栈会被拆成几十条独立记录,检索时完全没法看。fields 字段用来给日志打标签,标识它来自哪个应用、哪个环境,后续在 Kibana 里过滤会非常方便。output 部分可以直接输出到 Elasticsearch,但推荐先经过 Logstash 做一层缓冲和处理,这样下游的 Elasticsearch 压力更小,而且 Logstash 挂了不会直接丢数据。
Logstash 管道处理的核心逻辑Logstash 是整个链路中最吃资源的组件,它的工作模式是 input-filter-output 三段式管道。input 接收 Filebeat 发来的数据,filter 做解析和转换,output 写入 Elasticsearch。一个生产级的 Logstash 配置通常长这样:
input {
beats {
port => 5044
client_inactivity_timeout => 300
}
}
filter {
if [fields][app] == "nginx" {
grok {
match => { "message" => "%{IPORHOST:client_ip} - %{DATA:user} \[%{HTTPDATE:timestamp}\] \"%{WORD:method} %{DATA:request} HTTP/%{NUMBER:http_version}\" %{NUMBER:status} %{NUMBER:bytes} \"%{DATA:referrer}\" \"%{DATA:user_agent}\" %{NUMBER:request_time}" }
}
date {
match => [ "timestamp", "dd/MMM/YYYY:HH:mm:ss Z" ]
target => "@timestamp"
}
mutate {
convert => { "status" => "integer" }
convert => { "bytes" => "integer" }
convert => { "request_time" => "float" }
remove_field => [ "timestamp" ]
}
}
if [fields][app] == "java" {
json {
source => "message"
skip_on_invalid_json => true
}
}
}
output {
elasticsearch {
hosts => ["es-node1:9200", "es-node2:9200", "es-node3:9200"]
index => "app-logs-%{+YYYY.MM.dd}"
template => "/etc/logstash/templates/app-template.json"
template_name => "app-logs"
manage_template => true
}
}
grok 是 Logstash 最强大的 filter 插件,它用正则表达式把非结构化文本映射成结构化字段。上面这个 grok 表达式能解析标准的 Nginx 组合日志格式,把客户端 IP、请求方法、状态码、响应时间等全部提取出来。date 插件用日志里的时间戳替换 Logstash 处理时间,这样 Kibana 里看到的时间才是真实业务时间,而不是延迟后的入库时间。mutate 插件负责字段类型转换,把字符串类型的状态码转成整数,后续在 Kibana 里才能做数值范围查询和聚合计算。
对于 JSON 格式的日志,直接用 json 插件解析就行,不需要写 grok。现在很多应用直接输出 JSON 格式日志,这是最推荐的做法,省去了解析的麻烦,字段类型也能自动识别。如果你们的应用还在输出传统文本日志,建议推动开发团队改成 JSON 格式,这会让整个日志管道的复杂度下降一个数量级。
Elasticsearch 集群的索引规划与性能调优Elasticsearch 是日志的最终落脚点,它的索引设计直接决定了查询速度和存储成本。日志类数据有明显的时效性特征,绝大多数查询只关注最近几天的数据,所以必须按天建索引。上面的 output 配置里 index 参数用了日期变量,每天会自动生成一个新索引,比如 app-logs-2025.06.15。
按天建索引的好处是管理方便。可以用 Curator 或者 Elasticsearch 自带的 Index Lifecycle Management 来自动删除过期索引,比如保留 30 天数据,第 31 天的索引自动删掉,存储空间保持在一个稳定范围。ILM 策略配置建议设置 hot-warm-delete 三个阶段:hot 阶段数据刚进来,用高性能节点存储;warm 阶段数据查询频率下降,自动迁移到普通磁盘节点,同时把副本数降为 1 节省空间;delete 阶段到期后自动清理。
分片数量是另一个关键参数。日志索引写入量大但单条数据小,分片数不宜过多。一般建议每个索引 2 到 5 个主分片,每个分片大小控制在 30GB 到 50GB 之间。分片太多会导致写入时 IO 压力分散但查询时需要聚合更多分片的结果,反而变慢。副本数设为 1 即可保证高可用,数据量特别大的场景甚至可以暂时设为 0 来提升写入速度,等写入高峰过后再调整。
字段映射也需要提前规划。默认情况下 Elasticsearch 会动态映射所有字段,但日志里有些字段其实不需要索引,比如原始 message 字段如果已经解析过了,可以设置为不索引只存储。还有像用户代理字符串这种长文本,应该用 text 类型做全文检索,而状态码、响应时间这类数值字段用 integer 或 float 类型才能做范围查询和聚合。模板配置示例:
{
"template": "app-logs-*",
"settings": {
"number_of_shards": 3,
"number_of_replicas": 1,
"refresh_interval": "30s",
"index.lifecycle.name": "logs-policy",
"index.lifecycle.rollover_alias": "app-logs"
},
"mappings": {
"properties": {
"message": { "type": "text", "index": false },
"status": { "type": "integer" },
"response_time": { "type": "float" },
"client_ip": { "type": "ip" },
"user_agent": { "type": "text", "fields": { "keyword": { "type": "keyword", "ignore_above": 256 } } }
}
}
}
refresh_interval 从默认的 1 秒调到 30 秒,能明显降低写入负载,日志场景下 30 秒的延迟完全可接受。ip 类型专门用于存储 IP 地址,支持 CIDR 范围查询,排查问题时可以直接搜某个 IP 段的所有请求。
Kibana 可视化与告警的实战用法Kibana 不只是个花里胡哨的图表工具,它最核心的价值是 Discover 页面的交互式检索。学会用 Lucene 查询语法或者 KQL,能在几秒内从几十亿条日志里捞出想要的数据。几个高频查询场景:查某个时间段内状态码为 500 的所有请求,直接输入 status:500;查响应时间超过 3 秒的慢请求,输入 response_time:>3;查包含特定订单号的日志,输入 message:"ORDER123456"。
Dashboard 适合做监控大盘。把关键指标拖成图表固定在面板上,比如每分钟请求量曲线、各状态码占比饼图、P99 响应时间趋势图、错误日志 Top 10 应用排行。这些图表不只是好看,更重要的是能一眼发现异常。比如请求量突然跌到零,或者 500 错误占比从 0.1% 飙升到 5%,立刻就能在图表上看到突变。
Kibana 的 Alerting 功能可以直接基于查询条件设置告警规则。比如设置一个规则:最近 5 分钟内 500 错误数量超过 100 次就触发告警,通知方式可以选邮件、Slack、Webhook 等。这个告警比传统监控更灵活,因为告警条件可以是任意复杂的查询语句,不局限于固定指标。
高并发场景下的缓冲与削峰策略日志量大的时候,Elasticsearch 可能跟不上写入速度,导致数据丢失或集群不稳定。这时候需要在 Logstash 和 Elasticsearch 之间加一层消息队列做缓冲。常用的方案是 Kafka 或者 Redis。Filebeat 先把日志发到 Kafka,Logstash 从 Kafka 消费,这样即使 Logstash 或 Elasticsearch 短暂不可用,数据也不会丢,恢复后从 Kafka 的 offset 继续消费就行。
Kafka 方案的架构是 Filebeat -> Kafka -> Logstash -> Elasticsearch。Kafka 的 topic 按日志类型划分,比如 app-logs、nginx-logs、system-logs 各一个 topic。分区数根据吞吐量设置,一般 8 到 16 个分区足够应对绝大多数场景。Logstash 消费端用 consumer group 模式部署多个实例,实现并行消费,消费能力可以线性扩展。
如果暂时不想引入 Kafka 的复杂度,也可以在 Logstash 里用 persistent queue 做内置缓冲。在 logstash.yml 里开启 queue.type: persisted,设置队列存储路径和最大容量。这个方案部署简单,但缓冲能力受限于单机磁盘,适合日志量中等且对可靠性要求较高的场景。
日志规范与团队协作的落地建议工具搭得再好,日志本身不规范也白搭。最核心的一条原则是:每条日志必须包含可追溯的上下文信息。对于 Web 请求,至少要有 trace id、用户 ID、请求路径、耗时。对于后台任务,要有任务 ID、执行时间、处理数据量。这些字段统一用 JSON 格式输出,字段名保持全局一致,比如 trace_id 不要有的服务叫 traceId,有的叫 requestId。
日志级别也要严格规范。DEBUG 日志只在开发环境开启,生产环境默认 INFO 级别,关键业务节点打 WARN 和 ERROR。很多人习惯在代码里随意打 INFO 日志,导致真正有用的信息被淹没在噪音里。建议团队定一个日志规范文档,明确每个级别该打什么内容,Code Review 时一并检查日志打点是否合理。
ELK 平台本身也需要持续维护。Elasticsearch 集群的磁盘使用率要监控,索引模板要随着业务字段变化及时更新,Kibana 的 Saved Objects 要定期备份。这些运维工作虽然琐碎,但直接决定了平台的长期可用性。建议安排专人负责,或者至少写几个自动化脚本把日常巡检工作自动化掉。
