← 返回学习台
D1 · 摄取与转换 34%

Ch03 · Lambda 摄取模式 — 限制、并发、Fan-out

Lambda 是 serverless 万金油,但 15min 上限、冷启动、DLQ、幂等——这些坑要逐个踩。

🎯 先试试

你给 japanese-climb 新加了话题"日本の祭り",要从 200 个 URL 一次性抓取素材(混着 YouTube 链接和文章页)。每个 URL 处理 5–30 秒,YouTube 还要下音频+Whisper 转写可能 1–2 分钟。搬 AWS 实现一次抓 200 个的 ingest,最对的架构?

💡 讲透

🎣 钩子:B 和 C 都能 fan-out 并行,为啥 B 才是"最对"?题目想测什么?

🏠 生活类比:你要发 200 份外卖订单。
A 单人:你一个人一单一单跑——天亮还没送完,路上挂了订单全丢。
B SQS+Lambda:你把 200 单丢进派单系统(SQS),平台自动派给 200 个骑手(Lambda)并发跑;某个骑手摔了,平台自动重派给别人(DLQ 兜底)。
C Step Functions Map:你雇了项目经理(编排)+ 派单系统,效果一样但每张单子要给经理记账费——纯 fan-out 不值。
D EC2:你买了辆车自己跑——开关车、加油、修轮胎全你来。

👶 深入浅出

  • SQS + Lambda 事件源映射:发 200 条到队列 → AWS 自动拉消息批量调 Lambda(默认 batch 10,可调 1-10000)→ 默认账户级 1000 并发上限
  • 失败自动重试:visibility timeout 后消息重投,maxReceiveCount 之后进 DLQ + CloudWatch Alarm
  • 幂等设计是必修:用 sha1(URL) 当 DynamoDB 主键,Lambda 进来先查"处理过没"
graph LR CLI[ingest --topic 祭り] -->|SendMessageBatch x20| Q[SQS Standard
200 URL 消息] Q -->|event source mapping| LMB[Lambda
ingest.py 单 URL 版] LMB -->|成功| S3[S3 raw/] LMB -->|状态| DDB[DynamoDB
幂等表 sha1url] Q -.maxReceive=3.-> DLQ[SQS DLQ] DLQ -.CW Alarm.-> EMAIL[告警邮件] style Q fill:#fff4ef,stroke:#ff6f3c style DLQ fill:#fdf2f2,stroke:#e03131

🎯 每个选项为什么对/错

❌ A · 单 Lambda 串行
200 × 平均 1 分钟 = 200 分钟 — 远超 Lambda 15 分钟硬上限。即使每个 5 秒(1000 秒 = 16 分钟)也爆。
真实后果:执行到一半 timeout 被杀,已处理的没记重跑要从头来——不幂等。
❌ C · Step Functions Map
确实能并行 fan-out 调 Lambda——但你为了纯 fan-out 引入了 Step Functions 的复杂度+费用(每个 state transition 计费)。
真实后果:能跑、账单贵一倍、没用上的复杂度(分支/重试声明/可视化)变成维护负担。Step Functions 的甜区是多步骤+分支+长跑流程(见 Ch04),纯 fan-out 用 SQS+Lambda 才对。
❌ D · EC2 单机
倒退回运维 EC2:开机/关机/失败重试/并发度全手动。串行还是慢;多线程要自己写。
真实后果:每月多花 + serverless 优势全废 + 失败要 ssh 上去看日志。
✅ B · SQS + Lambda
200 条消息发进 SQS,AWS 自动并发拉调 Lambda——理论上 1 分钟搞定(200 并发,每个 1min)。失败自动重试,超 maxReceive 进 DLQ + 告警。幂等:用 sha1(URL) 当 DDB 主键,重投不重复处理。

📌 考点速记

  • Lambda 硬上限:15 分钟、10 GB 内存、512 MB /tmp(可付费扩到 10 GB)、并发 1000(账户级默认,可申请提升到几万)
  • 冷启动:~100 ms – 1 s(VPC Lambda 更慢);解法 = Provisioned Concurrency(预热实例)或 Java 用 SnapStart
  • Reserved vs Provisioned concurrency:Reserved = 上限(保留专属配额);Provisioned = 预热(开机即用)
  • 触发方式:同步(API GW)失败客户端立刻看到;异步(S3/SNS)Lambda 内置重试 2 次→DLQ;事件源(SQS/Kinesis)队列控制重试
  • SQS Standard 不保序、可能重复送达;FIFO 保序但 300 msg/s(无 batch)或 3000(有 batch)
  • DLQ + maxReceiveCount = 防"毒消息"卡死队列的必备组合
  • Lambda 幂等没有内置保证——业务键去重是的责任

🛠 回到 japanese-climb

对应代码:src/jclimb/ingest.py + src/jclimb/source_corpus.py

当前实现:CLI 串行处理 URL,本地 yt-dlp/trafilatura,失败写 ingest.log 继续下一个。单机够用,多用户/大批量崩。

搬 AWS:

  1. CLI 改成 aws sqs send-message-batch 批发 200 条消息(每批 10 条,20 次调用)
  2. Lambda 配 SQS event source mapping → 自动并发,每条消息一个 Lambda 实例处理
  3. 幂等关键:Lambda 进来先查 DynamoDB ingest_state 表(PK=sha1(URL)),已存在 + 状态 success 直接跳过
  4. 处理成功 → 写 s3://japanese-climb/raw/<topic>/<source_id>.txt + 更新 DDB 状态
  5. 失败 3 次进 DLQ;CloudWatch Alarm 监控 DLQ 深度 > 0 发邮件

YouTube + Whisper 流程别走 SQS+Lambda——单条 1-2 分钟接近 Lambda 15min 上限的危险区,应该走 Step Functions(下章)。

📌 闪卡

Lambda 最长执行时间?最大内存?
15 分钟(900 秒)、10 GB 内存。超过任一上限就需要拆步骤(Step Functions / Fargate)。
SQS visibility timeout 默认多少?作用?
默认 30 秒。消息被消费者拉取后在这段时间对其他消费者隐藏;超时前消费者没显式 delete = 视为失败,消息重投。设置应 ≥ Lambda 处理时间的 6 倍(避免长任务被误重投)。
Lambda 异步调用失败默认重试几次?
2 次(共 3 次尝试),间隔指数 backoff(~1min / ~2min);之后进 DLQ。同步调用:调用方自己处理。事件源(SQS/Kinesis):队列驱动重试,配 maxReceiveCount。
Reserved concurrency vs Provisioned concurrency?
Reserved = "给我留这么多并发额度"(也是上限);Provisioned = "预热这么多实例 always-on",免冷启动但要钱。常一起用:reserved=10、provisioned=5。

🔁 变体题

变体 1:Lambda 处理 SQS 消息成功了但忘了 delete,30 秒后又被另一个 Lambda 实例处理一遍。这是什么问题?怎么避免?

✅ C:即使 FIFO 也有 at-least-once 语义在极少情况下出现,分布式系统永远不能假设 exactly-once。幂等设计是必修课——用 messageId 或业务键去重是事件驱动系统的铁律。B 是治标不治本:调长 timeout 只是减少重投概率,没消除。

变体 2:你的 Lambda 调用 Aurora PostgreSQL,并发 500 时数据库连接数爆了。最合适的修法?

✅ C:经典题型。RDS Proxy 复用连接池(防短连接打爆数据库)是首选;如果数据库写入本身是瓶颈,限流 Lambda 并发也对。生产里两个手段经常一起上。考 AWS 题碰到"A 和 B 都对"要敢选。