Airbnb 数据工程师面试实录 2026:从取消率异常到 A/B 实验平台 完整复盘
airbnbdata-engineerinterviewsqlsparkkafkadelta-lakedata-modelinga-b-testing

Airbnb 数据工程师面试实录 2026:从取消率异常到 A/B 实验平台 完整复盘

Airbnb Data Engineer 面试真实经历:从取消率异常排查切入,SQL 数据倾斜优化、Spark 异常检测、A/B 实验数据平台设计完整复盘。第一人称面经,含面试官对话与解题思路。

Sam · · 16 分钟阅读

公司:Airbnb 岗位:Data Engineer (L4) 面试形式:Phone Screen + Virtual Onsite (4 轮) 结果:Pass → Offer

2026 年 2 月通过内推投递了 Airbnb 的 Data Engineer 岗位。整个流程历时约 4 周。

Airbnb 的 DE 面试给我最深的印象是:他们不考模板题,而是从真实的业务问题出发。 电话面第一道题就是”某天早上 CEO 收到告警说 Cancellation Rate 飙升了 200%,你怎么排查”——不是让你背 SQL 语法,而是考验你面对真实数据危机的思考方式。

下面我按时间线完整复盘。


Phone Screen:从取消率异常出发的 SQL 排查

电话面由一位 Airbnb 的 Sr. Data Engineer 进行,45 分钟。

题目:找出在滚动 30 天内住过 3 个以上国家的用户

Airbnb 想做一个”环球旅行者”的营销活动,需要你找出那些在过去 30 天内入住过至少 3 个不同国家的用户。

-- 表结构
-- bookings: booking_id, user_id, checkin_date, checkout_date, country_code, status

-- 我的解答
WITH recent_bookings AS (
    SELECT
        user_id,
        country_code
    FROM bookings
    WHERE status IN ('confirmed', 'completed')
      AND checkin_date >= DATE_SUB(CURRENT_DATE(), INTERVAL 30 DAY)
),
country_count AS (
    SELECT
        user_id,
        COUNT(DISTINCT country_code) AS countries_visited
    FROM recent_bookings
    GROUP BY user_id
)
SELECT user_id, countries_visited
FROM country_count
WHERE countries_visited >= 3;

面试官追问:

“如果 bookings 表有 50 亿行,这个查询在 Spark SQL 上跑不动怎么办?”

我回答:

首先分析问题——checkin_date 过滤是有效的分区裁剪条件,但 COUNT(DISTINCT country_code) 会导致全局 shuffle。优化思路:

-- 优化 1: 分区裁剪 + 预聚合
-- 按日期分区,只扫描最近 30 天的分区
WITH recent AS (
    SELECT user_id, country_code
    FROM bookings
    WHERE checkin_date >= DATE_SUB(CURRENT_DATE(), INTERVAL 30 DAY)
      AND status IN ('confirmed', 'completed')
),
-- 先去重再聚合,减少 shuffle 数据量
user_countries AS (
    SELECT DISTINCT user_id, country_code
    FROM recent
)
SELECT user_id, COUNT(*) AS countries_visited
FROM user_countries
GROUP BY user_id
HAVING COUNT(*) >= 3;

面试官进一步追问:

“如果某个超级用户(比如 Airbnb 内部员工测试账号)有百万级 booking 记录,导致数据倾斜怎么办?”

我给出了几个方案:

-- 方案 1: Broadcast 小表(如果 user 维度不大)
SET spark.sql.autoBroadcastJoinThreshold = -1;  -- 强制不 broadcast

-- 方案 2: Salting — 给倾斜 key 加随机前缀
WITH salted AS (
    SELECT
        user_id,
        country_code,
        INT(RAND() * 10) AS salt
    FROM user_countries
)
SELECT user_id, COUNT(DISTINCT country_code) AS countries
FROM salted
GROUP BY user_id, salt
-- 最后再合并各 salt 的结果;

-- 方案 3: AQE (Adaptive Query Execution) - Spark 3.0+
SET spark.sql.adaptive.enabled = true;
SET spark.sql.adaptive.skewJoin.skewedPartitionFactor = 5;
SET spark.sql.adaptive.skewJoin.skewedPartitionThresholdInBytes = '1GB';

面试官听完 AQE 方案后点了点头,说他们生产环境确实用了这套配置。


VO Round 1:Spark 编码 — 异常预订检测

这一轮由一位 Airbnb Trust & Safety 团队的 DE 进行,60 分钟。

题目:实时异常预订模式检测

设计一个 Spark Streaming 管道,实时检测以下两类异常:

  1. 同一用户 1 小时内预订 5+ 次(可能是刷单或 bot)
  2. 订单金额超过该用户历史平均金额的 3 个标准差
from pyspark.sql import SparkSession
from pyspark.sql.functions import col, count, avg, stddev_pop, window, to_timestamp
from pyspark.sql import functions as F

spark = SparkSession.builder \
    .appName("Airbnb_Anomaly_Detection") \
    .getOrCreate()

# ====== 异常类型 1: 1 小时内 5+ 次预订 ======

bookings_stream = spark \
    .readStream \
    .format("kafka") \
    .option("kafka.bootstrap.servers", "kafka:9092") \
    .option("subscribe", "bookings.topic") \
    .load() \
    .selectExpr("CAST(value AS STRING)") \
    .select(F.from_json(F.col("value"), """
        user_id STRING, booking_id STRING, timestamp STRING,
        amount DOUBLE, country_code STRING, property_id STRING
    """).alias("data")) \
    .select("data.*") \
    .withColumn("event_time", to_timestamp(F.col("timestamp")))

# 滑动窗口:1 小时窗口,每 5 分钟触发一次
anomaly_1 = bookings_stream \
    .withWatermark("event_time", "2 hours") \
    .groupBy(
        window(col("event_time"), "1 hour"),
        col("user_id")
    ) \
    .agg(count("booking_id").alias("booking_count")) \
    .filter(col("booking_count") >= 5) \
    .select(
        col("window.start").alias("window_start"),
        col("user_id"),
        col("booking_count"),
        F.lit("FREQUENT_BOOKING").alias("anomaly_type")
    )

# ====== 异常类型 2: 金额 > 3 sigma ======

# 用户历史统计(批处理,从 Delta Lake 加载)
user_stats = spark \
    .read \
    .format("delta") \
    .load("s3://airbnb-data/historical/user_booking_stats") \
    .select("user_id", "avg_amount", "stddev_amount")

# 流 + 批 JOIN
anomaly_2 = bookings_stream \
    .withWatermark("event_time", "2 hours") \
    .join(
        user_stats,
        bookings_stream.user_id == user_stats.user_id,
        "left_outer"
    ) \
    .filter(
        (col("amount") > col("avg_amount") + 3 * col("stddev_amount")) &
        (col("stddev_amount").isNotNull())
    ) \
    .select(
        col("booking_id"),
        col("user_id"),
        col("amount"),
        col("avg_amount"),
        (col("amount") / col("avg_amount")).alias("ratio_to_avg"),
        F.lit("AMOUNT_ANOMALY").alias("anomaly_type")
    )

# 合并两种异常
anomalies = anomaly_1.unionByName(anomaly_2, allowMissingColumns=True)

# 写入 Delta Lake
anomalies.writeStream \
    .format("delta") \
    .outputMode("append") \
    .option("checkpointLocation", "s3://airbnb-checkpoints/anomalies") \
    .start("s3://airbnb-data/anomalies/realtime/")

面试官追问:

“这个管道跑了一周后,发现有很多误报——有些用户确实是真实的高频预订者(比如企业差旅用户)。你怎么减少误报?”

我回答:

# 思路:引入用户画像特征做过滤
# 1. 企业用户白名单
# 2. 历史行为模式(是否一直是高频用户)
# 3. 设备指纹 + IP 异常检测

from pyspark.sql.functions import when

anomaly_refined = anomalies \
    .join(
        spark.read.format("delta").load("s3://airbnb-data/users/profiles"),
        on="user_id",
        how="left"
    ) \
    .filter(
        # 排除企业用户
        (col("is_corporate") == False) &
        # 排除历史就是高频的用户
        (col("historical_monthly_bookings") < 5) &
        # 排除长期活跃用户
        (col("account_age_days") < 30)
    )

# 同时增加置信度评分
anomaly_scored = anomaly_refined \
    .withColumn("confidence_score",
        when(col("anomaly_type") == "FREQUENT_BOOKING", 
             col("booking_count") / 5.0 * 0.6) +
        when(col("anomaly_type") == "AMOUNT_ANOMALY",
             col("ratio_to_avg") * 0.4)
    )

VO Round 2:系统设计 — A/B 实验数据平台

这一轮由一位 Staff Data Engineer 进行,60 分钟。

题目:设计 Airbnb 的 A/B 实验数据管道

Airbnb 每天有数百个 A/B 实验在运行(价格优化、搜索排序、UI 改版等)。设计一个能支撑这些实验的数据管道。

我的架构设计:

┌──────────────────────────────────────────────────────────────┐
│                     Data Sources                              │
│  ┌──────────┐  ┌──────────┐  ┌──────────┐  ┌─────────────┐ │
│  │ Search   │  │ Booking  │  │ Listing  │  │ Review      │ │
│  │ Events   │  │ Events   │  │ Views    │  │ Events      │ │
│  └────┬─────┘  └────┬─────┘  └────┬─────┘  └──────┬──────┘ │
│       │              │              │               │        │
└───────┼──────────────┼──────────────┼───────────────┼────────┘
        │              │              │               │
        ▼              ▼              ▼               ▼
┌──────────────────────────────────────────────────────────────┐
│                  Kafka (Event Ingestion)                       │
│  ┌─────────────┐  ┌─────────────┐  ┌──────────────────────┐ │
│  │ search.topic│  │ booking.topic│ │  listing_view.topic   │ │
│  └─────────────┘  └─────────────┘  └──────────────────────┘ │
└──────────────────────┬───────────────────────────────────────┘


┌──────────────────────────────────────────────────────────────┐
│              Exposure Assignment Service                       │
│  ┌─────────────────────────────────────────────────────────┐  │
│  │  User → Hash(user_id + experiment_id) → Bucket (0-99)   │  │
│  │  Bucket 0-49 → Control, Bucket 50-99 → Treatment        │  │
│  │  写入 exposure_log: user_id, experiment_id, variant      │  │
│  └─────────────────────────────────────────────────────────┘  │
└──────────────────────┬───────────────────────────────────────┘


┌──────────────────────────────────────────────────────────────┐
│              Spark Streaming (Enrichment)                      │
│  ┌──────────────┐  ┌──────────────┐  ┌────────────────────┐ │
│  │ Event +      │  │ Windowed     │  │ Metric             │ │
│  │ Exposure JOIN│  │ Aggregation  │  │   Calculation      │ │
│  └──────────────┘  └──────────────┘  └────────────────────┘ │
└──────────┬───────────────────────────────┬───────────────────┘
           │                               │
           ▼                               ▼
┌──────────────────────┐    ┌─────────────────────────────────┐
│  Delta Lake          │    │  Redshift / Presto              │
│  (Raw + Processed)   │    │  (Aggregated Experiment Results)│
└──────────┬───────────┘    └──────────────┬──────────────────┘
           │                               │
           ▼                               ▼
┌──────────────────────┐    ┌─────────────────────────────────┐
│  Experiment          │    │  Dashboard (Superset/Looker)    │
│  Analysis Notebook   │    │  (Real-time Results)            │
└──────────────────────┘    └─────────────────────────────────┘

关键设计决策:

  1. Exposure Assignment:采用一致的哈希方案,确保用户在同一次实验中始终分配到同一个 variant
  2. Exposure Logging:每次用户触发实验事件时,记录 exposure log,用于后续分析
  3. 数据分层:Raw → Exposure Enriched → Aggregated → Serving
# Exposure Assignment 伪代码
import hashlib

def assign_variant(user_id: str, experiment_id: str, num_variants: int = 2) -> int:
    """一致的哈希分配方案"""
    hash_key = f"{user_id}:{experiment_id}"
    hash_val = int(hashlib.md5(hash_key.encode()).hexdigest(), 16)
    return hash_val % num_variants

def assign_bucket(user_id: str, experiment_id: str) -> str:
    variant = assign_variant(user_id, experiment_id)
    return "control" if variant == 0 else "treatment"
-- 实验效果分析 SQL
WITH experiment_data AS (
    SELECT
        e.experiment_id,
        e.variant,
        e.user_id,
        COUNT(CASE WHEN b.status = 'confirmed' THEN 1 END) AS bookings,
        SUM(CASE WHEN b.status = 'confirmed' THEN b.amount ELSE 0 END) AS revenue
    FROM exposure_log e
    LEFT JOIN bookings b
        ON e.user_id = b.user_id
        AND b.booking_date BETWEEN '2026-01-01' AND '2026-01-31'
    WHERE e.experiment_id = 'price_display_v2'
    GROUP BY e.experiment_id, e.variant, e.user_id
),
variant_stats AS (
    SELECT
        experiment_id,
        variant,
        COUNT(DISTINCT user_id) AS users,
        ROUND(AVG(bookings), 3) AS avg_bookings,
        ROUND(AVG(revenue), 2) AS avg_revenue
    FROM experiment_data
    GROUP BY experiment_id, variant
)
SELECT
    a.experiment_id,
    a.variant AS control_variant,
    b.variant AS treatment_variant,
    ROUND((b.avg_bookings - a.avg_bookings) / a.avg_bookings * 100, 2) AS bookings_lift_pct,
    ROUND((b.avg_revenue - a.avg_revenue) / a.avg_revenue * 100, 2) AS revenue_lift_pct
FROM variant_stats a
JOIN variant_stats b
    ON a.experiment_id = b.experiment_id
WHERE a.variant = 'control' AND b.variant = 'treatment';

面试官追问:

“如果两个实验有重叠用户,怎么分析交互效应(intersectionality)?”

我回答:这是 A/B 实验中的经典问题。Airbnb 内部用的是 Layered Experimentation 方案——不同实验放在不同的 layer 里,search layer 和 booking layer 的 bucket 是独立的,这样同一个用户可以同时参与多个实验而不冲突。


VO Round 3:行为面试

”讲一次你处理数据质量危机的经历”

我分享了一个真实经历:之前所在公司的核心报表数据突然少了 30%,原因是上游系统改了一个字段名但没通知数据团队。

STAR 框架:

  • Situation:周一早上发现 DAU 报表数据掉了一半,CEO 等着看周会数据
  • Task:需要在 2 小时内定位问题并恢复数据
  • Action:先查 pipeline 日志发现 transform 步骤有大量 NULL,回溯发现上游 API response 字段从 user_id 改成了 uid,没更新 schema
  • Result:紧急修复 + 加了 schema drift detection 的自动化告警,后续再也没有出现过类似问题

”你怎么和数据分析师在指标定义上有分歧?”

我分享了关于”活跃用户”定义的经历——分析师认为只要有登录就算活跃,我认为应该有实质性操作才算。最后我们做了 A/B 验证,用两个定义跑了一周的预测模型,发现我的定义对业务指标预测能力更强。


面试总结

成功经验

  1. 从业务出发:Airbnb 的面试不是纯技术题,每个问题都有业务背景。能理解业务场景,回答才有说服力
  2. 数据倾斜处理:面试中多次考察数据倾斜场景,提前准备 broadcast join、salting、AQE 等方案
  3. 实验文化:Airbnb 非常重视 A/B 测试,对实验设计和分析的理解是加分项

注意事项

  1. 电话面就有深度:不要以为电话面只是简单筛选,Airbnb 的电话面就有 SQL 优化和数据倾斜的讨论
  2. System Design 贴近实际:A/B 实验平台是他们真实的业务场景,面试中会考察你对实验设计的理解
  3. 行为面试准备 STAR:虽然不像 Amazon 那样严格,但用 STAR 框架回答会更清晰

推荐阅读


💡 需要面试辅导?

我们的辅导团队来自 Airbnb、Google、Meta 等大厂,可以提供一对一的 DE 面试模拟辅导。

联系我们

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

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

联系我们