游戏性能监控

用生活化的比喻,让你从"玩家说卡但不知道为什么"到"能精准定位每一个性能瓶颈"

前置知识:2_3 第03章 浏览器全链路内存(前端性能基础)


阅读指南(初学者必看)

为什么你需要学习游戏性能监控?

没有监控 = 瞎子运维:

  • 玩家反馈"卡" → 你不知道哪里卡
  • 服务器 CPU 100% → 你不知道哪个接口慢
  • 内存泄漏 → 你不知道哪个对象在增长

学完本章,你能回答:

  • 前端 FPS/内存怎么监控?怎么上报?
  • 后端 Prometheus + Grafana 怎么搭建?
  • 全链路追踪怎么实现?P99 延迟怎么看?
  • 游戏性能的目标值是什么?
  • JVM 和数据库监控怎么做?

本文结构

第一部分:前端性能监控
第二部分:后端性能监控
第三部分:全链路追踪
第四部分:性能目标与告警

一、前端性能监控

生活类比:前端监控就像"车载仪表盘"——实时显示速度、油量、温度,让你一眼知道车况。

游戏前端监控 SDK

class GameMonitor {
  constructor() {
    this.metrics = { fps: 0, memory: 0, loadTime: 0, errorCount: 0 };
    this.startFPSMonitor();
    this.startMemoryMonitor();
    this.startErrorMonitor();
  }
  
  // FPS 监控
  startFPSMonitor() {
    let frames = 0;
    let lastTime = performance.now();
    
    const checkFPS = () => {
      frames++;
      const now = performance.now();
      if (now - lastTime >= 1000) {
        this.metrics.fps = Math.round(frames * 1000 / (now - lastTime));
        frames = 0;
        lastTime = now;
        
        if (this.metrics.fps < 30) {
          this.reportLowFPS(this.metrics.fps);
        }
      }
      requestAnimationFrame(checkFPS);
    };
    requestAnimationFrame(checkFPS);
  }
  
  // 内存监控
  startMemoryMonitor() {
    setInterval(() => {
      if (performance.memory) {
        this.metrics.memory = performance.memory.usedJSHeapSize / 1024 / 1024;
        if (this.metrics.memory > 300) {
          this.reportHighMemory(this.metrics.memory);
        }
      }
    }, 5000);
  }
  
  // 错误监控
  startErrorMonitor() {
    window.onerror = (msg, url, line, col, error) => {
      this.reportError({ msg, url, line, col, stack: error?.stack });
    };
    window.addEventListener('unhandledrejection', (e) => {
      this.reportError({ type: 'unhandledRejection', reason: e.reason });
    });
  }
  
  // 上报
  report(data) {
    navigator.sendBeacon('/api/monitor', JSON.stringify({
      ...data,
      timestamp: Date.now(),
      sessionId: this.sessionId,
      userId: this.userId,
      deviceInfo: this.getDeviceInfo()
    }));
  }
}

加载时间监控

window.addEventListener('load', () => {
  setTimeout(() => {
    const perf = performance.timing;
    const metrics = {
      dnsTime: perf.domainLookupEnd - perf.domainLookupStart,
      tcpTime: perf.connectEnd - perf.connectStart,
      ttfb: perf.responseStart - perf.requestStart,
      domParseTime: perf.domComplete - perf.domLoading,
      loadTime: perf.loadEventEnd - perf.navigationStart,
    };
    console.log('性能指标:', metrics);
  }, 0);
});

场景加载时间监控

class SceneLoadMonitor {
  static start(sceneName) {
    this.currentScene = sceneName;
    this.startTime = performance.now();
  }

  static end(sceneName) {
    if (this.currentScene !== sceneName) return;
    const duration = performance.now() - this.startTime;
    fetch('/api/metrics/scene-load', {
      method: 'POST',
      body: JSON.stringify({ scene: sceneName, duration }),
    });
    if (duration > 5000) {
      console.error('场景加载过慢:', sceneName, duration);
    }
  }
}

二、后端性能监控

生活类比:后端监控就像"工厂的生产监控"——实时显示每条产线的产量、良品率、故障率。

Prometheus + Micrometer

@Service
public class RoomMetrics {
    private final Counter roomCreatedCounter;
    private final AtomicInteger activeRooms;
    private final Timer battleTimer;
    
    public RoomMetrics(MeterRegistry registry) {
        roomCreatedCounter = Counter.builder("game.rooms.created")
            .description("Total rooms created")
            .register(registry);
            
        activeRooms = registry.gauge("game.rooms.active", new AtomicInteger(0));
        
        battleTimer = Timer.builder("game.battle.duration")
            .description("Battle duration")
            .register(registry);
    }
    
    public void onRoomCreated() {
        roomCreatedCounter.increment();
        activeRooms.incrementAndGet();
    }
    
    public void onRoomDestroyed() {
        activeRooms.decrementAndGet();
    }
    
    public void recordBattleDuration(long milliseconds) {
        battleTimer.record(milliseconds, TimeUnit.MILLISECONDS);
    }
}

Prometheus 配置

# prometheus.yml
global:
  scrape_interval: 15s

scrape_configs:
  - job_name: 'game-server'
    static_configs:
      - targets: ['localhost:8080']
    metrics_path: '/actuator/prometheus'

JVM 监控重点

指标 含义 关注原因
堆内存使用 Java 对象占用的内存 OOM 风险
GC 频率 每分钟 GC 次数 影响停顿时间
GC 耗时 每次 GC 花费的时间 导致卡顿
线程数 活跃线程数量 线程泄漏
CPU 使用 JVM 进程 CPU 占用 计算密集型问题

常用 JVM 监控命令:

# 查看 GC 情况
jstat -gcutil [pid] 1000

# 查看堆内存详情
jmap -heap [pid]

# 查看线程堆栈
jstack [pid] > thread_dump.txt

# 生成堆转储文件
jmap -dump:format=b,file=heap.hprof [pid]

数据库监控

MySQL 慢查询配置:

SET GLOBAL slow_query_log = 'ON';
SET GLOBAL long_query_time = 1;

查看当前连接数:

SHOW STATUS LIKE 'Threads_connected';
SHOW STATUS LIKE 'Max_used_connections';

Grafana 看板关键指标

看板 指标 告警阈值
在线人数 当前连接数 下降 50%
房间数 活跃房间数 > 容量 80%
API 延迟 P50/P95/P99 P99 > 500ms
错误率 5xx 比例 > 1%
CPU 使用率 > 80%
内存 使用率 > 85%
GC 暂停时间 > 200ms
数据库 慢查询数 > 10/min

三、全链路追踪

生活类比:全链路追踪就像"快递追踪"——从发货到签收,每一步的地点和时间都清清楚楚。

OpenTelemetry 集成

全链路追踪:从玩家点击到数据返回,经过的每一个服务

玩家点击 → [网关 5ms] → [匹配服务 3ms] → [房间服务 2ms] → [数据库 10ms]

Trace ID:贯穿整个请求链路
Span ID:每个服务的调用记录

手动实现 Trace ID 传递

网关层生成 Trace ID:

@Component
public class TraceFilter implements Filter {
    
    @Override
    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
            throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest) req;
        String traceId = request.getHeader("X-Trace-ID");
        if (traceId == null) {
            traceId = UUID.randomUUID().toString().replace("-", "");
        }
        MDC.put("traceId", traceId);
        
        HttpServletResponse response = (HttpServletResponse) res;
        response.setHeader("X-Trace-ID", traceId);
        
        chain.doFilter(req, res);
        MDC.clear();
    }
}

HTTP 调用时传递 Trace ID:

public class TraceHttpClient {
    public String get(String url) {
        String traceId = MDC.get("traceId");
        HttpRequest request = HttpRequest.newBuilder()
            .uri(URI.create(url))
            .header("X-Trace-ID", traceId)
            .GET()
            .build();
        // 发送请求...
    }
}

日志中打印 Trace ID:

<!-- logback.xml -->
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
    <encoder>
        <pattern>%d{HH:mm:ss.SSS} [%thread] [%X{traceId}] %-5level %logger{36} - %msg%n</pattern>
    </encoder>
</appender>

日志输出示例:

14:23:45.123 [http-nio-8080-exec-1] [a1b2c3d4e5f6] INFO  c.g.s.PlayerService - 玩家登录: user123
14:23:45.234 [http-nio-8080-exec-1] [a1b2c3d4e5f6] DEBUG c.g.s.PlayerService - 查询数据库耗时: 45ms
14:23:45.312 [http-nio-8080-exec-1] [a1b2c3d4e5f6] INFO  c.g.s.PlayerService - 登录完成

四、性能目标与告警

游戏性能目标

场景 指标 目标
登录 P99 延迟 < 500ms
匹配 P99 延迟 < 200ms
战斗操作 P99 延迟 < 100ms
排行榜 P99 延迟 < 300ms
FPS 平均帧率 > 30fps
内存 前端堆内存 < 300MB
加载 首屏时间 < 3s

关键性能指标

指标 定义 游戏目标
P50 延迟 50% 请求的响应时间 < 50ms
P95 延迟 95% 请求的响应时间 < 200ms
P99 延迟 99% 请求的响应时间 < 500ms
QPS 每秒请求数 > 10,000
错误率 失败请求比例 < 0.1%
可用性 服务可用时间比例 > 99.99%

告警规则

# Prometheus 告警规则
groups:
- name: game-alerts
  rules:
  - alert: HighAPILatency
    expr: histogram_quantile(0.99, rate(http_request_duration_seconds_bucket[5m])) > 0.5
    for: 2m
    labels:
      severity: P1
    annotations:
      summary: "API P99 延迟超过 500ms"
      
  - alert: HighErrorRate
    expr: rate(http_requests_total{status=~"5.."}[5m]) / rate(http_requests_total[5m]) > 0.01
    for: 1m
    labels:
      severity: P0
    annotations:
      summary: "5xx 错误率超过 1%"
      
  - alert: LowFPS
    expr: avg(game_client_fps) < 25
    for: 5m
    labels:
      severity: P2
    annotations:
      summary: "客户端平均 FPS 低于 25"

自问自答

Q1:前端监控数据量太大怎么办?

采样上报:1% ~ 10% 的用户上报完整数据,所有用户只上报关键指标(FPS、崩溃)。用 sendBeacon 异步上报不阻塞主线程。

Q2:Prometheus 和 Grafana 怎么选?

Prometheus 是数据采集和存储,Grafana 是可视化。两者配合使用,不是二选一。Prometheus 负责采集指标,Grafana 负责展示图表和告警。

Q3:全链路追踪对性能有影响吗?

有。OpenTelemetry 的开销约 1~3%。生产环境可以采样(10% ~ 50% 的请求开启追踪)。错误请求 100% 追踪。

Q4:游戏性能目标怎么定?

参考竞品 + 玩家反馈。核心原则:P99 比 P50 重要——99% 的玩家体验不能差。先定目标,再优化。

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

监控是"指标"(数字),如 CPU 使用率 80%。日志是"事件"(文本),如"用户登录失败"。监控看趋势,日志查原因。两者互补。

Q6:前端监控数据上报会不会影响游戏性能? A: 会有一点影响,但可以优化:1)批量上报;2)使用 sendBeacon(页面关闭也能发送);3)非关键指标采样上报;4)使用独立的域名避免阻塞主请求。


实践任务

  1. 实现前端监控 SDK:FPS + 内存 + 错误 + 上报
  2. 搭建 Prometheus + Grafana:监控游戏服务器核心指标
  3. 实现全链路追踪:OpenTelemetry + Jaeger,追踪一个请求的完整链路
  4. 配置告警规则:P0/P1/P2 三级告警,推送到企业微信
  5. 性能优化实战:找到一个 P99 > 500ms 的接口,优化到 < 200ms
  6. JVM 监控:配置 GC 日志和堆内存监控

与其他章节的关联

本节内容 关联章节 关联点
前端内存 2_3 第03章 浏览器全链路内存 前端内存监控的原理
GC 监控 3_1 第09章 JVM 调优实战 GC 暂停时间的监控
容器化 第03章 Kubernetes 编排 K8s 环境下的监控部署
日志系统 第07章 游戏日志系统 监控和日志互补
告警 第08章 告警与应急响应 监控数据驱动告警

⬅️ 上一章:游戏自动化测试 | ➡️ 下一章:游戏日志系统