在真实的分布式项目里,日志是最容易启动又最难做好的基础设施之一。机器少的时候,登录服务器执行tail -f就能定位问题;机器一旦超过几十台,业务日志、中间件日志、监控日志散落在不同目录,定位一次接口超时可能要翻七八台机器,效率非常低。这几年我在团队内部持续维护一套日志采集与分析平台,项目代号叫“雾山实录”,当前版本迭代到了 29.7。这一版本的重点,是把日志从应用产生到 Kibana 可检索的时间控制在分钟级,同时让异常堆栈、调用链 TraceID 和业务字段都能被结构化解析。本文以这个项目为线索,完整记录设计和落地过程,包括架构选型、关键配置、运行验证,以及上线后最容易踩的坑。
1. 先想清楚:日志散落的时候,问题到底出在哪
1.1 一套日志系统要解决的不只是“看日志”
先看一个典型场景。线上商品服务报了一个“库存扣减失败”,但库存服务在另一批机器上,订单服务在第三批机器上,网关日志又在单独的目录里。要定位这个问题,通常需要这样操作:
- 登录订单服务机器,搜订单号。
- 登录库存服务机器,搜商品 ID 和扣减流水。
- 登录网关机器,看入口请求参数。
- 查看数据库慢日志,确认是不是 SQL 执行超时。
- 最后把时间点对齐,人工拼接整个调用过程。
这个过程的第一个问题是慢。第二个问题是容易漏,因为不同服务日志格式不统一,有的打印 JSON,有的打印一行无规则文本,有的把堆栈折成多行,导致grep很不方便。
如果把各服务日志集中到一个平台上,再按时间、服务名、TraceID 做索引,定位过程就可以从“登录很多台机器”压缩成“在一个查询框里输入关键字”。
从这张对比表能看出,统一日志平台解决的是排查效率问题,而不仅是日志存储问题:
| 能力点 | 分散日志 | 统一日志平台 |
|---|---|---|
| 日志查找 | 逐台登录服务器 | 一个搜索入口 |
| 跨服务关联 | 人工拼接 | TraceID 串联 |
| 异常分析 | 看单行文本 | 结构化字段聚合 |
| 历史回溯 | 覆盖或丢失 | 按索引长期保存 |
| 告警能力 | 脚本轮询文件 | 按规则实时触发 |
1.2 统一日志平台的核心职责
“雾山实录”v29.7 在职责划分上很明确,它只做五件事:
- 采集:从应用日志文件、标准输出、中间件日志中读取日志。
- 缓冲:把日志写入 Kafka,避免应用直接依赖 Elasticsearch,防止 ES 抖动时拖垮业务。
- 清洗:用 Logstash 解析日志内容,补全字段,删除无用字段,统一时区。
- 存储与检索:写入 Elasticsearch,建立索引,提供服务端全文检索和聚合能力。
- 可视化与告警:通过 Kibana 展示日志,通过查询规则触发异常告警。
这里最容易被忽略的是“缓冲”这一层。很多团队刚开始只做 Filebeat 到 ES,日志量小的时候没问题,一旦突发流量导致 ES 写入变慢,Filebeat 会积压,应用日志目录会膨胀,甚至触发磁盘报警。加一层 Kafka,等于给日志生产和日志消费之间加了一个削峰填谷的缓冲,生产端不用关心消费端是否健康。
1.3 方案选型:什么时候用 ELK,什么时候用 Loki,什么时候用 ClickHouse
“雾山实录”当前版本使用的是 Filebeat + Kafka + Logstash + Elasticsearch + Kibana 这套组合,业内一般称为 ELK 技术栈。选型之前也对比过其他方案,核心结论是:没有最好的技术栈,只有适合当前场景的技术栈。
| 方案 | 擅长场景 | 主要短板 | 落地成本 |
|---|---|---|---|
| ELK 技术栈 | 全文检索、复杂查询、字段类型丰富 | 组件多,运维成本高 | 较高 |
| Loki | Kubernetes 环境,轻量级日志聚合 | 全文检索能力弱,适合标签检索 | 较低 |
| ClickHouse | 日志聚合分析、量大、统计类查询 | 单条日志检索和高亮体验不如 ES | 中等 |
如果团队规模不大,日志量以 GB 为量级,可以直接用 Loki。如果需要按关键字全文搜索、做上下文排查、保留复杂查询,ELK 更成熟。ClickHouse 更适合已经有明确分析维度、以统计分析为主的日志仓库,不太适合当成一对一的在线排查入口。
“雾山实录”选 ELK 的原因,是业务侧排查问题主要依赖关键字搜索和字段过滤,Elasticsearch 的全文索引和 Kibana 的交互式查询体验更适合一线开发使用。
2. 雾山实录 v29.7 的整体架构和数据结构
2.1 版本号不能只看数字,要看它承载的迭代目标
“雾山实录”这个名字是内部代号,“雾山”表示生产环境,因为生产环境很多时候就像被雾罩住一样,不把日志和指标捞出来,根本看不清线上到底发生了什么;“实录”强调日志必须真实、完整、不可随意丢弃。29.7 表示当前是第 29 个大版本下的第 7 个小版本迭代。
29.7 这个版本解决的核心问题有三个:
- 让业务日志从产生到 Elasticsearch 可检索的时间控制在分钟级。
- 统一所有服务的日志格式,强制使用 JSON 结构化日志。
- 把 TraceID 与日志字段打通,使一次请求在多服务之间的日志能被一键串联。
版本迭代到这一步,说明平台不是从零开始,而是已经在生产中运行了很长时间。阅读本文时,不必纠结这个具体版本号,只需理解它代表的工程阶段:已有稳定底座,正在优化细节。
2.2 组件角色划分
整套链路包含五个核心组件,每个组件的职责要分清楚,否则后续排查问题时不知道日志卡在哪一层。
| 组件 | 职责 | 关键关注点 |
|---|---|---|
| 业务应用 | 产生结构化日志 | 日志格式、TraceID 传递、脱敏 |
| Filebeat | 读取日志文件并传输 | 文件路径、offset 管理、性能 |
| Kafka | 日志缓冲与分发 | Topic 分区数、副本数、消费组 |
| Logstash | 解析与清洗日志 | Pipeline 配置、字段映射、时区 |
| Elasticsearch + Kibana | 存储、检索与展示 | 索引模板、生命周期、查询性能 |
2.3 数据流转架构
用文本图可以直观表示整条日志链路:
应用日志文件 | v Filebeat 采集 | v Kafka Topic: app-log | v Logstash Consumer | v Elasticsearch Index: app-log-2025.01.15 | v Kibana Discovery / Alert数据流上,Filebeat 只负责读文件和写 Kafka;Logstash 只负责消费 Kafka 和写 ES;业务应用不直接依赖 Logstash 或 ES。这样拆的好处是每一层的稳定性可以被单独保证,任意一层挂掉,上游数据都不会丢。
2.4 日志事件的数据模型
日志写入 Kafka 时,已经是 JSON 格式。下面是一个标准日志事件示例:
{ "@timestamp": "2025-01-15T10:30:00.000+08:00", "level": "ERROR", "logger": "com.demo.order.service.StockService", "message": "库存扣减失败", "thread": "http-nio-8080-exec-3", "traceId": "a1b2c3d4e5f6a7b8", "spanId": "a1b2c3d4e5f6a7b9", "service": "order-service", "host": "10.0.3.11", "env": "prod", "exception": "java.lang.RuntimeException: stock not enough\n\tat com.demo.order.service.StockService.deduct(StockService.java:88)" }字段含义如下:
| 字段 | 含义 | 是否必填 |
|---|---|---|
| @timestamp | 日志产生时间 | 是 |
| level | 日志级别 | 是 |
| logger | 记录该日志的类或包名 | 是 |
| message | 业务描述信息 | 是 |
| traceId | 调用链追踪 ID | 建议必填 |
| spanId | 单次调用单元 ID | 建议必填 |
| service | 服务名 | 是 |
| host | 主机 IP | 是 |
| env | 环境标识 | 是 |
| exception | 异常堆栈 | 异常时必填 |
这个模型的关键是“结构化”。非结构化日志是一行文本,解析时只能靠正则;结构化日志本身就是 JSON,Logstash 可以直接把字段映射到 ES,后续查询和聚合都方便得多。
3. 环境准备:把 Kafka、Elasticsearch 和 Filebeat 跑起来
3.1 环境与版本要求
下面以 Kafka 3.5、Elasticsearch 8.8、Logstash 8.8、Filebeat 8.8、Kibana 8.8 为例说明配置方式。实际项目中,版本必须提前对齐,尤其是 Elasticsearch、Logstash、Filebeat、Kibana 这四者,尽量使用同一主版本,否则可能出现协议不兼容或字段解析异常。
环境要求可以按下表准备:
| 依赖项 | 建议值 |
|---|---|
| JDK | 17 或与 Elasticsearch 版本匹配的 JDK |
| 内存 | Filebeat 1GB,Logstash 2GB,ES 4GB 以上 |
| Kafka | 3.x 以上,建议使用 KRaft 模式 |
| 磁盘 | 预留日志索引容量,建议 SSD |
| 操作系统 | Linux 发行版均可 |
个人学习和测试环境,可以先用 Docker Compose 起单机版,生产环境至少考虑三节点 ES 和 Kafka 集群。
3.2 启动 Kafka 并创建 Topic
Kafka 在高版本中推荐使用 KRaft 模式,不再依赖 ZooKeeper。下面是启动流程:
wget https://archive.apache.org/dist/kafka/3.5.0/kafka_2.13-3.5.0.tgz tar -xzf kafka_2.13-3.5.0.tgz cd kafka_2.13-3.5.0 # 生成集群 ID KAFKA_CLUSTER_ID=$(bin/kafka-storage.sh random-uuid) # 格式化存储目录 bin/kafka-storage.sh format -t $KAFKA_CLUSTER_ID -c config/kraft/server.properties # 启动 Kafka bin/kafka-server-start.sh config/kraft/server.properties启动后再创建日志专用 Topic:
bin/kafka-topics.sh --create \ --topic app-log \ --partitions 3 \ --replication-factor 1 \ --bootstrap-server localhost:9092分区数需要根据日志量评估。测试环境 3 个分区够用,生产环境建议按吞吐量估算:单个分区写入压力有限,分区越多 Logstash 并发消费能力越强,但 ES 端写入分片也需要同步评估,并非越多越好。
验证 Topic 是否创建成功:
bin/kafka-topics.sh --describe --topic app-log --bootstrap-server localhost:90923.3 安装 Filebeat 并读取应用日志
Filebeat 是整套链路中最轻量的一层,部署在业务服务器上,负责读取日志文件。下载安装后,重点修改filebeat.yml:
filebeat.inputs: - type: filestream enabled: true paths: - /logs/app/order-service/*.log parsers: - ndjson: target: "" add_error_key: true output.kafka: hosts: ["localhost:9092"] topic: "app-log" partition.round_robin: reachable_only: false required_acks: 1 compression: gzip max_message_bytes: 10485760 logging.level: info这里有几个关键点:
filestream类型是 Filebeat 7.13 之后推荐的输入方式,它比log输入方式更善于管理文件 offset。parsers.ndjson表示按 JSON 解析每一行日志,解析结果会作为顶层字段输出到 Kafka。required_acks: 1表示 Kafka 写入确认级别。日志场景可以接受少量丢失,优先保证吞吐。max_message_bytes控制单条消息最大值,默认 10MB,避免异常堆栈超过默认值时直接报错。
启动 Filebeat:
./filebeat -e -c filebeat.yml如果日志目录下已经有历史日志文件,Filebeat 会从文件末尾开始读,也可通过配置ignore_older和scan_frequency控制读取策略。需要注意,新版本默认行为可能与旧版本不同,落地前先在小流量环境验证。
4. 应用接入:从 logback 到 Kafka 的结构化日志
4.1 为什么在应用层就做结构化?
有些团队把日志解析的工作完全交给 Logstash,应用层打印普通文本,再用正则解析。这个方案能跑通,但有两个问题:
- 正则解析非常脆弱,代码里只要多打印一个空格,ES 里的字段就解析失败。
- 应用层不结构化,开发人员在本地看日志也不够直观。
“雾山实录”推荐的方式是:应用层直接输出 JSON 日志,Logstash 只做字段补全和类型纠正,不依赖复杂正则。这样可以大幅降低清洗层的故障率。
4.2 Maven 依赖配置
以 Spring Boot 项目为例,在pom.xml中加入 logstash-logback-encoder:
<dependency> <groupId>net.logstash.logback</groupId> <artifactId>logstash-logback-encoder</artifactId> <version>7.4</version> </dependency>实际项目要根据自己使用的 logback 版本选择匹配的 encoder 版本,最好先查看对应版本的兼容说明,不要在未确认版本的情况下直接复制依赖。
4.3 logback-spring.xml 配置 Kafka Appender
要让日志从应用直接写入 Kafka,需要在logback-spring.xml中配置 Kafka appender。下面是一个最小可运行配置:
<configuration> <appender name="KAFKA" class="com.github.danielwegener.logback-kafka-appender.KafkaAppender"> <encoder class="net.logstash.logback.encoder.LogstashEncoder"> <includeMdc>true</includeMdc> <customFields>{"service":"order-service"}</customFields> <throwableConverter class="net.logstash.logback.encoder.composite.loggingevent.ThrowableProxyConverter"> <maxLength>20000</maxLength> </throwableConverter> </encoder> <topic>app-log</topic> <keyingStrategy class="com.github.danielwegener.logback-kafka-appender.keying.RoundRobinKeyingStrategy"/> <producerConfig>bootstrap.servers=localhost:9092</producerConfig> <producerConfig>max.block.ms=1000</producerConfig> </appender> <root level="INFO"> <appender-ref ref="KAFKA"/> </root> </configuration>这里需要说明,logback-kafka-appender 是一个第三方库,不同版本对 logback 的兼容性不同。生产环境更稳妥的方式是:应用只写本地日志文件,Filebeat 再采集日志文件写入 Kafka。这样应用和 Kafka 之间不直接耦合,Kafka 短暂不可用时业务日志依然留在磁盘上。
“雾山实录”在生产环境采用的就是“应用写文件 -> Filebeat 采集”的方式,避免第三方 appender 带来的稳定性风险。学习阶段可以直接使用上述配置验证链路,生产环境建议退回文件采集方案。
4.4 通过 MDC 写入 TraceID
日志里的 TraceID 不是自动出现的,需要应用在入口处生成,并放到 SLF4J 的 MDC 中。下面是典型做法:
import org.slf4j.MDC; public class TraceIdFilter implements javax.servlet.Filter { @Override public void doFilter(javax.servlet.ServletRequest request, javax.servlet.ServletResponse response, javax.servlet.FilterChain chain) throws java.io.IOException, javax.servlet.ServletException { String traceId = UUID.randomUUID().toString().replace("-", ""); MDC.put("traceId", traceId); try { chain.doFilter(request, response); } finally { MDC.remove("traceId"); } } }关键点有两个:
- 在 finally 中移除 MDC,否则线程池复用线程时,TraceID 会串到其他请求上。
- 调用远程服务时,需要把 TraceID 放到 HTTP Header 中向下游传递,例如
X-Trace-Id。
logback 配置中使用%X{traceId}可以把 MDC 中的值打进日志:
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender"> <encoder> <pattern>%date{ISO8601} [%thread] %-5level %logger{36} traceId=%X{traceId} - %msg%n</pattern> </encoder> </appender>如果使用 LogstashEncoder,并且开启了includeMdc,traceId字段会自动出现在 JSON 里。
4.5 从源头做敏感信息脱敏
日志平台上线后,最怕的就是把手机号、身份证号、Token 直接打印到日志里。一旦日志被采集到中央平台,删除成本很高,因此脱敏必须从源头做起。
最基础的做法是定义日志工具类,对需要打印的业务对象统一做脱敏处理:
public class MaskUtils { public static String maskMobile(String mobile) { if (mobile == null || mobile.length() != 11) { return mobile; } return mobile.substring(0, 3) + "****" + mobile.substring(7); } public static String maskIdCard(String idCard) { if (idCard == null || idCard.length() < 8) { return idCard; } return idCard.substring(0, 4) + "**********" + idCard.substring(14); } }更完整的方式是自定义 logback converter,在输出前对日志内容做全局正则替换。注意,这种情况下只能在日志内容层面脱敏,已经进入业务方法参数里的值是否安全,需要结合代码审查一起控制。
5. 数据入库:Logstash 清洗和 Elasticsearch 索引设计
5.1 Logstash Pipeline 核心配置
Logstash 负责从 Kafka 拉取日志,经过 filter 处理后写入 ES。下面是pipeline.conf的核心配置:
input { kafka { bootstrap_servers => "localhost:9092" topics => ["app-log"] group_id => "logstash-app-log" codec => "json" consumer_threads => 4 } } filter { mutate { rename => { "log" => "raw_entry" } } date { match => ["@timestamp", "ISO8601"] timezone => "Asia/Shanghai" } mutate { remove_field => ["raw_entry", "ecs", "agent", "input"] } } output { elasticsearch { hosts => ["http://localhost:9200"] index => "app-log-%{+YYYY.MM.dd}" } }这里有几个容易踩坑的地方:
codec => "json"表示把 Kafka 消息按 JSON 解析。如果应用侧已经按 JSON 输出,这里才能正常工作。rename => { "log" => "raw_entry" }是把 Filebeat 包装层留下的原始字段改名,避免覆盖真正的日志字段。date插件用来解析@timestamp,如果日志时间戳是2025-01-15T10:30:00.000+08:00,使用ISO8601即可。- 删除
ecs、agent、input等字段,可以减少 ES 索引体积。生产环境建议保留host和service,删除掉无用的元数据字段。
5.2 Elasticsearch 索引模板与生命周期
为了让日志索引具备合理的分片、副本和字段类型,需要提前创建索引模板:
PUT _index_template/app-log { "index_patterns": ["app-log-*"], "template": { "settings": { "number_of_shards": 3, "number_of_replicas": 1, "index.lifecycle.name": "app-log-policy", "index.lifecycle.rollover_alias": "app-log" }, "mappings": { "properties": { "@timestamp": { "type": "date" }, "level": { "type": "keyword" }, "message": { "type": "text", "analyzer": "ik_max_word" }, "logger": { "type": "keyword" }, "traceId": { "type": "keyword" }, "spanId": { "type": "keyword" }, "service": { "type": "keyword" }, "host": { "type": "keyword" }, "env": { "type": "keyword" }, "exception": { "type": "text" } } } } }字段类型的选择很关键。level、service、traceId这类字段应该用keyword,因为它们主要用于精确匹配、聚合、排序;message和exception应该用text,因为要支持全文检索。如果日志量很大,message的全文索引会占用较多磁盘,需要结合保留周期一起规划。
5.3 创建索引生命周期策略
索引生命周期策略用来控制日志保留时间:
PUT _ilm/policy/app-log-policy { "policy": { "phases": { "hot": { "min_age": "0ms", "actions": { "rollover": { "max_size": "50GB", "max_age": "30d" } } }, "delete": { "min_age": "180d", "actions": { "delete": {} } } } } }这个策略的含义是:单个索引超过 50GB 或 30 天后滚动新索引,日志保留 180 天后删除。生产环境把保留时长作为参数放在配置中心,方便按业务和合规要求调整。
5.4 写入性能调整
当日志量变大时,Logstash 和 ES 的默认参数可能不够用。常见调整方向如下:
| 参数 | 位置 | 作用 | 调整建议 |
|---|---|---|---|
consumer_threads | Logstash 输入 | 增加 Kafka 消费并发 | 与分区数一致,不超过分区数 |
batch.size | Logstash 输出 | 增大批量写入条数 | 默认 125,可调到 500 观察 |
index.refresh_interval | ES 索引设置 | 控制可检索延迟 | 默认 1s,日志场景可调 5s |
index.translog.durability | ES 索引设置 | 控制数据落盘策略 | 可调 async 提升性能 |
number_of_shards | 索引模板 | 控制分片数量 | 根据写入峰量和节点数评估 |
注意,索引refresh_interval调大后,日志出现在 Kibana 中的时间会更晚。日志平台通常可以接受 5 到 10 秒的延迟,不需要追求毫秒级可见。
6. 运行验证:从乱日志到可检索字段
6.1 链路启动检查清单
完整链路启动后,不要直接去 Kibana 搜日志,要先按下面顺序确认每一层是通的:
- Kafka Topic 存在,消费者组能看到 Logstash 连接。
- Filebeat 日志无报错,registry 文件有记录。
- Logstash 管道状态为 running,无 error 日志。
- Elasticsearch 中已经有
app-log-*索引。 - Kibana 中已经创建 Data View。
Kafka 查看消费组命令:
bin/kafka-consumer-groups.sh --describe --group logstash-app-log --bootstrap-server localhost:9092如果消费组LAG一直增大,说明 Logstash 消费速度跟不上生产速度,需要重点排查 Logstash 输出到 ES 的效率。
6.2 制造一条测试日志
在业务应用中触发一条 ERROR 日志:
log.error("库存扣减失败, orderId={}, skuId={}", orderId, skuId, new RuntimeException("stock not enough"));然后观察本地日志文件:
{"@timestamp":"2025-01-15T10:30:00.000+08:00","level":"ERROR","logger":"com.demo.order.service.StockService","message":"库存扣减失败, orderId=123456, skuId=1001","thread":"http-nio-8080-exec-3","traceId":"a1b2c3d4e5f6a7b8","service":"order-service"}6.3 在 Kibana 中检索结果
在 Kibana 的 Discover 页面,用以下查询条件过滤:
service:order-service AND level:ERROR搜索结果里应该能看到刚才那条日志。点击展开后,能直接看到traceId、message、exception等字段。
验证检索是否生效,还可以测试message的全文搜索:
message:"库存扣减失败"如果返回结果为空,但精确搜索service字段正常,多半是message字段类型写成keyword了。
6.4 验证多服务链路串联
如果订单服务和库存服务都接入了同一套日志平台,可以在业务代码中让库存服务收到请求后,从 Header 中读取 TraceID,并写入自己的 MDC。这样两边日志里的traceId相同。
在 Kibana 中搜索:
traceId:"a1b2c3d4e5f6a7b8"如果能看到订单服务、库存服务、网关服务的多条日志按时间顺序排列,说明全链路日志关联已经打通。这一步是整个日志平台价值最大、也最容易做坏的地方,很多系统虽然接了日志平台,但因为没有 TraceID,跨服务定位依然靠猜。
7. 上线后最常见的问题排查清单
7.1 Filebeat 能读文件,但 Kafka 没有消息
现象:Filebeat 启动正常,日志文件也有内容,Kafka Topic 里却消费不到数据。
排查顺序:
- 先确认 Topic 名称是否一致,
filebeat.yml中output.kafka.topic的值必须和 Kafka 中创建的主题完全一致。 - 查看 Filebeat 日志中是否有权限错误或连接失败。
- 查看 Filebeat 的 registry 文件位置,确认是否已经消费过该文件。
- 确认日志路径通配符是否匹配实际文件名。
如果本地文件有大量历史日志,Filebeat 会从文件末尾开始读,可以先删除 registry 文件后重启测试,但要小心,删除 registry 文件可能导致重复消费历史日志。
7.2 日志进了 Kafka,但 ES 中没有索引
现象:Kafka 中消息堆积,Logstash 消费正常,但 ES 没有产生新索引。
常见原因:
| 常见原因 | 检查方式 | 处理建议 |
|---|---|---|
| Logstash 配置了错误 Topic | 查看 Logstash 日志 | 修改topics参数 |
| ES 认证或分片数超限 | 查看 Logstash 输出日志 | 检查 ES 集群健康状态 |
| JSON 解析失败 | 查看 Logstash 日志中_jsonparsefailure | 回看应用侧日志格式 |
| Logstash group_id 与旧版重复 | 查看消费组 offset | 重置消费组或更换 group_id |
7.3 时间字段和本地时间差了 8 小时
现象:日志产生时间是北京时间 18:00,Kibana 中显示的却是 10:00。
原因很明确:日志时间戳带时区,但 Logstash 解析时没有指定 timezone,或者应用打印时间时使用了 UTC。
处理方式:
- 统一规范:所有应用只输出带时区的 ISO8601 时间。
- Logstash 的
date插件中明确设置timezone => "Asia/Shanghai"。 - Kibana 高级设置中确认当前时区为
Asia/Shanghai或浏览器时区。
生产环境建议统一使用 UTC 存储、展示时按本地时区转换,避免不同机房、不同环境下出现歧义。
7.4 异常堆栈被截断或变成一行不可读
现象:Kibana 中exception字段只有一半内容,或者多行堆栈被折叠成一行。
原因有两个方向:
- logback 的
ThrowableProxyConverter设置了maxLength,堆栈超过长度后被截断。 - 日志写入文件时没有做换行转义,Filebeat 按 ndjson 解析失败,堆栈被当作多行文本读取。
解决方式:在 logback 中将maxLength调大到 20000 或 30000,同时保证 JSON encoder 对换行符做转义,Filebeat 侧使用multiline配置时更要注意不要把 JSON 结构拆开。推荐做法是应用层直接输出单行 JSON,由 logstash-logback-encoder 负责把堆栈编码成可解析的 JSON 字符串。
7.5 日志重复消费或丢失
现象:ES 中同一个 traceId 出现两条内容相同的日志,或者某个时间点日志缺失。
排查重点:
| 方向 | 原因 | 处理建议 |
|---|---|---|
| 重复消费 | group.id变更导致 Kafka 从旧 offset 重新消费 | 使用稳定的 group_id,记录消费进度 |
| 重复采集 | Filebeat registry 被删除 | 避免随意删除 registry 文件 |
| 日志丢失 | Filebeat 来不及读取,日志被 logrotate 切割 | 调大scan_frequency,配置 logrotate 延迟压缩 |
| 日志丢失 | Kafkamax.message.bytes小于单条日志大小 | 同步调整 Filebeat 和 Kafka Broker 参数 |
对于“重复消费”,日志平台本身可以容忍少量重复,关键是不能丢失。因此生产环境 Kafka 的acks可以设置为all,相比日志吞吐,数据完整性更重要。
8. 生产环境最佳实践与下一步扩展
8.1 日志分级和采样策略
不能所有日志都全量采集,否则日志量大到一定程度,存储成本会迅速失控。合理的策略是:
| 日志级别 | 策略 | 说明 |
|---|---|---|
| ERROR | 全量采集 | 必须完整保留,用于问题定位 |
| WARN | 全量或按错误率采样 | 需要关注但不需要每一条都保留 |
| INFO | 按需保留关键业务日志 | 避免在循环中打印大量 INFO |
| DEBUG | 默认关闭 | 排查时临时开启,事后关闭 |
对于调用链日志,还可以按 TraceID 做采样。比如 1% 的请求保留完整链路日志,其余请求只保留 ERROR 日志,这样可以大幅降低日志量,同时保留排障能力。
8.2 敏感信息脱敏规则
日志平台建设越深入,安全要求越高。上线前需要建立一份脱敏清单:
- 手机号:保留前 3 后 4 位,中间用
****代替。 - 身份证号:保留前 4 后 4 位,中间脱敏。
- 银行卡号:只保留后 4 位。
- 密码、Token、Cookie:一律不输出。
- SQL 参数:避免打印用户输入中的敏感内容。
脱敏逻辑应该在应用层完成,不能只依赖 Logstash 过滤。因为日志一旦落盘或写入 Kafka,就存在被其他系统读取的风险。
8.3 存储成本控制
Elasticsearch 是磁盘占用大户。控制成本的手段包括:
- 删除无用字段,减少单条日志体积。
- 调整
refresh_interval,降低索引写入开销。 - 使用索引生命周期策略,将旧索引转入冷存储或直接删除。
- 对
message和exception字段按需设置全文索引,不是所有字段都需要分词。 - 对不需要全文检索的字段使用
keyword,减少倒排索引开销。
注意:不要为了省空间把
message字段也设成keyword,那样等于放弃了关键字检索能力。日志平台的存核心价值就是“能搜到”,存储成本要和检索能力一起权衡。
8.4 从日志中心走向可观测性
“雾山实录”当前版本已经从日志平台扩展成三类数据的统一入口:
- 日志(Log):负责输出业务细节和异常堆栈。
- 指标(Metrics):用 Prometheus 采集 CPU、内存、QPS、错误率。
- 链路(Trace):用 SkyWalking 或 Tempo 采集分布式调用链。
三类数据用同一个 TraceID 关联,排查问题时可以在一套界面上先看指标,再点进链路,最后看对应日志。这个演进方向比单纯堆日志组件更有价值,因为很多线上故障是“指标正常但日志异常”或“日志正常但链路超时”,只有三种数据关联起来,才能完整还原现场。
8.5 可复用的上线检查清单
每次新服务接入日志平台,建议按下面清单检查:
| 检查项 | 检查方式 |
|---|---|
| 日志文件路径是否被 Filebeat 覆盖 | 查看 Filebeat 配置和日志 |
| 是否输出 JSON 结构化日志 | 查看本地日志文件 |
| 是否正确写入 TraceID | 调用一次接口,比对 MDC |
| 敏感字段是否脱敏 | 在 Kibana 中搜索手机号、Token 样例 |
| 日志级别是否关闭 DEBUG | 检查 logback 配置 |
| 时区是否统一 | 对比本地时间与 Kibana 展示时间 |
| 索引模板是否生效 | 查看_index_template和实际索引 settings |
| 生命周期策略是否绑定 | 检查索引 setting 中的 ILM 策略 |
| 消费组是否稳定 | 观察 Kafka 消费组 LAG |
| 磁盘容量是否充足 | 监控 ES 节点磁盘使用率 |
这套清单在接入新服务时可以直接复用,每项检查时间不超过 5 分钟,但能避免 80% 以上的基础配置问题。
日志平台这种基础设施,最怕的不是组件多,而是接入不规范。只要从第一天要求所有应用输出结构化日志、统一 TraceID 格式、明确脱敏规则,后面维护成本会低很多。下一步可以重点把链路追踪和日志平台打通,再逐步用告警规则替代人工搜索,让“雾山实录”从“被动查日志”变成“主动发现问题”的工程基础。