跳转到正文
zeno's blog
返回

分布式基础(二):可扩展性-垂直扩展与水平扩展

专题: 分布式基础

Table of contents

Open Table of contents

TL;DR

可扩展性是”加资源后处理能力能否线性提升”,不是”单次请求多快”(那是性能)。Amdahl 定律决定了串行瓶颈是理论天花板——加再多机器绕不过一把全局锁。正确路径:先垂直扩展 + 缓存(ROI 最高),瓶颈出现后再水平扩展(复杂度最高),每一步都由真实数据驱动而非预测。


可扩展性 vs 性能

维度性能可扩展性
关注点单次操作延迟/吞吐负载增长时的行为
度量p50/p99 延迟、QPS资源-吞吐量比例
典型问题”这个接口为什么慢?""用户量翻 10 倍还能撑住吗?”

一个 C++ 单机内存数据库 100 万 QPS 但无法水平扩展 = 高性能低可扩展性。Cassandra 单机性能不如它但可线性扩展到数百节点 = 中性能高可扩展性。

理论天花板

Amdahl’s Law加速比 = 1 / (S + (1-S)/N),S 是串行比例。即使 N→∞,加速比被 1/S 封死。5% 串行瓶颈 → 最大加速比永远不超过 20 倍。

USL(通用可扩展性定律)加入节点间协调开销:当协调系数 β > 0 时,吞吐量在某个 N 值后下降——加节点反而更慢。这解释了为什么有些系统加到 8 节点后性能反降。

垂直 vs 水平

维度垂直扩展水平扩展
成本曲线指数增长(高配硬件溢价)近线性(商品化硬件堆叠)
扩展上限硬上限(单机最大配置)理论无限(受串行瓶颈限制)
故障影响单点挂 = 全挂局部故障
复杂度高(分布式经典难题全来)
延迟更低(进程内通信)更高(网络 + 序列化)
一致性天然强一致需要权衡

垂直扩展什么时候用:数据库层(水平扩展复杂度极高);负载尚未超出单机能力;有强一致性需求。过早引入分布式是最常见的过度工程。

各层策略

应用层:无状态 + 自动伸缩

核心:应用不保存状态,Session/文件/缓存全外置。每个实例等价,流量可任意分配。

HPA 关键配置:扩容 stabilizationWindow 60s 防抖动;缩容 300s 更保守;设 maxReplicas 防成本失控。VPA 适合不宜水平扩展的场景(有状态服务),两者不建议同 metric 同时使用。

异步处理:不需要同步返回的操作扔消息队列,削峰填谷。消费者必须幂等(at-least-once 意味着消息可能重复)。

数据层:读写分离 → 分片

读写分离适用于读写比 > 10:1 的场景。代价是复制延迟——写后立即读可能读到旧数据。解决:写后短期内读主库。

分片策略对比

策略优势劣势
Hash Sharding数据分布均匀范围查询需扫所有分片
Range Sharding范围查询高效容易数据倾斜
Geo Sharding低延迟 + 数据合规跨区域查询复杂

分片的代价(经常被低估):

  1. 跨分片查询:按 user_id 分片,merchant_id 查询要扫所有分片(scatter-gather)
  2. 分布式事务:跨分片 ACID 需要 2PC/Saga
  3. Rebalancing:在线数据迁移可能持续数天
  4. 全局唯一 ID:自增 ID 失效,需要 Snowflake/UUID
  5. 分片键一旦选错,变更代价接近重建整个数据层

缓存层

Cache-Aside(旁路缓存)最常用:读先查缓存→未命中查 DB→写入缓存。写时删除缓存而非更新(删除是幂等的)。

三大缓存问题

问题现象解决方案
穿透查不存在的数据,每次打 DB布隆过滤器 / 缓存空值(短 TTL)
击穿热点 key 过期,并发打 DBsingleflight / 永不过期 + 异步刷新
雪崩大量 key 同时过期TTL 加随机扰动 / 多级缓存

关键设计模式

CQRS

读写用不同模型甚至不同数据库。写入范式化,读取反范式化。代价:最终一致性 + 系统复杂度显著增加。绝大多数 CRUD 应用不需要 CQRS。

背压(Backpressure)

上游生产 > 下游消费时,缓冲区无限增长直到 OOM。背压 = 下游告诉上游”慢点”。Go 中 buffered channel 满了 sender 阻塞就是最简单的背压。

Rate Limiting

算法特点
Token Bucket允许突发,最常用
Leaky Bucket输出平滑,不允许突发
Sliding Window Counter精确且省内存

分布式 Rate Limiting 通常用 Redis INCR + EXPIRE。注意 Redis 本身也可能成为瓶颈——可混合使用本地 + 全局 limiter。

演进路径

阶段 0 (DAU <1万):   单机单体 → 不要过早优化
阶段 1 (1万~10万):   垂直扩展 + Redis 缓存 + 慢查询优化
阶段 2 (10万~100万): 读写分离 + 应用层水平扩展 + Session 外置
阶段 3 (100万~1000万): 服务拆分 + 消息队列 + 分布式追踪
阶段 4 (>1000万):    数据库分片 + 多 Region + 边缘计算

核心原则:每一步都由真实瓶颈驱动,没有性能数据支撑的架构升级就是赌博。

Pitfalls

  1. 有状态服务直接水平扩展 — Session 在内存里,LB 分到不同实例登录态丢失。对策:Session 外置到 Redis 或 JWT
  2. 分片键选错导致热点 — 少量超级用户集中在某些分片。对策:分析数据分布,考虑复合分片键
  3. 缓存雪崩 — 大量 key 同时过期。对策:TTL 加随机抖动
  4. 同步调用链过长 — A→B→C→D→E,延迟是总和,任一故障全链失败。对策:识别可异步化的调用,不可避免的设超时和熔断
  5. 数据库连接数耗尽 — 50 台应用 × 20 连接 = 1000,PG 默认 max_connections=100。对策:PgBouncer transaction-level pooling
  6. 忽略写扩展 — 读写分离只解决读,写入量增长后主库仍是瓶颈。对策:写扩展需要分片,这是最难的部分
  7. 过早引入分布式 — DAU 1000 就上微服务 + Kafka + 分片,3 人团队 80% 时间维护基础设施。对策:先最简架构跑起来,有瓶颈再升级

生产 Checklist


分享这篇文章:

上一篇
tower(四):tower-http-HTTP 专用中间件
下一篇
tower(三):内置中间件-限流、重试、超时与缓冲