游戏性能监控
用生活化的比喻,让你从"玩家说卡但不知道为什么"到"能精准定位每一个性能瓶颈"
前置知识: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)使用独立的域名避免阻塞主请求。
实践任务
- 实现前端监控 SDK:FPS + 内存 + 错误 + 上报
- 搭建 Prometheus + Grafana:监控游戏服务器核心指标
- 实现全链路追踪:OpenTelemetry + Jaeger,追踪一个请求的完整链路
- 配置告警规则:P0/P1/P2 三级告警,推送到企业微信
- 性能优化实战:找到一个 P99 > 500ms 的接口,优化到 < 200ms
- JVM 监控:配置 GC 日志和堆内存监控
与其他章节的关联
| 本节内容 | 关联章节 | 关联点 |
|---|---|---|
| 前端内存 | 2_3 第03章 浏览器全链路内存 | 前端内存监控的原理 |
| GC 监控 | 3_1 第09章 JVM 调优实战 | GC 暂停时间的监控 |
| 容器化 | 第03章 Kubernetes 编排 | K8s 环境下的监控部署 |
| 日志系统 | 第07章 游戏日志系统 | 监控和日志互补 |
| 告警 | 第08章 告警与应急响应 | 监控数据驱动告警 |
⬅️ 上一章:游戏自动化测试 | ➡️ 下一章:游戏日志系统