游戏日志系统

用生活化的比喻,让你从"靠 printf 调试"到"能用 ELK 建立完整的日志体系"

前置知识:第06章 游戏性能监控(监控和日志的关系)


阅读指南(初学者必看)

为什么你需要学习游戏日志系统?

没有日志 = 黑盒运维:

  • 线上出 Bug → 不知道哪行代码出错
  • 玩家反馈"金币少了" → 没有操作记录无法查证
  • 服务器崩溃 → 不知道崩溃前发生了什么

学完本章,你能回答:

  • 日志级别怎么划分?每级记什么?
  • JSON 日志格式怎么设计?
  • ELK(Elasticsearch + Logstash + Kibana)怎么搭建?
  • Loki 轻量日志方案怎么用?
  • 游戏日志有什么特殊需求?

本文结构

第一部分:日志规范
第二部分:ELK 日志系统
第三部分:Loki 轻量方案
第四部分:游戏日志特殊需求
第五部分:日志分析与查询

一、日志规范

生活类比:日志就像"飞机黑匣子"——平时没人看,出事了就是最重要的证据。

日志级别

ERROR ── 影响功能的错误,需要立即处理
  例:数据库连接失败、支付回调异常、OOM

WARN ── 潜在问题,不影响当前功能但需要关注
  例:GC 耗时过长、队列接近满、重试成功

INFO ── 关键业务操作
  例:玩家登录、创建房间、战斗结算、充值成功

DEBUG ── 调试信息,生产环境关闭
  例:消息收发详情、状态变化、缓存命中/未命中

TRACE ── 更细的调试信息(几乎不用)

JSON 日志格式

{
  "timestamp": "2026-04-23T16:30:00.123Z",
  "level": "INFO",
  "service": "room-server",
  "traceId": "abc123",
  "spanId": "def456",
  "userId": "player_123",
  "roomId": "room_456",
  "action": "battle_start",
  "duration": 1500,
  "message": "Battle started"
}

结构化日志格式(带上下文)

{
  "timestamp": "2024-01-15T14:23:45.123Z",
  "level": "INFO",
  "traceId": "a1b2c3d4e5f6",
  "service": "game-server",
  "thread": "http-nio-8080-exec-1",
  "logger": "com.game.service.PlayerService",
  "message": "玩家登录成功",
  "context": {
    "playerId": "12345",
    "ip": "192.168.1.1",
    "device": "iPhone14,2"
  }
}

日志脱敏规则

数据类型 脱敏方式 示例
密码 *** pass***
手机号 中间4位* 138****1234
身份证 中间8位* 310***********1234
银行卡 保留后4位 ************1234
IP 最后一节* 192.168.1.*
Token 截断 eyJhbG***

日志内容禁忌

  • 不要打印密码、Token、身份证号等敏感信息
  • 不要打印超大对象(一次日志几 MB)
  • 不要在循环中打印 INFO 级别日志
  • 异常日志要打印堆栈,不要只打印 message

二、ELK 日志系统

生活类比:ELK 就像"图书馆"——Elasticsearch 是书架(存储和搜索),Logstash 是图书管理员(整理和分类),Kibana 是检索系统(可视化查询)。

日志采集流程

应用 → Filebeat → Logstash → Elasticsearch → Kibana
         采集       清洗转换     存储搜索       可视化查询

Logstash 配置

# logstash-game.conf
input {
  beats {
    port => 5044
  }
}

filter {
  json {
    source => "message"
  }
  
  # 添加地理位置
  geoip {
    source => "clientIp"
  }
  
  # 日志级别转换
  mutate {
    uppercase => ["level"]
  }
}

output {
  elasticsearch {
    hosts => ["elasticsearch:9200"]
    index => "game-logs-%{+YYYY.MM.dd}"
  }
}

ELK Docker Compose

version: '3.8'

services:
  elasticsearch:
    image: elasticsearch:8.11.0
    environment:
      - discovery.type=single-node
      - xpack.security.enabled=false
    ports:
      - "9200:9200"
    volumes:
      - es_data:/usr/share/elasticsearch/data

  logstash:
    image: logstash:8.11.0
    volumes:
      - ./logstash.conf:/usr/share/logstash/pipeline/logstash.conf:ro
    ports:
      - "5044:5044"
    depends_on:
      - elasticsearch

  kibana:
    image: kibana:8.11.0
    ports:
      - "5601:5601"
    depends_on:
      - elasticsearch

volumes:
  es_data:

Kibana 常用查询

# 查某个玩家的所有操作
userId: "player_123"

# 查所有 ERROR 级别日志
level: "ERROR"

# 查某个房间的战斗日志
roomId: "room_456" AND action: "battle_*"

# 查最近 1 小时的慢请求
duration: >1000 AND @timestamp: [now-1h TO now]

# 查支付相关日志
action: "payment_*" AND level: "ERROR"

三、Loki 轻量方案

Loki 是 Grafana Labs 推出的轻量级日志系统,只索引标签不索引日志内容,成本更低。

# docker-compose.yml for Loki
services:
  loki:
    image: grafana/loki:2.9.0
    ports:
      - "3100:3100"
    volumes:
      - ./loki-config.yml:/etc/loki/local-config.yaml

  promtail:
    image: grafana/promtail:2.9.0
    volumes:
      - ./promtail-config.yml:/etc/promtail/config.yml
      - /var/log:/var/log:ro

Promtail 配置:

server:
  http_listen_port: 9080

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

scrape_configs:
  - job_name: game-server-logs
    static_configs:
      - targets:
          - localhost
        labels:
          job: game-server
          __path__: /var/log/game/*.log

Grafana 中查询 Loki 日志:

{job="game-server"} |= "ERROR"
{job="game-server"} |= "playerId=12345"
{job="game-server"} | json | level="ERROR" | line_format "{{.message}}"

四、游戏日志特殊需求

游戏日志分类

日志类型 保留时间 存储位置 查询频率
系统日志 30 天 ES 高(排查问题)
业务日志 90 天 ES + HDFS 中(运营分析)
战斗日志 30 天 ES 高(玩家投诉)
审计日志 180 天 ES + 归档 低(合规审计)
性能日志 7 天 Prometheus 高(实时监控)

游戏日志数据量

10万在线 × 每秒10条日志 = 100万条/秒
每条日志约 500 字节 = 500MB/秒 = 43TB/天

优化策略:
1. 分级别存储(ERROR 全存,INFO 采样 10%)
2. 分索引(按服务/日期分开)
3. 冷热分离(7天内热数据 SSD,7天后冷数据 HDD)
4. 生命周期管理(30天后自动删除或归档)

关键业务日志设计

// 玩家登录日志
log.info(JSON.stringify({
  action: "player_login",
  userId: player.getId(),
  deviceId: player.getDeviceId(),
  ip: player.getIp(),
  platform: player.getPlatform(),
  version: player.getClientVersion(),
  loginTime: System.currentTimeMillis()
}));

// 战斗结算日志
log.info(JSON.stringify({
  action: "battle_settle",
  roomId: room.getId(),
  battleId: battle.getId(),
  duration: battle.getDuration(),
  players: battle.getPlayers().stream()
    .map(p -> Map.of("id", p.getId(), "result", p.getResult(), "scoreChange", p.getScoreChange()))
    .collect(toList()),
  rewards: battle.getRewards()
}));

// 充值日志(最重要,涉及钱)
log.info(JSON.stringify({
  action: "payment_success",
  orderId: order.getId(),
  userId: order.getUserId(),
  amount: order.getAmount(),
  currency: order.getCurrency(),
  channelId: order.getChannelId(),
  productId: order.getProductId(),
  gameId: order.getGameId()
}));

五、日志分析与查询

常用分析场景

1. 实时在线人数
   → 按 1 分钟聚合 count(userId)

2. 每日活跃用户(DAU)
   → 按 1 天聚合 count(distinct userId)

3. 玩家流失分析
   → 最后登录时间 > 7 天的用户

4. 战斗平均时长
   → avg(duration) WHERE action = "battle_settle"

5. 充值金额统计
   → sum(amount) WHERE action = "payment_success"

自问自答

Q1:日志该记多少?

原则:关键操作必须记(登录/支付/战斗),调试信息可开关。太多影响性能,太少排查不了问题。建议:ERROR 全记,INFO 记关键业务,DEBUG 开关控制。

Q2:日志打到文件还是直接发 ES?

推荐打到文件 + Filebeat 采集。原因:1. 应用和日志系统解耦(ES 挂了不影响应用);2. 文件有本地缓存不怕丢;3. Filebeat 负责转发和重试。

Q3:JSON 日志比文本日志好在哪里?

JSON 可直接被 ES 解析和索引,查询效率高。文本日志需要写 Grok 正则解析,容易出错且性能差。

Q4:日志量太大 ES 存不下怎么办?

  1. 分级别保留(ERROR 30天,INFO 7天);2. 冷热分离(热数据 SSD,冷数据归档到 HDFS/S3);3. 采样(非关键日志只记 10%)。

Q5:日志和监控有什么区别?

日志记录"事件"(发生了什么),监控记录"指标"(数值趋势)。日志用于查原因,监控用于看趋势。两者互补。

Q6:日志太多怎么查? A: 加搜索!Kibana!按时间、关键字、日志级别查!使用结构化日志后,可以按字段精确过滤。


实践任务

  1. 制定日志规范:定义日志级别、格式、脱敏规则
  2. 搭建 ELK:Docker Compose 一键启动 ES + Logstash + Kibana
  3. 接入游戏日志:应用输出 JSON 日志 → Filebeat → ELK
  4. 创建 Kibana 看板:实时在线、战斗统计、错误率
  5. 日志告警:ERROR 日志 > 10条/分钟 触发告警
  6. 对比 Loki:搭建 Loki + Grafana,对比 ELK 的资源占用

初学者常见错误

错误1:日志没有统一规范

问题: 有的打印中文、有的打印英文、有的用 JSON、有的用文本。 解决: 制定团队日志规范,使用结构化日志,统一字段命名。

错误2:没有日志分级存储

问题: 所有日志存 30 天,存储成本爆炸。 解决: ERROR 存 30 天,INFO 存 7 天,DEBUG 存 1 天。

错误3:日志量太大导致应用卡顿

问题: 同步打印日志阻塞业务线程。 解决: 使用异步日志(Logback AsyncAppender),批量刷盘。


与其他章节的关联

本节内容 关联章节 关联点
监控 第06章 游戏性能监控 日志和监控互补
告警 第08章 告警与应急响应 日志异常触发告警
CI/CD 第01章 CI/CD 流水线 日志规范纳入代码检查
Docker 第02章 Docker 容器化 容器日志采集方案
JVM 调优 3_1 第09章 JVM 调优实战 GC 日志分析
全链路追踪 第06章 游戏性能监控 Trace ID 贯穿日志

⬅️ 上一章:游戏性能监控 | ➡️ 下一章:告警与应急响应