Modal · 官方

使用 Pingora、Envoy 和 Spanner 的无服务器路由

Routing for serverless servers with Pingora, Envoy, and Spanner

二〇二六年六月二十五日 · 英文原文

Modal 推出 Server 产品,用于在云端运行超低延迟 HTTP、WebSocket 和 gRPC 服务。Server 通过路由层提供区域化、自动扩缩的副本池,请求路径上无网络调用,延迟控制在 5-7ms。架构包括 AWS NLB、Envoy 边缘代理、自研 fprs 代理(基于 Cloudflare pingora 库构建)及计算平面 worker。fprs 从 Spanner 读取配置并维护内存缓存,实现域关联、负载均衡、代理认证、自动扩缩指标和流量镜像功能。Server 面向 LLM 推理等对延迟敏感的应用场景。

工程

2026年6月25日·10分钟阅读

Modal 让在云端运行高性能代码变得简单:Python 函数、agent 运行时、notebook、批处理作业等等。现在,你还可以在 Modal 上运行超低延迟的 Server,用于 HTTP、WebSocket 和 gRPC 流量。

Server 专为每一毫秒都至关重要的应用而设计,例如面向交互式 agent 的 LLM 推理。Server 在 Modal 的路由层后面为你提供一个区域化、自动扩缩的 HTTP server 副本池,并具备我们认为理所当然的部署人体工学、快速反馈循环和自动扩缩能力(面向人类和 agent)。

这听起来可能很熟悉:通过 Modal Web Functions,你已经可以在 Modal 上暴露 HTTP 端点。但 Web Functions 的架构设计侧重于开箱即用的健壮性——队列、重试和平台管理的请求生命周期——而非极致的延迟。随着推理延迟的急剧下降,消除正常路径中的延迟并将其他一切推到应用层变得越来越关键。那几十毫秒就是胜负之差

因此,我们在 Modal 上构建了一个开销最小且不牺牲核心功能的 HTTP 服务方案。

请求延迟

在这篇博客中,我们将解释如何做到这一点。难点不在于接受和路由 HTTP 流量;Envoy 可以做到。难点在于保留 Modal 的语义——认证、动态副本放置、区域路由、自动扩缩、推理功能和租户隔离——而不在热路径上放置控制平面查询或队列。

Modal Servers 用于什么?

Modal Servers 通过反向代理路由系统,实现客户端与 Modal 上区域化、自动扩缩的 HTTP server 副本池之间的通信。

图片1

将那个轻量级系统(下图右侧)与 Modal Web Functions 对比,后者包含一个提供队列和重试功能的输入平面(下图左侧)。

图片2

为了更好地理解这些架构及其选择,考虑一下传输控制协议(TCP)和用户数据报协议(UDP)之间的区别。TCP 为互联网应用提供可靠、有序的字节流。UDP 提供一种更低延迟的原语,但要求应用自行处理乱序/丢失的数据报。抽象地说,两者没有“更好”之分。它们针对不同的目标进行了优化。

某些应用(例如 GPU 间通信、视频通话)基于 UDP 构建——通过融合以太网上的 RDMA (RoCE) v2通过 Web 实时通信 (WebRTC)。它们实现了比 TCP 更低的抖动/延迟(对比 iWARP RDMA-over-TCP 的有限采用)。其代价是以特定于应用的方式重新实现更高层的可靠性/排序(对比“端到端论证”)。

Modal Web Functions 更接近 TCP:内置了重试和队列,因此客户端无需担心这些。Modal Servers 更接近 UDP:请求走一条更轻量、更低延迟的路径,但应用必须处理粗糙的边缘情况。如果没有可用的副本,客户端会收到 503 Service Unavailable——与服务不存在时相同的响应。队列和负载丢弃(通过 503)同样被推送到容器应用(目前如此!)。

但在某些应用(如低延迟 LLM 推理)中,进行这种权衡是合理的。

顺便说一句:这种区别并非字面上的——你可以在 Modal 容器上使用任一协议栈通过 UDP 打洞来服务 UDP 应用,例如驱动机器人在网络摄像头画面中检测物体。但工程约束、我们的选择以及你的选项是类似的。

设计 Modal Servers

在高层面上,Modal Servers 的路由层包含一个用于 I/O 的流式边缘代理、一个智能无状态代理和一个计算负载均衡器。无状态代理由共享全局状态配置,计算负载均衡器与用户容器以及创建和销毁它们的 Autoscaler 通信。

大致如下所示:

图片3

两个原则指导了我们为此架构及其内部所做的每一个选择:

  1. 最大化资源共享,同时最小化干扰。我们需要跨租户和请求池化资源(工作窃取、连接池、流复用)以获得聚合性能,但我们也需要为正确性以及每个租户/每个请求的性能提供专用资源的假象。
  2. 请求路径上无网络调用。没有元数据获取,没有 KV 存储,没有回退到 blob 存储。这阻止了我们在此层处理队列和重试(尽管我们以后可以将其作为可选功能重新添加)。它要求我们在路由层内物化最小状态,同时保持一致性。

第一个原则最为重要,值得评论。高性能代理自然会围绕其管理的资源组织工作:工作线程、连接、流、缓冲区和流控窗口。但 Modal 的服务路径有一组不同的边界,这些边界对用户(因此对我们)更重要:客户流量到其 HTTP Server 副本。这些是用户定义正确性和体验延迟的边界。

当这两种视图不一致时,共享的代理资源可能成为共享的命运。连接池作为优化添加,但成为负载下哪些请求相互干扰的决定因素。队列添加用于缓冲,但成为隐式调度器。流控窗口最初是传输细节,但在规模上定义了背压如何跨流组合。因此,并没有“一个让延迟降低的奇怪技巧”。相反,这是一系列微观调整,以使资源使用与我们的宏观目标保持一致。

Modal Servers 如何工作?

最终实现如下所示:

图片4

路由系统的核心运行在我们的 AWS Elastic Kubernetes Service 中。正如我们喜欢说的,Kubernetes 并非为推理而构建——但它_确实_是为扩展高可用分布式代理而构建的。相比之下,用户容器和支持它们的自定义容器运行时由我们的调度系统管理,而非 Kubernetes,并且它们运行在全球各地的数十个云上。

接下来,我们将遍历此系统中一个请求的生命周期。

客户端请求由 Modal Servers 支持的资源

资源由类似 https://[domain].[routing_region].modal.direct/[path] 的 URL 标识。

  1. [domain] 组件标识特定的 Modal Server(即自动扩缩的容器池)
  2. [routing_region] 选择代理系统的区域化部署(这些部署共同构成我们的路由平面)
  3. modal.direct 向公共互联网标识我们
  4. [path] 组件标识服务器上的资源——连同请求体和查询参数,这是用户与你的业务逻辑交互的方式

我们通过 AWS NLB(一个 L4 代理)连接到客户端。由于它在 L4(此处为 TCP)而非 L7(HTTP)上运行,因此无法实现 L7 功能,所以它被保留为一个简单的负载均衡器。

Envoy 将请求转换为 h2 流

第一个 L7 功能由流式边缘代理实现,我们为此使用 Envoy。此组件终止 TLS,以便我们可以在 VPC 内部无需 TLS 进行通信,并且可以操作标头。它还将所有 HTTP 流量标准化为 HTTP/2。

通过 HTTP/2,我们可以将客户端流复用到一个从池中获取的单个 TCP 连接上。此处的池化减少了延迟,复用控制了总资源需求——随总负载而非直接随客户端基数扩展。但同一连接上不同客户端之间引入的争用需要深入审查。

fprs,我们的内部代理,将客户端流映射到 Server 副本

在架构层面,下一个目标是将 URL 中的 domain 映射到特定的用户部署的 Modal Server 应用(“域关联”),并在 Modal 计算平面中跨该应用的副本进行负载均衡。大多数非路由功能在此层实现。

Envoy 专为卓越性能而设计,但它不允许我们实现这些功能。由于没有它们我们就无法交付产品,我们将 Envoy 保留在边缘,并选择构建我们自己的域关联和负载均衡系统,以便我们可以注入核心功能。

我们喜欢深入并内部构建,但我们并非轻率地做出这个决定。我们专注于支持 LLM 推理,但并非排除其他工作负载,因此我们不能使用像 NVIDIA Dynamollm-d 这样的系统。我们在运营 AWS NLBnginx 方面拥有丰富的生产经验,但我们需要一套独特的功能——例如,处理上游的快速变动,因为用户创建和销毁服务(而服务创建和销毁副本)。

fprs 使我们能够轻松交付所需功能

我们使用 CloudFlare 出色的 pingora 库构建了 fprs。我们是相当有经验的 Rustaceans,因此我们很放心将其引入我们的技术栈并大规模运营。正如创建者在他们的博客中指出的那样,底层的 Tokio 运行时非常适合流式、复用的 HTTP 代理。Rust 轻量级、线程内协程并发模型中的工作窃取优雅地将共享资源与共享命运分开。

话虽如此,我们仍需做一些工作才能在我们独特的用例中实现 pingora 的严格尾部延迟。例如,我们的域周转速度远快于典型情况。当我们调查一些偶尔的停顿问题时,我们确定存在由这种周转触发的隐藏 DNS 解析路径,我们需要仔细缓存它。

我们将此代理设计为无状态,以便易于扩展和缩减。但配置就是状态。此处的配置包括 url: host 映射,这些映射变化很快。配置错误是搞垮代理服务的简单方法,因此我们希望确保正确处理。我们从 Spanner(一个全局复制的数据库)读取配置状态。

当然,我们不会在每个请求上都读取 Spanner。坚持我们的设计原则,避免在热路径上进行网络调用,我们维护了一个域关联的内存缓存。该缓存通过变更流与全局状态并发协调(Spanner 的外部一致性保证在此处派上了用场)。

处理完性能和路由后,我们可以添加关键功能。

自动扩缩指标

此层还向 Modal Autoscaler 发出指标,Autoscaler 决定是否创建新容器以处理增加的负载(或在不再需要时将其关闭)。

Autoscaler 需要一个信号和一个算法。路由层发出的信号是基于(逻辑)连接计数的每个容器正在处理的请求数。算法是当平均连接数超过/低于用户指定的 target_concurrency 时,增加/减少容器数量,并受 scaleup_/scaledown_window 约束。

代理认证

我们还需要路由层认证。用户服务可以自行实现认证,但这仅在至少一个副本启动并能处理请求后才生效。如果你需要启动八块 B200(每块消耗一千瓦功率)仅仅是为了回复几字节 HTTP 流量中的 401 Unauthorized,那你就是在给拒绝服务攻击者提供杠杆。

因此,我们实现代理认证以在路由层阻止这些请求,在它们到达用户容器之前。

镜像

推理系统是机器学习系统的最终产品。机器学习系统有一些不同于其他系统的独特需求。最值得注意的是,它们非常关心所运行数据的语义,并且其生产行为难以预测。因此,理解生产中的用户数据至关重要,因此我们将镜像作为一等公民功能支持。

一个简单的例子是 A/B 测试。与 UI 变更一样,有时评估 ML 系统变更的唯一方法是在生产中进行测试,理想情况下使用随机对照试验,即 A/B 测试。对于推理系统,通常通过镜像来模拟生产流量就足够了。

我们感到兴奋的另一个镜像用例是持续学习。ML 系统随着更多数据而变得更好。我们通过训练自定义推测模型以加速推理直接看到了这一点对推理系统性能的影响。这也是模型质量持续改进循环的关键部分——我们预计,随着行业在最近专有模型服务领域的动荡之后转向开放权重推理,更多行业参与者将意识到这一事实。

计算平面中的 Worker 将 HTTP 请求中继到容器

fprs 转发的请求到达 Modal 计算平面中的容器运行时 worker。此容器运行时包装用户容器并提供其运行时语义。计算平面包含从普通 CPU 服务器到冰箱大小的 NVLink 机架的一切,由从发明云计算的大型云服务商到从加密货币转型、刚刚好到足以通过我们的质量评估的新兴云服务商运营。

这些 worker 与 fprs 使用 HTTP/2 通信。它们与支持 HTTP/2 的用户服务器使用 HTTP/2 通信,但可以在需要时切换到 HTTP/1——而不会影响我们系统其余部分的 HTTP/2 流。Worker 还通过跟踪请求完成来处理容器的优雅关闭。

路径反向运行以将响应返回给客户端

系统中的每个组件(在 AWS NLB 之后)都是一个 HTTP 服务器,因此它们必须按相反顺序向其客户端返回响应,直到我们到达原始客户端。整个过程在 5-7ms 内完成。这似乎是欣赏计算和网络能力的好时机。上述所有工作的完成速度比神经冲动传递到你的腿部还要快。

为什么要构建这个?

在一个氛围编码和短暂软件的时代,Modal 仍然相信基础构建块的力量。这些高质量、可重用、可组合的组件可用于构建新服务,并为我们和我们的用户解锁新的工作负载。如果说有什么不同的话,那就是随着机器使我们能够以更低的成本创建更多软件以更好地满足最终用户的需求,核心基础设施的强度和效率变得更加重要。

Servers 是 Modal 原语的新成员。用户可以通过我们的 SDK 直接使用。我们期望某类用户会接受并运行它——正如一些人已经做的那样

但它们也被我们的新 Endpoints 产品作为组件使用,该产品使以最先进的性能服务 LLM 推理变得毫不费力

如果你对解决棘手的基础设施问题和构建强健的骨架感兴趣,我们正在招聘

在 HackerNews 上吐槽我们,请点击此处

译自 Modal · 官方 · 录于 二〇二六年六月二十五日