- 这次对 Meta 的 隐私感知基础设施(PAI) 探讨 PrivacyLib。
- 隐私要求必须在数据流经 Meta 多样化的数据处理系统时始终得到满足。过去,支持一项新要求意味着要在每个系统中分别实现相同的执行逻辑。PrivacyLib 用一个通用库取代了这种做法,该库运行在 Meta 数据处理系统的客户端内部。
- 系统只需与 PrivacyLib 集成一次,新的隐私能力便可以作为模块接入,并复用该集成。PrivacyLib 每秒执行数十亿次检查,变更按模块进行模拟和发布。
- 对于数据访问路径可能无法预先确定的内部 AI 智能体,在受支持路径上的工具调用会将智能体上下文传递给 PrivacyLib。传播的上下文将每次访问与其会话关联起来,因此模块可以观测数据使用情况,并阻止不被允许的操作。
在 Meta,许多隐私要求最终都归结为同一个问题:我们能否证明某条数据只流向它被允许去的地方?在 Meta 的规模下,我们不再仅仅依赖手动隐私审计——随着系统变化,这种审计可能会失去同步——而是开发了 隐私感知基础设施(PAI) ,在基础设施内部回答这个问题。PAI 在 Meta 的基础设施中提供高效、可靠的一流隐私构造,使产品能够持续快速演进。
在之前的文章中,我们介绍了 数据理解, 数据血缘以及 策略区域 (我们的信息流控制技术)在 PAI 中发挥的作用。现在,我们将深入探讨 PrivacyLib,这是一个通用库,运行在我们数百个数据与处理系统的客户端内,位于数据访问点。
PrivacyLib 在更广泛的 PAI 工作流的最后阶段发挥着重要作用:持续执行并监控数据流。

在 PrivacyLib 之前,我们针对每项要求,将隐私执行分别集成到每个数据处理系统和数据系统的客户端中。采用这种方法,跨系统实现一项新要求可能需要数月时间。每项新要求,无论是目的限制还是数据流发现,都需要同一组能力:覆盖广泛的系统;在数周而非数月内完成部署;在每次数据访问时运行而不成为延迟问题;以及在不使整个集群停机的情况下推出执行变更。随着要求和系统的演进,这种方法变得越来越难以扩展。
PrivacyLib 将系统集成与要求实现分离。我们对每个系统只集成一次。当某项要求可以在数据访问点得到满足时,我们将其实现为一个 PrivacyLib 模块,在已集成的系统之间共享,因此新要求不再需要在每个系统中单独实现。
模块分为两种:
- 执行模块可以阻止数据访问。例如, 策略区域 执行 目的限制.
- 观测模块只记录一次数据访问。例如, Privacy Probes 会收集用于发现数据流的运行时信号。
随着内部 AI 智能体成为访问数据的新方式,我们还新增了 Agent Observability 模块,用于观测与智能体相关的数据访问事件,并帮助保护智能体能够触及的数据。已经集成 PrivacyLib 的系统无需新的集成工作即可启用该模块,这正是“一次集成”的价值所在:每项新的隐私能力都以模块形式到来,而不是作为一个新项目。

PrivacyLib 在 Meta 规模下面临的工程挑战与方法
PrivacyLib 在 Meta 运行于广泛的数据访问路径之上:
- 过去有一百多个库用于访问数据服务,从键值存储到 AI 功能。
- 数千个生产服务运行着已部署库的各个版本。
- 每天有数百万资产被访问。
- 每秒执行数十亿次检查。
在这样的规模下运行会带来三项工程挑战:
| 挑战 | 方法 |
|---|---|
| 覆盖范围: 系统可能拥有多个客户端库和遗留集成,接受直接网络调用,并提供对后端存储的直接数据库访问。无论访问路径如何,都必须处理所有这些路径,以确保要求得到满足。 | 我们在两个层面进行集成:类型化客户端库,PrivacyLib 在其中拥有完整的请求上下文;以及其余 RPC 路径上的较低网络边界,以实现全面覆盖。每个已集成的系统都会将其原生 API 映射到 PrivacyLib 的通用操作,并提供其已配置模块所需的输入。一旦这些部分就位,同一个模块就可以在多个系统中启用,而无需针对特定系统单独集成。 |
| 规模: 在每秒数十亿次检查的规模下,即使每次检查的计算成本很小,在整个集群中也会变得举足轻重。热路径中的每个服务都要承担这一成本。 | 我们在构建时将要求编译为其最简单的可求值形式,并从进程内的缓存中回答大多数检查,因此常见路径避免了网络调用。 |
| 可靠性: 隐私逻辑运行在关键系统的热路径中,而这些系统由各自按自己的时间表发布版本的团队负责。变更必须能够独立测试并独立回滚,因为全集群回滚不能作为恢复路径。 | 执行逻辑作为拥有它的服务的一部分进行部署,因此变更通过该服务自己的流水线到达生产环境,而不是通过全集群的配置推送。 |
演练:保护消息元数据
为了说明这一切如何在 Meta 的规模下协同运作,让我们演练一项在处理消息元数据时必须遵守的隐私要求。
用户通过 Messenger 等应用交换消息, 这些应用支持端到端加密. 当 Messenger 路由并投递这些消息时,在适用法律允许的范围内,其后端系统会处理非内容消息数据(即消息元数据),例如有关消息发送或投递时间的信息,以提升产品性能、检测滥用行为,并保护用户免受有害行为侵害。
这些后端服务中有多种使用 Logger(一个在服务进程中运行的客户端库)将此元数据写入数据仓库表。在写入记录之前,Logger 内部的 PrivacyLib 集成会调用执行模块 Policy Zones,以检查该元数据是否可以流向目标表。此检查分为两部分:做出决策需要哪些输入,以及该检查在运行时实际如何评估。在考察这两部分之后,我们将了解 PrivacyLib 如何跨系统扩展,以及变更如何在不中断路径中服务的情况下推出。

标注消息元数据
每个 Logger 目标表都有一个 LoggerConfig,用于描述写入其中的记录。MetadataTraceLoggerConfig 为消息投递跟踪定义了这样一种配置,并将其标记为包含消息元数据:
final class MetadataTraceLoggerConfig extends LoggerConfigBase {
use MessagingTrafficMetadataPolicyTrait;
public function configure(
LoggerFieldDefiner $fd,
): LoggerFieldDefiner {
$this->annotateWithTableLevelPolicy();
// Field and destination definitions
}
}
一个日志目标可以有许多写入方,因此描述其输出数据的注解属于该目标,而不属于每个向其写入的服务。如果每个服务都单独声明该注解,这些声明可能会相互矛盾,或者新的写入方可能完全省略它。在 LoggerConfig 中声明一次,就能让每个写入方对数据获得相同的描述。
对于此目标,MessagingTrafficMetadataPolicyTrait 提供 MESSAGING_TRAFFIC_METADATA 表级注解,annotateWithTableLevelPolicy() 会将其附加到该目标上。
LoggerConfig 是 Logger 自身的 schema,每个系统都以自己的格式描述其数据。为了让 PrivacyLib 有统一的消费形式,Logger 的代码生成流水线会将配置转换为 PrivacyLib 兼容的产物。生成的 MetadataTraceLoggerConfigPrivacyLibArtifact 派生自 MetadataTraceLoggerConfig,并携带目标标识和消息注解:
/**
* @codegen-source MetadataTraceLoggerConfig
*/
class MetadataTraceLoggerConfigPrivacyLibArtifact
extends LoggerInputPrivacyLibArtifact {
final public static function getAssetIDImpl(
): LoggerInputAssetID {
return LoggerInputAssetID::get(
'MetadataTraceLoggerConfig',
);
}
protected static function getPurposePolicyVars(
): TPurposePolicyVars {
return PurposePolicy::makeVars(
TPurposePolicyName::MESSAGING_TRAFFIC_METADATA,
);
}
// Other generated fields omitted.
}
上面生成的类由 Logger 的客户端在 Hack(Meta 的类型化 PHP 语言)中使用。对于其 C++ 和 Python 客户端,Logger 会将目标标识和注解发布到中央目录。PrivacyLib 使用该信息生成特定语言的产物。在下一节的 C++ 示例中,PrivacyLib 使用 LoggerConfig 名称来加载该目标的 C++ 产物。
运行检查
上一节中的产物描述了一个 Logger 目标。PrivacyLib 还需要知道 Logger 写入的含义以及应运行哪些模块。Logger 的 Hack 集成会为所有目标统一定义这一点:
$config->libraryConfig()->enablePrivacyLib(
PrivacyLibOperation::write()
->input(
PrivacyLibInput::instance(
nameof ILoggerInputPrivacyLibPayloadProvider,
),
)
->enforcements(
PrivacyLibWriteEnforcement::zones(
ZoneDataType::LOGGER,
)
->withPropagation()
->enabled(),
PrivacyLibWriteEnforcement::telemetryLogging()->enabled(),
// Other modules
),
);
PrivacyLibOperation::write() 声明了 PrivacyLib 针对 Logger 的写入操作,enforcements() 则列出在其上运行的模块。Policy Zones 模块可以影响结果,而 Telemetry Logging 模块会记录评估过程,但不影响结果。在此处添加模块无需改动各个目标或 Logger 调用点。ZoneDataType::LOGGER 告知 Policy Zones 它正在检查一个 Logger 目标。
在 C++ 服务中,Logger 按如下方式构造 PrivacyLib 请求:
const auto assetList =
std::vector<std::string>{configName};
PrivacyLibBuilder builder(
"LOGGER_WRITE_CLIENT",
assetList,
LoggerWriteAPIEnumHelper::apiToHashedString(
PolicyEnforcedLoggerWriteAPI::log),
Caller::LOGGER,
getLoggerWriteImplementation());
auto result =
PrivacyLibRequest(std::move(builder)).dispatch();
PrivacyLib 使用 configName 加载该目标的产物。经过哈希处理的 PolicyEnforcedLoggerWriteAPI::log 值将 Logger 的 API 映射到 PrivacyLib 的写入操作。构建器的其余参数将 Logger 标识为调用方,并提供执行写入的实现。dispatch() 会调用已配置的模块并记录其结果。对于 Policy Zones 模块,PrivacyLib 会提供来自所生成产物的目标注解以及当前处理上下文。在此示例中,Policy Zones 会评估两个条件:
- 当前上下文表示对消息元数据的一次允许使用。
- 目标产物带有 MESSAGING_TRAFFIC_METADATA 注解。
启用强制执行后,只要任一条件不满足,Policy Zones 模块就会拒绝该写入。PrivacyLib 会将各强制执行模块的结果合并为一个响应。一旦被拒绝,写入会在 Logger 将记录发送到下游之前停止。观测模块会发出遥测数据,但不影响响应。

跨系统扩展隐私检查
PrivacyLib 通过通用执行模型、轻量级评估与遥测,以及对绕过集成客户端的请求的覆盖,使隐私检查能够跨异构系统扩展。
跨系统的通用执行模型
PrivacyLib 的集成覆盖了 API 差异极大的各类系统:键值存储、图数据库、机器学习流水线、文件系统,以及像 Logger 这样的日志系统。每个集成都会将其访问点映射到五种通用操作之一,PrivacyLib 会在每个操作处调用其模块:
- 读取: 在获取数据时运行。
- 写入: 在存储数据时运行。
- RPC: 在调用另一个服务时运行。
- 请求入口: 在服务开始处理请求时运行一次。
- 内联执行: 封装较大请求中的特定计算,例如评估某个功能。该计算期间进行的数据访问会使用其内联隐私上下文进行评估。
模块是针对这五种操作编写的,而不是针对各系统的原生 API。读取、写入和 RPC 还有操作后变体,因此观测模块可以在访问决策做出之后运行。一旦系统提供了映射,每个兼容模块都可以复用它,这就是为什么在已集成的系统上启用额外模块只需几天。
PrivacyLib 运行在数据和处理系统的客户端内,因为客户端已经拥有模块所需的目标身份和请求上下文。将评估保留在同一进程内,可以避免在每次访问时跨进程边界传递这些输入。这种设计还有两个优势:
- 更丰富的上下文: PrivacyLib 可以在调用方的身份、端点和用途信息仍然可用时使用这些信息。如果检查在数据存储上评估,这些信息往往会丢失。
- 故障隔离: 有缺陷的变更被限制在运行该版本代码的服务内,使共享数据存储处于故障边界之外。
保持检查的计算开销低廉
进程内评估还必须具备计算效率,因为它会在每次数据访问时运行。按请求缓存可消除同一请求内的重复检查,按主机缓存则保留频繁使用的配置和结果。这避免了早期设计所需的远程配置获取(仅该获取一项就比进程内路径慢数千倍)。结果是,PrivacyLib 在整个集群中每秒执行数十亿次检查,每次检查延迟很低,同时只消耗 Meta 总算力的一小部分。
在 PrivacyLib 的规模下,遥测也必须保持轻量。PrivacyLib 记录每个模块的状态、对延迟进行采样并记录错误,使我们能够全集群了解强制执行在生产环境中的表现。完整记录每次调用会使接收这些事件的系统不堪重负,因此目标是去重重复事件,同时让罕见事件保持可见。
有两种机制协同实现这一点:每台主机上的布隆过滤器以固定速率对高流量模式进行降采样,细粒度的采样率则随流量变化而调整。
覆盖直接 RPC 路径
并非每个调用方都通过集成了 PrivacyLib 的客户端库访问数据;有些调用方会发出原始 RPC。RPC 层仍然可以检查这些请求,但它看到的上下文远少于客户端。PrivacyLib 通过一个经过加密签名的令牌来弥合这一差距:当客户端侧检查成功时,PrivacyLib 会将令牌附加到请求上,并由 ServiceRouter 在较低边界处对受支持的 RPC 流量进行验证,确认该请求已经通过了更丰富的客户端侧检查。
当请求到达 RPC 层时没有携带令牌,ServiceRouter 会退回到仅基于网络负载中可见上下文的较粗略检查。由于该上下文有限,较低层会采用最严格的解释,宁可产生误报也不留下缺口。两者结合,这两层覆盖了通过 Meta 管理的客户端以及受支持的 RPC 路径进行的访问。这些边界之外的路径不受此机制覆盖,需要各自进行 PrivacyLib 集成。
综合来看,结果是:评估尽可能保持在本地且计算开销低廉,在无法进行本地评估的地方则有较粗略的兜底机制,而模块只需在一个地方运行,就能覆盖原本毫无共享的系统。剩下的则是更棘手的运维问题:如何在不破坏实时流量或做出错误访问决策的情况下改变强制执行行为。
安全地逐步推行强制执行变更
隐私要求会随时间变化。目标注解或强制执行模块的更新可能会改变某次写入是否被允许。PrivacyLib 为这两类变更都提供了安全的逐步推行路径。
更改注解
添加或移除注解会更改 LoggerConfig 中的两行:导入 MessagingTrafficMetadataPolicyTrait 的声明,以及对 annotateWithTableLevelPolicy() 的调用。由于目标位置可能有许多写入方,这两行的编辑可能同时开始阻塞来自许多服务的写入。
因此,评审的设计核心是向作者和评审者展示该编辑实际会产生什么效果。在代码评审期间,Logger 的代码生成流水线会更新目标位置的 PrivacyLib 工件。随后,模拟会针对新工件和现有工件分别重放观测到的数据流,并报告任何结果发生变化的数据流。源编辑、生成的工件和模拟结果会一起接受评审,因此决策依据的是变更效果的实际证据,而不仅仅是阅读代码变更。如果模拟报告了意外变化,评审者会在发布前暂扣该工件。
一旦获批,工件会通过各服务通常的构建和部署流水线到达服务。这样在变更部署之前就有了两种可见性:模拟报告的决策变化,以及生成代码准确展示将要部署的内容;此外还有一种控制手段,因为每个服务自身的部署决定变更何时到达生产环境。它还限定了影响范围:部署后发现的问题仅限于运行该构建的服务。
缺点是变更不会一次性到达整个机群,因此变更传播比全局配置更新更慢。在实践中,需求负责人会使用显式跟踪,在要求的截止日期前更新范围内的服务,而需要更早获得更新的服务可以使用其紧急部署流程。

逐步推行模块
注解变更只应用于一个 Logger 目标位置。添加到 Logger 写入路径的模块会对每个 Logger 目标位置运行。因此,我们会先部署该模块,然后再将其启用至所有流量。
举个例子,下面说明 Policy Zones 是如何在这条路径上引入的。我们首先在 Logger 的 PrivacyLib 写操作配置中将 Policy Zones 模块设置为 rollingOut():
PrivacyLibWriteEnforcement::zones(ZoneDataType::LOGGER)
->rollingOut()
发布键 logger.input.write.zones 标识了这一 Logger 输入、写操作和 Policy Zones 模块的组合。rollingOut() 将模块执行置于运行时控制之下。随后,我们分阶段将该控制从 0 提升到 100%,同时监控拒绝、错误和延迟。如果有效写入开始失败,我们可以在不重建或重新部署服务的情况下降低百分比。Telemetry Logging 模块会继续运行,因为它是独立配置的。
当 Policy Zones 模块在 Logger 流量中稳定运行后,我们将配置从 rollingOut() 改为 enabled()。此时,Logger 集成运行该模块时不再参考发布百分比。

上述两条变更路径在不同阶段都设有防护措施。如果模拟显示出意外的流量变化,我们会在发布前暂扣注解产物。如果某个模块在逐步推行开始后出现异常,我们会利用运行时控制降低其推行百分比。
这些安全机制改变了在Meta满足隐私要求所需的条件。PrivacyLib现在运行在数百个数据处理系统的客户端内,每秒执行数十亿次检查,同时只消耗Meta算力的一小部分。构建在其之上的各项要求,例如通过 策略区域 以及通过 隐私探针复用这些集成,而不是重新构建它们,因此在已被覆盖的系统上启用一个模块只需几天,而不是过去定制集成所需的数月。当变更确实出现问题时,故障会局限在运行已部署变更的服务内部。我们现在可以将这一基础用于更新的一类工作负载:AI 智能体。
面向 AI 智能体的 PrivacyLib 控制
AI 智能体比传统服务更难推理,因为它们的行为是动态的,而且它们访问的数据是在运行时才确定的。因此,我们必须从一开始就为智能体安全进行设计,通过强制执行诸如策略区域之类的控制措施,不仅针对我们在构建智能体时预期它们会访问的内容,也针对它们在运行时尝试访问的内容。在启用这些控制措施的地方,它们可以在返回任何数据之前阻止被禁止的访问。
智能体调用工具来执行任务。一次工具调用可能以嵌套方式跨越多个服务,最终才到达数据。为应对这一新挑战,我们基于 PrivacyLib 的设计原则构建了 AgentLib,这是一个通用运行时,用于观测和治理内部智能体发起的工具调用。在受支持的路径上,AgentLib 会将每次工具调用与其智能体会话关联起来,下游客户端和服务会传播该上下文,直到调用到达集成了 PrivacyLib 的系统。在该数据访问边界处,PrivacyLib 会结合上下文、所请求的操作以及目的地的注解,然后将它们传递给各个模块以做出最终决策。
在PrivacyLib内部,Agent Observability模块会记录智能体会话和工具调用标识符,以及目的地、客户端API、集成点和任何错误。工程师可以按会话查询这些事件,以调查工具调用所涉及的服务和目的地。当我们引入Agent Observability模块时,PrivacyLib现有的广泛覆盖使我们能够对已集成该库的系统进行插桩,而无需将该模块手动集成到数百个系统中。即使借助AI辅助开发,这也将是一项艰巨的任务。
为了执行某些策略,我们在未作修改的情况下使用了现有的 Policy Zones 模块,因为它依赖相同的输入来执行策略。在启用该控制的路径上,它会在允许操作之前考虑目的地注解和智能体上下文。当 Policy Zones 阻止某项操作时,PrivacyLib 会将该结果沿调用链回传至智能体环境。即使智能体选择了其开发者未曾预料到的工具,Policy Zones 也会应用此检查。访问决策不依赖于智能体是否按预期行事。

构建和运营 PrivacyLib 的经验教训
在Meta各系统中构建和运营PrivacyLib的过程,让我们汲取了几条重要经验。
面向演进系统和需求的设计: 在PrivacyLib之前,我们会在各个调用点添加隐私控制,以满足每项新需求。随着时间推移,Meta构建了新的数据和数据处理系统,服务获得了新的数据处理方式,隐私要求也不断扩展。每一次变更都带来了更多的集成工作,而新的访问路径有时出现得比我们能够覆盖的速度更快。我们得到的教训是:在稳定的数据访问边界进行集成,定义系统共享的一小组操作,并将注解与数据保持在一起。各模块可以复用相同的集成,从而随着时间推移减少工程投入。
可靠性赢得采用: 早期,一个无效配置导致整个集群中的进程崩溃,违背了我们自己的安全发布原则。其他事故则源于注解变更:这些变更虽然编译成功,却改变了访问决策并导致产品故障。这些事故表明,只有当服务所有者能够在发布前看到变更如何影响其流量,并且能够从有问题的变更中恢复而无需进行全集群回滚时,他们才会在关键数据访问路径中信任 PrivacyLib。
覆盖范围包括部署: 当新需求需要对模块或受支持的策略和注解进行更改时,较旧的服务需要部署具备更新后 PrivacyLib 能力的版本。我们已确保明确的清单、跟踪和发布机制到位,以协调这些部署,从而使需求负责人能够仅更新范围内的服务,安全地按时完成任务。
展望未来
这些经验教训塑造了 PrivacyLib 接下来的发展方向,因为 AI 正将数据访问推向越来越新颖的环境和调用路径。我们继续在两个方面丰富 PrivacyLib 的能力:
- AI 训练: 用于训练的数据从各种环境进入我们的系统。其中许多数据在不同地点产生或经过转换。我们正在将 PrivacyLib 风格的方法扩展到这一领域,使同样的保障能够随之扩展。有关 Meta 如何处理 AI 训练数据的更多信息,请参阅 Meta AI:关于我们训练数据的透明度.
- 内部 AI 智能体: 观察尝试的数据访问并阻止被禁止的访问,对于 Meta 内部用于工程和生产力场景的智能体而言,只是一个起点,而非完整的图景。更棘手的问题还在后面:在内部智能体行为发生时对其进行评估,以及推理这些智能体之间的通信——在其中,一次数据访问可能与提出请求的人相隔多个智能体。
致谢
作者们谨此感谢多年来为开发 PrivacyLib 发挥关键作用的许多现任和前任 Meta 员工所做出的贡献。我们尤其要特别感谢(按字母顺序)Adwait Sathe、Andrew Vaslas、Darrien Glasser、Diana Marsala、Hanzhi Zhang、Kousik Nandy、Matthew Hillman、Matthieu Martin、Michael Dietz、Nathan Maue、Ramy Wassef、Sen Luo、Spencer Baumgardner、Vince Yang 和 Zhong Hu。我们还要感谢本博文的所有审阅者,包括(按字母顺序)Aleksandar Ilic、Alex Kagan、Alex Ponomarenko、Arash Nikkar、Avtar Brar、Chloe Lu、Chris Wiltz、Emil Vazquez、Emily Van Deuren、Rituraj Kirti、Sushaant Mujoo 和 Wenlong Dong。 我们尤其要感谢 Jonathan Bergeron 监督这项工作,并提供了所有指导与宝贵反馈;还要感谢 Chloe Lu 汇集所需支持,使这篇博文得以完成。