在 Amazon Global Store,我负责过一个服务:把商品数据从 AWS 全球区传入 AWS 中国区。一次全量大约 2TB。链路走 Direct Connect 而不是公网,往返 150 毫秒。哪些字段可以跨过分区边界有明确规定。下面每个决定都是在安全开销和长距离吞吐之间做的取舍。

写于 2026 年,距这项工作已有四年,全凭记忆,没有参考任何内部文档。运营数字只保留到决定每个选择的数量级;算术示例用的是 TCP、KMS 和 S3 的公开事实。

示意:一趟全量任务的轮廓
稳态        增量同步,只有一趟全量的一小部分
全量        约 2TB,很少发生:只在增量不够用的时候
链路        Direct Connect,RTT 约 150 毫秒,有效吞吐在几百 Mbps 量级
时长        一个夜间窗口
batch       每个一个业务实体分片,个位数 GB,一趟千个量级
part        8 到 16MB,S3 multipart

一、任务的形状

2TB 不是每天的数字。稳态是增量同步,只有全量的一小部分。全量只在增量不够用、目标端必须重建整份 catalog 的少数时候发生。所以这个服务优化的不是持续吞吐,而是两件事。一趟低频的大批量任务不能中途失败,跑完之后每个字节都能核对。后面的选择大多由此而来。复杂度花在恢复和证据上,吞吐只要够一趟任务在一个晚上跑完。

二、为什么要自建

AWS 全球区和中国区是两个独立的 partition,这个分隔写在每一个标识符里:

证据:两个 partition 在 ARN 里的样子
arn:aws:s3:::<bucket>        us-west-2     源端
arn:aws-cn:s3:::<bucket>     cn-north-1    目标端

两边的 IAM 互不相通,不能跨 partition 去 assume role。S3 的跨区复制不能跨 partition,DataSync 也不能。AWS 提供的所有托管复制都只在一个 partition 之内工作。就算通用工具能用,还有两个理由排除它们。它们搬的是文件,而我们要的是一批商品实体一起到达、按实体重试。它们也给不出每批数据过境时必须留下的记录:用了哪个 key,谁批准的,传输前去掉了哪些字段。《数据安全法》和《个人信息保护法》在 2021 年下半年先后生效,正在这项工作期间,这份记录是硬性要求。

三、密钥怎么过去

加密用的是信封加密。每个 batch 向 KMS 要一次 data key。对象在本地用这把 key 做 AES-GCM 加密,KMS 不接触数据本身。如果按对象调 KMS,远不到 2TB 就会触发 API 限流,吞吐也会随之下降。batch 按业务实体切,一个类目分片一个 batch,调用次数正好在限流之下:

示意:一趟全量任务的 KMS 流量
按 batch    2TB ÷ 每个个位数 GB      ≈  千次量级的 data key 请求,远低于 KMS 限流
按 part     8 到 16MB,S3 multipart   →  完全不经过 KMS

难点在密钥怎么到对面。对称 data key 的密文只有生成它的那个 KMS 能解开,而目标端没有办法调用源端的 KMS,因为两套 IAM 互不相通。所以两边不共享任何对称秘密。

示意:一个 batch 的密钥怎么过去。凭记忆重建,字段名只是示意
cn-north-1 KMS   建一对 RSA 非对称 CMK;公钥下发到 us-west-2
us-west-2        向 KMS 要一把 AES data key,用它加密整个 batch(AES-GCM),
                 再用 RSA 公钥把这把 AES key wrap 起来
信封             { key_id, wrapped_data_key, ciphertext }   →   随 batch 一起过去
cn-north-1       用自己的私钥 KMS Decrypt(wrapped_data_key)  →  AES key  →  数据

由此得到三个性质。第一,源端始终只持有公钥。这条要成立,还有两个条件。明文 AES key 只在进程内存里保留到这个 batch 加密完成,源端 KMS 返回的那份密文副本不留存。两条都做到,源端即使被攻破,也解不开自己送出去的任何数据。第二,不需要 cross-account trust,而这本来也建不起来。第三,轮换是例行操作。目标端换一对新密钥,重新下发公钥。信封里带着 key id,新旧可以并存。两套 KMS 都是 regional 的,互不关联,所有调用都走 VPC endpoint。

四、链路的限制

具体的 sysctl 值我记不清了,这里不引用。下面的计算足以说明问题:

证据:150 毫秒下的带宽时延积
1 Gbps × 150 ms    =  在途 150 Mbit  ≈  单流需要 18.75 MB 窗口
默认 tcp_rmem 上限                    ≈  6 MB
6 MB ÷ 150 ms                        ≈  单流最多 320 Mbps

服务采用多流并发、中等分片,而不是逐台调整内核的接收窗口。分片更大,稳态吞吐更好。但长肥管道上重传很常见,一次重试要重传整个 part,所以 8 到 16MB 是折中。并发把链路填满了,也把失败隔离开了。一个 part 失败,只重传这一个 part。

五、断点续传

续传是一个状态机,不是一个重试循环。每个 batch 带一份 manifest,列出对象、大小和 checksum。每个对象在 DynamoDB 里有一行,记着三种状态之一。重启就是读 checkpoint,然后对每种状态执行一条规则:

示意:重启时每个对象的处理
VERIFIED    跳过
UPLOADED    重新校验 checksum
PENDING     拿 multipart 的 upload ID 调 ListParts,只补缺的 part

对象的 key 由实体确定性地生成,同一个 batch 跑两遍写的是同样的 key。整条流水线因此是幂等的。重跑不会产生重复,也不会留下写了一半的对象。

六、校验

校验分三层。对象层是内容的 SHA-256。batch 层是 manifest:对象数、总字节、滚动哈希。再往上,一个定期任务拉取 S3 Inventory 和源端清单做 diff,补上缺失的部分。要注意的问题在最底层:

证据:为什么 multipart 的 ETag 不是内容哈希
ETag(multipart)  =  MD5( MD5(part 1) ‖ MD5(part 2) ‖ … ‖ MD5(part N) ) + "-" + N

ETag 取决于 part 是在哪里切的,不只取决于字节本身。用它对账会得出错误结果,而且不会有任何报错。让最上面那一层真正成立的是 S3 Object Lock。目标端的桶一次写入后不可修改,校验通过的对象在保留期内不能被改写或删除。代价是删除的灵活性和存储成本。两样都接受了,因为合规上要求传过去的数据不可被改写,而且对账结论必须在任务结束之后依然成立。

代价

几点体会

先想清楚优化的是稳态还是最坏情况。2TB 听起来是吞吐问题,实际上大半是恢复问题。安全原语放不进热路径时,把它挪到路径之外,而不是省掉它。KMS 每批调一次,安全性没有降低。两个 partition 互不信任这个限制,也带来了一个有用的性质。发送方自始至终无法读取自己发出的内容。