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)    │           │
│  └──────────────────┘    └──────────────────────┘           │
└──────────────────────────────────────────────────────────────┘

关键设计决策:

  1. 本地缓冲:游戏服务器本地缓存事件,批量发送而不是逐条发送,减少网络开销
  2. Kafka 分区策略:按 match_id 分区,确保同一场对局的事件有序处理
  3. 三层处理:Raw events → Aggregated stats → Summary,逐层减少数据量

面试总结

成功经验

  1. 游戏数据思维:理解游戏对局数据的特点(高并发、事件流、时序性)
  2. 实时排行榜设计:知道如何用 Redis + Spark Streaming 实现实时排行榜
  3. 大规模数据处理:能处理每秒百万级事件的管道设计

注意事项

  1. 数据量级:Fortnite 每天有数十亿条事件,管道设计要考虑扩展性
  2. 低延迟要求:排行榜需要在对局结束后几秒内更新
  3. 数据分区策略:理解 Kafka 分区和 Spark 分区对性能的影响

推荐阅读


💡 需要面试辅导?

联系我们

准备好拿下下一次面试了吗?

获取针对你的目标岗位和公司的个性化辅导方案。

联系我们