📖 开场:破案的线索
想象你是一个侦探 🔍:
没有日志(盲人摸象):
系统出bug了 💀
↓
你:什么时候出的?
开发:不知道 🤷
↓
你:哪个服务出的?
开发:不知道 🤷
↓
你:什么错误?
开发:不知道 🤷
↓
你:......💀
结果:
- 无从下手 ❌
- 只能瞎猜 ❌
- 修复时间长 ❌
有日志系统(明察秋毫):
系统出bug了 💀
↓
你:打开日志系统 🔍
↓
搜索:"ERROR"
↓
找到:
- 时间:2023-10-24 14:30:15
- 服务:order-service
- 错误:NullPointerException
- 堆栈:com.example.OrderService.createOrder(OrderService.java:123)
↓
定位问题 ✅
5分钟修复 ✅
结果:
- 快速定位 ✅
- 精准修复 ✅
- 节省时间 ✅
这就是日志系统:侦探的放大镜!
🤔 核心功能
功能1:日志收集 📥
分布式系统:
- 订单服务:10台服务器
- 用户服务:10台服务器
- 支付服务:10台服务器
- 共30台服务器
每台服务器:
- 日志文件:/var/log/app.log
- 每天:1GB日志
- 总计:30GB日志/天 💀
问题:
- 如何收集这30台服务器的日志?
- 如何统一存储?
- 如何快速检索?
功能2:日志检索 🔍
场景:
系统报警:订单服务出错
↓
你:搜索"order-service ERROR"
↓
1秒内返回结果 ✅
需求:
- 全文检索
- 秒级响应
- 支持复杂查询
功能3:日志分析 📈
场景:
老板:昨天有多少个500错误?
↓
你:打开日志分析系统
↓
输入:status:500 AND timestamp:[昨天0点 TO 昨天24点]
↓
结果:12345个 ✅
统计:
- 错误数量
- 错误分布
- 趋势分析
功能4:实时监控 🔔
场景:
订单服务:ERROR日志突然增多
↓
日志系统:触发报警 🚨
↓
发送钉钉/邮件/短信
↓
开发人员:立即处理 ✅
实时性:
- 秒级监控
- 实时报警
🎯 架构设计
ELK架构(推荐)⭐⭐⭐
ELK架构
┌────────────────────────────────────────┐
│ 应用服务器(30台) │
│ │
│ app.log → Filebeat → Logstash │
│ - 订单服务 │
│ - 用户服务 │
│ - 支付服务 │
└──────────────┬─────────────────────────┘
│
↓
┌────────────────────────────────────────┐
│ Logstash(日志处理) │
│ │
│ - 日志解析 │
│ - 日志过滤 │
│ - 日志转换 │
│ - 日志分发 │
└──────────────┬─────────────────────────┘
│
↓
┌────────────────────────────────────────┐
│ ElasticSearch(存储+检索) │
│ │
│ - 分布式存储 │
│ - 全文检索 │
│ - 聚合分析 │
└──────────────┬─────────────────────────┘
│
↓
┌────────────────────────────────────────┐
│ Kibana(可视化) │
│ │
│ - 日志查询 │
│ - 图表展示 │
│ - 报表统计 │
└────────────────────────────────────────┘
ELK = ElasticSearch + Logstash + Kibana
🎯 核心设计
设计1:日志收集(Filebeat)📥
为什么用Filebeat?
方案对比:
方案1:应用直接写入ES
缺点:
- 应用耦合 ❌
- 网络故障影响应用 ❌
方案2:Logstash收集
缺点:
- 资源消耗大(JVM)❌
方案3:Filebeat收集 ⭐⭐⭐
优点:
- 轻量级(Go语言)✅
- 占用资源少 ✅
- 可靠(断点续传)✅
Filebeat配置
# ⭐ filebeat.yml
filebeat.inputs:
# 订单服务日志
- type: log
enabled: true
paths:
- /var/log/order-service/*.log
fields:
service: order-service
env: prod
# 用户服务日志
- type: log
enabled: true
paths:
- /var/log/user-service/*.log
fields:
service: user-service
env: prod
# ⭐ 输出到Logstash
output.logstash:
hosts: ["logstash:5044"]
# 或者直接输出到ElasticSearch
# output.elasticsearch:
# hosts: ["es1:9200", "es2:9200", "es3:9200"]
# index: "logs-%{+yyyy.MM.dd}"
启动Filebeat
# 下载Filebeat
wget https://artifacts.elastic.co/downloads/beats/filebeat/filebeat-7.10.0-linux-x86_64.tar.gz
tar -xzf filebeat-7.10.0-linux-x86_64.tar.gz
cd filebeat-7.10.0
# 启动
./filebeat -e -c filebeat.yml
设计2:日志处理(Logstash)🔄
Logstash的作用
原始日志:
2023-10-24 14:30:15 ERROR [order-service] [http-nio-8080-exec-1] com.example.OrderService - 订单创建失败,userId=123
经过Logstash处理:
{
"timestamp": "2023-10-24T14:30:15Z",
"level": "ERROR",
"service": "order-service",
"thread": "http-nio-8080-exec-1",
"logger": "com.example.OrderService",
"message": "订单创建失败,userId=123",
"userId": 123
}
好处:
- 结构化 ✅
- 易检索 ✅
- 易分析 ✅
Logstash配置
# ⭐ logstash.conf
input {
# 接收Filebeat发送的日志
beats {
port => 5044
}
}
filter {
# ⭐ 解析日志(Grok)
grok {
match => {
"message" => "%{TIMESTAMP_ISO8601:timestamp} %{LOGLEVEL:level} [%{DATA:service}] [%{DATA:thread}] %{DATA:logger} - %{GREEDYDATA:msg}"
}
}
# ⭐ 解析时间戳
date {
match => ["timestamp", "yyyy-MM-dd HH:mm:ss"]
target => "@timestamp"
}
# ⭐ 提取userId
if [msg] =~ /userId=(\d+)/ {
grok {
match => {
"msg" => "userId=%{NUMBER:userId}"
}
}
}
# ⭐ 过滤DEBUG日志
if [level] == "DEBUG" {
drop {}
}
}
output {
# ⭐ 输出到ElasticSearch
elasticsearch {
hosts => ["es1:9200", "es2:9200", "es3:9200"]
index => "logs-%{service}-%{+yyyy.MM.dd}"
}
# 输出到控制台(调试用)
stdout {
codec => rubydebug
}
}
Grok语法
常用Grok Pattern:
%{TIMESTAMP_ISO8601:timestamp} → 2023-10-24T14:30:15Z
%{LOGLEVEL:level} → ERROR/WARN/INFO/DEBUG
%{DATA:service} → order-service
%{NUMBER:userId} → 123
%{GREEDYDATA:msg} → 任意文本
自定义Pattern:
%{IP:client_ip} - - [%{HTTPDATE:timestamp}] "%{WORD:method} %{URIPATHPARAM:request} HTTP/%{NUMBER:http_version}" %{NUMBER:status} %{NUMBER:bytes}
匹配:
192.168.1.1 - - [24/Oct/2023:14:30:15 +0800] "GET /api/order?id=123 HTTP/1.1" 200 1234
结果:
{
"client_ip": "192.168.1.1",
"timestamp": "24/Oct/2023:14:30:15 +0800",
"method": "GET",
"request": "/api/order?id=123",
"http_version": "1.1",
"status": "200",
"bytes": "1234"
}
设计3:日志存储(ElasticSearch)💾
为什么用ElasticSearch?
对比:
MySQL:
- 全文检索慢 ❌
- 不支持聚合分析 ❌
MongoDB:
- 全文检索慢 ❌
ElasticSearch:⭐⭐⭐
- 全文检索快(倒排索引)✅
- 支持聚合分析 ✅
- 分布式(TB级数据)✅
索引设计
索引命名规则:
logs-{service}-{yyyy.MM.dd}
例子:
logs-order-service-2023.10.24
logs-user-service-2023.10.24
logs-payment-service-2023.10.24
好处:
- 按服务隔离 ✅
- 按日期分片 ✅
- 易于删除旧数据 ✅
索引Mapping
PUT /logs-order-service-2023.10.24
{
"mappings": {
"properties": {
"@timestamp": {
"type": "date"
},
"level": {
"type": "keyword"
},
"service": {
"type": "keyword"
},
"thread": {
"type": "keyword"
},
"logger": {
"type": "keyword"
},
"message": {
"type": "text",
"analyzer": "standard"
},
"userId": {
"type": "long"
},
"orderId": {
"type": "long"
}
}
}
}
索引模板(自动创建索引)
PUT /_index_template/logs-template
{
"index_patterns": ["logs-*"],
"template": {
"settings": {
"number_of_shards": 3,
"number_of_replicas": 1,
"index.lifecycle.name": "logs-policy"
},
"mappings": {
"properties": {
"@timestamp": {"type": "date"},
"level": {"type": "keyword"},
"service": {"type": "keyword"},
"message": {"type": "text"}
}
}
}
}
设计4:日志检索(Kibana)🔍
Kibana查询
基本查询:
# 查询ERROR日志
level:ERROR
# 查询订单服务的日志
service:order-service
# 查询特定用户的日志
userId:123
# 组合查询
level:ERROR AND service:order-service AND userId:123
# 时间范围查询
@timestamp:[2023-10-24T00:00:00 TO 2023-10-24T23:59:59]
# 通配符查询
message:*NullPointerException*
# 正则查询
message:/userId=\d+/
DSL查询(ElasticSearch API)
POST /logs-order-service-*/_search
{
"query": {
"bool": {
"must": [
{"term": {"level": "ERROR"}},
{"term": {"service": "order-service"}},
{"range": {
"@timestamp": {
"gte": "2023-10-24T00:00:00",
"lte": "2023-10-24T23:59:59"
}
}}
]
}
},
"sort": [
{"@timestamp": {"order": "desc"}}
],
"size": 100
}
Java API查询
@Service
public class LogSearchService {
@Autowired
private RestHighLevelClient esClient;
/**
* ⭐ 查询日志
*/
public List<LogEntry> searchLogs(String service, String level,
Date startTime, Date endTime) throws IOException {
// 构建查询
BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();
if (service != null) {
boolQuery.must(QueryBuilders.termQuery("service", service));
}
if (level != null) {
boolQuery.must(QueryBuilders.termQuery("level", level));
}
if (startTime != null && endTime != null) {
boolQuery.must(QueryBuilders.rangeQuery("@timestamp")
.gte(startTime.getTime())
.lte(endTime.getTime()));
}
// 构建搜索请求
SearchRequest searchRequest = new SearchRequest("logs-*");
SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
searchSourceBuilder.query(boolQuery);
searchSourceBuilder.sort("@timestamp", SortOrder.DESC);
searchSourceBuilder.size(100);
searchRequest.source(searchSourceBuilder);
// 执行搜索
SearchResponse response = esClient.search(searchRequest, RequestOptions.DEFAULT);
// 解析结果
List<LogEntry> logs = new ArrayList<>();
for (SearchHit hit : response.getHits().getHits()) {
Map<String, Object> sourceMap = hit.getSourceAsMap();
LogEntry log = new LogEntry();
log.setTimestamp((String) sourceMap.get("@timestamp"));
log.setLevel((String) sourceMap.get("level"));
log.setService((String) sourceMap.get("service"));
log.setMessage((String) sourceMap.get("message"));
logs.add(log);
}
return logs;
}
/**
* ⭐ 统计ERROR数量
*/
public long countErrors(String service, Date startTime, Date endTime) throws IOException {
// 构建查询
BoolQueryBuilder boolQuery = QueryBuilders.boolQuery()
.must(QueryBuilders.termQuery("service", service))
.must(QueryBuilders.termQuery("level", "ERROR"))
.must(QueryBuilders.rangeQuery("@timestamp")
.gte(startTime.getTime())
.lte(endTime.getTime()));
// 构建count请求
CountRequest countRequest = new CountRequest("logs-*");
countRequest.query(boolQuery);
// 执行count
CountResponse response = esClient.count(countRequest, RequestOptions.DEFAULT);
return response.getCount();
}
}
设计5:日志分析(聚合)📈
聚合查询
POST /logs-*/_search
{
"size": 0,
"aggs": {
"error_count_by_service": {
"filter": {
"term": {"level": "ERROR"}
},
"aggs": {
"services": {
"terms": {
"field": "service",
"size": 10
}
}
}
}
}
}
返回:
{
"aggregations": {
"error_count_by_service": {
"services": {
"buckets": [
{"key": "order-service", "doc_count": 123},
{"key": "user-service", "doc_count": 45},
{"key": "payment-service", "doc_count": 12}
]
}
}
}
}
时间维度聚合
POST /logs-*/_search
{
"size": 0,
"aggs": {
"error_over_time": {
"filter": {
"term": {"level": "ERROR"}
},
"aggs": {
"timeline": {
"date_histogram": {
"field": "@timestamp",
"calendar_interval": "hour"
}
}
}
}
}
}
返回:
{
"aggregations": {
"error_over_time": {
"timeline": {
"buckets": [
{"key": 1698134400000, "key_as_string": "2023-10-24T00:00:00", "doc_count": 10},
{"key": 1698138000000, "key_as_string": "2023-10-24T01:00:00", "doc_count": 15},
{"key": 1698141600000, "key_as_string": "2023-10-24T02:00:00", "doc_count": 8}
]
}
}
}
}
设计6:日志生命周期管理 🔄
问题
日志数据量:
- 每天:30GB
- 每月:900GB
- 每年:10TB 💀
问题:
- 存储成本高 ❌
- 查询速度慢 ❌
解决方案:ILM(Index Lifecycle Management)
日志生命周期:
Hot(热数据):0-7天
↓
- 高性能SSD存储
- 频繁查询
↓
Warm(温数据):7-30天
↓
- 普通存储
- 偶尔查询
↓
Cold(冷数据):30-90天
↓
- 低成本存储
- 很少查询
↓
Delete(删除):90天后
↓
- 删除数据 ✅
ILM策略配置
PUT /_ilm/policy/logs-policy
{
"policy": {
"phases": {
"hot": {
"actions": {
"rollover": {
"max_size": "50GB",
"max_age": "1d"
}
}
},
"warm": {
"min_age": "7d",
"actions": {
"shrink": {
"number_of_shards": 1
},
"forcemerge": {
"max_num_segments": 1
}
}
},
"cold": {
"min_age": "30d",
"actions": {
"freeze": {}
}
},
"delete": {
"min_age": "90d",
"actions": {
"delete": {}
}
}
}
}
}
🎓 面试题速答
Q1: ELK架构的优势是什么?
A: 三大优势:
-
ElasticSearch:
- 全文检索快(倒排索引)
- 分布式(TB级数据)
- 支持聚合分析
-
Logstash:
- 日志解析(Grok)
- 日志过滤
- 日志转换
-
Kibana:
- 可视化查询
- 图表展示
- 报表统计
Q2: 如何收集分布式系统的日志?
A: Filebeat收集:
# 每台服务器部署Filebeat
filebeat.inputs:
- type: log
paths:
- /var/log/app.log
fields:
service: order-service
# 输出到Logstash
output.logstash:
hosts: ["logstash:5044"]
优点:
- 轻量级(占用资源少)
- 可靠(断点续传)
- 解耦(不影响应用)
Q3: Grok是什么?
A: 日志解析工具:
# Grok Pattern
%{TIMESTAMP_ISO8601:timestamp} %{LOGLEVEL:level} - %{GREEDYDATA:message}
# 原始日志
2023-10-24 14:30:15 ERROR - 订单创建失败
# 解析结果
{
"timestamp": "2023-10-24 14:30:15",
"level": "ERROR",
"message": "订单创建失败"
}
作用:将非结构化日志转换为结构化数据
Q4: 如何快速检索日志?
A: 倒排索引:
文档1:订单创建失败
文档2:用户登录失败
文档3:支付失败
倒排索引:
"订单" → [文档1]
"创建" → [文档1]
"失败" → [文档1, 文档2, 文档3]
"用户" → [文档2]
"登录" → [文档2]
"支付" → [文档3]
搜索"失败":
→ 立即找到 [文档1, 文档2, 文档3] ✅
时间复杂度:O(1)
Q5: 如何管理日志生命周期?
A: ILM策略:
Hot(0-7天):
- SSD存储
- 频繁查询
Warm(7-30天):
- 普通存储
- 偶尔查询
Cold(30-90天):
- 低成本存储
- 很少查询
Delete(90天后):
- 删除数据
好处:
- 节省成本
- 提高性能
Q6: 日志量太大如何优化?
A: 三大优化:
-
日志分级:
- 生产环境:只记录WARN/ERROR
- 开发环境:记录DEBUG
-
采样:
- 高频日志只记录10%
-
过滤:
- Logstash过滤DEBUG日志
filter {
if [level] == "DEBUG" {
drop {}
}
}
🎬 总结
日志收集和分析系统
┌────────────────────────────────────┐
│ 1. Filebeat(日志收集)⭐ │
│ - 轻量级 │
│ - 断点续传 │
└────────────────────────────────────┘
┌────────────────────────────────────┐
│ 2. Logstash(日志处理) │
│ - Grok解析 │
│ - 日志过滤 │
│ - 日志转换 │
└────────────────────────────────────┘
┌────────────────────────────────────┐
│ 3. ElasticSearch(存储+检索)⭐ │
│ - 倒排索引 │
│ - 全文检索 │
│ - 聚合分析 │
└────────────────────────────────────┘
┌────────────────────────────────────┐
│ 4. Kibana(可视化) │
│ - 日志查询 │
│ - 图表展示 │
│ - 报表统计 │
└────────────────────────────────────┘
┌────────────────────────────────────┐
│ 5. ILM(生命周期管理) │
│ - Hot/Warm/Cold/Delete │
│ - 节省成本 │
└────────────────────────────────────┘
🎉 恭喜你!
你已经完全掌握了日志收集和分析系统的设计!🎊
核心要点:
- Filebeat收集:轻量级,断点续传
- Logstash处理:Grok解析,日志过滤
- ElasticSearch存储:倒排索引,全文检索
- Kibana可视化:日志查询,图表展示
- ILM管理:Hot/Warm/Cold/Delete
下次面试,这样回答:
"日志收集系统采用ELK架构。Filebeat部署在每台应用服务器上,轻量级地收集日志文件并发送到Logstash。Filebeat支持断点续传,服务器重启后能从上次断点继续读取,不会丢失日志。
Logstash负责日志处理。使用Grok插件解析非结构化日志,将其转换为结构化的JSON格式。配置过滤器过滤掉DEBUG级别的日志,减少存储压力。解析后的日志发送到ElasticSearch。
ElasticSearch负责日志存储和检索。使用倒排索引实现全文检索,查询速度达到秒级。按照'logs-{service}-{yyyy.MM.dd}'的格式创建索引,每天一个索引,便于按日期删除旧数据。配置3个分片和1个副本,保证高可用和性能。
Kibana提供可视化界面。通过KQL语法查询日志,如'level:ERROR AND service:order-service'。支持聚合分析,统计每个服务的错误数量,绘制时间维度的错误趋势图。
日志生命周期通过ILM策略管理。0-7天的Hot数据存储在高性能SSD上,7-30天的Warm数据迁移到普通存储,30-90天的Cold数据归档,90天后自动删除。这样既保证了查询性能,又节省了存储成本。"
面试官:👍 "很好!你对日志系统的设计理解很深刻!"
本文完 🎬
上一篇: 210-设计一个直播弹幕系统.md
下一篇: 212-设计一个分布式缓存系统.md
作者注:写完这篇,我觉得自己可以去做运维工程师了!📊
如果这篇文章对你有帮助,请给我一个Star⭐!