在 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,这个分隔写在每一个标识符里:
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,调用次数正好在限流之下:
按 batch 2TB ÷ 每个个位数 GB ≈ 千次量级的 data key 请求,远低于 KMS 限流
按 part 8 到 16MB,S3 multipart → 完全不经过 KMS
难点在密钥怎么到对面。对称 data key 的密文只有生成它的那个 KMS 能解开,而目标端没有办法调用源端的 KMS,因为两套 IAM 互不相通。所以两边不共享任何对称秘密。
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 值我记不清了,这里不引用。下面的计算足以说明问题:
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,补上缺失的部分。要注意的问题在最底层:
ETag(multipart) = MD5( MD5(part 1) ‖ MD5(part 2) ‖ … ‖ MD5(part N) ) + "-" + N
ETag 取决于 part 是在哪里切的,不只取决于字节本身。用它对账会得出错误结果,而且不会有任何报错。让最上面那一层真正成立的是 S3 Object Lock。目标端的桶一次写入后不可修改,校验通过的对象在保留期内不能被改写或删除。代价是删除的灵活性和存储成本。两样都接受了,因为合规上要求传过去的数据不可被改写,而且对账结论必须在任务结束之后依然成立。
代价
- 时间。一趟全量要跑大半个晚上,所以只能排进窗口,不能随时发起。
- 存储和灵活性。Object Lock 锁住的对象会一直保留,传过去的数据不能事后修改。
- 一整套基础设施。manifest、DynamoDB 状态表、对账任务、密钥轮换流程,全是为一条只偶尔运行的路径建的。
几点体会
先想清楚优化的是稳态还是最坏情况。2TB 听起来是吞吐问题,实际上大半是恢复问题。安全原语放不进热路径时,把它挪到路径之外,而不是省掉它。KMS 每批调一次,安全性没有降低。两个 partition 互不信任这个限制,也带来了一个有用的性质。发送方自始至终无法读取自己发出的内容。