SET化架构:从单元化原理到大规模落地实践

目录

系统扩容到多组应用和数据库后,仍可能被一个共享缓存、一次错误发布或全局依赖故障拖垮。问题不只是机器够不够,而是能否把一次故障限制在一部分业务范围内。

SET 化的做法是按业务维度划分流量,让对应的应用、缓存和数据尽量在一个单元内形成闭环。扩容和发布也以单元为边界推进。它的价值是让容量与故障范围可规划、可验证,而不是承诺无限扩展。

本文以按用户分片的电商交易系统为说明性案例,依次讨论分片、路由、共享依赖、迁移和容灾。示例中的分片数量用于解释设计,不代表某家公司的实际部署。

单元化改变了什么

传统分层集群可以分别扩应用、缓存和数据库,但一次请求往往仍穿过全局共享资源。单元化进一步要求:知道请求属于谁、由哪个单元处理,以及它的数据由谁负责写入。

关注点 单元化的设计目标 必须检查的条件
请求闭环 大部分业务在所属单元内完成 同步调用是否频繁跨单元
故障隔离 单元内故障主要影响该单元 是否共用数据库、队列、缓存或资源配额
容量规划 为单元确定经过验证的承载上限 热点租户和不同业务负载是否可比
渐进发布 先在部分单元验证,再扩大范围 发布是否同时修改共享组件或全局配置
扩容 增加单元并重新分配部分负载 有状态数据是否已经迁移就绪

AWS 的 Cell-based Architecture 强调以分区键划分工作负载,并用较薄的路由层将请求送到对应单元;控制面负责创建单元、迁移客户等管理工作。AWS 单元化架构说明

国内常用“SET 化”描述类似的隔离与扩展思路,具体边界并不完全相同。美团 2023 年的公开实践同时介绍了 N+1 和 SET 两种容灾架构,并未将 SET 视为所有业务必须走向的终态。美团数据库容灾体系建设实践

服务化、分库分表和单元化解决的问题不同,可以组合使用。模块化单体也可以按租户部署成多个单元;微服务很多,也不意味着已经具备单元隔离。

路由键决定哪些操作能够闭环

先分析关键写入和业务不变量,再选择路由键。按用户划分便于把订单、购物车和用户资产放在一起,但不能让商品库存、商户汇总和跨用户转账自动变成本地操作。

候选路由键 容易闭环的操作 需要额外处理的场景
用户 ID 用户订单、购物车 商户聚合、用户间交易
商户或租户 ID 租户内配置、业务单据 大租户热点、跨租户业务
地域 本地配送、区域服务 用户迁移、跨区域履约
订单 ID 单笔订单的状态变化 创建前定位、按用户查历史订单

本例选择用户 ID,但库存保留在独立的库存服务中。由此得到的隔离边界是“用户交易单元”,不能宣称一次库存服务故障只会影响某一个 SET。

用虚拟分片隔开业务标识和物理部署

可以先将用户映射到固定数量的虚拟分片,再查表定位物理单元:

虚拟分片 当前单元 路由版本 状态
0–341 SET-1 17 可写
342–682 SET-2 17 可写
683–1023 SET-3 17 可写

计算方式例如 shard = stableHash(userId) mod 1024。哈希算法、输入编码和分片总数属于持久契约,不能随着应用升级随意改变。

扩容到四个单元时,可以从三个旧单元各迁移一部分虚拟分片到 SET-4,不必重排所有已有分片。虚拟分片让映射调整更灵活,数据搬迁、写入归属切换和旧请求处理仍然必须完成。

把物理 SET ID 直接编码进业务 ID 会让后续迁移受限。可以编码稳定的逻辑分片,或通过索引查找归属;生成全局唯一 ID 与表达路由信息是两个独立要求。

入口、RPC 和异步任务使用同一份归属信息

DNS 通常根据域名、地域和健康状态等进行解析,看不到 HTTP 请求中的用户 ID。按用户精确路由应在能够读取并验证业务身份的网关或应用路由层完成。

层次 负责的决策 需要避免的问题
DNS / GSLB 选择可访问的地域入口 把就近入口当成数据所在单元
API 网关 校验身份、提取路由键、查询归属 信任客户端伪造的 SET Header
RPC 客户端或代理 将调用送往正确单元的服务实例 静默回退到任意健康单元写入
消息消费与定时任务 根据业务键定位当前写入者 只改在线流量,后台任务仍写旧库

请求中可携带路由版本和逻辑分片信息,但服务端仍需校验写入归属。迁移时网关、本地缓存和长连接不可能在同一瞬间更新,旧路由请求需要被拒绝、转发或重试到新归属,具体策略应避免循环转发。

路由控制面不可用时,已有请求可以在有效的本地路由快照上继续运行;创建新单元和迁移等管理操作则可能暂停。这里需要区分“控制面暂时不可用”和“现有业务流量必须全部停止”。

共享数据和全局服务如何处理

本例将数据分为三组:

数据 权威写入位置 常见读取方式
用户订单、购物车 用户所属单元 单元内读取
商品目录、类目 商品服务 本地只读副本或缓存
库存、跨单元资金记录 对应业务权威服务 按操作语义访问,不能用过期副本完成扣减
商户报表、全站统计 汇总视图 异步归集,注明新鲜度

是否能本地化取决于业务允许多旧的数据。商品描述和资金余额都叫“数据”,但两者不能共用一个“同步延迟小于一秒即可”的标准。

本地副本需要处理重复事件、乱序、删除和重建,并能发现同步停滞。复制位点差可以衡量积压,不能直接当成时间延迟;时间指标还需使用源端提交时间或心跳,并考虑时钟误差。

全局配置、身份认证、库存和 ID 分配也可能成为共同故障源。对它们的治理包括容量隔离、超时、受控降级和分批发布。Redis namespace 只隔离键名,共用同一个 Redis 实例仍会共享 CPU、内存和故障域。

ID 服务切换为本地生成也不是天然安全的兜底。两种模式必须拥有不相交的编号空间,管理好机器标识、时钟回拨和重启状态,才能保证不产生重复 ID。

一次下单怎样穿过这些边界

用户 A 的交易数据归属 SET-1。一个可行的流程是:

  1. 网关校验 A 的身份,根据逻辑分片和路由版本定位 SET-1
  2. 订单服务使用请求幂等键创建待处理订单,保存商品和价格的必要快照
  3. 库存服务按订单号预占库存,重复请求返回同一业务结果
  4. 预占成功后推进订单;失败时关闭订单,结果未知时查询或重试,不能直接按失败释放别人的预占
  5. 发起支付后,根据可信回调或主动查询更新状态;取消与支付成功需要通过状态条件协调
  6. 对长时间未收敛的订单执行对账和补偿

用户订单写入可以在 SET-1 内完成,但库存和支付渠道仍是外部依赖。单元化提供部署和归属边界,不会替代这些流程的一致性设计。

商户查看所有用户订单时,可以读取异步构建的商户视图。直接扇出查询所有 SET 也能实现聚合,但需要限制超时、并发、分页和部分失败语义,不能让每次后台查询拖慢所有单元。

扩容首先是数据迁移,其次才是改路由

下面采用单一写入者加增量复制的迁移方案。它是一种设计选择,并非所有数据库都支持相同操作。

  1. 准备目标单元:部署依赖,验证容量,创建需要迁移的逻辑分片
  2. 全量复制:获取一致性快照,并记录与快照对应的增量起点
  3. 追增量:继续由旧单元接受写入,向新单元重放变更,处理幂等与顺序
  4. 校验:比较记录、业务汇总与必要不变量,确认目标已追到可切换位置
  5. 切换写入归属:通过短暂停写、租约或版本栅栏等协议,阻止旧写入者继续提交,再激活新写入者
  6. 更新路由并观察:处理旧缓存和在途请求,验证在线流量、消息、批任务都使用新归属
  7. 结束回退窗口:确认增量和备份完整后,再清理旧数据

如果选择应用双写,就必须回答一边成功一边失败、写入顺序相反和重试重复的问题。双写本身不保证一致性。

回退也有条件:新单元接受写入后,旧单元可能已经落后。不能仅把路由表改回去;必须让旧数据追平,或执行事先设计的反向迁移协议。

故障切换与计划迁移不同

计划迁移可以等待数据追平,故障切换可能无法再访问原主库。此时需要明确恢复目标:

  • RPO:允许恢复到距故障时点多早的数据状态
  • RTO:业务恢复需要多长时间
  • 接管容量:备用资源能否承受切来的请求和积压任务
  • 写入排他性:网络隔离中的原主是否仍可能写入

异步复制可能丢失尚未送达的数据。健康检查失败只能说明探测异常,不能证明原主已经停止工作;新主接管前需要可靠的写入隔离或 fencing,避免两个写入者并存。

故障影响也不能直接由 SET 数量计算。三个 SET 中一个故障,不一定只影响三分之一用户:分片可能不均,热点用户负载可能更高,共享服务还可能扩大影响范围。

同城或跨地域都可以部署单元。跨地域增加了网络延迟、复制和接管约束,不能通过“把单元放到不同城市”自动得到异地多活。验证应覆盖地域断连、旧主恢复、路由缓存滞后、共享依赖失效和切回。

用可观察的结果推进落地

先从一条能够闭环的链路试点,为每个阶段设置退出条件:

阶段 退出条件
梳理归属 路由键、权威数据和跨单元依赖有明确清单
建立试点 在线请求、消息和任务都能正确定位单元
验证隔离 单元故障不会通过共享资源放大到预期范围之外
验证迁移 能完成复制、校验、切换和约定条件下的回退
扩大部署 单元容量、发布、观测和运维流程可重复
增加地域 容灾演练证明 RPO、RTO 与接管容量符合业务目标

监控至少按单元呈现请求成功率、延迟、容量、跨单元调用比例和复制积压。扩容能否解决问题,应由这些指标判断,而不是由 SET 的数量判断。

单元化最值得投入的部分,是把“这份数据归谁写、这次故障影响谁、这个单元何时能接管”变成明确的工程契约。有了这些契约,增加单元才是可重复的扩展动作。

评论