日志与监控体系
问题
服务端如何设计日志和监控体系?前端需要了解哪些?
服务端日志和监控体系怎么设计? 核心是「可观测性三支柱」:日志(Logging)、指标(Metrics)、链路追踪(Tracing):
- 日志:
Winston/Pino输出结构化 JSON(带traceId、userId),通过Filebeat → Logstash → Elasticsearch → Kibana(ELK)或Loki + Grafana检索。 - 指标:
Prometheus抓/metrics端点(QPS、P99、错误率、CPU/内存),Grafana出图,Alertmanager告警。 - 链路追踪:
OpenTelemetry标准 +Jaeger/Zipkin,每次请求生成traceId串起所有服务,定位跨服务慢调用。 - 日志分级:
error/warn/info/debug严格区分,生产用info,错误日志单独落文件触发告警。
前端工程师为什么也要懂?
- 排查线上问题靠
traceId串联前后端日志,前端上报和后端日志要对齐。 - BFF/Node 服务出错时,自己得会看 Kibana、Grafana,不能凡事找运维。
- 前端监控(Sentry、自建 SDK)的设计思路和服务端监控是相通的。
答案
监控体系全景
日志分级
import winston from 'winston';
const logger = winston.createLogger({
level: process.env.LOG_LEVEL || 'info',
format: winston.format.combine(
winston.format.timestamp(),
winston.format.json(),
),
transports: [
new winston.transports.Console(),
new winston.transports.File({ filename: 'error.log', level: 'error' }),
new winston.transports.File({ filename: 'combined.log' }),
],
});
// 日志级别(从高到低)
// error: 错误,需要立即处理
// warn: 警告,可能有问题
// info: 信息,正常业务流程
// debug: 调试,开发阶段使用
| 级别 | 用途 | 示例 |
|---|---|---|
| error | 系统错误 | 数据库连接失败、未捕获异常 |
| warn | 潜在问题 | 接口超时重试、缓存未命中率过高 |
| info | 业务流程 | 用户登录、订单创建、支付完成 |
| debug | 调试信息 | 请求参数、SQL 查询、中间状态 |
结构化日志
// ✅ 结构化日志(易于搜索和分析)
logger.info('Order created', {
orderId: 'ORD-001',
userId: 'USR-123',
amount: 99.99,
duration: 45, // 处理耗时 ms
traceId: 'abc-def-123',
});
// ❌ 非结构化日志(难以检索)
logger.info(`User USR-123 created order ORD-001, amount: 99.99`);
请求链路追踪
import { v4 as uuid } from 'uuid';
// 中间件:为每个请求添加 traceId
const traceMiddleware = (req: Request, res: Response, next: NextFunction) => {
const traceId = req.headers['x-trace-id'] as string || uuid();
req.traceId = traceId;
res.setHeader('x-trace-id', traceId);
// 记录请求日志
logger.info('Request received', {
traceId,
method: req.method,
path: req.path,
userAgent: req.headers['user-agent'],
ip: req.ip,
});
// 记录响应日志
const start = Date.now();
res.on('finish', () => {
logger.info('Response sent', {
traceId,
statusCode: res.statusCode,
duration: Date.now() - start,
});
});
next();
};
日志全生命周期:从产生到归档
面试时如果只说"用 Winston 记日志"是不够的——面试官想听的是从应用产生日志到最终归档的完整链路。下面按阶段讲清楚。
① 日志产出:写到哪里?
| 输出方式 | 说明 | 适用场景 |
|---|---|---|
| stdout/stderr | 直接打到标准输出,由容器运行时(Docker/K8s)接管 | 容器化部署推荐,符合 12-Factor App 原则 |
| 本地文件 | 写入 /var/log/app/*.log,需配置轮转 | 传统虚拟机部署 |
| 直接推送 | 应用内直接调 HTTP/TCP 推送到日志服务 | 边缘场景、Serverless |
在 Docker/K8s 环境中,应用只管往 stdout 输出,不要自己管理文件。容器运行时会自动把 stdout 写入 /var/lib/docker/containers/<id>/<id>-json.log,再由日志采集器统一收集。好处是应用和日志基础设施完全解耦。
// 生产环境输出 JSON 到 stdout,由 Filebeat/Fluentd 采集
const logger = winston.createLogger({
format: winston.format.combine(
winston.format.timestamp(),
winston.format.json(), // 必须是 JSON,不要 pretty-print
),
transports: [
new winston.transports.Console(), // stdout
],
});
② 日志采集:谁来收集?
应用产出日志后,需要一个采集 Agent 把日志从各节点收集起来,送到下一步处理。
| 采集器 | 特点 | 典型搭配 |
|---|---|---|
| Filebeat | Elastic 官方,Go 写,轻量,适合发送到 Logstash/ES | ELK 全家桶 |
| Fluentd / Fluent Bit | CNCF 项目,插件丰富,K8s 标配 | K8s + Loki/ES/S3 |
| Vector | Rust 写,高性能,灵活路由 | 替代 Logstash + Filebeat |
| Promtail | Grafana 官方,专门采集发送到 Loki | Loki + Grafana |
采集器的工作方式:
- 以 DaemonSet(K8s)或 Sidecar 方式部署在每个节点
- Tail 监听日志文件或 Docker stdout
- 添加元信息(Pod 名、Namespace、Node 等)
- 缓冲 + 批量发送到下游,支持断点续传(offset 记录)
filebeat.inputs:
- type: container
paths:
- /var/lib/docker/containers/*/*.log
# 自动解析 Docker JSON 格式
json.keys_under_root: true
json.add_error_key: true
processors:
- add_kubernetes_metadata: ~ # 自动关联 K8s Pod 信息
output.logstash:
hosts: ["logstash:5044"]
③ 日志处理:清洗与转换
采集器收到原始日志后,通常需要经过一层处理管道做清洗、解析、丰富:
- 解析:从非结构化文本中提取字段(Grok 正则、JSON 解析)
- 丰富:补充 GeoIP、用户信息、环境标签
- 脱敏:手机号/身份证/Token 等敏感字段替换为
*** - 过滤:丢弃健康检查等无意义日志
- 路由:错误日志走 ES(需要全文检索),访问日志走 S3(只做归档)
input {
beats { port => 5044 }
}
filter {
# 解析 JSON 结构
json { source => "message" }
# 脱敏:替换手机号
mutate {
gsub => ["phone", "\d{4}$", "****"]
}
# 丢弃健康检查日志
if [path] == "/health" {
drop {}
}
# GeoIP 丰富
geoip { source => "clientIp" }
}
output {
# 错误日志 → ES 热节点
if [level] == "error" {
elasticsearch {
hosts => ["es-hot:9200"]
index => "error-logs-%{+YYYY.MM.dd}"
}
}
# 其他日志 → ES 温节点
else {
elasticsearch {
hosts => ["es-warm:9200"]
index => "app-logs-%{+YYYY.MM.dd}"
}
}
}
④ 日志存储:存在哪里?
| 存储方案 | 优势 | 劣势 | 适用场景 |
|---|---|---|---|
| Elasticsearch | 全文搜索强、聚合能力好 | 资源消耗大、成本高 | 需要按内容搜索、复杂查询 |
| Grafana Loki | 轻量、只索引标签不索引内容、成本低 | 不适合复杂全文搜索 | 中小团队、已用 Grafana |
| S3/OSS + Athena | 极低成本、无限容量 | 查询慢、不支持实时 | 归档、合规审计 |
| ClickHouse | 列式存储、聚合超快 | 运维复杂 | 超大日志量、需要统计分析 |
Elasticsearch 支持 Index Lifecycle Management,按数据新鲜度自动迁移:
- 热节点(SSD,7天内):最近的日志,查询最频繁
- 温节点(HDD,7-30天):偶尔查询,降低成本
- 冷节点/冻结(30-90天):几乎不查,极低成本
- 删除(>90天):超出保留期的日志自动删除
{
"policy": {
"phases": {
"hot": {
"actions": {
"rollover": { "max_age": "1d", "max_size": "50gb" }
}
},
"warm": {
"min_age": "7d",
"actions": {
"shrink": { "number_of_shards": 1 },
"forcemerge": { "max_num_segments": 1 }
}
},
"cold": {
"min_age": "30d",
"actions": {
"searchable_snapshot": { "snapshot_repository": "s3-repo" }
}
},
"delete": {
"min_age": "90d",
"actions": { "delete": {} }
}
}
}
}
⑤ 日志消费:怎么查?怎么用?
日志存下来之后,消费方式有几种:
| 消费方式 | 工具 | 场景 |
|---|---|---|
| 实时搜索 | Kibana Discover / Grafana Explore | 线上问题排查:搜 traceId、userId、错误关键词 |
| 仪表盘 | Kibana Dashboard / Grafana Dashboard | 日常巡检:错误趋势、接口耗时分布 |
| 告警 | ElastAlert / Grafana Alerting | 自动告警:5分钟内错误日志 >100 条 → 钉钉/Slack 通知 |
| 分析 | ES Aggregation / ClickHouse SQL | 业务分析:Top 10 慢接口、用户行为路径 |
实战:线上问题排查流程
⑥ 日志归档与保留策略
为什么需要归档?
- 合规要求(金融、医疗行业日志需保留 3-7 年)
- 成本控制(ES 存储成本是 S3 的 10-50 倍)
- 容量管理(日志增长极快,不清理会打爆磁盘)
归档方案:
| 阶段 | 保留时间 | 存储位置 | 是否可查 |
|---|---|---|---|
| 实时 | 0-7 天 | ES 热节点 / Loki | 毫秒级查询 |
| 近线 | 7-30 天 | ES 温节点 / S3 Standard | 秒级查询 |
| 归档 | 30-365 天 | S3 Glacier / OSS 归档 | 需要恢复(分钟~小时) |
| 删除 | >365 天 | 自动清理 | 不可查 |
自动化归档流程:
- ES ILM 或 Loki Compactor 按策略自动迁移索引
- 到期的索引通过 Snapshot API 备份到 S3/OSS
- 超出保留期的 Snapshot 由 Curator 或 S3 Lifecycle Rule 自动删除
- 关键业务日志(支付、审计)走单独通道,保留周期更长
假设日均日志量 100GB:
- ES 热节点存 7 天:约 700GB SSD(≈ $200/月)
- ES 温节点存 30 天:约 3TB HDD(≈ $100/月)
- S3 标准存 90 天:约 9TB(≈ $200/月)
- S3 Glacier 存 365 天:约 36TB(≈ $150/月)
对比全量存 ES 365 天(≈ 36TB SSD/HDD ≈ $3000+/月),分层后成本降低 80%+。
完整的 ELK 数据流
把上面的步骤串起来,一张图看完整条链路:
常见面试问题
Q1: 前端错误发生后,如何通过日志快速定位问题?
答案:
- 前端上报时携带
traceId(从响应头获取) - 用
traceId在服务端日志系统中搜索完整链路 - 查看链路上每个服务的日志:请求参数 → 处理过程 → 返回结果
Q2: ELK 是什么?
答案:
- Elasticsearch:日志存储和搜索引擎
- Logstash:日志收集和处理管道
- Kibana:可视化查询和仪表盘
日志采集流程:应用 → Filebeat → Logstash → Elasticsearch → Kibana
Q3: 什么样的日志应该记录?什么不该记录?
答案:
应该记录:请求入口和出口、关键业务操作、错误堆栈、性能指标 不该记录:用户密码、信用卡号、Token 明文等敏感信息
Q4: Prometheus 和 ELK 的区别?
答案:
- Prometheus + Grafana:适合指标监控(CPU、内存、QPS、响应时间),时序数据
- ELK:适合日志分析(搜索具体请求、错误排查),全文搜索
两者互补,通常一起使用。
Q5: 日志量太大怎么办?
答案:
从产出到存储到归档的每个环节都可以优化:
产出侧减量:
- 日志分级:生产环境只记 info 及以上,debug 日志关闭
- 采样:正常请求按比例采样(如 10%),错误请求全量记录
- 去噪:在采集器或 Logstash 层丢弃健康检查、静态资源访问等低价值日志
存储侧降本:
4. 热温冷分层(ILM):7 天内热存储(SSD),30 天温存储(HDD),超期归档到 S3 Glacier
5. 索引优化:关闭不需要搜索的字段索引、减少副本数、使用 best_compression
6. 按需选型:不需要全文搜索的日志直接存 Loki 或 S3,成本降 10 倍
写入侧保障: 7. 异步写入:日志 SDK 内部用缓冲队列,批量异步发送,不阻塞业务主流程 8. 背压控制:采集器满载时丢弃低优先级日志而非阻塞应用
Q6: 如何设计一个完整的日志系统?
答案:
一个生产级日志系统需要覆盖「产出 → 采集 → 处理 → 存储 → 消费 → 归档」六个环节,面试时按以下框架回答:
1. 日志产出规范
- 使用
Winston/Pino输出结构化 JSON,每条日志必须包含:timestamp、level、traceId、service、message、业务字段 - 遵循 12-Factor App:应用只写 stdout,不管文件
- 日志分 4 级:
error(需告警)、warn(需关注)、info(业务流水)、debug(仅开发) - 敏感信息(密码、Token、手机号)在产出时即脱敏
2. 日志采集
- 容器环境用 Filebeat/Fluent Bit 以 DaemonSet 部署,Tail 容器 stdout 文件
- 采集器自动注入元数据:Pod 名、Namespace、Node、Container ID
- 支持断点续传(记录 offset),网络中断后不丢日志
3. 日志处理(管道层)
- Logstash 或 Vector 做中间处理:
- 解析:JSON 解析、Grok 正则提取
- 丰富:补 GeoIP、关联用户信息
- 过滤:丢弃健康检查、静态资源等噪声
- 路由:error 走 ES 热节点,access log 走 S3
4. 日志存储
- 热数据(7 天):Elasticsearch 热节点 / Loki,支持毫秒级全文检索
- 温数据(7-30 天):ES 温节点,降低副本数和分片
- 冷数据(30-365 天):S3 Standard / OSS,按需恢复查询
- 使用 ES ILM(Index Lifecycle Management) 自动在各层间迁移
5. 日志消费
- 搜索排查:Kibana / Grafana,用
traceId串联前后端全链路 - 仪表盘:监控错误趋势、接口 P99、QPS 等
- 告警:ElastAlert / Grafana Alerting,规则示例:
5 分钟内 error 数 > 100 → 通知 on-call - 分析:慢接口 Top N、错误码分布、用户行为漏斗
6. 归档与保留
- 制定保留策略:常规日志 90 天、审计日志 3 年、支付日志按合规要求
- 到期索引通过 Snapshot API 归档到 S3 Glacier
- S3 Lifecycle Rule 自动清理超期归档
- 定期验证归档可恢复性
一句话总结:应用产 JSON → Filebeat 采集 → Logstash 清洗路由 → ES 热温冷分层存储 → Kibana 查 + Alertmanager 报警 → ILM 自动归档到 S3 → Lifecycle 过期删除。