问题场景与约束
设计一个 5000 万用户的多租户 SaaS 数据平台,用户覆盖欧盟。用户点「删除我的账号」,你必须在 30 天内(GDPR Article 17)把他的 PII 从所有地方抹掉——但数据散落在 40 个微服务的 OLTP 库、Snowflake 数仓、Redis 缓存、Kafka 事件流、ES 索引,以及过去 90 天的每一份 DB 备份里。
为什么这是最反直觉的 System Design 题:分布式系统所有的设计习惯都在跟合规作对。append-only、不可变日志、多副本、事件溯源——全为「数据永不丢」;而 GDPR 要的恰是「让特定数据彻底消失并能证明」。约束还包括:删除须可验证、不能误删法定保留数据(税务/反欺诈有最短保留期)、脱敏数据不能被重识别、每次 PII 访问都要留痕。
高层架构
graph TD
U[用户 DSAR 请求
删除/导出/撤回同意] --> ORCH[隐私编排器
DSAR Orchestrator]
CAT[(PII 数据目录
Data Catalog / Registry)] -.定位数据.-> ORCH
ORCH -->|fan-out 删除| SVC1[微服务 A]
ORCH -->|fan-out 删除| SVC2[微服务 B ...N]
ORCH -->|销毁密钥| KV[[Key Vault
KMS / crypto-shred]]
KV -.持有 per-user DEK.-> DB[(加密数据存储)]
ORCH -->|每步落证据| AUD[[审计日志
append-only 哈希链]]
CG[同意网关
Consent Gate] -.拦截每次处理.-> SVC1
CG -.拦截每次处理.-> SVC2
classDef ctrl fill:#1a2530,stroke:#64c8ff,color:#e8eef5
classDef store fill:#2a1530,stroke:#ff7ab6,color:#e8eef5
classDef sec fill:#1a1a30,stroke:#ffb450,color:#e8eef5
class U,ORCH,CG ctrl
class DB,CAT store
class KV,AUD sec
编排器是「删除的事务协调者」;数据目录回答「PII 在哪」;密钥库让删除退化成删一把密钥;审计链证明「确实做了」
四个核心能力:①编排器把删除扇出成几十个子任务并追踪完成(本质是长事务 / Saga);②数据目录解决「PII 散在哪」这个最致命的问题;③密钥库 + crypto-shredding 把「在备份里物理删除」的不可能任务变成「删一把密钥」;④同意网关在每次处理前检查用户是否授权了该用途。
关键技术点
1. 删除权:Crypto-shredding vs 物理删除 vs Tombstone
原理:GDPR 的删除不要求物理擦除,只要求数据永久不可恢复。这打开了一条捷径——crypto-shredding(加密粉碎):给每个用户一把独立的数据加密密钥 DEK,用它加密该用户所有 PII;删除时只销毁这把 DEK,散落在 40 个库和 90 份备份里的密文瞬间全部变成无法解密的乱码,无需追杀每一份副本。欧盟数据保护委员会(前 Article 29 工作组)承认加密擦除是有效删除方式。
Trade-off:
- 物理 DELETE / 级联删除:✅ 语义最干净、真的没了;❌ 要遍历所有系统(含备份——你没法 UPDATE 一份冷备份里的一行)、跨服务外键、慢,且不可验证「是否漏了某个副本」。
- Tombstone(软删 + 标记):✅ 简单、可审计、可级联传播删除事件;❌ PII 仍物理存在,只是「藏起来」,严格说不满足 Article 17,只能作为触发真删的第一步。
- Crypto-shredding:✅ 一次删密钥覆盖所有副本含备份、O(1) 快、可验证(密钥没了=删了);❌ 密钥管理成为单点(KMS 挂了全站解不了密)、加密有 CPU 开销、密钥一旦泄露过就无法保证、schema 需从头按 per-user 密钥设计。
# Crypto-shredding:删除退化成删一把密钥 (伪代码)
def write_pii(user_id, field, value):
dek = kms.get_or_create_key(f"user:{user_id}") # per-user DEK
db.put(user_id, field, aes_encrypt(dek, value)) # 存的是密文
def read_pii(user_id, field):
dek = kms.get_key(f"user:{user_id}") # 密钥没了 → 抛异常 → 视为已删
return aes_decrypt(dek, db.get(user_id, field))
def erase_user(user_id):
kms.destroy_key(f"user:{user_id}") # 唯一动作:备份/数仓/Kafka 里的密文全部报废
audit.append("ERASE", user_id, proof=kms.key_destroyed_receipt(user_id))
现实案例:
- 信封加密(envelope encryption)是业界标准模式:AWS KMS / GCP Cloud KMS 都以 per-entity DEK + KEK 包裹为一等公民,crypto-shred 即删 KEK。
- Kafka 社区:topic 是 append-only 无法就地删单条,crypto-shredding 成为 Kafka 上做 GDPR 删除的主流方案(每条消息 PII 用 per-key DEK 加密,删 key 即失效)。
2. 数据脱敏:伪匿名化 vs 匿名化(重识别是死穴)
原理:数仓要做分析、测试环境要仿真数据,但不能直接用真 PII。两条路径法律后果完全不同:伪匿名化(pseudonymization)——把姓名换成 token、但保留一张可逆映射表——数据仍属于 PII,仍受 GDPR 全约束(映射表泄露即前功尽弃);匿名化(anonymization)——彻底切断与个人的关联、不可逆——才脱离 GDPR 管辖。难点是「彻底」几乎做不到:去掉姓名远远不够,准标识符(quasi-identifier)的组合能重识别——经典结论:邮编 + 生日 + 性别三者组合就能唯一定位美国绝大多数人。
Trade-off(隐私 vs 数据效用,此消彼长):
- k-匿名:让每条记录在准标识符上至少和 k-1 条无法区分(泛化年龄到区间、截断邮编)。✅ 直观;❌ 挡不住同质性攻击(一组 k 人若敏感属性都相同,还是泄露)。
- l-多样性 / t-closeness:在 k-匿名上再要求每组敏感属性足够多样。✅ 补住同质性;❌ 泛化更狠,数据效用掉得更多。
- 差分隐私(DP):给查询结果注入标定噪声,用 ε 数学量化隐私预算。✅ 唯一有严格数学保证、抗背景知识攻击;❌ 噪声牺牲精度、ε 预算会随查询次数耗尽、工程复杂。
现实案例:
- Apple / Google:在遥测和输入法统计上用本地差分隐私(数据离开设备前就加噪),收集趋势而不碰个人原始值。
- 美国人口普查局:2020 普查首次全面采用差分隐私发布统计,是 DP 最大规模的生产落地。
- GDPR 实务共识:伪匿名化被明确列为「安全措施」(Article 32)而非「脱管手段」——很多团队误以为 token 化就能随便用数据做分析,这是高频合规事故点。
3. 审计追踪:不可篡改地证明「谁碰了 PII」
原理:合规不仅要「做对」,还要能举证做对了。审计日志记录每一次 PII 的访问 / 修改 / 删除 / 导出——谁、何时、为何、动了哪条。要求是 append-only + tamper-evident(防篡改可检测):即便 DBA 有 root,也不能悄悄改历史。核心技术是哈希链:每条日志带上「前一条的哈希」,形成链条,改中间任一条会让后续所有哈希对不上——和区块链同源思想(见 Day 36),但无需共识,单机 Merkle/哈希链即可。
# Tamper-evident 审计链 (伪代码)
def append_audit(actor, action, subject_id, purpose):
prev = store.last().hash if store.last() else GENESIS
entry = {actor, action, subject_id, purpose, ts, prev_hash: prev}
entry.hash = sha256(canonical(entry)) # 含 prev_hash → 链式绑定
store.append(entry) # WORM / 只追加介质
# 校验:从头重算每条 hash,任一处对不上 → 有人篡改过
Trade-off:哈希链抗篡改但只能追加(改不了=也删不了,与「删除权」天然冲突,见陷阱);WORM 存储(S3 Object Lock)靠介质保证不可改,简单但贵、保留期一到即失保护;外部锚定(周期性把链头哈希公证/上链)能防「内部人重建整条链」,但引入外部依赖。多数团队:哈希链 + WORM 冷存 + 定期离线快照,够用且可负担。
现实案例:
- AWS CloudTrail 提供 log file integrity validation:对投递的日志做哈希 + 签名,事后可验证未被改动。
- 金融/医疗合规(SOX、HIPAA、PCI-DSS)普遍要求不可否认审计,WORM + 哈希链是标准配置。
- 数据库领域:AWS QLDB / SQL Server Ledger 把 Merkle 化的不可篡改历史做进引擎,原生提供密码学可验证审计。
4. 同意管理:把「用户授权了什么用途」当作真相源
原理:GDPR 的核心是目的限制(purpose limitation)——为「发货」收集的地址不能拿去「精准广告」。所以同意不是注册时勾一次的 checkbox,而是一个 (用户 × 用途 × 状态 × 时间) 的真相源,且必须能实时传播到每个下游处理点:营销服务处理前先问「这个用户同意 marketing 了吗?」——像 policy / feature flag 一样在数据流里当门禁。撤回同意(withdraw)要像删除一样级联:不仅停止未来处理,已流入下游管道的数据也要按用途清理。
Trade-off:
- 集中式同意服务(同步查询):✅ 永远拿到最新状态、单一真相;❌ 每次处理加一次 RPC、成为热路径依赖和单点。
- 同意随数据流走(consent-as-metadata):把同意范围打包进事件 / 记录本身。✅ 下游本地判断、无额外调用;❌ 撤回后「在途数据」携带的是旧同意 → 需要额外的失效广播。
- 纯事件广播:同意变更发到 Kafka,各服务本地物化。✅ 解耦、低延迟;❌ 最终一致,撤回到生效有窗口(合规上要能证明窗口够短)。
现实案例:
- IAB TCF(Transparency & Consent Framework):数字广告业的标准同意协议,用一串编码的 consent string 在广告链路各方间传递用户授权范围——正是「同意随数据流走」的工业级实现。
- CDP / 埋点平台(如 Segment 一类):在采集入口按用户同意的用途分流事件,未授权用途直接不落库,从源头做 purpose limitation。
- Cookie 同意横幅:前端 CMP 是面向用户的一端,后端才是真相源与执行点。
扩展与优化
- 数据目录自动化:手工维护「PII 在哪」必然漂移。用 schema 扫描 + 分类器(正则/ML 识别邮箱、身份证、信用卡)自动打标签,编排器据此扇出,避免漏删。
- 删除即 Saga:跨 40 个服务是长事务,用编排器 + 每服务幂等删除接口 + 重试 + 死信,任一步失败可补偿重放;核对「所有子任务 done」才对用户标记完成。
- 法定保留豁免:删除前先过「保留策略引擎」——税务发票、反欺诈记录有法定最短保留期,这些字段脱敏而非删除,删的是可删部分。
- 数据本地化联动:欧盟 PII 的密钥与密文都留在欧盟区(见 Day 42 多区域),crypto-shred 的密钥库也要按区隔离。
常见陷阱 + 面试问题
1. 备份里的数据怎么删? 这是压垮物理删除方案的问题——你无法 UPDATE 一份冷备份。标准答案:要么 crypto-shred(删密钥,备份里的密文报废);要么接受「备份按保留期自然滚动过期,期间用访问控制 + 记录『此用户已请求删除,恢复备份时须重新执行删除』」。面试官最爱追这个。
2. 审计日志不可篡改,那审计日志里的 PII 怎么删? 悖论:审计要 append-only,删除权要能抹除。解法是审计里只存引用不存 PII 明文(存 user_id 的假名 / 哈希 + 用途,不存姓名邮箱原文),或对审计中的 PII 同样 crypto-shred。别把 PII 明文写进不可删的链。
3. 伪匿名 ≠ 匿名。 大量团队把 token 化数据当「已脱敏」随意用于分析——只要映射表还在,法律上它仍是 PII,删除请求照样要覆盖它。这是审计中最常见的失分点。
- 用户请求删除,数据在 40 个服务 + 数仓 + 90 天备份里,你怎么设计端到端删除并证明删干净?
- Crypto-shredding 的密钥库本身是单点,KMS 数据损坏会怎样?怎么在「可删」和「别把活人数据锁死」之间平衡?
- 为什么去掉姓名的数据仍可能重识别?举例说明准标识符攻击,以及 k-匿名如何被同质性攻破。
- 用户撤回了广告同意,但事件已进入 Kafka 下游管道。怎么保证下游停止用它做广告,并证明?
- append-only 审计链和「删除权」直接冲突,你如何同时满足两者?
深入资源
- 《Designing Data-Intensive Applications》Ch 12(Kleppmann):结尾专门讨论数据的伦理、追踪与「被遗忘」的系统性难题。
- GDPR Article 17 & 25 原文:删除权与「Privacy by Design / by Default」——合规架构的法条起点。
- 《Forgotten @ Scale》(IBM, arXiv:1910.13784):大规模系统落地删除权的设计模式(定位 / 删除 / 证明)。
- Sweeney, k-Anonymity(2002):准标识符与重识别的奠基论文;配合差分隐私(Dwork)理解隐私的数学边界。
- AWS KMS / Kafka crypto-shredding 官方与社区文档:信封加密与 per-key 删除的工程落地。
深入思考
Crypto-shredding 把「删数据」变成「删密钥」,但如果攻击者在你删密钥之前就拷走了密文,事后你还能声称「已删除」吗?
不能完全声称。crypto-shred 的保证是「此后无人(含你自己)能再解密」,防的是持续持有密文者(备份、数仓、离职员工的旧快照)未来解密。但它不能撤销已发生的泄露——若攻击者在密钥有效期内已解密导出明文,删密钥无济于事。这是 crypto-shredding 的边界:它是删除机制,不是泄露补救机制。所以它满足 Article 17(此后不可恢复),但不改变泄露仍需按 Article 33/34 上报的义务。两件事正交:删除保证「未来的不可访问性」,泄露关注「过去的已访问性」。
你为每个用户生成一把 DEK 做 crypto-shredding。5000 万用户就是 5000 万把密钥——KMS 撑得住吗?瓶颈在哪,怎么绕?
每次读写都调 KMS 解 5000 万把 DEK 会打爆 KMS。工程上用两层信封 + 缓存:KMS 里只存少量 KEK(主密钥),每用户 DEK 用 KEK 加密后和密文存在一起(wrapped DEK),读时本地缓存解开的 DEK(几秒 TTL),避免每次打 KMS。删除销毁的是「解开该用户 DEK 的能力」:若共享 KEK,则要删掉散在备份里的 wrapped DEK(又回到老问题);真正干净的是per-user KEK——删除=在 KMS 删这一把 KEK,O(1) 且不依赖追备份。代价是 KMS 要承载千万级密钥的生命周期,这正是云 KMS「密钥即资源」要解决的规模问题。题眼:把「密钥数量」(靠分层)和「KMS 调用频率」(靠缓存)两个维度分开优化。
审计日志既要「不可篡改」又要「能删其中的 PII」,看似矛盾。除了「日志里不存明文」,还有别的架构解法吗?
有,分层解耦:把审计拆成不可变的「事件骨架」+ 可删的「PII 载荷」。骨架(谁、何时、什么类型操作、指向载荷的引用哈希)进哈希链、永不可改,证明「发生过一次对 user X 的访问」;PII 载荷(具体值)单独存在可 crypto-shred 的存储里。删除时销毁载荷密钥,骨架完好、链不断、篡改仍可检测,但载荷已不可读——「审计完整性」由骨架保证,「删除权」由密钥销毁保证,各归其位。更进一步可用可编辑区块链(redactable,chameleon hash 允许授权方不破坏链验证地改某条),但复杂度陡增,多数场景「骨架+载荷分离」已足够。核心心法:两个约束冲突时,往往不是二选一,而是把对象拆成受不同约束的两部分。
「删除即 Saga」跨 40 个服务,若第 37 个服务永久删不掉(比如一个已废弃、无人维护的系统还存着该用户数据),整个删除请求算成功还是失败?合规上怎么办?
合规上不能算成功——Article 17 要求删除「所有」副本,漏一处即未履行。但「僵尸系统」确实存在。正确处理分两层:技术上,编排器必须把该子任务标记 failed 并持续重试 + 告警,绝不能静默吞掉(最危险的是「39/40 成功就报完成」,第 40 处 PII 成了合规地雷);治理上,这暴露的是数据目录不完整 / 存在影子副本——根因是当初复制 PII 时没登记进目录。这也是 crypto-shredding 的战略价值:若这 40 个服务存的都是同一 per-user 密钥加密的密文,你根本不需每个服务配合——删中央那把密钥,僵尸系统里的密文自动报废。它把「N 个分布式删除必须全部成功」的 AND 难题,压成「删一把密钥」的单点操作。
差分隐私给了数学保证,为什么业界大量「匿名化」数据集仍被重识别?DP 没被普遍采用的真实障碍是什么?
因为大多数「匿名化」根本不是 DP,而是去标识 + k-匿名这类语法匿名,对「攻击者拥有外部辅助数据」毫无保证。经典翻车:Netflix Prize 数据集被 IMDb 公开评分交叉重识别。这些数据「删了姓名、做了 k-匿名」,但准标识符 + 外部知识组合仍能定位;DP 的强大正在于它对任意背景知识都成立。那为什么 DP 没普及?三个真实障碍:①效用损失——噪声让细粒度 / 小样本查询几乎不可用;②隐私预算 ε 会耗尽——同一数据反复查询每次泄一点,多团队共享时预算管理极难;③ε 取多少无普适答案,大了没保护、小了没数据。所以 DP 在聚合统计发布(普查、遥测)落地好,在行级细节分析里仍难替代。理解这个断层,比记住「DP 很安全」重要。