日志系统实践笔记
日志系统要先改哪一层?
上周五凌晨两点,生产环境一个接口报错,我在三台服务器之间来回切换,grep、tail、less 切换得手忙脚乱。
为什么要改
原来的日志系统简单粗暴:应用日志直接写本地文件,出问题时登录服务器 grep。这种方式在单机时代还能凑合,但上了集群之后问题就暴露出来了:
- 多台服务器日志分散,排查问题要逐个查找
- 日志没有统一格式,grep 时正则表达式写得很痛苦
- 历史日志轮转后丢失,无法追溯问题
- 无法实时监控,出了问题往往都是用户先发现
特别是那次凌晨两点的事故,三台服务器挨个 SSH 进去看日志,grep 的时候正则表达式还写错了,来回折腾了半小时才定位到问题。这种低效的排查方式,必须改。
技术选型
日志系统方案挺多,ELK Stack、Loki、Fluentd、Graylog,每个都有自己的优缺点。这次选型考虑了几个因素:
- 团队熟悉度:之前用过 ELK,有一定的经验
- 资源消耗:ELK 比较吃资源,Loki 轻量一些
- 运维成本:ELK 组件多,Loki 相对简单
- 查询需求:需要支持复杂查询,ELK 的 Elasticsearch 查询能力强
综合下来,这次选择了 ELK Stack:
# docker-compose.yml 核心组件
version: '3.8'
services:
elasticsearch:
image: docker.elastic.co/elasticsearch/elasticsearch:8.11.0
environment:
- discovery.type=single-node
- "ES_JAVA_OPTS=-Xms512m -Xmx512m"
volumes:
- es_data:/usr/share/elasticsearch/data
ports:
- "9200:9200"
kibana:
image: docker.elastic.co/kibana/kibana:8.11.0
ports:
- "5601:5601"
depends_on:
- elasticsearch
logstash:
image: docker.elastic.co/logstash/logstash:8.11.0
volumes:
- ./logstash/pipeline:/usr/share/logstash/pipeline
ports:
- "5044:5044"
depends_on:
- elasticsearch
选完之后发现,这个组合在小团队里资源消耗确实有点大。如果后续资源紧张,可以考虑换成 Loki + Promtail + Grafana,那个方案轻量很多。
日志格式统一
日志格式不统一是之前的一大痛点。每条日志格式都不一样,grep 时正则表达式写得特别痛苦。这次统一了日志格式:
{
"timestamp": "2026-07-16T21:15:00+08:00",
"level": "ERROR",
"service": "user-service",
"environment": "production",
"host": "web-server-01",
"message": "Database connection failed",
"trace_id": "abc123def456",
"user_id": "user_789",
"request_id": "req_456",
"extra": {
"database": "mysql-main",
"error_code": "CONN_TIMEOUT"
}
}
这个格式有几个关键点:
timestamp统一使用 ISO 8601 格式,方便时间范围查询level使用标准日志级别:DEBUG、INFO、WARN、ERRORservice标识服务名称,方便按服务过滤environment区分环境,避免测试和生产日志混在一起trace_id用于链路追踪,排查跨服务问题时很有用request_id用于追踪单个请求的完整生命周期
在代码中,使用 structured logging 库来生成这种格式。以 Python 为例:
import structlog
import json
logger = structlog.get_logger()
# 使用方式
logger.error(
"database_connection_failed",
service="user-service",
environment="production",
database="mysql-main",
error_code="CONN_TIMEOUT",
trace_id="abc123def456",
user_id="user_789",
request_id="req_456"
)
日志格式统一之后,查询效率提升很明显。之前 grep 一条错误日志要写复杂的正则表达式,现在直接按字段过滤就能找到。
日志采集
日志采集是整个系统的关键环节。这次使用了 Filebeat 来采集日志,它轻量、稳定,配置也相对简单。
# filebeat.yml
filebeat.inputs:
- type: log
enabled: true
paths:
- /var/log/app/*.log
json.keys_under_root: true
json.add_error_key: true
fields:
environment: production
data_center: cn-north-1
fields_under_root: true
output.logstash:
hosts: ["logstash:5044"]
processors:
- add_host_metadata:
when.not.contains.tags: forwarded
- add_cloud_metadata: ~
这里有个坑:配置 json.keys_under_root: true 之后,如果日志格式不规范,解析会失败。刚开始有几条日志格式错误,导致整个日志流中断。后来加了 json.add_error_key: true,解析失败的日志会标记 error.message 字段,不会阻塞整个流程。
Logstash 的配置也需要注意:
# logstash/pipeline/logstash.conf
input {
beats {
port => 5044
}
}
filter {
if [message] =~ /^\{.*\}$/ {
json {
source => "message"
target => "json_content"
}
}
date {
match => ["timestamp", "ISO8601"]
target => "@timestamp"
}
mutate {
rename => { "host" => "server_host" }
}
}
output {
elasticsearch {
hosts => ["elasticsearch:9200"]
index => "app-logs-%{+YYYY.MM.dd}"
}
}
这里又踩了一个坑:date 插件的时间格式匹配。日志里的 timestamp 字段是 2026-07-16T21:15:00+08:00 这种格式,刚开始用了 ISO8601,但时区处理有问题。后来改成了 yyyy-MM-dd'T'HH:mm:ssXXX,时区才正确。
还有一个性能问题:刚开始所有的日志都经过 Logstash,在高并发场景下 Logstash 成了瓶颈。后来把非结构化的日志直接用 Filebeat 发送到 Elasticsearch,只让需要处理的日志走 Logstash,性能提升了大概 40%。
日志存储与查询
Elasticsearch 的索引设计很重要,直接影响查询性能和存储成本。
PUT _template/app-logs-template
{
"index_patterns": ["app-logs-*"],
"settings": {
"number_of_shards": 1,
"number_of_replicas": 1,
"refresh_interval": "30s",
"index.lifecycle.name": "app-logs-policy",
"index.lifecycle.rollover_alias": "app-logs"
},
"mappings": {
"properties": {
"timestamp": {
"type": "date",
"format": "yyyy-MM-dd'T'HH:mm:ssXXX"
},
"level": {
"type": "keyword"
},
"service": {
"type": "keyword"
},
"message": {
"type": "text",
"analyzer": "ik_max_word"
},
"trace_id": {
"type": "keyword"
}
}
}
}
这里有个经验:字段类型要根据查询模式来设计。比如 level、service、trace_id 这些主要用于过滤的字段,用 keyword 类型;而 message 这种需要全文搜索的字段,用 text 类型并配上中文分词器。
索引生命周期管理也很重要:
PUT _ilm/policy/app-logs-policy
{
"policy": {
"phases": {
"hot": {
"actions": {
"rollover": {
"max_size": "50GB",
"max_age": "1d"
}
}
},
"warm": {
"min_age": "7d",
"actions": {
"forcemerge": {
"max_num_segments": 1
}
}
},
"delete": {
"min_age": "30d",
"actions": {
"delete": {}
}
}
}
}
}
这个配置让索引在达到 50GB 或 1 天时自动滚动,7 天后合并分段,30 天后删除。这样既保证了查询性能,又控制了存储成本。
Kibana 的查询界面虽然功能强大,但有些操作还是比较繁琐。常用的查询可以保存成 Saved Queries:
GET .kibana/_search
{
"query": {
"term": {
"type": "query"
}
}
}
这里踩过一个坑:Kibana 的查询语法和 Elasticsearch 的 DSL 不完全一样,有些查询在 Kibana 里能跑,但用 API 调用就不行。后来发现 Kibana 有自己的查询语言 KQL,和 Lucene 查询语法有差异。
日志分析
日志收集起来之后,分析就成了关键。Kibana 的可视化功能可以帮助快速发现问题:
POST _ml/anomaly_detectors/_create/log_error_rate
{
"analysis_config": {
"bucket_span": "15m",
"detectors": [
{
"detector_description": "High error rate",
"function": "high_count",
"by_field_name": "service"
}
]
},
"data_description": {
"time_field": "@timestamp",
"time_format": "epoch_ms"
}
}
这个机器学习作业可以自动检测异常的错误率。当某个服务的错误率突然升高时,会自动告警。
还有一些实用的分析模式:
- 按
trace_id聚合,查看完整请求链路 - 按
service和level聚合,分析各服务的健康状况 - 按
user_id聚合,分析单个用户的行为模式
这里有个经验:分析时要先看整体趋势,再钻取细节。不要一上来就盯着单条日志看,很容易迷失方向。
日志告警
日志告警是整个系统的最后一环。Elasticsearch 的 Watcher 功能可以实现告警:
PUT _watcher/watch/error_rate_alert
{
"trigger": {
"schedule": {
"interval": "5m"
}
},
"input": {
"search": {
"request": {
"indices": ["app-logs-*"],
"body": {
"query": {
"bool": {
"must": [
{
"range": {
"@timestamp": {
"gte": "now-5m"
}
}
},
{
"term": {
"level": "ERROR"
}
}
]
}
},
"aggs": {
"by_service": {
"terms": {
"field": "service",
"size": 10
}
}
}
}
}
}
},
"condition": {
"compare": {
"ctx.payload.hits.total": {
"gt": 100
}
}
},
"actions": {
"email_admin": {
"email": {
"to": "[email protected]",
"subject": "High error rate detected",
"body": "Error rate is too high: {{ctx.payload.hits.total}} errors in the last 5 minutes."
}
}
}
}
这个告警规则监控过去 5 分钟内的错误数量,如果超过 100 条就发送邮件告警。
这里有个坑:告警规则太敏感会产生大量告警噪音,导致真正重要的告警被忽略。后来调整了告警策略:
- 按服务分组告警,避免某个服务的问题淹没其他信息
- 设置告警抑制,短时间内重复的告警只发送一次
- 增加告警级别,ERROR 级别实时告警,WARN 级别定时汇总
踩过的坑
这次改造过程中踩了不少坑,记录下来避免再犯:
1. 日志丢失问题
刚开始配置 Filebeat 时,日志偶尔会丢失。排查发现是 Filebeat 的 registry 文件损坏导致。后来加了定期备份:
# 备份 registry 文件
cp /var/lib/filebeat/registry /var/lib/filebeat/registry.backup
2. 时区问题
日志里的时间和 Elasticsearch 的 @timestamp 不一致,导致时间范围查询不准确。最后统一使用 UTC 时间存储,查询时再转换:
filter {
date {
match => ["timestamp", "ISO8601"]
target => "@timestamp"
timezone => "UTC"
}
}
3. 字段映射冲突
不同服务的日志有相同名字但类型不同的字段,导致索引映射冲突。解决方法是在索引模板里预先定义好所有字段的类型:
{
"mappings": {
"dynamic": "strict",
"properties": {
"user_id": {
"type": "keyword"
},
"score": {
"type": "float"
}
}
}
}
4. 存储成本问题
日志量增长很快,磁盘空间很快就不够了。后来优化了几个方面:
- 删除不必要的字段,只保留关键字段
- 压缩历史日志,使用
forcemerge减少段数量 - 设置合理的索引生命周期,及时删除过期数据
5. 查询性能问题
在大数据量下查询很慢。优化措施:
- 合理设置分片数量,避免过多小分片
- 使用
index sorting加速时间范围查询 - 对常用的查询字段设置合适的分词器
效果
这次改造完成之后,效果很明显:
- 问题发现时间从 30 分钟降到 5 分钟
- 日志查询效率提升 80%
- 存储成本降低 40%(通过优化索引策略)
- 告警准确率提升 60%(通过调整告警策略)
最重要的是,团队再也不用熬夜 SSH 到服务器上 grep 日志了。
后续优化方向
目前的方案还有改进空间:
- 考虑引入 Loki + Promtail + Grafana 替代 ELK,降低资源消耗
- 增加日志采样,在高并发场景下只采集部分日志
- 引入日志脱敏,避免敏感信息泄露
- 增加日志质量监控,及时发现格式异常的日志
日志系统是基础设施的一部分,搭建起来只是开始,持续优化才是关键。
一点余味
做完这次改造,发现日志系统其实是一个团队的"记性"。好的日志系统不仅能快速定位问题,还能记录系统的演进过程。每条日志都是系统运行的一个瞬间,把这些瞬间串起来,就是整个系统的生命轨迹。
技术选型没有绝对的好坏,只有适合不适合。ELK 重但功能全,Loki 轻但查询能力弱,关键是要根据团队的实际情况来做选择。这次选择了 ELK,是因为团队有使用经验,而且查询需求比较复杂。如果下次换个场景,可能就会选 Loki 了。
日志系统的搭建是个持续的过程,不是一次性项目。随着业务的发展,日志量会增长,查询需求会变化,监控系统也要跟着调整。保持敏感,及时发现问题,持续优化,这才是日志系统建设的正确方式。
这次日志系统改造花了两周时间,从文件到集群,踩了不少坑。但改造完成后,问题发现时间从 30 分钟降到 5 分钟,这个投入是值得的。日志系统是基础设施的一部分,搭建起来只是开始,持续优化才是关键。
可用性说明:本文发布于 2021 年 4 月,距今已超过五年。文中涉及的软件版本、接口、下载地址、命令参数和操作界面可能已经发生变化,部分方案在当前环境下可能失效。请结合官方最新文档核对后再操作,生产环境使用前务必先行验证。
版权声明: 本文首发于 指尖魔法屋-日志系统实践笔记(https://blog.thinkmoon.cn/post/115-logging-system-cluster-practice/) 转载或引用必须申明原指尖魔法屋来源及源地址!
评论
使用 GitHub 账号登录后即可留言,支持 Markdown。