日志是系统的眼睛。没有日志,运维就是盲人摸象。本文从日志基础讲起,覆盖 ELK/EFK 全栈搭建、架构选型、成本控制,到一个完整的实战项目,带你从零构建企业级日志平台。
一、日志基础:先搞清楚你在看什么
1.1 日志级别
别小看日志级别,乱用 ERROR 和 DEBUG 是新手最常见的坑。
经验法则: 生产环境开 INFO,排查问题时临时开 DEBUG(用动态日志级别),永远别在生产写 DEBUG 日志到磁盘——你会后悔的。
1.2 结构化日志
别再写 "User login success" 这种裸字符串了。结构化日志 = 可搜索 + 可聚合 + 可告警。
{
"timestamp": "2026-06-10T14:05:23.456Z",
"level": "INFO",
"service": "user-service",
"trace_id": "abc123def456",
"span_id": "span-789",
"message": "User login success",
"user_id": "u_10086",
"ip": "10.0.1.55",
"latency_ms": 42
}关键字段:
timestamp— ISO 8601 格式,带时区,别用 Unix 时间戳(人看不懂)trace_id— 分布式链路追踪的命根子,全链路必须透传service— 哪个服务写的,日志聚合时按服务分组
1.3 日志格式规范
给团队定一个日志规范,省得后面吵架:
# 推荐的日志行格式(非 JSON,适合 syslog 类场景)
2026-06-10T14:05:23.456+08:00 [INFO] [user-service] [trace:abc123] User login success user_id=u_10086 ip=10.0.1.55 latency=42ms
# 禁止的写法
logger.info("something happened") # 没有上下文
logger.error(e.toString()) # 没有堆栈
logger.info("user=" + userId + " login") # 拼接地狱,用参数化Java 示例(SLF4J + Logback):
// ✅ 正确
log.info("User login success, userId={}, ip={}, latency={}ms", userId, ip, latency);
// ❌ 错误
log.info("User " + userId + " login from " + ip);二、ELK / EFK 栈概述
2.1 两个栈,一个家族
选型建议:
传统服务器 / 虚拟机 → ELK(Logstash 功能全面)
Kubernetes / 容器化 → EFK(Fluent Bit 是 DaemonSet 天然适配)
预算紧张 / 只要日志查询 → Grafana Loki(下面单独讲)
三、Elasticsearch:日志存储引擎
3.1 安装(Docker 单节点,快速上手)
# 创建数据目录
mkdir -p /opt/elasticsearch/data
chown -R 1000:1000 /opt/elasticsearch
# 启动
docker run -d \
--name elasticsearch \
-p 9200:9200 -p 9300:9300 \
-e "discovery.type=single-node" \
-e "xpack.security.enabled=false" \
-e "ES_JAVA_OPTS=-Xms2g -Xmx2g" \
-v /opt/elasticsearch/data:/usr/share/elasticsearch/data \
docker.elastic.co/elasticsearch/elasticsearch:8.13.0验证:
curl -s http://localhost:9200 | jq .
# 返回 cluster_name、version 等信息即成功3.2 索引管理
# 创建索引(带 settings)
curl -X PUT "http://localhost:9200/app-logs-2026.06.10" -H 'Content-Type: application/json' -d '{
"settings": {
"number_of_shards": 3,
"number_of_replicas": 1,
"refresh_interval": "5s"
},
"mappings": {
"properties": {
"timestamp": { "type": "date" },
"level": { "type": "keyword" },
"service": { "type": "keyword" },
"trace_id": { "type": "keyword" },
"message": { "type": "text", "analyzer": "standard" },
"user_id": { "type": "keyword" },
"ip": { "type": "ip" },
"latency_ms": { "type": "integer" }
}
}
}'
# 查询日志
curl -X GET "http://localhost:9200/app-logs-*/_search" -H 'Content-Type: application/json' -d '{
"query": {
"bool": {
"must": [
{ "match": { "message": "login" } },
{ "term": { "level": "ERROR" } }
],
"filter": [
{ "range": { "timestamp": { "gte": "now-1h" } } }
]
}
},
"sort": [{ "timestamp": "desc" }],
"size": 20
}'3.3 映射设计要点
keyword vs text:
- keyword → 精确匹配、聚合、排序(level、service、user_id)
- text → 全文检索(message、description)
- 两者可同时设:{ "type": "text", "fields": { "keyword": { "type": "keyword" } } }
日期格式:
- 统一用 ISO 8601:"2026-06-10T14:05:23.456+08:00"
- ES 自动识别,但显式指定 format 更稳
IP 类型:
- "type": "ip" 支持 CIDR 查询:10.0.0.0/83.4 生产集群(3 节点最小可用)
# docker-compose.yml — 3 节点集群
version: "3.8"
services:
es01:
image: docker.elastic.co/elasticsearch/elasticsearch:8.13.0
environment:
- node.name=es01
- cluster.name=prod-logs
- discovery.seed_hosts=es02,es03
- cluster.initial_master_nodes=es01,es02,es03
- "ES_JAVA_OPTS=-Xms4g -Xmx4g"
- xpack.security.enabled=true
volumes:
- es01-data:/usr/share/elasticsearch/data
ports:
- "9200:9200"
deploy:
resources:
limits:
memory: 8G
es02:
image: docker.elastic.co/elasticsearch/elasticsearch:8.13.0
environment:
- node.name=es02
- cluster.name=prod-logs
- discovery.seed_hosts=es01,es03
- cluster.initial_master_nodes=es01,es02,es03
- "ES_JAVA_OPTS=-Xms4g -Xmx4g"
- xpack.security.enabled=true
volumes:
- es02-data:/usr/share/elasticsearch/data
es03:
image: docker.elastic.co/elasticsearch/elasticsearch:8.13.0
environment:
- node.name=es03
- cluster.name=prod-logs
- discovery.seed_hosts=es01,es02
- cluster.initial_master_nodes=es01,es02,es03
- "ES_JAVA_OPTS=-Xms4g -Xmx4g"
- xpack.security.enabled=true
volumes:
- es03-data:/usr/share/elasticsearch/data
volumes:
es01-data:
es02-data:
es03-data:四、Logstash:日志处理管道
4.1 三大组件
Input → Filter → Output
│ │ │
│ │ └─ Elasticsearch / Kafka / S3
│ └─ Grok / Mutate / GeoIP / Drop
└─ File / Beats / Kafka / Syslog / HTTP4.2 Input 配置
# logstash.conf
input {
# 从文件读取
file {
path => "/var/log/nginx/access.log"
start_position => "beginning"
sincedb_path => "/var/lib/logstash/sincedb_nginx"
codec => "json"
}
# 接收 Beats 输入
beats {
port => 5044
}
# 接收 Syslog
syslog {
port => 5514
type => "syslog"
}
# 从 Kafka 消费
kafka {
bootstrap_servers => "kafka1:9092,kafka2:9092"
topics => ["app-logs"]
group_id => "logstash-consumer"
codec => json
}
}4.3 Filter:Grok 解析
Grok 是 Logstash 的灵魂——用正则把非结构化文本变成结构化字段。
filter {
# 解析 Nginx 访问日志
if [type] == "nginx-access" {
grok {
match => {
"message" => '%{IPORHOST:client_ip} - %{DATA:user} \[%{HTTPDATE:timestamp}\] "%{WORD:method} %{URIPATHPARAM:request} HTTP/%{NUMBER:http_version}" %{NUMBER:status:int} %{NUMBER:bytes:int} "%{DATA:referrer}" "%{DATA:user_agent}" %{NUMBER:latency_ms:float}'
}
}
# 解析时间戳
date {
match => ["timestamp", "dd/MMM/yyyy:HH:mm:ss Z"]
target => "@timestamp"
}
# GeoIP 解析
geoip {
source => "client_ip"
target => "geo"
}
# User-Agent 解析
useragent {
source => "user_agent"
target => "ua"
}
# 条件丢弃健康检查日志
if [request] =~ /^\/health/ {
drop {}
}
}
# 解析 Java 异常堆栈(多行合并后)
if [type] == "java-exception" {
grok {
match => {
"message" => '(?<timestamp>\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3})\s+\[(?<thread>[\w\-\.]+)\]\s+(?<level>\w+)\s+(?<logger>[\w\.]+)\s*-\s*(?<log_message>.*)'
}
}
# 提取异常类名
if [log_message] =~ /Exception/ {
grok {
match => {
"log_message" => '(?<exception_class>[\w\.]*Exception[\w]*):\s*(?<exception_message>.*)'
}
}
}
}
}4.4 Output 配置
output {
# 写入 Elasticsearch
elasticsearch {
hosts => ["https://es01:9200", "https://es02:9200"]
user => "logstash_writer"
password => "${ES_PASSWORD}"
index => "app-logs-%{+YYYY.MM.dd}"
ssl => true
cacert => "/etc/logstash/certs/ca.crt"
}
# 错误日志同时写入告警主题
if [level] == "ERROR" {
kafka {
bootstrap_servers => "kafka1:9092"
topic_id => "alert-logs"
codec => json
}
}
# 调试用:输出到 stdout
# stdout { codec => rubydebug }
}五、Kibana:可视化与查询
5.1 安装
docker run -d \
--name kibana \
-p 5601:5601 \
-e "ELASTICSEARCH_HOSTS=http://elasticsearch:9200" \
-e "xpack.security.enabled=false" \
docker.elastic.co/kibana/kibana:8.13.05.2 KQL 查询语法
KQL(Kibana Query Language)比 Lucene 更直观:
# 精确匹配
level: "ERROR"
# 全文搜索
message: "timeout" and service: "order-service"
# 范围查询
latency_ms > 1000
# 通配符
service: user*
# 嵌套字段
geo.country: "China"
# 组合查询
(level: "ERROR" or level: "FATAL") and service: "payment-service" and latency_ms > 500
# 排除
not message: "health check"5.3 Dashboard 实战
创建一个 服务健康大盘,包含以下可视化:
1. 日志量趋势(Line Chart)
- X 轴:@timestamp(按 1 分钟聚合)
- Y 轴:Count
- 分组:按 level 着色
2. 错误率(Gauge)
- 公式:ERROR 数 / 总日志数 × 100%
- 阈值:> 5% 红色,> 1% 黄色
3. Top 10 慢请求(Data Table)
- 过滤:latency_ms > 500
- 列:timestamp, service, request, latency_ms
- 排序:latency_ms desc
4. 错误分布(Pie Chart)
- 聚合:按 exception_class 分组
- 过滤:level: "ERROR"
5. 地理分布(Map)
- 字段:geo.location
- 过滤:最近 24 小时六、Filebeat:轻量级日志采集
6.1 为什么用 Filebeat?
Logstash 吃内存(512MB+),Filebeat 只要几十 MB。在每台服务器上放一个 Filebeat 做采集,Logstash 集中做处理——这是经典架构。
6.2 安装与基础配置
# filebeat.yml
filebeat.inputs:
- type: log
enabled: true
paths:
- /var/log/nginx/access.log
json.keys_under_root: true
json.overwrite_keys: true
- type: log
enabled: true
paths:
- /var/log/app/*.log
fields:
service: my-app
env: production
fields_under_root: true
output.logstash:
hosts: ["logstash1:5044", "logstash2:5044"]
loadbalance: true
# 或直接输出到 Elasticsearch
# output.elasticsearch:
# hosts: ["es01:9200"]
# index: "filebeat-%{+yyyy.MM.dd}"
logging.level: info
logging.to_files: true
logging.files:
path: /var/log/filebeat
name: filebeat.log
keepfiles: 76.3 Module 系统
Filebeat 的 module 把常见日志的采集、解析、Dashboard 一键搞定:
# 启用 Nginx module
filebeat modules enable nginx
# 启用 System module(syslog + auth)
filebeat modules enable system
# 启用 MySQL 慢查询
filebeat modules enable mysql# modules.d/nginx.yml
- module: nginx
access:
enabled: true
var.paths: ["/var/log/nginx/access.log"]
error:
enabled: true
var.paths: [/var/log/nginx/error.log"]
# modules.d/system.yml
- module: system
syslog:
enabled: true
var.paths: ["/var/log/syslog", "/var/log/messages"]
auth:
enabled: true
var.paths: ["/var/log/auth.log", "/var/log/secure"]6.4 多行日志处理
Java 异常堆栈是典型的多行日志,不处理的话每一行都会变成一条独立日志——等于废了。
filebeat.inputs:
- type: log
paths:
- /var/log/app/*.log
multiline.type: pattern
multiline.pattern: '^\d{4}-\d{2}-\d{2}' # 以日期开头的是新日志
multiline.negate: true # 不匹配的行归入上一条
multiline.match: after # 归入上一条之后
multiline.max_lines: 100 # 最多合并 100 行
# 或者更精确地匹配 Java 异常
# multiline.pattern: '^\s+(at |\.{3}\s|Caused by|java\.|javax\.)'
# multiline.negate: false
# multiline.match: after七、Fluentd / Fluent Bit:K8s 时代的日志方案
7.1 Fluentd vs Fluent Bit
经典组合: Fluent Bit(DaemonSet 采集)→ Fluentd(聚合层)→ Elasticsearch
7.2 Fluent Bit 配置示例
# fluent-bit.conf
[SERVICE]
Flush 5
Daemon Off
Log_Level info
Parsers_File parsers.conf
[INPUT]
Name tail
Tag kube.*
Path /var/log/containers/*.log
Parser docker
DB /var/log/flb_kube.db
Mem_Buf_Limit 5MB
Skip_Long_Lines On
Refresh_Interval 10
[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
Kube_Tag_Prefix kube.var.log.containers.
Merge_Log On
K8S-Logging.Parser On
K8S-Logging.Exclude Off
[OUTPUT]
Name es
Match kube.*
Host elasticsearch.logging.svc
Port 9200
Index k8s-logs
Type _doc
Logstash_Format On
Logstash_Prefix k8s
Retry_Limit 5# parsers.conf
[PARSER]
Name docker
Format json
Time_Key time
Time_Format %Y-%m-%dT%H:%M:%S.%L
Time_Keep On7.3 K8s DaemonSet 部署
# fluent-bit-daemonset.yaml
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: fluent-bit
namespace: logging
spec:
selector:
matchLabels:
app: fluent-bit
template:
metadata:
labels:
app: fluent-bit
spec:
serviceAccountName: fluent-bit
containers:
- name: fluent-bit
image: fluent/fluent-bit:2.2
resources:
requests:
cpu: 50m
memory: 64Mi
limits:
cpu: 200m
memory: 256Mi
volumeMounts:
- name: varlog
mountPath: /var/log
- name: containers
mountPath: /var/lib/docker/containers
readOnly: true
- name: config
mountPath: /fluent-bit/etc/
volumes:
- name: varlog
hostPath:
path: /var/log
- name: containers
hostPath:
path: /var/lib/docker/containers
- name: config
configMap:
name: fluent-bit-config八、架构选型:ELK vs EFK vs Loki
8.1 三种方案对比
8.2 决策树
你的日志需要全文检索吗?
├─ 是 → 预算充足?
│ ├─ 是 → ELK / EFK
│ └─ 否 → 考虑 ES 冷热架构 + ILM
└─ 否 → 在 K8s 上?
├─ 是 → Loki + Grafana(便宜好用)
└─ 否 → Loki 也行,或者 EF(只用 Fluentd + ES)九、日志管理最佳实践
9.1 ILM(Index Lifecycle Management)
别让索引无限增长,ES 会先慢后挂。
# 创建 ILM 策略
curl -X PUT "http://localhost:9200/_ilm/policy/logs-lifecycle" -H 'Content-Type: application/json' -d '{
"policy": {
"phases": {
"hot": {
"min_age": "0ms",
"actions": {
"rollover": {
"max_primary_shard_size": "30gb",
"max_age": "1d"
},
"set_priority": { "priority": 100 }
}
},
"warm": {
"min_age": "3d",
"actions": {
"shrink": { "number_of_shards": 1 },
"forcemerge": { "max_num_segments": 1 },
"set_priority": { "priority": 50 }
}
},
"cold": {
"min_age": "30d",
"actions": {
"set_priority": { "priority": 0 },
"freeze": {}
}
},
"delete": {
"min_age": "90d",
"actions": {
"delete": {}
}
}
}
}
}'
# 创建索引模板,绑定 ILM
curl -X PUT "http://localhost:9200/_index_template/logs-template" -H 'Content-Type: application/json' -d '{
"index_patterns": ["app-logs-*"],
"template": {
"settings": {
"number_of_shards": 3,
"number_of_replicas": 1,
"index.lifecycle.name": "logs-lifecycle",
"index.lifecycle.rollover_alias": "app-logs"
}
}
}'9.2 成本控制清单
✅ 开启 ILM,自动清理过期索引
✅ Warm/Cold 节点用便宜磁盘(HDD / 低频 OSS)
✅ 设置 max_docvalue_fields_search 限制
✅ 关闭不需要的 _source(谨慎!影响 reindex)
✅ 用 Frozen 索引节省 60%+ 内存
✅ 日志采样:DEBUG 日志采样 10%,不是全量
✅ 索引别名 + Rollover,避免巨型单索引
✅ 压缩传输:Logstash → ES 开启 gzip
❌ 不要把 ES 当长期存储(> 90 天用对象存储归档)
❌ 不要所有字段都用 text(keyword 省空间)9.3 安全加固
# ES 安全配置要点
xpack.security.enabled: true
xpack.security.transport.ssl.enabled: true
xpack.security.http.ssl.enabled: true
# 最小权限角色
# logstash_writer: 只有 create_index + write
# kibana_reader: 只有 read
# admin: 全权限十、实战:从零搭建完整日志平台
10.1 架构图
┌──────────────────────────────────────────────────────────────┐
│ 应用服务器 │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ App Logs │ │Nginx Logs│ │ Syslog │ │
│ └────┬─────┘ └────┬─────┘ └────┬─────┘ │
│ └──────────────┼────────────┘ │
│ ┌───────▼────────┐ │
│ │ Filebeat │ (每台服务器一个) │
│ └───────┬────────┘ │
└──────────────────────┼───────────────────────────────────────┘
│ :5044
┌────────▼─────────┐
│ Logstash │ (2 节点,负载均衡)
│ Filter + Grok │
└────────┬─────────┘
│
┌─────────────┼─────────────┐
│ │ │
┌────▼────┐ ┌─────▼────┐ ┌────▼─────┐
│ES Hot │ │ES Warm │ │Kafka │
│(SSD) │ │(HDD) │ │(缓冲层) │
└────┬────┘ └────┬─────┘ └──────────┘
└─────────────┘
│
┌──────▼──────┐
│ Kibana │
│ Dashboard │
└─────────────┘10.2 一键部署(docker-compose)
# docker-compose-logging.yml
version: "3.8"
services:
elasticsearch:
image: docker.elastic.co/elasticsearch/elasticsearch:8.13.0
container_name: elasticsearch
environment:
- discovery.type=single-node
- "ES_JAVA_OPTS=-Xms2g -Xmx2g"
- xpack.security.enabled=false
ports:
- "9200:9200"
volumes:
- es-data:/usr/share/elasticsearch/data
healthcheck:
test: ["CMD-SHELL", "curl -f http://localhost:9200/_cluster/health || exit 1"]
interval: 30s
timeout: 10s
retries: 5
logstash:
image: docker.elastic.co/logstash/logstash:8.13.0
container_name: logstash
ports:
- "5044:5044"
volumes:
- ./logstash/pipeline:/usr/share/logstash/pipeline
depends_on:
elasticsearch:
condition: service_healthy
environment:
- "LS_JAVA_OPTS=-Xms512m -Xmx512m"
kibana:
image: docker.elastic.co/kibana/kibana:8.13.0
container_name: kibana
ports:
- "5601:5601"
environment:
- ELASTICSEARCH_HOSTS=http://elasticsearch:9200
depends_on:
elasticsearch:
condition: service_healthy
filebeat:
image: docker.elastic.co/beats/filebeat:8.13.0
container_name: filebeat
user: root
volumes:
- ./filebeat/filebeat.yml:/usr/share/filebeat/filebeat.yml:ro
- /var/log:/var/log:ro
- filebeat-data:/usr/share/filebeat/data
depends_on:
- logstash
volumes:
es-data:
filebeat-data:# logstash/pipeline/logstash.conf
input {
beats {
port => 5044
}
}
filter {
if [fields][service] == "nginx" {
grok {
match => {
"message" => '%{IPORHOST:client_ip} - %{DATA:user} \[%{HTTPDATE:access_time}\] "%{WORD:method} %{URIPATHPARAM:request} HTTP/%{NUMBER:http_version}" %{NUMBER:status:int} %{NUMBER:bytes:int}'
}
}
date {
match => ["access_time", "dd/MMM/yyyy:HH:mm:ss Z"]
}
mutate {
remove_field => ["access_time", "message"]
}
}
}
output {
elasticsearch {
hosts => ["http://elasticsearch:9200"]
index => "%{[fields][service]}-%{+YYYY.MM.dd}"
}
}# filebeat/filebeat.yml
filebeat.inputs:
- type: log
enabled: true
paths:
- /var/log/nginx/*.log
fields:
service: nginx
fields_under_root: false
output.logstash:
hosts: ["logstash:5044"]
logging.level: warning10.3 启动与验证
# 启动
docker-compose -f docker-compose-logging.yml up -d
# 等待 ES 就绪(约 30 秒)
until curl -sf http://localhost:9200/_cluster/health; do sleep 2; done
# 验证 Logstash
curl -s http://localhost:9600 | jq . # Logstash API
# 验证 Kibana
open http://localhost:5601 # 浏览器打开
# 生成测试日志
for i in $(seq 1 100); do
echo "192.168.1.$((RANDOM%255)) - - [$(date '+%d/%b/%Y:%H:%M:%S %z')] \"GET /api/test HTTP/1.1\" 200 $((RANDOM%5000))" >> /var/log/nginx/access.log
done
# 在 Kibana Discover 中查看索引
curl -s "http://localhost:9200/_cat/indices?v&h=index,docs.count,store.size"10.4 生产环境 Checklist
□ ES 集群至少 3 节点,开启 xpack.security
□ Logstash 至少 2 节点,前面加负载均衡
□ Filebeat 每台服务器部署,配置 at-least-once 语义
□ Kafka 作为缓冲层(日志量 > 10K/s 时必须加)
□ ILM 策略配置完毕,Hot/Warm/Cold/Delete
□ Kibana Dashboard 创建:错误率、延迟分布、日志量趋势
□ 告警规则:ERROR 率突增、日志断流、ES 集群 Red
□ 备份:ES Snapshot 定期备份到 S3/OSS
□ 监控 ES 自身:集群健康、JVM 堆使用、磁盘水位
□ 安全:TLS 加密传输、最小权限角色、网络隔离小结
日志平台不是一蹴而就的。先跑起来,再迭代优化。记住:能搜到的日志才有价值,搜不到的就是噪音。