Ch.1-2: 权威数据源与派生数据的世界观 + 快稳弹的度量 — 看延迟用分位数, 防过载靠背压, 容错是让 fault 不升级为 failure
DDIA 的世界观是一句话: 一个权威数据源 (system of record), 万千派生视图 — OLTP 库是唯一的"账本", 缓存、搜索索引、物化视图、数仓都是"账本的影印件", 丢了随时重印。性能度量像地铁客流统计: 平均值说"每班车间隔 10 分钟", 但你等的那班可能 40 分钟 — 所以要看 p50/p99; 过载防御像游乐园分流: 门口限流 (token bucket)、临时关闭低优项目 (load shedding)、坏掉的项目先挂牌检修 (熔断)。
-- OLTP: 按 key 取一条 SELECT * FROM orders WHERE id = 42; -- OLAP: 扫全年做聚合 SELECT date_trunc('day', ts), sum(amount) FROM orders GROUP BY 1;
# 经典管道: 业务库 → ETL → 数仓 → BI extract: cdc_from(mysql) # 从 WAL 抽变更 transform: clean + join # 库外或库内 (ELT) load: warehouse / lake # Parquet 落湖
# 判定法: 这个数据丢了要紧吗? # 订单表丢了 → 要命 → SoR (备份/复制) # 搜索索引丢了 → 重放源数据重建 → 派生
# 首页时间线: 不实时查关注者 (慢), 预写 timelines 表 # 发推时: fan-out 到粉丝的 timeline (写扩散) # 读时: SELECT * FROM timeline WHERE user_id=? (点查)
-- fact_sales: 每行一次购买 SELECT f.amount, d.week, p.brand FROM fact_sales f JOIN dim_date d ON f.date_id = d.id -- 维度外键 JOIN dim_product p ON f.product_id = p.id;
# 云数仓配置示意 compute: autoscale 2~32 nodes # 算力弹性 storage: s3://lake/parquet # 持久层独立 # 缩掉 compute 不丢数据 — 存算分离的意义
# 服务端: handler 只花 2ms # 客户端: 2ms + 排队 180ms + 网络 20ms = 202ms # 服务端监控永远"健康" — 队头阻塞它看不见
lat = [10]*98 + [200] + [2000] mean = sum(lat)/len(lat) # → 40ms, 看着还行 # sorted[98] → p99=200ms sorted[99] → p999=2s
p = 0.01 # 单个后端仅 1% 概率慢 for n in (1, 20, 100): print(n, 1-(1-p)**n) # 1% / 18.2% / 63.4%
# fault: raid1 坏一块盘 (预期内) # failure: "整个订单库不可写" (违约) # 容错设计问题: "这块盘坏掉, 系统还正确吗?"
# 风暴闭环: timeout(1s) < p999(2s) 时必然触发 # 止血: 重启 (打破状态) → 修 retry 预算 → 熔断兜底
delay = 0.1 * 2**n # 指数: 0.1/0.2/0.4s delay *= 1 + random() # jitter 防同步踩点 # budget: 重试总量 ≤ 正常流量 10%
# 5 亿帖/天 ≈ 5800/s 均值, 峰 15万/s # 轮询读: 200万 qps × 4亿次查找/s → 扛不住 # 写扩散: 发推 15万/s × 平均粉丝数 → 选型依据
分析师一条全表聚合把 MySQL CPU 打满, 下单接口 P99 飙升 — 分析负载必须搬出 OLTP。
-- 症状: 这条 SQL 在业务库跑了 40s, 行锁+CPU 双杀 SELECT count(*) FROM orders WHERE created_at >= '2026-01-01' AND status = 'paid'; # 改造: nightly ETL 把 orders 摄取到数仓 (列存) # pipeline.yaml source: mysql.replica # 从从库抽, 不碰主库 sink: clickhouse.orders # 列存, 扫描快 1000× schedule: "0 2 * * *" # 低峰执行 # 效果: 同一查询 40s → 0.4s, OLTP 恢复平静
要实时就上 CDC (第 9 页), 这里 nightly 已解决 90% 的报表需求。
大盘平均 30ms 一片绿, 客服投诉不断 — 把监控换成分位数视图。
import statistics lat = load_client_side_rtt() # 必须是客户端采样! def pct(data, q): s = sorted(data) return s[min(int(len(s)*q), len(s)-1)] print(pct(lat, 0.50), pct(lat, 0.95), pct(lat, 0.99)) # 10ms 30ms 200ms — 尾部真相出来了 # Prometheus 侧: histogram bucket 覆盖到 2.5s+ # Buckets: [.005, .01, .025, .05, .1, .25, .5, 1, 2.5, 5]
配套告警: p99 > 500ms 持续 5m, 而不是 avg。
每个下游单独看都是"好服务", 详情页却 18% 的请求踩到慢请求。
backends, p_slow = 20, 0.01 overall = 1 - (1-p_slow)**backends # → 0.182: 每次详情页有 18% 概率被一个慢下游拖住 # 缓解 1: 削减扇出 — 20 个下游合并成 4 个聚合服务 overall = 1 - (1-p_slow)**4 # → 3.9% # 缓解 2: hedged request — 95 分位仍未返回就发第二份 # 缓解 3: 给非关键下游设 deadline, 超时降级不阻塞渲染
改造后详情页 P99 从 1.8s 回到 210ms。
依赖抖动 30 秒, 全网重试把系统按进过载状态, 流量恢复后依然瘫 — 只能重启, 然后补三件套。
# 事故还原: timeout=1s, 但过载时 p999=2.5s # → 请求注定超时 → 全员重试 ×3 → 负载 ×4 → 永远过载 # 止血三件套 (改造后): # 1) 退避+抖动: 100ms×2^n ± 50%, 打散重试时间轴 # 2) 熔断: 30s 窗口错误率 > 50% 且 ≥20 请求 → open 30s # 3) 负载丢弃: 过载时先拒 /recommend 这类非关键请求 # 4) retry budget: 重试流量 ≤ 正常流量 10%
原则: 每层重试预算独立审批 — 三层各 ×3 就是 ×27。
twitter 案例数字化: 用负载参数算清两种架构的天花板再动手。
# 负载参数: 5亿帖/天 ≈ 5800/s (峰 15万/s), 均粉丝 750 posts = 150_000 # 峰值发帖/s avg_followers = 750 # 方案 A: 写扩散 — 发帖时投递到每个粉丝的时间线 fanout_write = posts * avg_followers # ≈ 1.1 亿写/s ✗ # 方案 B: 读时聚合 (轮询) — 刷时间线时现查关注列表 poll_read = 200 * 10_000 * 4 # 200万qps×4亿查找 ✗ # 事实方案: 混合 — 普通用户写扩散, 大V (百万粉) 不扩散, # 读时把大V的帖子 merge 进来
启示: 没有数字就没有架构 — 先算两头再选中间。
把业务库的多张表重组成分析友好的星型模型, 查询从 8 张表 JOIN 变 2 张。
-- 事实表: 每行一次购买 (事件), 度量 + 维度外键 CREATE TABLE fact_sales ( id bigint, ts timestamp, product_id int, store_id int, date_id int, -- 维度外键 amount decimal(10,2), qty int -- 度量 ); -- 维度表: 描述性参照 CREATE TABLE dim_product (id int, brand text, category text); -- OBT 变体: brand 直接折叠进事实表 — 空间换扫描速度
分析查询从 8 JOIN 降到 2 JOIN, 列存下扫描量再降一个量级。
推荐模型在数仓训练, 但线上服务只认业务库 — 分析产出需要回流。
# 流向: 业务库 → ETL → 数仓 → 训练 → score → 回流 → 业务库 # 回流用 outbox 思想 (详见流处理页), 不直连双写: -- 业务库: score 表由管道写入, 业务只读 CREATE TABLE user_score ( user_id bigint primary key, score real, updated_at timestamp ); # 管道每天批量 upsert, 失败可整批重跑 — 保持"派生"属性
关键: 回流数据永远是派生 — 丢了重跑模型即可, 别当 SoR 维护。
报表集群只在白天忙 — 存算分离后 compute 按时扩缩, 账单减半。
# snowflake 式配置: 存储不动, 计算弹性 # warehouse = medium (4 nodes) 09:00~19:00 报表时段 # warehouse = xsmall (1 node) 夜间只跑轻任务 # storage = s3://lake/ (不变, 按量计费) # 本地盘的角色: ephemeral 缓存 # 热分区缓存命中 → 免一次 S3 网络往返 (~50ms) # 节点重建 → 缓存重新预热, 无数据丢失
迁移成本: 查询延迟对网络更敏感 — 热数据缓存命中率要进监控。
容错逻辑写了不等于容错 — 每月例行注入故障, 验证 fault 不升级为 failure。
# chaos/experiment.yaml — 预发环境 target: payment-svc fault: network_delay params: {latency_ms: 2000, duration: 60s} assert: - checkout p99 < 1s # 熔断生效, 主流程不受伤 - degrade_banner_shown == true # 降级提示可见 - error_rate < 0.1% # 结论模板: fault 注入后系统行为 = 预期? 无责复盘记录
Netflix Chaos Monkey 随机杀实例的哲学: 与其等生产随机故障, 不如主动来。
SLO 定 100% 等于自缚手脚 — 错误预算才是"可以冒险"的度量。
# slo.yaml objective: checkout availability sli: http 5xx-free responses / total target: 99.9% # 月度 — 错误预算 = 43.2 分钟/月 burn_rate_alert: fast: 14.4x over 1h # 快烧 → page slow: 6x over 6h # 慢烧 → ticket # 预算花完: 冻结发布, 只修可靠性 — 用制度而非吵架分配风险
SLO 是内部目标, SLA 是对外合同 (违约赔钱) — 别把二者混着写。
p50/p95/p99/p999 四线。 # 错: avg=40ms → "达标" # 对: p999=2s → 每千次 1 个用户等 2 秒
# 错: "服务端 latency 202ms, 该查了" # 对: service 2ms + queue 180ms → 查排队, 不是查代码
# 错: 只看 handler 耗时 # 对: 客户端 SDK 打点 端到端 RTT
# 错: 生产库跑全表 GROUP BY # 对: ETL → 数仓, OLTP 只留点查
# 错: BI 工具 JDBC 直连主库 # 对: 专用 replica + 连接白名单
# 错: "Redis 里有, MySQL 不用存了" # 对: Redis = 派生缓存, SoR 在库
# 错: 索引坏了 → 手工补数三天 # 对: rebuild_index(from=snapshot) 一键重放
# 错: order.count 与 orders 表各写各的 # 对: 同事务更新 或 CDC 增量维护派生列
# 错: dim_product → dim_brand → dim_company … # 对: brand 冗余进 dim_product 或 fact 表
# 错: s3://lake/2026/xx.json 万行一文件 # 对: Parquet + Iceberg 表 + dt 分区
# 错: "上了 HTAP 就不用数仓了" # 对: 大流量仍 TP/AP 分离, HTAP 只救小规模
# 错: 单库分表方案用到 10 亿行 # 对: 量级过档 → 重新选型评审
# 错: p99=80ms 达标收工 # 对: p999=4s 单独归因 (GC/重传/大查询)
# 错: "每个都 99% 可靠, 没问题" # 对: n=20 → 82% 全好 — 扇出收敛到 4~6
# 错: while fail: retry_now() # 对: 0.1s×2^n ± rand, budget 10%
# 错: 失败也照常全量调用 # 对: 错误率超阈值 → open 快速失败
# 错: queue.append(job) 无限收 # 对: bounded(10000) + full → 429
# 错: 过载 → autoscale ×4 → 仍瘫 # 对: 重启 + 修风暴源头 (timeout vs p999)
# 错: SLO=100% → 发布即违反 # 对: 99.9% + burn rate 告警
# 错: 复盘结论 = 当事人记过 # 对: 结论 = 流程/防呆改进 + 责任人跟进