Airbnb 数据工程师面试实录 2026:从取消率异常到 A/B 实验平台 完整复盘
Airbnb Data Engineer 面试真实经历:从取消率异常排查切入,SQL 数据倾斜优化、Spark 异常检测、A/B 实验数据平台设计完整复盘。第一人称面经,含面试官对话与解题思路。
公司: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 小时内预订 5+ 次(可能是刷单或 bot)
- 订单金额超过该用户历史平均金额的 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) │
└──────────────────────┘ └─────────────────────────────────┘
关键设计决策:
- Exposure Assignment:采用一致的哈希方案,确保用户在同一次实验中始终分配到同一个 variant
- Exposure Logging:每次用户触发实验事件时,记录 exposure log,用于后续分析
- 数据分层: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 验证,用两个定义跑了一周的预测模型,发现我的定义对业务指标预测能力更强。
面试总结
成功经验
- 从业务出发:Airbnb 的面试不是纯技术题,每个问题都有业务背景。能理解业务场景,回答才有说服力
- 数据倾斜处理:面试中多次考察数据倾斜场景,提前准备 broadcast join、salting、AQE 等方案
- 实验文化:Airbnb 非常重视 A/B 测试,对实验设计和分析的理解是加分项
注意事项
- 电话面就有深度:不要以为电话面只是简单筛选,Airbnb 的电话面就有 SQL 优化和数据倾斜的讨论
- System Design 贴近实际:A/B 实验平台是他们真实的业务场景,面试中会考察你对实验设计的理解
- 行为面试准备 STAR:虽然不像 Amazon 那样严格,但用 STAR 框架回答会更清晰
推荐阅读
💡 需要面试辅导?
我们的辅导团队来自 Airbnb、Google、Meta 等大厂,可以提供一对一的 DE 面试模拟辅导。