3、日志系统基础:日志级别、结构化日志格式与日志采集存储

在大模型服务的部署与运维中,日志系统是观测系统内部状态的第一入口。与传统的 Web 服务不同,大模型推理服务通常涉及长时请求、GPU 资源调度、Token 级流式输出、多模型版本切换等复杂场景,日志的维度更多、噪声更大、关联性更强。如果日志体系设计不合理,排障时往往面临“有日志但看不懂、有报错但查不到上下文、有指标但无法关联”的困境。本章将围绕日志级别、结构化格式、采集与存储三个核心模块展开,帮助读者建立一套适合大模型服务的高质量日志基础设施。

大模型服务日志系统全景 日志级别 DEBUG / INFO WARN / ERROR CRITICAL / FATAL 结构化日志格式 时间戳 / 级别 / 追踪ID 服务名 / 模型名 / 版本 请求参数 / 耗时 / Token数 采集与存储 Filebeat / Fluentd ELK / Loki 索引 / 标签 / 生命周期 应用日志 → 采集Agent → 消息队列/直写 → 存储引擎 → 查询/告警 🔑 关键设计原则 • 级别可动态调整,生产默认INFO • 全链路trace_id贯穿所有日志 • 日志与指标、链路追踪三合一 ⚠️ 常见误区 • 日志级别混乱,DEBUG刷爆磁盘 • 非结构化文本,无法自动解析 • 采集器单点,日志丢失无感知

3.1 日志级别:从 DEBUG 到 FATAL 的合理运用

3.1.1 核心原理:日志级别是信息过滤的第一道闸门

日志级别的本质是对日志事件重要性和紧急程度的分级标注。在 Python 的 logging 模块中,标准级别从低到高依次为 DEBUG、INFO、WARNING、ERROR、CRITICAL。对于大模型服务,建议额外引入 FATAL 级别(或复用 CRITICAL)用于表示进程无法继续运行、必须立即人工介入的极端情况。级别的作用不仅是给人看的标签,更是运行时过滤机制——当日志级别设置为 INFO 时,DEBUG 日志根本不会被写入输出流,从而减少 I/O 开销和磁盘占用。

大模型推理服务有其特殊性:一次推理请求可能耗时数秒到数十秒,期间会产生大量中间日志(如 KV Cache 命中率、显存占用变化、采样参数等)。如果全部用 INFO 输出,日志量会呈指数级增长;如果全部用 DEBUG,则排障时无法快速定位。因此,合理的级别分配策略是日志系统设计的第一步。

3.1.2 级别划分策略与判断方法

以下是一套适用于大模型推理服务的日志级别分配参考表:

级别适用场景典型内容生产环境建议
DEBUG开发调试、问题复现模型权重加载明细、每层推理耗时、KV Cache 命中率、采样候选 Token 列表默认关闭,按需动态开启
INFO关键生命周期事件服务启动完成、模型加载成功、请求开始/结束、Token 数统计、推理耗时默认开启,用于日常监控
WARN潜在风险但不影响当前请求显存接近上限、请求排队超时、模型版本即将过期、GPU 温度偏高开启,需关注告警
ERROR请求失败或功能异常推理超时、OOM、非法输入、下游服务调用失败开启,需立即响应
FATAL进程级不可恢复错误CUDA 驱动异常、模型文件损坏、端口被占用导致启动失败开启,触发紧急告警

判断某个日志应该放在哪个级别,可以遵循“三问法”:这条日志是否影响用户请求的成功与否?如果是,至少 ERROR;这条日志是否反映系统资源或性能的潜在风险?如果是,WARN 或 INFO;这条日志是否只有开发者在调试时才关心?如果是,DEBUG。

3.1.3 实战场景:动态调整日志级别

某大模型推理服务在生产环境出现偶发的高延迟,但 INFO 日志中无法看到具体瓶颈。运维人员通过管理接口将指定 Pod 的日志级别动态调整为 DEBUG,无需重启服务:

# 通过 HTTP 接口动态调整日志级别
curl -X POST http://localhost:8080/admin/log-level \
  -H "Content-Type: application/json" \
  -d '{"level": "DEBUG", "scope": "inference_engine"}'

调整后,DEBUG 日志输出了每个 Transformer 层的耗时分布,发现第 24 层的 Attention 计算耗时异常偏高,进一步定位到是某个 GPU 卡出现降频。整个过程从发现问题到定位根因仅用了 15 分钟,且没有中断线上服务。这个案例说明,日志级别必须支持运行时动态调整,否则排障代价会非常高。

常见误区:将 DEBUG 日志长期开启在生产环境。大模型推理的 DEBUG 日志量可达 INFO 的 50-100 倍,会严重拖慢推理速度并快速填满磁盘。正确做法是默认关闭,通过环境变量或配置中心按需开启,并设置自动关闭时间。

3.2 结构化日志格式:让日志成为可解析的数据

3.2.1 核心原理:从文本到键值对

传统日志以纯文本形式输出,例如:2025-06-15 10:23:45 ERROR request failed。这种格式人类可读,但机器难以解析。结构化日志将日志内容组织为键值对或 JSON 格式,使得日志可以被 Logstash、Loki 等工具自动索引和查询。对于大模型服务,结构化日志的字段设计需要额外考虑模型相关维度。

一个标准的大模型推理日志条目应包含以下字段组:基础字段(时间戳、级别、服务名、主机名)、追踪字段(trace_id、span_id、request_id)、模型字段(model_name、model_version、framework)、性能字段(latency_ms、prompt_tokens、completion_tokens、gpu_utilization)、业务字段(user_id、session_id、prompt_hash)。

3.2.2 结构化日志的格式规范

推荐使用 JSON 作为结构化日志的载体,因为 JSON 具有自描述性、跨语言支持和广泛的工具链兼容性。以下是一个大模型推理服务的结构化日志示例:

{
  "timestamp": "2025-06-15T10:23:45.123Z",
  "level": "INFO",
  "service": "llm-inference",
  "host": "gpu-node-03",
  "trace_id": "8f3a2b9c1d4e5f6a",
  "request_id": "req_20250615_102345_001",
  "model_name": "qwen2.5-72b",
  "model_version": "v2.3.1",
  "latency_ms": 2345.67,
  "prompt_tokens": 128,
  "completion_tokens": 512,
  "total_tokens": 640,
  "gpu_utilization": 87.5,
  "gpu_memory_mb": 45200,
  "status": "success"
}

在 Python 中,可以使用 python-json-logger 库快速实现结构化输出:

from pythonjsonlogger import jsonlogger
import logging

logger = logging.getLogger("llm_service")
handler = logging.StreamHandler()
formatter = jsonlogger.JsonFormatter(
    "%(timestamp)s %(level)s %(name)s %(message)s",
    rename_fields={"levelname": "level", "name": "service"}
)
handler.setFormatter(formatter)
logger.addHandler(handler)
logger.setLevel(logging.INFO)

3.2.3 案例拆解:通过结构化日志定位慢请求

某团队发现线上部分请求的响应时间超过 10 秒,但无法确定是模型推理慢还是网络传输慢。通过分析结构化日志中的 latency_ms 字段,发现所有慢请求的 prompt_tokens 都超过 2000,且 gpu_utilization 接近 100%。进一步按 model_version 分组统计,发现 v2.3.1 版本在长 prompt 场景下的平均耗时比 v2.3.0 高出 40%。最终定位到是 v2.3.1 版本引入了新的 Attention 实现,在长序列场景下存在效率回退。这个案例说明,结构化日志的价值不仅在于单条查询,更在于可以按任意字段进行聚合分析。

经验提醒:在结构化日志中始终携带 trace_id,并确保该 ID 在网关层生成、贯穿整个调用链。这样在排查跨服务问题时,可以通过一个 ID 拉取所有相关日志,避免在多个服务之间手动匹配时间戳。

3.3 日志采集与存储:ELK 与 Loki 的选型与实践

3.3.1 核心原理:采集与存储的分层架构

日志采集与存储的核心目标是解决“日志产生速度 > 消费速度”和“日志分散在多台机器”两个问题。典型的采集链路为:应用进程 → 日志文件/标准输出 → 采集 Agent(Filebeat/Fluentd/Promtail)→ 消息队列(可选)→ 存储引擎(Elasticsearch/Loki)→ 查询与告警。对于大模型服务,由于日志量巨大且包含敏感的业务数据,采集链路还需要考虑采样、脱敏和限流。

3.3.2 ELK 与 Loki 的对比选型

维度ELK(Elasticsearch + Logstash + Kibana)Loki(Grafana Loki + Promtail)
存储原理全文索引,基于 Lucene压缩日志块,仅索引标签
查询语言Lucene 语法 / KQLLogQL(类 PromQL)
资源消耗高(需要大量内存做索引)低(适合大规模日志)
与指标联动需额外集成与 Prometheus/Grafana 原生集成
适用场景复杂全文搜索、业务分析Kubernetes 环境、快速排障
大模型场景适配适合需要深度分析 Token 分布、Prompt 内容的场景适合快速检索 trace_id、按标签过滤

选型建议:如果团队已经深度使用 Grafana 监控体系,且日志主要用于排障和关联指标,优先选择 Loki;如果需要对日志内容做复杂的全文检索、聚合分析或长期留存,ELK 更合适。对于大模型服务,通常建议 Loki 作为第一优先,因为其资源消耗低,且与 Prometheus 指标联动方便。

3.3.3 操作流程:基于 Loki 的日志采集配置

以下是一个使用 Promtail 采集大模型推理服务日志并发送到 Loki 的配置示例:

# promtail.yaml
server:
  http_listen_port: 9080
  grpc_listen_port: 0

positions:
  filename: /tmp/positions.yaml

clients:
  - url: http://loki:3100/loki/api/v1/push

scrape_configs:
  - job_name: llm-inference
    static_configs:
      - targets: [localhost]
        labels:
          job: llm-inference
          service: llm-inference
          __path__: /var/log/llm/*.log
    pipeline_stages:
      - json:
          expressions:
            level: level
            trace_id: trace_id
            model_name: model_name
            latency_ms: latency_ms
      - labels:
          level:
          trace_id:
          model_name:

配置完成后,可以在 Grafana 中通过 LogQL 查询特定 trace_id 的所有日志:

{service="llm-inference"} |= "8f3a2b9c1d4e5f6a"

3.3.4 案例拆解:大模型服务的日志链路完整落地

某大模型平台团队需要为 20 个推理节点、日均 500 万请求搭建日志系统。初始方案采用 ELK,但运行一周后发现 Elasticsearch 集群内存占用超过 80%,且查询响应变慢。团队评估后切换为 Loki + Promtail 方案,具体实施如下:

  1. 采集层:每个推理节点部署 Promtail,从容器标准输出采集 JSON 结构化日志,提取 level、trace_id、model_name 作为标签。
  2. 存储层:部署 Loki 集群(3 个 ingester + 3 个 querier),配置 15 天保留期,每日日志量约 200GB。
  3. 查询层:在 Grafana 中配置日志面板,与 Prometheus 的 GPU 利用率、请求 QPS 指标联动。
  4. 告警层:设置 LogQL 告警规则,例如 5 分钟内 ERROR 级别日志超过 10 条即触发告警。

切换后,资源消耗降低约 70%,查询速度提升 3 倍。更重要的是,通过 trace_id 标签可以秒级拉取一次请求的全链路日志,排障效率大幅提升。该案例的代价是需要额外维护 Promtail 的 pipeline 配置,且 LogQL 的查询能力相比 Lucene 语法有一定限制,但对于排障场景完全够用。

总结复盘:日志系统的建设不是一蹴而就的。建议从“最小可用”开始——先保证结构化日志输出和集中采集,再逐步完善级别策略、标签设计和告警规则。对于大模型服务,务必在日志中携带模型名称、版本和 Token 统计信息,这些字段在性能分析和成本核算中至关重要。
风险提示:日志中可能包含用户的 Prompt 内容,涉及隐私和数据合规问题。建议在采集层对 Prompt 字段进行哈希处理或脱敏,只保留 Token 数和哈希值,避免敏感信息进入日志存储系统。