Lyft 数据工程师面试实录 2026:H3 地理网格 + Kafka 实时 ETA 管道 完整复盘
Lyft Data Engineer 面试真实经历:H3 地理网格编码、Kafka 实时流处理、ETA 预测数据管道设计完整复盘。第一人称面经,含面试官对话与解题思路。
公司:Lyft 岗位:Data Engineer (L5) 面试形式:Phone Screen + Virtual Onsite (4 轮) 结果:Pass → Offer
2026 年 3 月通过 Lyft 内推投递了 Data Engineer 岗位。整个流程大约 3 周。
Lyft 的 DE 面试和其他公司最大的不同:每一道题都围绕实时数据和地理位置展开。 他们每天都在处理海量的 GPS 数据流——数百万司机和乘客的实时位置、路线匹配、ETA 预测。面试中大量涉及 H3 地理网格编码、Kafka 流处理和低延迟数据管道设计。
下面我按时间线完整复盘。
Phone Screen:实时区域流量统计
电话面由一位 Lyft 的 Sr. Data Engineer 进行,45 分钟。
题目 1:滑动窗口统计各区域实时叫车量
给定司机和乘客的 GPS 事件流,实时统计每个地理区域(每 5 分钟窗口)的活跃司机数和待命乘客数。
-- GPS 事件表
-- gps_events: event_id, user_id, user_type ('driver'/'rider'),
-- lat, lng, timestamp, status ('active'/'idle'/'in_trip')
-- 我的解答(Spark SQL 风格)
SELECT
h3_index,
window_start,
COUNT(DISTINCT CASE WHEN user_type = 'driver' AND status = 'active' THEN user_id END) AS active_drivers,
COUNT(DISTINCT CASE WHEN user_type = 'rider' AND status = 'idle' THEN user_id END) AS waiting_riders
FROM (
SELECT
*,
ST_Point(lat, lng) AS location,
window(timestamp, '5 minutes') AS win
FROM gps_events
WHERE timestamp >= TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL 30 MINUTE)
)
GROUP BY h3_index, window_start;
面试官追问:
“如果芝加哥这样的城市每分钟有 100 万条 GPS 事件,这个查询怎么处理得过来?”
我回答:
核心问题在于 COUNT(DISTINCT) 在全局维度上会触发大规模 shuffle。解决方案:
-- 方案 1: 预聚合到 H3 粒度再合并
-- 每个 executor 处理自己的 H3 区域,最后 merge
WITH per_driver AS (
-- 每个司机在每个窗口只算 1 次
SELECT user_id, h3_index, window_start
FROM (
SELECT
user_id,
h3_index,
window_start,
ROW_NUMBER() OVER (PARTITION BY user_id, window_start ORDER BY timestamp) AS rn
FROM gps_events_enriched
)
WHERE rn = 1
)
SELECT
h3_index,
window_start,
SUM(CASE WHEN user_type = 'driver' THEN 1 ELSE 0 END) AS active_drivers,
SUM(CASE WHEN user_type = 'rider' THEN 1 ELSE 0 END) AS waiting_riders
FROM per_driver
GROUP BY h3_index, window_start;
面试官继续问:
“说说 Kafka 的 Exactly-Once Semantics 是什么意思?Lyft 用在哪里?”
我回答:
Kafka 的 Exactly-Once 保证每条消息被消费且仅被处理一次,即使有故障恢复也不会重复或丢失。
# Exactly-Once 生产者
from kafka import KafkaProducer
producer = KafkaProducer(
bootstrap_servers='kafka:9092',
# 开启事务
acks='all',
retries=3,
enable_idempotence=True, # 幂等性 — 保证单分区内不重复
max_in_flight_requests_per_connection=5
)
# 事务性处理(跨分区 exactly-once)
producer.init_transactions()
producer.begin_transaction()
try:
for msg in events:
producer.send('gps_events.topic', value=msg)
producer.commit_transaction()
except Exception as e:
producer.abort_transaction()
raise e
# 消费端:配合 isolation_level='read_committed'
from kafka import KafkaConsumer
consumer = KafkaConsumer(
'gps_events.topic',
bootstrap_servers='kafka:9092',
group_id='eta_pipeline',
isolation_level='read_committed', # 只读已提交的事务消息
auto_offset_reset='latest'
)
Lyft 用它来保证:
- 司机位置更新不丢不重 — 影响匹配算法的准确性
- 计费事件精确一次 — 避免重复收费或漏收费
- ETA 计算输入一致 — 确保预测模型看到的是准确的数据快照
VO Round 1:Kafka Streaming — 实时司机匹配管道
这一轮由一位 Lyft 匹配引擎团队的 DE 进行,60 分钟。
题目:基于 H3 网格的实时司机-乘客匹配
设计一个实时管道,当乘客下单时,快速找到周围 2 公里内最近的空闲司机。
from pyspark.sql import SparkSession
from pyspark.sql.functions import col, udf, lit
from pyspark.sql.types import StringType
import h3
spark = SparkSession.builder \
.appName("Lyft_Matching_Pipeline") \
.getOrCreate()
# H3 编码 UDF — 将经纬度转为 H3 网格 ID
@udf(StringType())
def latlng_to_h3(lat: float, lng: float, resolution: int = 7) -> str:
return h3.geo_to_h3(lat, lng, resolution)
# ====== 读取司机位置流 ======
driver_stream = spark \
.readStream \
.format("kafka") \
.option("kafka.bootstrap.servers", "kafka:9092") \
.option("subscribe", "driver_locations.topic") \
.load() \
.selectExpr("CAST(value AS STRING)") \
.select(F.from_json(F.col("value"), """
driver_id STRING, lat DOUBLE, lng DOUBLE,
status STRING, timestamp STRING
""").alias("data")) \
.select("data.*") \
.filter(col("status") == "available") \
.withColumn("h3", latlng_to_h3(col("lat"), col("lng"), lit(7)))
# ====== 读取乘客请求流 ======
ride_requests = spark \
.readStream \
.format("kafka") \
.option("kafka.bootstrap.servers", "kafka:9092") \
.option("subscribe", "ride_requests.topic") \
.load() \
.selectExpr("CAST(value AS STRING)") \
.select(F.from_json(F.col("value"), """
request_id STRING, rider_id STRING,
pickup_lat DOUBLE, pickup_lng DOUBLE, timestamp STRING
""").alias("data")) \
.select("data.*") \
.withColumn("h3", latlng_to_h3(col("pickup_lat"), col("pickup_lng"), lit(7)))
# ====== 两阶段匹配策略 ======
# 阶段 1: H3 网格粗匹配(同网格内的司机)
# 使用 streaming-waterfall join(流 JOIN 静态快照)
driver_snapshot = driver_stream \
.withWatermark("timestamp", "1 minute") \
.groupBy("h3", "driver_id", "lat", "lng") \
.agg(F.first("driver_id").alias("driver_id"))
coarse_match = ride_requests.join(
driver_snapshot,
ride_requests.h3 == driver_snapshot.h3,
"inner"
)
# 阶段 2: 精确距离过滤(2 公里内)
from pyspark.sql.functions import sqrt, pow
@udf(DoubleType())
def haversine_distance(lat1, lng1, lat2, lng2):
R = 6371 # km
dlat = (lat2 - lat1) * 3.14159 / 180
dlng = (lng2 - lng1) * 3.14159 / 180
a = (pow(3.14159/180 * dlat/2, 2) +
(3.14159/180 * lat1) * (3.14159/180 * lat2) * pow(3.14159/180 * dlng/2, 2))
c = 2 * 2 * 0.559 # atan2(2*sqrt(a), sqrt(1-a))
return R * 2 * 0.559 # 简化版
precise_match = coarse_match \
.withColumn("distance_km", haversine_distance(
col("pickup_lat"), col("pickup_lng"), col("lat"), col("lng")
)) \
.filter(col("distance_km") <= 2.0) \
.orderBy(col("distance_km")) \
.select(
col("request_id"),
col("rider_id"),
col("driver_id"),
col("distance_km"),
col("timestamp")
)
# ====== 回退策略:扩大搜索范围 ======
# 如果同网格没找到司机,搜索相邻 H3 网格
@udf(StringType())
def get_neighbors(h3_id):
return h3.k_ring(h3_id, 1) # 获取 K-ring 1 的邻居
# 写入匹配结果
precise_match.writeStream \
.format("kafka") \
.option("kafka.bootstrap.servers", "kafka:9092") \
.option("topic", "driver_matches.topic") \
.option("checkpointLocation", "s3://lyft-checkpoints/matching") \
.start()
面试官追问:
“如果某区域司机密度很低(比如郊区),2 公里内找不到司机怎么办?”
我回答:
# 回退策略:逐步扩大搜索半径
def expand_search(request_h3, max_k_ring=3):
"""逐步扩大 H3 搜索范围"""
for k in range(1, max_k_ring + 1):
neighbors = h3.k_ring(request_h3, k)
# 查询这些网格内的司机
drivers = query_drivers_in_h3(neighbors)
if drivers:
return drivers
return None # 最终返回 null,前端显示"附近暂无司机"
# 同时配合动态 ETA 调整
# 如果最近司机在 3 公里外,ETA 从 "2 分钟" 调整为 "5 分钟"
# 如果超过 10 公里,建议乘客使用 Lyft Line 或其他方案
VO Round 2:系统设计 — 实时 ETA 管道
这一轮由一位 Staff Data Engineer 进行,60 分钟。
题目:设计 Lyft 的实时 ETA(到达时间预测)数据管道
Lyft 需要在乘客下单时给出准确的 ETA。设计一个数据管道,实时计算每个区域的预估到达时间。
我的架构设计:
┌──────────────────────────────────────────────────────────────┐
│ GPS Data Sources │
│ ┌────────────┐ ┌────────────┐ ┌────────────┐ │
│ │ Driver App │ │ Rider App │ │ Traffic │ │
│ │ (GPS Pings)│ │ (GPS Pings)│ │ API │ │
│ └──────┬─────┘ └──────┬─────┘ └──────┬─────┘ │
│ │ │ │ │
└─────────┼───────────────┼───────────────┼────────────────────┘
│ │ │
▼ ▼ ▼
┌──────────────────────────────────────────────────────────────┐
│ Kafka (Event Ingestion) │
│ ┌──────────────┐ ┌──────────────┐ ┌───────────────────┐ │
│ │ driver_gps │ │ rider_gps │ │ traffic_events │ │
│ │ .topic │ │ .topic │ │ .topic │ │
│ └──────────────┘ └──────────────┘ └───────────────────┘ │
└──────────────────────┬───────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────────┐
│ Spark Structured Streaming │
│ ┌────────────────┐ ┌────────────────┐ ┌────────────────┐ │
│ │ GPS → Speed │ │ H3 Zone Speed │ │ ETA Prediction │ │
│ │ Segment Calc │ │ Aggregation │ │ (ML Model) │ │
│ └────────────────┘ └────────────────┘ └────────────────┘ │
└──────────┬───────────────────────────┬───────────────────────┘
│ │
▼ ▼
┌──────────────────┐ ┌──────────────────────────────────────┐
│ Redis Cache │ │ Delta Lake (Historical Speed Data) │
│ (Real-time ETA │ │ (Model Training Data) │
│ Lookup < 1ms) │ └──────────────────────────────────────┘
└──────────────────┘
│
▼
┌──────────────────┐
│ Lyft Mobile App │
│ (Show ETA to │
│ Rider) │
└──────────────────┘
核心处理逻辑:
# ====== 第一步:GPS 轨迹 → 速度片段 ======
@udf(DoubleType())
def calc_speed(lat1, lng1, lat2, lng2, time1, time2):
"""计算两点间的速度(km/h)"""
distance = haversine(lat1, lng1, lat2, lng2) # km
time_diff = (time2 - time1) / 3600.0 # hours
return distance / time_diff if time_diff > 0 else 0.0
# ====== 第二步:按 H3 网格聚合实时速度 ======
speed_by_zone = gps_stream \
.withWatermark("timestamp", "5 minutes") \
.groupBy(
window(col("timestamp"), "2 minutes"),
col("h3_zone")
) \
.agg(
F.percentile_approx(col("speed_kmh"), 0.5).alias("median_speed"),
F.percentile_approx(col("speed_kmh"), 0.9).alias("p90_speed"),
F.count("driver_id").alias("sample_size")
) \
.filter(col("sample_size") >= 10) # 至少 10 个样本才可靠
# ====== 第三步:写入 Redis(低延迟查询) ======
# 使用 Spark + Redis connector
speed_by_zone.writeStream \
.foreachBatch(lambda batch_df, batch_id:
batch_df.write \
.format("org.apache.spark.redis") \
.option("redis.host", "redis://redis:6379") \
.option("tablePrefix", "eta:") \
.mode("overwrite") \
.save()
) \
.option("checkpointLocation", "s3://lyft-checkpoints/eta") \
.start()
# Redis 查询示例(毫秒级响应)
# GET eta:8a2a1072b59ffff → {"median_speed": 35.2, "p90_speed": 45.0}
面试官追问:
“Redis 里的 ETA 数据如果丢了怎么办?比如 Redis 集群重启了。”
我回答:
- 多副本:Redis Cluster 做主从复制 + sentinel 自动故障转移
- 降级策略:如果 Redis 挂了,回退到 Delta Lake 中的最近 5 分钟数据
- 冷启动:服务启动时先从 Delta Lake 加载历史速度数据填充 Redis
VO Round 3:行为面试
”讲一次你排查复杂流处理管道的经历”
我分享了一个真实经历:之前负责的 Kafka 消费管道突然出现消息堆积,延迟从秒级飙升到分钟级。
排查过程:
- 先看 Kafka Lag 监控——发现某个 partition 的 lag 异常高
- 查 consumer group 状态——发现一个 consumer 实例反复 rejoin
- 深入日志——发现是某个异常消息(null key + 超大 value)导致 deserialization 超时
- 根因:上游系统 bug 导致偶尔产生畸形消息
**解决:**加了 dead letter queue(DLQ),异常消息进入 DLQ 而不是阻塞整个分区。同时加了消息大小的 pre-check。
“你怎么平衡实时准确性和延迟?”
我回答:这是每个实时系统都要做的 trade-off。我的经验是:
- 定义 SLA:ETA 预测的准确率目标是多少(比如 90% 在 ±1 分钟内)
- 分级处理:核心指标(当前速度)要求低延迟,辅助指标(历史趋势)可以接受稍高延迟
- 监控退化:当数据质量下降时,自动降级到更保守的估计值
面试总结
成功经验
- H3 地理网格:Lyft 大量使用 Uber 开源的 H3 做空间索引,面试前了解 H3 的基本概念很有帮助
- 流处理深度:不仅是会用 Spark Streaming,还要理解 watermark、late data handling、exactly-once 语义
- Redis 缓存设计:实时系统离不开低延迟存储,知道怎么设计缓存和降级策略是加分项
注意事项
- GPS 数据处理是核心:从坐标转换到速度计算到空间索引,面试全程围绕地理数据
- 实时性要求高:Lyft 的业务场景对延迟敏感,方案设计时要考虑毫秒级响应
- Kafka 是基础:不仅要知道 API,还要理解 partition、replication、consumer group 等底层概念