S53

复制与分片

点击正文段落选定收藏或划线位置;未选定时使用屏幕内第一段。

课内目录

1 复制与分片要回答什么

服务器上的数据先表现为具体的文件:进程把记录写进日志,文件系统分配空间,网络连接把字节送到别的主机。只要数据只在 1 台主机上,进程退出、磁盘损坏或主机失联就可能让服务停下。复制要回答的是:相同数据保留在哪里,哪份副本有资格接收请求,发生故障后如何继续服务。分片要回答的是:数据超过单机容量或处理能力时,按什么规则分给不同节点,以及请求怎样找到负责的节点。

这 2 个问题经常同时出现,但解决方向不同。复制增加同一份数据的副本,主要改善可用性,也可能分担允许读旧值的查询;分片让不同节点负责不同数据,主要扩大总容量和可并行的处理能力。把 100 GB 数据复制到 3 台主机,每台仍需容纳约 100 GB;把它均匀分成 4 片,每片约 25 GB,再为每片保留 3 份副本,才同时具有分散存储和故障接替的条件。日志、索引与空闲空间还会增加实际占用。

复制保存相同数据,分片划分不同数据,每片内部还可以复制
复制保存相同数据,分片划分不同数据,每片内部还可以复制

设想运维团队维护设备状态目录:每条记录包含设备编号、最近观测时间和状态。采集进程持续写入,值班人员用 shell 查询异常设备。主机 A 上的进程运行正常,不等于 B 已经得到最新状态;B 能建立 TCP 连接,也不等于它有权成为新的写入入口。操作系统提供进程、文件和连接这些基础能力,复制与分片系统还要在这些基础上定义角色、顺序和确认条件。

本课用“写入位置”表示日志已经处理到哪里。实际系统可能使用不同的日志编号或位置标记;只有属于同一条日志历史、按系统规则可比较的位置,才适合比较大小。我们同时区分客户端收到的成功、节点保存的日志和查询能读到的状态。后文的实验使用本地文件模拟角色判断,目的在于把故障切换的证据串起来;它不会把普通文本文件变成真正的复制数据库。

2 主从复制、仲裁与故障切换

主从复制通常让 1 个主节点接收写入,再把变更发送给副本节点。副本接收日志、保存日志并应用变更,随后才能用相应状态回答查询。这些动作之间可能有等待:主节点磁盘写入变慢,网络暂时拥塞,副本进程忙于处理旧日志,都可能拉开位置差距。因此,“副本在线”只能说明某种连通或健康条件成立,不能替代对复制进度的检查。

异步复制允许主节点在副本完成所需动作之前返回成功,通常减少等待,但主节点突然丢失时,已返回成功的部分写入可能还没有安全副本。同步复制会等待指定副本达到指定阶段,再按照约定返回。这里必须问清楚等待的是收到内存、写入文件、完成持久化,还是应用到可查询状态。“同步”本身不是足够完整的承诺,副本数量、确认阶段和故障假设共同决定承诺的含义。

仲裁用于让角色决定遵循明确规则。例如,3 个有投票资格的成员使用多数规则时,需要 2 票才能形成多数。网络把成员分成 2 个和 1 个的连通组,只有较大组可能继续取得多数,较小组不能仅因为看不到旧主就自行开放写入。参与投票的成员不一定都保存完整业务数据,计算多数时要按实际成员配置,而不能把监控进程、普通只读副本都算进来。

故障切换至少涉及故障判断、候选选择、写权限移交和入口更新。探测超时只说明在期限内没有得到回答,可能是进程退出,也可能是网络分区或主机负载过高。候选节点既要有资格,也要满足系统对日志完整性的要求。新主开放写入之前,还要通过任期校验、租约规则或可靠隔离手段,让旧主不能继续以有效主节点身份写入。具体实现留待后续,本课先记住这个必要条件。

写入从主节点进入,确认条件决定返回时机,副本应用决定读取可见性
写入从主节点进入,确认条件决定返回时机,副本应用决定读取可见性

入口更新同样属于切换过程。客户端可能缓存地址,连接池可能仍握着旧连接,读请求也可能继续发往落后的副本。因此故障切换完成不能只看“新主进程已启动”,还应检查旧主写权限已失效、新主状态符合要求,以及客户端实际连接到了正确入口。多数机制能帮助限制冲突角色,但不能自动消除所有复制延迟,更不能代替备份恢复被误删的数据。

3 分片键与再均衡边界

分片键决定记录归谁负责。设备状态目录可以按设备编号分片:同一设备的更新总是到负责它的分片,查询单个设备时也能直接定位。若按观测日期分片,当天所有设备的写入可能挤进最新分片。分片键并非只影响磁盘分布,它还决定写入负载、查询需要联系的节点,以及以后移动数据的难度。选键之前,要知道最常见的请求带着什么条件到来。

范围分片把连续区间交给不同分片,例如设备编号 0000 到 2499 属于分片 A。它便于理解区间查询,但编号增长方向可能形成热点。哈希分片先对键计算稳定的映射,再选择分片,通常更容易打散大量不同键;代价是按原始编号查大范围时,可能需要访问多个分片。无论采用哪种映射,同一个极热键如果始终只归 1 个分片负责,就不会因为其他分片空闲而自动获得更多处理能力。

分片路由可以由客户端、代理或服务内部负责。路由信息至少要表达“哪些键属于哪个分片”以及当前版本。运维人员看到某节点没有记录时,先确认请求的键和路由版本,不能马上认定数据丢失。过期路由可能把请求送到已迁出数据的节点;合理的系统会拒绝、重定向或刷新映射。把“查不到”和“查错地方”分开,是分片系统排障的基础。

增加节点往往伴随再均衡:复制目标数据、追赶迁移期间的新写入、切换归属,最后按规则回收旧数据。迁移不是把目录直接拖到另一台主机。源端仍在接收更新,目标端需要得到一致的起点和后续变化;切换期间还要防止请求绕过新的归属规则。直接用 cp 拷贝正在变化的数据文件,未必得到可恢复的状态,生产系统应使用支持的快照、迁移或备份接口。

再均衡有实际资源边界。搬运 200 GB 数据需要读取源盘、占用网络、写入目标盘,还会与正常复制竞争资源。即使新增节点容量充足,迁移仍可能使副本延迟上升或查询超时。方案应定义每批移动多少数据、允许的带宽、暂停阈值和回退条件,并为旧副本保留观察时间。扩容的完成标准是数据归属和服务负载达到预期,而不只是新进程已经加入列表。

4 动手:演练一次副本晋升路径

下面做 1 次不需要真实集群的判断演练。假设旧主 A 已失联,B、C 是具备候选资格的数据副本。在本次假设中,系统给出了可比较的持久化日志位置和应用位置,已确认写入的安全下界为 120。这个下界来自系统可信的提交信息;事故中不能通过取“现存节点最大值”自行发明。我们用临时目录存储这些观测,所有命令仅修改刚创建的实验文件。

demo_dir=$(mktemp -d /tmp/s53-demo.XXXXXX)
printf '120\n' > "$demo_dir/required"
printf '120 118\n' > "$demo_dir/B.state"
printf '117 117\n' > "$demo_dir/C.state"
printf 'unknown\n' > "$demo_dir/old-primary-fenced"
printf 'B: '; cat "$demo_dir/B.state"
printf 'C: '; cat "$demo_dir/C.state"

每行的第 1 个数是持久化位置,第 2 个数是应用位置。B 保存到了 120,但查询状态只应用到 118;C 只保存到 117。B 有机会恢复到要求位置,C 目前不能满足这个下界。应用落后与日志缺失是不同问题:前者可能通过重放已保存日志追上,后者需要获得缺少的日志或采用明确的数据损失处理流程。生产环境应使用系统提供的位置与恢复状态,不要从日志文件大小推断提交位置。

接着写出最小放行检查。它只演示“已隔离旧主、候选日志足够、应用追上”这些门槛,不实现投票,也不能作为生产晋升脚本。真实系统还必须检查成员资格、日志历史和仲裁结果。这里保留失败退出码,让 shell 使用者看到:检查没有通过,就不应继续执行开放写入的动作。

check_candidate() {
  candidate=$1
  read -r durable applied < "$demo_dir/$candidate.state" || return 1
  read -r required < "$demo_dir/required" || return 1
  read -r fenced < "$demo_dir/old-primary-fenced" || return 1
  [ "$fenced" = yes ] || { printf '拒绝:旧主隔离未确认\n'; return 1; }
  [ "$durable" -ge "$required" ] || { printf '拒绝:日志不足\n'; return 1; }
  [ "$applied" -ge "$required" ] || { printf '等待:应用落后\n'; return 1; }
  printf '检查通过:仍需真实系统的仲裁与晋升接口\n'
}
check_candidate B

首次运行会因隔离状态未知而拒绝。在纸面演练中,假设值班人员已通过可信控制路径完成旧主写入隔离,再记录这个结果;修改文本本身没有隔离任何真实机器。B 随后仍因应用位置为 118 而不能放行。最后模拟恢复进程将 B 应用到 120,再检查 B 与 C,就能看到日志不足和应用落后的不同失败路径。

printf 'yes\n' > "$demo_dir/old-primary-fenced"
check_candidate B
printf '120 120\n' > "$demo_dir/B.state"
check_candidate B
check_candidate C

把演练对应到真实主机时,先用 ps -eo pid,comm,stat 看进程状态,用 ss -ltn 看监听端口,用 df -h 看可用空间;需要服务日志时,可用 journalctl -u example-store --since '-10 min',其中单元名必须替换成实际服务名。这些命令分别提供进程、网络入口、磁盘和事件线索,均不能单独证明复制正确。最终要把它们与系统的角色、任期、日志位置和客户端访问结果对齐。

5 一致性与扩展取舍

一致性首先体现为用户能观察到什么。采集进程写入设备新状态并得到成功,随后值班脚本立即查询:它必须看到刚写入的值,还是允许短暂看到旧值?如果查询可落到异步副本,后者很可能发生。所谓“读自己的写入”,可以通过把后续读路由到满足条件的节点,或等待指定位置已应用来实现;具体接口取决于系统,不能仅靠把请求重试几次来证明。

等待更多副本确认,通常增加正常写入的等待,并让更多节点或网络故障影响写入可用性;减少等待则可能扩大故障时的数据风险。跨主机、跨机房的副本还会引入不同网络往返时间。应该用观测到的延迟分布描述等待成本,而不是只看平均值。某条慢连接反复触发超时,可能把偶发等待变成大量重试,进而增加主节点负载。

这个交互采用简化模型,帮助比较确认数量与等待条件,不预测真实吞吐。真实的多数确认还依赖成员规则和协议保证,不能通过“收到任意 2 个普通请求回复”代替。副本读也有代价:副本既要持续应用写入,又要回答查询。如果把大量扫描查询转过去,应用速度可能下降,原本期望缓解主节点压力的做法反而拉大读取陈旧程度。

分片扩大了总处理能力,却增加跨分片操作的协调范围。查询 1 个设备通常可以只访问它所属的分片;统计全部设备则可能需要每片都返回局部结果,再合并。只要其中 1 片很慢,整体结果就可能等待。涉及多个分片的修改还需要系统定义失败时如何处理,不能默认多条单片成功就等于整个操作原子成功。本课先识别操作范围,后续再学习实现这些保证的机制。

做取舍时应把目标写成可检查的句子。例如:“单主机故障后可接受最多 5 s 不可写”“已确认的关键配置更新不允许丢失”“监控列表允许旧值最多 3 s”。这些目标可能要求不同读写路径,也可能无法在当前资源与网络条件下同时满足。先说明业务允许什么,再选择复制方式和分片方式,比笼统追求“强一致、高可用、高性能”更容易形成可执行方案。

6 常见坑:分片键热点打满单分片

设备状态目录上线后,团队发现 4 个分片中只有 1 个持续超时。最初有人建议增加所有主机的 CPU,但观测显示其他分片大部分时间空闲。问题可能来自分片键:若按日期划分,当天的新状态都进入最新分片;若按设备编号哈希,某个高频设备仍可能独占它所在分片的大量写入。总数据量均匀、键的数量均匀和请求量均匀,是 3 个不同结论。

先按分片查看请求速率、错误率、处理延迟和复制延迟,再向热分片下钻到键或键范围。采集键级信息时可使用受控抽样和聚合,避免把大量设备标识写入排障日志。主机侧用 vmstat 1 5 观察运行队列与等待,用 df -h 排除空间不足,再结合服务的磁盘写入耗时判断瓶颈。CPU 不高并不能排除饱和,进程也可能在等待磁盘、锁或网络。

从入口请求到分片与副本位置,把系统指标和主机证据按时间对齐
从入口请求到分片与副本位置,把系统指标和主机证据按时间对齐

如果热点来自大量连续新键,调整范围或采用更合适的散列映射可能有效;如果来自单个热键,单纯增加分片数通常无效。可考虑降低不必要的采样频率、合并重复状态更新,或按应用允许的粒度拆分记录。但拆开同一设备的数据会改变读取与合并逻辑,需要先定义如何还原状态,不能把随机后缀直接当作通用修复。

再均衡也可能加重热点。将热分片的一部分迁走时,源端同时承担正常请求、数据扫描和日志追赶,目标端承担写入与副本复制。此时应限制迁移速率,观察请求延迟和复制落后是否越过阈值,必要时暂停迁移。故障现场最容易犯的错误,是看到扩容任务未完成就继续提高搬运速度,把本来还能服务的节点压到无法及时响应。

修复完成后,应使用包含真实访问偏斜的负载验证。只把随机键均匀发送到所有分片,会掩盖生产中的高频设备与时间集中写入。验证至少覆盖正常高峰、单个热键、迁移期间负载和副本追赶。结论要具体到“哪种请求下降了多少、哪个资源不再持续饱和”,而不是只记录节点数量增加。分片键是长期的数据组织决定,需要把变更与回退路径一起设计。

7 案例:复制延迟被当成已提交

某次设备配置维护中,运维脚本向主节点 A 写入新配置版本 42,A 按异步复制策略返回成功。脚本随后通过只读入口查询,入口将请求发到副本 B,B 仍返回版本 41。值班人员看到 B 的服务状态为“运行中”,误以为所有副本都已经提交版本 42,于是把读旧值归咎于 shell 缓存,并重复执行配置写入。事实上,进程运行与复制进度之间没有这种保证。

事件时间线显示:14:03:00,A 记录写入位置 842 并返回;14:03:01,B 持久化到 840、应用到 839;14:03:02,查询返回旧值;14:03:04,B 才应用到 842。日志中的“发送完成”只表示发送侧完成了相应动作,不能证明接收端已经持久化或应用。这里应把 A 的确认条件、B 的位置和查询目标分别记录,才能解释为什么成功写入之后仍读到旧状态。

如果 A 在 B 追上之前发生不可恢复的磁盘故障,风险会进一步扩大。假设 C 也只保存到 840,那么存活副本中可能没有位置 842 的记录。此时“客户端曾收到成功”与“存活副本能恢复该记录”可以同时一真一假,原因正是原来的成功承诺没有包含所需副本持久化。不能通过强行晋升 C 来补出缺失日志,也不能把恢复后的旧值描述为一次普通查询延迟。

先确认查询目标与确认语义,再区分应用落后、日志缺失和错误路由
先确认查询目标与确认语义,再区分应用落后、日志缺失和错误路由

排查时先保存客户端时间、请求标识和配置版本,再核对实际读写节点、角色变化及日志位置。主机时钟存在偏差时,要以可关联的请求和日志标记辅助排序,避免仅凭 2 台机器的墙上时间作精确推断。服务日志应区分“接收”“持久化”“应用”“返回成功”等事件;如果接口只提供部分阶段,就明确证据缺口,不能用一个模糊的健康灯填补。

修复应回到配置更新的要求。如果更新成功后必须立即读到新值,可选择符合系统保证的主节点读取或位置等待接口;如果关键更新要求在指定故障范围内保留,就需要相应的复制确认策略。脚本重试也应携带稳定的操作标识或版本条件,防止未知结果导致重复副作用。监控同时增加副本持久化落后、应用落后与读取陈旧程度,告警说明则写清它们各自意味着什么。

8 把副本拓扑写进数据方案

拓扑图必须能回答数据到底放在哪里。对每个分片列出主节点、数据副本、投票成员及其所在主机或故障域。若 3 个副本进程都运行在同一台主机,进程故障可能有接替空间,主机故障却会同时带走全部副本;若多台主机共用同一块底层存储,也要把这个共同故障点记录下来。副本数量只有结合独立性才具有实际意义。

方案还应把读写路径写到接口层。采集进程使用什么入口,查询脚本是否允许访问副本,写入成功等待哪个阶段,连接失败和角色变化时客户端如何刷新目标,都应有明确答案。若存在代理或名称解析,需要说明缓存与连接池怎样影响切换时间。切换完成的验收应从客户端执行,检查写入、随后读取和错误返回,而不能只在新主本机查询角色标签。

容量计划要逐分片估算。记录数据大小、索引与日志空间、增长速度、复制开销和迁移预留,再检查最忙分片能否承受高峰。磁盘剩余空间需要覆盖正常写入与追赶期间的日志积累;副本长时间离线可能需要重新同步,不能默认永远保留所有历史日志。备份则单独说明保存周期、存放位置和恢复验证方式,因为误删与错误配置通常会沿复制链传播。

最后把故障演练转成操作边界:谁宣布旧主隔离完成,哪个接口执行晋升,哪些日志位置必须保存,何时更新入口,哪些信号触发暂停或回退。不要在故障现场第一次决定“允许丢多少数据”。对无法满足下界的候选,应停止自动放行并进入预先定义的恢复或授权流程。这样,值班人员面对的是已约定的判断条件,而不是临时猜测哪个节点看起来更健康。

9 从复制与分片走向分布式存储系统(S54)

现在可以把 2 条主线接起来:分片决定数据由哪个组负责,复制决定组内怎样保存多份以及怎样接替角色。客户端首先需要找到正确分片,再在该分片允许的角色上执行操作。任何一层信息过期,都可能表现为超时、重定向或读到旧值。因此观察系统时,至少应保留“键或范围、路由版本、分片、节点角色、复制位置”之间的对应关系。

真实分布式存储还需要管理这些关系本身。节点加入、容量变化、分片迁移、成员替换和备份恢复都会改变系统状态。负责数据归属与成员信息的元数据同样需要可靠保存;如果所有数据副本都正常,但客户端拿不到正确映射,服务仍可能不可用。S54 将在这些基础上讨论存储系统如何组织数据与管理信息,本课不提前展开内部协议和复杂恢复算法。

例如,设备 1357 从分片 A 迁到分片 B 时,B 内部又发生主节点切换。客户端即使刷新了分片映射,仍可能连接到 B 的旧主;只更新主节点地址,也可能继续把请求送进 A。请求处理应分别校验数据归属与写入角色,过期请求按系统规则拒绝或重定向。若写入后连接中断,客户端还不能判断写入是否生效,应携带原操作标识查询结果或安全重试,避免在迁移两端重复执行同一项配置变更。

进入下一课前,可以用 3 个问题检查自己的判断:主节点返回成功时,究竟有哪些副本完成了什么;增加节点后,哪类请求真的获得了更多并行能力;旧主失联时,哪些证据允许候选开放写入。如果答案只是“有 3 个节点”“连接正常”或“状态为绿色”,说明仍缺少确认语义、负载分布或角色控制的信息。

把这些问题带回日常操作,也能改善普通的故障记录。记录某进程退出,只解释了进程层面的事件;补上日志位置、读写入口和分片归属,才能解释用户为什么读旧值或为何只有部分设备不可访问。复制与分片的入门目标,就是建立这种从主机证据连接到数据承诺的能力,让下一步的系统设计和排障有明确落点。

放进方案里

为设备状态目录补写 1 页数据方案,先写可验收的目标:数据量与每日增长是多少,高峰每秒写入和查询各有多少,哪些查询允许旧值,关键配置在写入成功后需要什么读取保证。分别给出可接受的数据损失范围与恢复时间,不把这 2 个目标合并成“高可用”。所有数量注明测量时间与来源,尚未测得的项目标为待采集。

画出分片和副本拓扑,标明分片键、键到分片的映射方式、主节点、数据副本与投票成员,并标出主机及共同故障点。写明客户端从哪个入口进入、谁维护路由、缓存何时刷新。对每类写入说明返回成功需要哪些节点达到哪个阶段,对副本读取说明允许落后多少以及超限后的处理方式。

列出扩容和迁移边界:何时触发再均衡,预计搬运多少数据,源端与目标端保留多少空间,迁移速率和暂停阈值是什么。单独检查热键与时间集中写入,说明新增分片能改善哪部分负载。容量估算应包含日志、索引、副本和迁移预留,并记录备份位置、保留周期与最近的恢复验证结果。

最后写出故障切换顺序及责任人:确认故障证据,取得有效仲裁,核对候选日志,确认旧主隔离,执行支持的晋升操作,刷新入口,再从客户端验证写入和读取。每步列出放行条件、失败出口与保留证据;候选不满足要求时不得自行降低数据保证。将进程、端口、磁盘、复制位置、分片负载和客户端错误关联到同一事件记录,并约定定期演练。交付时让另一名值班人员仅凭这页方案复述切换路径,找出仍需口头补充的地方。

10 小结:下一步分布式存储系统(S54)

复制让同一份数据存在于多个位置,分片把不同数据交给不同责任组。判断复制是否满足要求,要看确认阶段、日志进度和角色规则;判断分片是否有效,要看实际请求分布与迁移边界。进程在线、端口可连和磁盘有空间都是必要线索,但数据承诺需要更多证据。

自测时尝试解释:为什么 B 已保存日志却仍可能读到旧值;为什么增加 2 个分片未必缓解单个热键;为什么旧主超时后不能立即任选副本开放写入。能用本课的阶段、路由和隔离条件回答,就可以继续到 S54,把这些局部机制组织成完整的分布式存储系统。

同阶段相关课

AI 答疑

默认关闭。启用后,只发送本课标题、最近阅读的屏幕内正文摘录(最多 6000 字)与本次问题。

配置自己的服务