ZGateway: Learnings from Putting a Proxy in Front of ZippyDB

TL;DR · AI 摘要
Meta通过ZGateway代理优化ZippyDB流量,提升系统稳定性和资源效率。
核心要点
- ZGateway减少ZippyDB客户端连接数至可控范围,降低资源消耗。
- 代理层实现准入控制和跨区域弹性,提升系统容错能力。
- 直接访问模式导致百万级TLS连接,浪费内存和文件描述符。
结构提纲
按章节快速跳转。
思维导图
用一张图看清主题之间的关系。
查看大纲文本(无障碍 / 无 JS 友好)
- ZGateway代理架构
- 核心功能
- 连接管理
- 请求批处理
- 准入控制
- 问题解决
- 连接爆炸
- 资源浪费
- 运维复杂度
金句 / Highlights
值得收藏与分享的关键句。
代理层将客户端连接数从十万级压缩至可控范围,降低资源浪费。
直接访问模式下单个客户端可能维护上万个TLS连接。
代理层实现分钟级策略变更,替代传统客户端全量更新流程。
ZGateway:在ZippyDB前添加代理的实践心得 - Meta工程团队
POSTED ON
2026年9月3日
TO
核心基础设施
ZGateway:在ZippyDB前添加代理的实践心得
.entry-meta
.entry-header
作者
Rittik Banik
,
Yunhao Cao
- 我们推出了ZGateway,这是用于统一ZippyDB流量的代理,ZippyDB是Meta使用最广泛的关键值存储系统。
- 作为额外优势,它还实现了准入控制、负载均衡、跨区域弹性和更丰富的运维功能。
ZippyDB是Meta使用最广泛的关键值存储系统,为产品元数据、计数器和配置提供支持,能够在全球分布式集群中每秒处理数十亿次操作。
之前的一篇文章描述了ZippDB的工作原理。本文将重点介绍其前端的层级:ZGateway,这是我们统一ZippyDB客户端流量的代理。
ZGateway的诞生源于需要管理ZippyDB客户端舰队的扩张,其价值最终被证明是结构性的。一个ZippyDB客户端可能是由数百个团队管理的超过百万台主机之一,我们无法快速更改这些主机。代理则处于完全不同的位置——它同时处于大量客户端的路径中,这种共享的视角使其能够实现单个客户端无法做到的事情。
以下所有功能在统一管理层级中实现起来都更容易、更安全,或者只有在此层级才能实现,而不是在百万个客户端二进制文件中分别实现。我们通过连接管理与请求批处理这两个最清晰的案例来讲述这个故事。
为什么ZippyDB需要代理层
代理出现在任何大型、多样化的客户端群体与共享后端通信的地方:连接池器、服务网格sidecar、CDN边缘节点、API网关。在众多调用者与共享资源之间插入代理可以带来三方面优势:
- 问题边界明确:后端不再看到客户端群体,而是看到由自身运营团队控制的舰队
- 共享工作场所:连接池、重试、路由、缓存、准入控制等共性工作由最了解后端的团队统一解决,而不是在每个客户端中重复实现
- 控制点创建:这是唯一能全面观察工作负载的地方,可以将负载归因于生成者,并在几分钟内改变行为,而无需等待全舰队客户端更新
权衡在于增加了一个跳转和一个额外的层级需要维护;当客户端群体庞大、多样且无法更改时,这种权衡会带来显著回报。ZippyDB是一个极端案例。
在直接访问模式下,每个ZippyDB客户端都需要连接到其所需的每个数据库主机。单个客户端在稳定窗口内可能访问数万个不同的分片,这些分片分布在数十万台数据库主机上。结果是一个密集的多对多TLS连接网络:典型客户端需要维护数万个出站连接,而典型数据库主机可以接受数万个入站连接。
图1:直接访问会产生无限制的多对多网络;ZGateway将客户端的扇出和数据库主机的扇入都限制在可控范围内。
该网络结构既浪费资源又脆弱。每个开放连接都会消耗两端的内存、CPU 和文件描述符,尤其是在空闲时,而入站连接会随着客户端数量增长,因此每个新客户端群体都会使每个数据库主机的负载变得更重。由于每个客户端都自行管理连接池和故障转移,一旦出现连接复用率骤降的情况(例如客户端群体重启或部署更新),就会导致整个集群爆发大量新连接;我们曾追踪到因文件描述符耗尽和内存溢出导致主机崩溃的案例,正是由于这种原因。
从客户端层面修复这个问题非常困难,因为两个系统同时在变化。客户端集群持续调整连接池策略并扩展规模,而数据库集群则按照自己的节奏进行整合。直接耦合这两个系统的感觉就像在两辆行驶中的车上跳跃。代理层将它们解耦,将网络结构简化为两个可控的跳转步骤,这在效率、性能、可扩展性和最重要的可靠性方面都带来了显著提升。
最后一个因素最为关键。在直接访问模式下,重新连接风暴会演变为灾难。在一次事件中,一个路由错误导致每个客户端都为每个分片打开连接,主机突破了文件描述符限制,整个集群陷入重启循环。而通过 ZGateway 的路径,这种风暴被限制在代理层——这是我们能够控制、监控并集中加固的集群。连接管理并未消失在代理层之后,而是转移到了我们能够解决的唯一位置。
直接访问模式在 ZippyDB 的早期阶段是合理的设计。随着客户端数量增加,扇入式架构的资源消耗也随之增长:在几千个客户端时,这种网络结构是一种效率低下,而在大规模部署时则成为可靠性瓶颈。解决手段与这一阶段同步出现——ServiceRouter 功能改进、Thrift 过载保护、轻量级客户端等特性使得共享层级的实现成为可能,而更早构建 ZGateway 意味着需要先构建这些功能。
ZGateway 是什么
ZGateway 是 ZippyDB 客户端与数据库(ZServer)集群之间的无状态代理层。它每秒可处理超过十亿次操作,承载约 40% 的 ZippyDB 流量(预计未来将超过 60%),同时仅增加约 6% 的计算开销。它通过 ServiceRouter(Meta 的超大规模服务网格解决方案)以区域层级运行,将每个客户端连接到最近的网关,提供两种共享同一管道的版本:纯代理和读取缓存。它使用我们专有的厚客户端 C++ 实现作为引擎,每个使用场景对应一个内部客户端。ZGateway 是作为托管服务运行的 ZippyDB 客户端,这使得能力迁移至此变得自然。
图 2:从客户端的区域粘性连接到 ZServer 副本的 ZGateway 请求路径。
客户端通过粘性连接向区域 ZGateway 主机发送请求,ZGateway 终止 TLS,根据使用场景的访问控制列表进行授权,并应用租户级准入控制、验证和流量整形。ZGateway 解析分片,在缓存层级检查本地缓存,将未命中和写入请求与其他同分片的进行中请求进行批处理和合并,然后发送到正确的副本。响应会分发回调用方,同时沿途记录每个使用场景的指标、追踪和配额使用情况。
关键特性是连接数量的不对称性。每个客户端只需与区域ZGateway主机保持粘性连接池,而每个ZServer仅接收来自ZGateway舰队的连接,且舰队规模由我们控制。部分职责刻意保留在原处——Thrift/ServiceRouter堆栈中的TLS、分片定位器中的键到分片映射、嵌入式客户端中的副本选择和冗余策略。ZGateway负责流量管理,而非重新实现数据库客户端。
扇入/扇出缩减
以下是计算公式:
将舰队建模为球入箱问题。将B个球(主机接触的分片)投入H个箱子(主机),统计被击中的不同箱子数量。
期望值为:
特定箱子被击中的概率为:
扇出是不同箱子数量,扇入是该概率乘以调用者数量。以具体数值估算——20个区域、500,000个数据库主机、30,000个代理主机、1,000,000个客户端、每个客户端50,000个分片——模型得出:
图3:每台主机的连接数量减少约97-98%。(基于模型的估算;每对TLS倍数因子在比例计算中被抵消)
连接并未消失,而是转移到专为承载它们而构建的层级。端到端来看,持久连接总数仍减少约19倍,因为每个后端连接可复用多个客户端。
但一次性的缩减幅度再显著,也不是重点所在。真正的重点在于扩展行为的变化。在直接模型中,数据库主机的扇入量为H_client · p——与客户端数量成线性关系,因此每个新用户群体都会使每个数据库主机状况恶化。而使用ZGateway后,客户端数量完全消失:扇入量简化为R · S_host,即区域数乘以每台主机的分片密度,与两个舰队规模无关。唯一剩余的调节杠杆是分片密度,而这由我们掌控。原本由所有人驱动的无界数值,变成了我们可控的有界数值。
压缩请求流:批处理与合并
由于ZGateway位于众多客户端路径上,它可以完成任何客户端库都无法实现的事情——跨不同调用者合并工作。每台主机上的共享批处理器会根据使用场景和物理分片,将发往相同目的地的请求分组,并合并为一个后端RPC。
它还能进行请求合并。当多个调用者同时请求相同键时,网关只需获取一次结果并分发给所有调用者。客户端侧的批处理器只能合并自身进程的请求;而网关可以跨客户端合并请求。
每个RPC都带有固定开销,与请求大小无关——Thrift序列化、分片查找、授权、系统调用等——因此将多个操作合并为一个可分摊所有开销,这意味着更少但更大的后端请求、更低的QPS和CPU消耗,以及随着滞留窗口平滑微突发而更稳定的负载。而且由于使用场景按发送的QPS计费,批处理可以延长其速率限制预算,无需任何额外工作即可减少限流。
批处理的两个优势与效率完全无关。第一个是合并在热键下的效果。数千个并发调用者会被压缩为一个后端读取,因此热键永远不会演变为对单个副本的踩踏式访问。
第二个是它让我们能够移除某些东西。多年来,想要批处理的客户必须运行客户端库,这些库脆弱、消耗大量CPU、需要单独调优,并且由于这些复杂性存在于我们无法控制的百万个二进制文件中,一直是事故的稳定来源。共享批处理器可以跨客户端进行批处理,这是那些库无法做到的——让我们得以淘汰它们。
每个请求都会被暂存在一个内存中的批次中,当等待时间超过设定窗口、负载大小超过限制或请求数量达到上限时,批次会自动刷新,从而将延迟控制在可接受范围内;过大或刚迁移的批次会回退为单独发送。内存中暂存请求存在内存溢出风险,因此批次处理机制内置了两种安全机制。
空闲淘汰机制用于处理缓慢增长的流量:当批次映射条目在设定的生存时间(TTL)后仍处于空闲状态,会在下一次刷新时被自动清除。飞行中请求数量限制用于应对突发性过载。当后端处理变慢时,处理刷新批次的协程会堆积得比处理速度更快,因此当请求数量超过限制时,系统会拒绝新请求。持续的维护机制加上突发情况下的安全阀,使批次处理默认就是安全的。
图4:跨客户端的批次合并与聚合。
ZGateway 的演进历程
一旦流量通过某一层级,该层级就成为所有客户端原本需要自行实现的功能的天然承载地。批次处理是最明显的例子,以下是其他功能。
流量路由与安全迁移路径
将流量迁移到代理上是一项高风险操作,必须具备渐进性、可逆性和范围控制。ZGateway 的路由由客户端配置标志控制,每个服务和分片前缀都有独立的配置范围:百分比调节器用于逐步启用符合条件的流量,区域过滤器用于限制影响范围,全局关闭开关可实现即时回滚。这完全通过配置实现,无需修改客户端代码,因此可以在实时中精确控制发布过程。
租户隔离与准入控制
共享层级需要服务数百种使用场景,因此某个恶意租户不能影响其他租户的正常运行。ZGateway 的防御机制是判别式负载丢弃(DLS)。每个请求都会映射到按租户划分的专用桶中,该桶根据使用场景和优先级进行划分,并采用轮询方式释放负载。当某个租户大量涌入时,其专属桶会被填满,多余的请求会被丢弃,而其他租户的桶仍能正常释放负载——这种隔离是架构本身的属性,而非偶然因素。在DLS之前,CPU并发控制器通过AIMD循环调整共享令牌桶的处理速度;内存处理器则通过相同方式防止内存溢出。
丢弃机制保持判别性。在约1350个活跃租户桶中,当CPU使用率超过90%时发生受控过载,只有6个实际产生噪声的租户桶在丢弃流量;其余约1344个租户桶的请求执行率保持在99.9%,吞吐量维持在97-98%左右,整个机制消耗约8%的CPU资源。
图5:过载情况下的判别式负载丢弃。
带实时失效的读取缓存
在缓存层级,热点读取请求由进程内缓存直接提供服务;当缓存未命中时,网关会获取该键的填充锁,使针对同一键的大量并发请求合并为单个后端获取。数据新鲜度通过变更数据捕获流实现,该流会传输写入和检查点事件,根据显式有界陈旧性协议对受影响条目进行失效或刷新,每个主机通过一致性哈希算法负责键空间的一个切片。这种机制显著降低了存储的读取负载,在保持正确性的同时实现了更低的延迟。
层级内负载均衡
由于 ZGateway 是无状态的,区域层级中的任何主机都可以处理任何请求,因此我们可以引导流量以实现负载均衡。该层级并非均匀分布,它混合了 ~26 核到 ~126 核的主机,大规模任务替换可以在几分钟内重新分配容量。对不同性能的主机一视同仁会导致热点异常节点的出现,而热点 ZGateway 主机正是导致错误率峰值和服务路由限流的原因。由于 ServiceRouter 通过加权一致性哈希进行路由,调节的杠杆是每个主机的权重。
控制平面负载均衡器会计算这些权重。按照固定周期,它会读取每台主机最近的 CPU 使用率,将层级平均值归一化为 1.0,并根据负载反向调整每个主机的权重。防护机制确保稳定性——调整过程会进行阻尼和限制,分布会重新以目标中位数为中心,防止权重漂移至零,同时变更节流机制每次仅调整最不平衡的主机,限制分片重新分配(在缓存层级尤其昂贵,因为移动权重意味着移动键)。新加入的主机初始权重会根据硬件容量进行缩放。
教训是单一固定策略无法同时应对平静层级和处于冲击状态的层级。因此负载均衡器正在变得自适应,会分类判断每个层级的状态——稳定漂移、任务波动、初始权重扁平化、双峰负载、热点异常节点、区域偏斜——并应用相应的策略。
跨区域弹性
在大部分生命周期中,ZGateway 严格局限于区域,故障转移始终在区域内进行。这对延迟优化很有利,但当整个区域层级面临压力时,请求会在本地排队并超时,而相邻区域的健康容量却处于空闲状态。由于 ZGateway 基于 ServiceRouter,我们可以通过三种机制以可控方式实现跨区域路由:
- 全局路由构建跨区域的路由表,使饱和的本地层级可以故障转移到健康层级,而非在本地崩溃。
- 超大区域将地理上邻近的区域组合为一个局部区域,使溢出流量流向邻近区域,保留大部分延迟优势。
- 环形结构明确声明哪些区域相互备份以及备份比例。
每个机制都通过百分比旋钮在层级和区域层面独立启用。故障转移信号与路由策略同样重要。简单的区域 CPU 平均值可以平滑处理我们需要捕获的热点状态,因此故障转移依赖更精确的指标,该指标在区域负载达到临界点前触发,而非之后。
事务与更丰富的操作
事务需要一个存储客户端账本管理的地方:读取集、扫描范围、待写入数据。历史上这些数据存储在厚重客户端中。当 ZGateway 将客户迁移至轻量级客户端时,这些数据必须转移到网关。首次实现时保留了两种并行方案——为 ZGateway 定制开发的存储方案,以及引擎原本使用的内存路径——这是流程中最关键正确性部分的两个版本。
我们通过九个阶段逐步合并至单一方案,从高流量区域开始,最终实现 100% 事务流量迁移,且无可靠性退化。与引擎共享该路径使 ZGateway 与服务端事务演进保持同步:在层级内部一次演进功能,所有客户端都会继承该功能。
ZGateway 以大量服务器的形式部署在数十个地区,根据工作负载分为多个层级——一个用于长尾用例的大型通用层级,为最大客户提供的专用层级,以及一个独立的高吞吐量代理层级。这些层级在规模和容量上差异超过一个数量级,且单个层级本身也不统一,主要是由于堆叠架构。不同密度的任务被封装在同一台机器上,与全尺寸专用主机并存。在所有这些架构中,ZGateway 提供了针对每个用例的丰富可观测性——这种可见性使得在共享基础设施上执行的准入控制和负载均衡能够安全运行。
ZGateway 的未来方向
短期内统一所有 ZippyDB 流量通过 ZGateway 的路径保持不变。更有意思的问题是,一个被普遍采用的网关能实现什么。三个方向尤为突出,它们都围绕一个共同主题——ZGateway 既能看到最多,也能做出最多决策。
由代理操作的启发式算法。几乎所有功能都由控制环路和手动调节的参数控制:限流桶大小和 CPU 阈值、均衡器参数、故障转移触发器、批量刷新窗口、缓存过期范围。目前这些参数由人工调节,并通过定时任务进行微调;上述自适应均衡器本质上已经是代理,只是尚未明确命名。下一步是将其显式化,将这些启发式算法和内部状态作为结构化控制接口暴露出来,让 AI 代理能够像我们一样监控相同的数据,实现比任何值班人员更快的故障诊断(例如判断层级状态、将事件归因于噪声租户,并在防护机制下快速应用修复措施)。
共置部署。ZGateway 作为一个独立层级,需要额外的网络跳转和少量开销。对于对延迟或效率要求严苛的工作负载,我们可以将网关部分功能下推至 ZServer 主机旁,使网关与服务器之间的通信变为本地调用,同时控制平面保持集中。关键在于在不重新耦合我们刻意解耦的舰队的前提下实现这一点。连接管理与准入控制前端仍保持为共享区域层级,只有需要数据本地性的部分才会下推。
多进程网关。ZGateway 将多种职责集中在一个进程中运行,导致一个租户的内存膨胀可能危及整个主机上的所有服务。将其拆分为协作进程——连接/TLS 前端、请求工作进程、独立的缓存和事务组件——可实现硬隔离和独立生命周期。这一方案与代理操作的启发式算法相辅相成,代理可管理主机上的进程舰队,而当数据平面本身成为可独立部署的进程时,共置部署也会更加清晰。
这些改进将 ZGateway 从一个智能层级转变为一个可编程层级:由代理做出控制决策,部署位置随工作负载动态迁移,且通过架构设计实现故障域隔离。