lyftdata-engineerinterviewkafkasparkh3gisreal-timestreaming

Lyft 数据工程师面试实录 2026:H3 地理网格 + Kafka 实时 ETA 管道 完整复盘

Lyft Data Engineer 面试真实经历:H3 地理网格编码、Kafka 实时流处理、ETA 预测数据管道设计完整复盘。第一人称面经,含面试官对话与解题思路。

Sam · · 16 分钟阅读

公司: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 用它来保证:

  1. 司机位置更新不丢不重 — 影响匹配算法的准确性
  2. 计费事件精确一次 — 避免重复收费或漏收费
  3. 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 集群重启了。”

我回答:

  1. 多副本:Redis Cluster 做主从复制 + sentinel 自动故障转移
  2. 降级策略:如果 Redis 挂了,回退到 Delta Lake 中的最近 5 分钟数据
  3. 冷启动:服务启动时先从 Delta Lake 加载历史速度数据填充 Redis

VO Round 3:行为面试

”讲一次你排查复杂流处理管道的经历”

我分享了一个真实经历:之前负责的 Kafka 消费管道突然出现消息堆积,延迟从秒级飙升到分钟级。

排查过程:

  1. 先看 Kafka Lag 监控——发现某个 partition 的 lag 异常高
  2. 查 consumer group 状态——发现一个 consumer 实例反复 rejoin
  3. 深入日志——发现是某个异常消息(null key + 超大 value)导致 deserialization 超时
  4. 根因:上游系统 bug 导致偶尔产生畸形消息

**解决:**加了 dead letter queue(DLQ),异常消息进入 DLQ 而不是阻塞整个分区。同时加了消息大小的 pre-check。

“你怎么平衡实时准确性和延迟?”

我回答:这是每个实时系统都要做的 trade-off。我的经验是:

  1. 定义 SLA:ETA 预测的准确率目标是多少(比如 90% 在 ±1 分钟内)
  2. 分级处理:核心指标(当前速度)要求低延迟,辅助指标(历史趋势)可以接受稍高延迟
  3. 监控退化:当数据质量下降时,自动降级到更保守的估计值

面试总结

成功经验

  1. H3 地理网格:Lyft 大量使用 Uber 开源的 H3 做空间索引,面试前了解 H3 的基本概念很有帮助
  2. 流处理深度:不仅是会用 Spark Streaming,还要理解 watermark、late data handling、exactly-once 语义
  3. Redis 缓存设计:实时系统离不开低延迟存储,知道怎么设计缓存和降级策略是加分项

注意事项

  1. GPS 数据处理是核心:从坐标转换到速度计算到空间索引,面试全程围绕地理数据
  2. 实时性要求高:Lyft 的业务场景对延迟敏感,方案设计时要考虑毫秒级响应
  3. Kafka 是基础:不仅要知道 API,还要理解 partition、replication、consumer group 等底层概念

推荐阅读


💡 需要面试辅导?

联系我们

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

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

联系我们