在 Amazon Global Store,我负责过一个跨太平洋的数据传输服务。单个任务的量级在 2TB 左右,数据要从 AWS 中国区搬到美国区,中间隔着一条往返 150 毫秒的长肥管道,也隔着一条法律边界。这个系统里几乎每一个设计决策,最后都落在同一条轴上:安全开销和长距离吞吐,你往哪边让。

这篇写的是那条轴上的几个具体决定,以及每一个决定实际付出了什么。

先把 2TB 说清楚

2TB 是单个任务的量级,不是日均吞吐。日常跑的是增量同步,一天几十 GB。2TB 属于全量场景:新品类接入、大促之前的目录全量刷新、上游 schema 变更需要回刷。

这个区别几乎决定了整个架构。如果目标是「持续高吞吐」,你会去优化稳态;但真正的目标是「低频大批量的时候不能挂,而且必须可审计」。这两个目标推出来的系统不一样:前者优化平均延迟,后者优化失败恢复和证据链。这套系统里绝大部分复杂度,最后都花在了后者上。

搬的是 Global Store 的业务数据。具体是哪几类在这里并不重要,重要的是它的一个性质:同一条记录里,有些字段可以出境,有些必须在出境之前裁掉。2021 年下半年《数据安全法》与《个人信息保护法》相继落地,这层裁剪不是 nice-to-have,是硬约束。

为什么现成工具用不了

最硬的一条理由是 AWS 的分区隔离。中国区和 global 是两个独立的 partition:ARN 前缀就不一样(aws-cnaws),IAM 账号体系不互通,不能跨 partition 做 cross-account assume role,S3 的跨区复制也不跨 partition。AWS 自己那套「同一 partition 内的托管复制」在这个场景下直接不可用,DataSync 同理。这不是配置问题,是边界本身的定义。

第二条是业务语义。通用工具搬的是文件,我们要的是「一批商品实体的一致性同步」。这两者在一切正常的时候没有区别,在出错的时候差别很大:失败之后要按业务实体的粒度重试和对账,而不是按文件重试。一个实体可能对应十几个对象,补一半是没有意义的。

第三条是审计。每一批数据出境时用的是哪个密钥、谁批准的、裁掉了哪些字段,这条链现成工具给不出来。而在合规审查里,这条链几乎就是全部——你说你裁了,和你能证明你裁了,是两件事。

第一个取舍:把 KMS 挪出数据路径

做法是信封加密。每个 batch 向 KMS 要一次 data key,本地用 AES-GCM 加密对象,KMS 只管密钥,不碰数据本身。

为什么必须这样:如果每个对象都调一次 KMS,以 KMS 的 API 限流,2TB 的小对象会直接把配额打爆,吞吐随之崩掉。信封加密把 KMS 的调用量从「对象数量级」降到「批次数量级」,中间差了三个数量级。

代价是密钥的作用域变粗了。一个 data key 覆盖一整个 batch 而不是一个对象,所以一旦某个 data key 泄露,暴露面是整批而不是单个。这个代价我认,因为 batch 的划分本身就和业务实体对齐,泄露面和业务上的「一次同步」是同一个边界,而不是一个凭空多出来的范围。

最花时间的一处:跨分区的密钥怎么给

对称 data key 的密文只能由生成它的那个 KMS 解开。而中国区和 global 的 IAM 不互通,目标端没有任何办法回调源端的 KMS。走对称路线在这里是死路。

最后的做法是非对称:目标端在 us-west-2 的 KMS 里建一对 RSA 非对称 CMK,把公钥下发到中国区;源端用这个公钥 wrap 那个 AES data key,密文跟着信封一起过太平洋;目标端用自己 partition 里的 KMS 做 Decrypt,解出 data key。

这个安排有几个我很在意的性质。源端从头到尾只持有公钥,它泄露了不构成风险。跨 partition 不需要共享任何对称秘密,也不需要任何 cross-account trust 关系——两个 partition 之间不存在信任,只存在一个公钥。轮换也简单:目标端换一对 CMK,重新下发公钥,信封里带着 key id,所以新旧密钥可以共存,不需要停机切换。

分片与并发:为什么不做单流优化

跨太平洋的往返在 150 毫秒上下。单流 TCP 在这种长肥管道上撑不满带宽,这是带宽时延积决定的:150 毫秒的 RTT 下,单流要打满 1Gbps,窗口需要接近 19MB;而 Linux 的 tcp_rmem 默认上限通常在 6MB 量级,对应的单流天花板只有三百多 Mbps。

差距摆在这里,路就两条:调内核参数把单流做大,或者用并发把带宽堆满。我们选了后者,多流并发加中等大小的分片。

理由不只是省事。大分片的稳态吞吐更好,但长肥管道上重传概率不低,而一次重试的代价和分片大小成正比——分片越大,失败一次赔得越多。中等分片加并发在稳态上略有损失,换来的是失败被隔离在小范围内,这正好对上前面那个设计目标:低频大批量的时候不能挂。对象内部按 8 到 16MB 分 part 走 S3 multipart,这一层完全不碰 KMS。

断点续传:manifest 驱动

每个 batch 生成一份 manifest,记录对象列表、大小和 checksum。每个对象在 DynamoDB 里有一个状态机:PENDING、UPLOADED、VERIFIED。

任务重启的时候读 checkpoint:VERIFIED 的直接跳过;UPLOADED 但还没校验的重新校验;没传完的对象拿 multipart 的 upload ID 调 ListParts,只补缺失的那几个 part。对象的 key 是确定性生成的,所以整个流程幂等——重复执行不会产生脏数据,这一条在半夜自动重试的时候比什么都重要。

一致性:三层,以及一个容易踩的坑

对象层用 SHA-256,不用 ETag。这个坑值得单独说:multipart 上传的 ETag 是各个 part 的 MD5 再做一次 MD5、后面拼上分片数,它并不等于整体内容的哈希。拿它跨端对账,结论是错的,而且错得很安静。

batch 层比对 manifest:对象数、总字节、滚动哈希。

最上面还有一个定期对账任务,拉 S3 Inventory 和源端清单做 diff,产出差异集自动补传。这一层存在的理由是,前两层都只在传输的那一刻有效,而数据出境之后还要长期待在那里。

Object Lock:拿灵活性换不可改写

目标端的桶开了 S3 Object Lock,写进去的对象在保留期内不可改写、不可删除。代价很直接:删除的灵活性没有了,存储成本上去了。

这个我们认,因为合规上需要的正是「传出去的东西不可被改写」这个性质。它也让上面那个定期对账真正有意义:如果对象在校验通过之后还能被改,那么对账结果就没有持续有效性,你只知道它在某一刻是对的。

边界画在哪:VPC endpoint 与 IAM

所有对 S3、KMS、DynamoDB 的调用都走 VPC endpoint,不出公网。这一条和 Direct Connect 是同一个思路的两半:Direct Connect 让跨太平洋的流量绕开公网,VPC endpoint 让 region 内部到 AWS 服务的流量也不出私网。合起来的效果是,这条数据通路上没有任何一段跑在公共互联网上。

IAM 那边,两个 partition 各有一套完全独立的角色,互相不认识。源端的角色能读业务数据存储、能调本 region 的 KMS 生成 data key、能往中转位置写;目标端的角色能调本 region 的 KMS 做 Decrypt、能往那个开了 Object Lock 的桶写。没有任何一个角色同时具备两端的权限——跨 partition 本来就做不到,而这个「做不到」在这里恰好是想要的性质:它把「某个凭证被拿走会发生什么」的爆炸半径限制在了一个 partition 之内。

这套系统怎么跑起来

把上面几件事串成一条路径:

  1. 触发。全量任务排在业务低峰的夜间窗口,不是随时可以发起的;增量任务按调度跑。
  2. 切批。batch 不是按大小切的,是按业务实体分组切,一个类目分片一个 batch,落在 1 到 4GB 之间。全量 2TB 大概是一千个量级的 batch,所以整场任务对 KMS 的调用也就一千多次,稳稳待在配额里。
  3. 裁剪。按规则把不能出境的字段从记录里去掉,这一步的结果连同规则版本一起进审计日志。
  4. 加密。向本 region 的 KMS 要一个 data key,本地 AES-GCM 加密对象,再用目标端的 RSA 公钥 wrap 这个 data key,密文放进信封。
  5. 传输。多流并发,对象内部按 8 到 16MB 分 part 走 multipart,全程 Direct Connect。每个对象的状态写进 DynamoDB。
  6. 落地。目标端用自己 partition 的 KMS 解出 data key,解密,写进开了 Object Lock 的桶。
  7. 校验。对象层 SHA-256,batch 层比 manifest,通过之后标 VERIFIED。
  8. 对账。之后由定期任务拉 S3 Inventory 和源端清单做 diff,有差异就补传。

失败发生在任何一步,重跑都从 checkpoint 继续,而不是从头开始。这是整个设计里最贵、也最值的一部分。

一般化的部分

这套东西里能带走的,我觉得是三条。

先想清楚你优化的是稳态还是尾部。「2TB」听起来是个吞吐问题,实际上是个恢复问题。一旦看清这一点,绝大部分设计决定就自己有了答案。

把昂贵的安全原语挪出热路径,而不是绕过它。信封加密没有降低安全性,它只是让 KMS 出现在它该出现的地方,每批一次而不是每个对象一次。真正糟糕的做法是因为慢而干脆不加密,那是用安全性去换吞吐,而不是用设计去换。

边界做不到的事情,有时候正是你想要的。两个 AWS partition 之间不能互相 assume role,这在项目初期是个纯粹的麻烦。但它最后逼出来的那个非对称密钥方案,比一个能互相 assume role 的方案更安全,因为源端从头到尾就没有能力解开自己送出去的东西。