📊 设计一个日志收集和分析系统:侦探的放大镜!

34 阅读10分钟

📖 开场:破案的线索

想象你是一个侦探 🔍:

没有日志(盲人摸象)

系统出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: 三大优势

  1. ElasticSearch

    • 全文检索快(倒排索引)
    • 分布式(TB级数据)
    • 支持聚合分析
  2. Logstash

    • 日志解析(Grok)
    • 日志过滤
    • 日志转换
  3. 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: 三大优化

  1. 日志分级

    • 生产环境:只记录WARN/ERROR
    • 开发环境:记录DEBUG
  2. 采样

    • 高频日志只记录10%
  3. 过滤

    • Logstash过滤DEBUG日志
filter {
  if [level] == "DEBUG" {
    drop {}
  }
}

🎬 总结

       日志收集和分析系统

┌────────────────────────────────────┐
│ 1. Filebeat(日志收集)⭐           │
│    - 轻量级                        │
│    - 断点续传                      │
└────────────────────────────────────┘

┌────────────────────────────────────┐
│ 2. Logstash(日志处理)             │
│    - Grok解析                      │
│    - 日志过滤                      │
│    - 日志转换                      │
└────────────────────────────────────┘

┌────────────────────────────────────┐
│ 3. ElasticSearch(存储+检索)⭐      │
│    - 倒排索引                      │
│    - 全文检索                      │
│    - 聚合分析                      │
└────────────────────────────────────┘

┌────────────────────────────────────┐
│ 4. Kibana(可视化)                 │
│    - 日志查询                      │
│    - 图表展示                      │
│    - 报表统计                      │
└────────────────────────────────────┘

┌────────────────────────────────────┐
│ 5. ILM(生命周期管理)              │
│    - Hot/Warm/Cold/Delete          │
│    - 节省成本                      │
└────────────────────────────────────┘

🎉 恭喜你!

你已经完全掌握了日志收集和分析系统的设计!🎊

核心要点

  1. Filebeat收集:轻量级,断点续传
  2. Logstash处理:Grok解析,日志过滤
  3. ElasticSearch存储:倒排索引,全文检索
  4. Kibana可视化:日志查询,图表展示
  5. 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⭐!