一次分布式存储读取通常经历哪些步骤?
简化版
一次读取通常经过鉴权与限流、定位分片、选择满足一致性要求的副本、读取缓存/MemTable/SSTable、校验版本与 checksum、合并结果并返回。性能优化不能越过语义:线性一致读需要 Leader/读屏障和应用进度,quorum 读需要版本比较,Follower 读则必须声明允许的陈旧度。
详细版
客户端或网关先把 key 映射到 shard,并带上路由 epoch。服务端发现 epoch 过期时返回新位置,避免旧 owner 错误服务。
副本选择取决于一致性:Leader read 要确认当前任期并等待 appliedIndex >= readIndex;bounded-staleness read 可选择 lag 不超过阈值的近端副本;无主系统可能并行读 R 个副本并合并最高版本。
本地引擎先查 block cache、MemTable 和 immutable MemTable,再按 Bloom Filter、索引定位 SSTable。墓碑和多版本需要按快照时间/版本合并,不能简单返回第一个命中。
最后校验 checksum、解压解密并记录延迟分段。超时重试要有总 deadline 和请求取消,避免慢请求已经返回后,后台重试仍持续放大负载。
完整版教学
一、路由是读路径第一关
分布式读先回答数据在哪。客户端缓存 range/token 元数据,把 key 映射到 shard;请求携带 epoch,旧路由由服务端拒绝并提示刷新。若无限重定向,元数据错误可能形成循环,因此要限制跳数。
request -> auth -> locate shard -> choose replica -> local engine -> verify -> response
二、副本选择由一致性决定
最近副本延迟最低,却可能应用落后。选择器应先过滤不满足语义的副本,再按 RTT、队列、错误率排序,不能先选最快再祈祷它够新。
| 读语义 | 可选副本 | 必要条件 |
|---|---|---|
| 线性一致 | 已确认 Leader/协议读副本 | 读屏障且 apply 追平 |
| 读己之写 | 达到会话 token 的副本 | replica waterline ≥ token |
| 有界陈旧 | 任意合格副本 | lag ≤ 指定阈值 |
| 最终一致 | 健康副本 | 接受短时旧值 |
三、Leader 读也不能直接读内存
旧 Leader 可能尚未发现自己失去多数派。ReadIndex 通过当前任期多数确认取得安全读水位,之后还要等状态机应用到该 index。租约读可减少网络往返,但依赖不重叠租约和单调时钟假设。
假设 commitIndex=1050、appliedIndex=1042、读屏障为 1048,立即读取可能漏掉 1043~1048 的更新;必须等待 apply 至少到 1048。
四、本地 LSM 读取如何发生
引擎先查缓存和内存表,再根据 Bloom Filter 跳过确定不存在 key 的 SSTable,通过索引定位数据块。多个层级可能同时存在同一 key 的不同版本,需要按 sequence number 和快照可见性选值,墓碑可能表示“最新结果是不存在”。
Bloom Filter 只有假阳性、没有假阴性这一前提依赖正确构建;假阳性只多一次 I/O,不能让系统错误返回不存在。缓存 key 还要包含文件版本,避免文件 Compaction 后复用旧块。
五、Quorum 读与冲突合并
无主复制可并行读取 R 个副本。R + W > N 只保证集合相交,不自动告诉系统哪个版本正确;还需要逻辑版本、向量时钟或 LWW 规则,并处理失败写留下的版本。
若三个副本返回 v5、v5、v4,且 v5 的版本证据有效,可以返回 v5 并异步修复 v4。若返回两个并发向量版本,则要交给冲突解决,不能按到达先后随意覆盖。
六、超时、重试与可观测性
客户端总 deadline 100 ms 时,不能让路由 50 ms、副本首次读 80 ms、重试再 80 ms 各自独立超时。每层必须传递剩余预算;hedged read 只在尾延迟收益大于额外负载时启用,并取消输掉的请求。
监控分解 route、queue、engine、disk、merge、network 各段延迟,同时看 cache hit、Bloom 命中、SSTable touched、follower lag、重试率和 checksum 错误。只有端到端 P99 无法定位瓶颈。
记忆钩子:读路径先证明“找对地方、选对版本”,再优化“更快拿到”;快但不满足读语义的副本不是候选。
七、常见误区与追问
- 误区:Leader 本地读天然线性一致。 旧 Leader 和 apply lag 都可能返回旧值。
- 误区:Quorum 相交就一定读到最新值。 还需版本选择和并发冲突规则。
- 误区:SSTable 找到 key 就可立即返回。 还要比较更新版本、墓碑和快照可见性。
- 追问:Hedged read 有什么代价? 增加副本负载,必须延迟触发、限额并取消多余请求。
- 追问:缓存如何避免旧块? 用不可变文件 ID/版本做 key,并在版本删除后安全回收。
- 追问:读超时如何分配? 从端到端 deadline 向下传递剩余预算,而非层层重新计时。
八、加强记忆
读路径按“路由—选副本—建读屏障—本地查找—版本合并—校验返回”记忆。一致性先过滤候选,延迟再做排序;Leader 读要等 apply,Quorum 读要解版本,本地 LSM 读要处理墓碑。最后用分段指标而非一个总耗时定位问题。