epic-gamesfortnitedata-engineerinterviewsparkkafkagamedataleaderboardreal-time
Epic Games 数据工程师面试实录 2026:Fortnite 对局分析 + 排行榜管道 完整复盘
Epic Games Data Engineer 面试真实经历:Fortnite 对局数据管道、实时排行榜系统、玩家行为分析、反作弊数据管道设计完整复盘。第一人称面经,含面试官对话与解题思路。
Sam · · 16 分钟阅读
公司:Epic Games 岗位:Data Engineer (L5) 面试形式:Phone Screen + Virtual Onsite (4 轮) 结果:Pass → Offer
2026 年 2 月通过内推投递了 Epic Games 的 Data Engineer 岗位。整个流程大约 4 周。
Epic Games 的 DE 面试核心围绕 Fortnite 的海量对局数据。Fortnite 每天有数千万场对局,每场对局产生数十 GB 的事件数据(击杀、建造、道具拾取、位置变化等)。面试中反复考察”如何处理这种规模的游戏数据”。
Phone Screen:对局数据分析
题目:分析玩家的武器使用偏好和胜率关系
给定 Fortnite 对局事件数据,分析哪些武器组合的胜率最高。
-- 对局事件表
-- match_events: event_id, match_id, player_id, event_type,
-- weapon_id, event_time, x, y, z, victim_id
-- 我的解答
WITH player_weapons AS (
-- 每个玩家在对局中使用过的武器
SELECT DISTINCT
me.match_id,
me.player_id,
me.weapon_id
FROM match_events me
WHERE me.event_type IN ('elimination', 'build')
AND me.weapon_id IS NOT NULL
),
player_match_result AS (
-- 每个玩家在每场对局中的排名
SELECT
m.match_id,
p.player_id,
p.place,
p.eliminations
FROM match_results m
JOIN match_players p ON m.match_id = p.match_id
),
weapon_win_rate AS (
SELECT
pw.weapon_id,
w.weapon_name,
w.weapon_type,
COUNT(DISTINCT pw.match_id) AS matches_used,
SUM(CASE WHEN pmr.place = 1 THEN 1 ELSE 0 END) AS wins,
ROUND(
SUM(CASE WHEN pmr.place = 1 THEN 1 ELSE 0 END) * 100.0
/ COUNT(DISTINCT pw.match_id), 2
) AS win_rate_pct,
ROUND(AVG(pmr.eliminations), 2) AS avg_eliminations
FROM player_weapons pw
JOIN player_match_result pmr
ON pw.match_id = pmr.match_id
AND pw.player_id = pmr.player_id
JOIN weapons w ON pw.weapon_id = w.weapon_id
GROUP BY pw.weapon_id, w.weapon_name, w.weapon_type
)
SELECT *
FROM weapon_win_rate
WHERE matches_used >= 1000
ORDER BY win_rate_pct DESC
LIMIT 20;
面试官追问:
“这个查询在 10 亿行数据上跑了 30 分钟还没结果,怎么优化?”
我回答:
-- 优化 1: 分区裁剪
-- 按日期分区,只扫描需要的分区
WHERE event_date = '2026-05-25'
-- 优化 2: 预聚合
-- 先把 match_events 聚合到 player_match 粒度
CREATE TABLE player_match_summary AS
SELECT
match_id,
player_id,
COLLECT_SET(weapon_id) AS weapons_used,
place,
eliminations
FROM match_events
GROUP BY match_id, player_id, place, eliminations;
-- 然后在聚合表上做 JOIN,数据量从 10 亿降到百万级
-- 优化 3: Broadcast 小表
-- weapons 表只有几百行,可以 broadcast
SET spark.sql.autoBroadcastJoinThreshold = 10000000; -- 10MB
VO Round 1:实时排行榜管道
题目:设计 Fortnite 的实时赛季排行榜
Fortnite 每个赛季都有全球排行榜,需要实时更新玩家的赛季积分(基于对局表现)。
from pyspark.sql import SparkSession
from pyspark.sql.functions import col, sum as f_sum, avg, row_number
from pyspark.sql.window import Window
spark = SparkSession.builder \
.appName("Fortnite_Leaderboard") \
.getOrCreate()
# ====== 实时排行榜管道 ======
# 1. 从 Kafka 读取对局完成事件
match_completions = spark \
.readStream \
.format("kafka") \
.option("kafka.bootstrap.servers", "kafka:9092") \
.option("subscribe", "match_completion.topic") \
.load() \
.selectExpr("CAST(value AS STRING)") \
.select(F.from_json(F.col("value"), """
match_id STRING, player_id STRING,
place INT, eliminations INT, damage_dealt INT,
time_played_seconds INT, game_mode STRING,
timestamp STRING
""").alias("data")) \
.select("data.*")
# 2. 计算每场对局的得分
scored_matches = match_completions \
.withColumn("score",
# 基础分:排名
(20 - col("place")) * 10 +
# 击杀分
col("eliminations") * 5 +
# 伤害分
col("damage_dealt") / 100 +
# 时长惩罚(速通加分)
CASE WHEN col("time_played_seconds") < 300 THEN 50
WHEN col("time_played_seconds") < 600 THEN 25
ELSE 0 END
)
# 3. 累加赛季总分(流 + 批 JOIN)
season_totals = spark \
.read \
.format("delta") \
.load("s3://epic-data/leaderboard/season_totals/")
# 使用 StreamingWaterfall 模式更新
updated_totals = scored_matches \
.groupBy("player_id") \
.agg(f_sum("score").alias("session_score"))
# 4. 写入 Delta Lake(MERGE 操作)
from_delta = DeltaTable.forPath(spark, "s3://epic-data/leaderboard/season_totals/")
updated_totals.writeStream \
.foreachBatch(lambda batch_df, batch_id:
from_delta.merge(
batch_df,
"target.player_id = source.player_id"
)
.whenMatchedUpdate(set={"season_score": "target.season_score + source.session_score"})
.whenNotMatchedInsertAll()
.execute()
) \
.option("checkpointLocation", "s3://epic-checkpoints/leaderboard") \
.start()
# 5. 排行榜查询(Top 10000)
leaderboard_window = Window.orderBy(col("season_score").desc())
leaderboard = spark \
.read \
.format("delta") \
.load("s3://epic-data/leaderboard/season_totals/") \
.withColumn("rank", row_number().over(leaderboard_window)) \
.filter(col("rank") <= 10000) \
.select("rank", "player_id", "season_score")
面试官追问:
“如果有数百万玩家同时在对局,排行榜怎么实时更新?”
我回答:
# 方案:分层更新策略
# - Top 100: 每次对局后立即更新(低延迟)
# - Top 100-10000: 每 5 分钟批量更新
# - Top 10000 以下: 每小时批量更新
# Top 100 实时管道
top_100_stream = scored_matches \
.join(top_100_players, on="player_id") \
.groupBy("player_id") \
.agg(f_sum("score").alias("score_delta"))
top_100_stream.writeStream \
.foreachBatch(lambda batch_df, batch_id:
# 直接更新 Redis
for row in batch_df.collect():
redis.incrby(f"leaderboard:{row.player_id}", row.score_delta)
redis.zadd("top_100_leaderboard", {row.player_id: redis.get(f"leaderboard:{row.player_id}")})
) \
.start()
# Redis 查询 Top 100(毫秒级)
# ZREVRANGE top_100_leaderboard 0 99 WITHSCORES
VO Round 2:系统设计 — 对局数据管道
题目:设计 Fortnite 的对局数据处理管道
Fortnite 每场对局产生大量数据(100 个玩家 × 数千个事件),设计一个能处理这种数据量的管道。
我的架构设计:
┌──────────────────────────────────────────────────────────────┐
│ Fortnite Match Data Pipeline │
│ │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ Game Servers │ │
│ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │
│ │ │ Server 1 │ │ Server 2 │ ... │ Server N │ │ │
│ │ │ (100 │ │ (100 │ │ (100 │ │ │
│ │ │ players)│ │ players)│ │ players)│ │ │
│ │ └────┬─────┘ └────┬─────┘ └────┬─────┘ │ │
│ │ │ │ │ │ │
│ │ ▼ ▼ ▼ │ │
│ │ ┌──────────────────────────────────────────────────┐ │ │
│ │ │ Match Event Buffer (本地缓冲,批量发送) │ │ │
│ │ │ Batch: 1000 events 或 5 秒(哪个先到) │ │ │
│ │ └──────────────────────┬───────────────────────────┘ │ │
│ └─────────────────────────┼──────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ Kafka (Event Ingestion) │ │
│ │ ┌────────────┐ ┌────────────┐ ┌──────────────────┐ │ │
│ │ │ match_raw │ │ match_agg │ │ match_summary │ │ │
│ │ │ .topic │ │ .topic │ │ .topic │ │ │
│ │ │ (10M msgs/ │ │ (100K msgs/│ │ (1K msgs/sec) │ │ │
│ │ │ sec) │ │ sec) │ │ │ │ │
│ │ └────────────┘ └────────────┘ └──────────────────┘ │ │
│ └──────────────────────────┬─────────────────────────────┘ │
│ │ │
│ ┌────────┴────────┐ │
│ ▼ ▼ │
│ ┌──────────────────────┐ ┌──────────────────────┐ │
│ │ Spark Streaming │ │ Flink (Real-time │ │
│ │ (Batch Aggregation) │ │ Analytics) │ │
│ └──────────┬───────────┘ └──────────┬───────────┘ │
│ │ │ │
│ ▼ ▼ │
│ ┌──────────────────┐ ┌──────────────────────┐ │
│ │ S3 / Delta Lake │ │ Redis (Leaderboard) │ │
│ │ (Raw + Processed│ │ Real-time Stats) │ │
│ └──────────────────┘ └──────────────────────┘ │
└──────────────────────────────────────────────────────────────┘
关键设计决策:
- 本地缓冲:游戏服务器本地缓存事件,批量发送而不是逐条发送,减少网络开销
- Kafka 分区策略:按
match_id分区,确保同一场对局的事件有序处理 - 三层处理:Raw events → Aggregated stats → Summary,逐层减少数据量
面试总结
成功经验
- 游戏数据思维:理解游戏对局数据的特点(高并发、事件流、时序性)
- 实时排行榜设计:知道如何用 Redis + Spark Streaming 实现实时排行榜
- 大规模数据处理:能处理每秒百万级事件的管道设计
注意事项
- 数据量级:Fortnite 每天有数十亿条事件,管道设计要考虑扩展性
- 低延迟要求:排行榜需要在对局结束后几秒内更新
- 数据分区策略:理解 Kafka 分区和 Spark 分区对性能的影响