Vercel 如何把 Redirect 扩展到百万级:低延迟与低成本并存

6 分钟阅读
2026 年 3 月 3 日
在小规模场景下,重定向(Redirect)很简单;但当规模达到数百万时,延迟和成本就会变成真正的系统工程问题。
此前在 Vercel,Redirect 主要通过 routing rules 和 middleware 处理。Routing rules 最多支持 2,000 条带通配符的复杂重定向,并按顺序逐条匹配执行。每条规则都可能涉及 regex 匹配,这意味着一次请求可能触发大量高成本计算。几千条规则时还能接受,但随着规则数量增长,请求侧工作量会线性上升。
Middleware 虽然更灵活,但会因为每个请求都要执行额外代码而增加延迟。为了在低延迟下承载数百万 Redirect,我们需要一条专用查询路径,让单次请求成本接近常数时间或对数时间。基于我们此前使用 Bloom filter 提升全球路由性能的工作,我们找到了将 Redirect 扩展到百万级的方法。
Link to heading我们优化的目标
-
规模:
- 每个项目支持数百万条静态 Redirect
-
运行时行为:
-
对未配置 Redirect 的项目不引入额外延迟成本
-
提供快速的“no redirect”路径(因为大多数请求其实不会发生重定向)
-
低进程内存占用,尽量依赖外部存储与缓存层
-
-
工程价值观:
-
相比过早优化,更重视简单性与可调试性
-
采用迭代演进,而不是一开始就追求“完美设计”
-
基于这些目标,我们先从能想到的最简单方案出发:把 Redirect 数据和 Bloom filter 放在同一个文件里。由于 Redirect 数据本身就是 JSON,而 Bloom filter 也已支持导出 JSON,我们决定使用 JSONL 格式来存储。
Link to headingJSON 与 Bloom filter:和草稿纸估算的较量
Bloom filter 是一种概率型数据结构,用于判断某元素是否属于某个集合。它可能出现假阳性(false positive),但不会出现假阴性(false negative);因此它只能回答“肯定不在集合中”或“可能在集合中”。通过先检查一个小且可缓存的 Bloom filter,我们可以让不匹配的请求直接跳过 Redirect 查询,从而让最常见的“no redirect”路径成本极低。只有命中正例时才去解析 JSON 文件。
这个思路很简单,但能扩展吗?草稿纸估算的结论是:不行。100 万条 Redirect 很容易产生数百 MB 的文件,拉取并解析这么大的数据会直接击穿我们的延迟和内存预算。我们必须避免一次性加载全量数据集。
Link to heading分片 + Bloom filter:兼顾低内存与快速查询
解决办法是分片(sharding)。我们不再使用单个超大 JSONL 文件,而是对 redirect path 做哈希,将记录分散到多个小分片中。这样每个请求只需加载对应的一小块数据,把压力从进程内存转移到外部存储和文件系统缓存。Bloom filter 仍位于前置路径,为绝大多数流量做短路;而当请求通过 Bloom filter 时,我们也只需拉取并解析一个小分片,而不是整个 Redirect 集合。
Link to heading分片结构
每个分片包含 3 部分:
-
一行 header,编码 Bloom filter 属性
-
base64 编码后的 Bloom filter
-
一个以 src path 为 key 的 Redirect JSON 对象
示例:
{"version":"bulk-redirects","bloom":{"n":3,"p":1e-7,"m":102,"k":23,"s":0}}"Mec7FxGVcJ0fHdj8HA=="{"/old-path":{"destination":"/new-path", ...},"/another-old-path":{"destination":"/another-new-path", ...}, ...}
包含 header、Bloom filter 和 redirect map 的分片格式
在构建阶段,我们会生成所有分片及其 Bloom filter,并上传到外部存储。运行时,服务器只需在收到请求时知道该项目或部署对应哪个数据集、分片数量是多少。
Link to heading查询路径会先检查 Bloom filter,再决定是否解析 JSON
请求到来时,bulk redirect 查询流程如下:
-
先检查该项目或部署是否配置了 bulk redirects。若没有,直接跳过并按常规流程处理。
-
根据请求计算 redirect key,并哈希得到对应分片。
-
从缓存或源站拉取分片,并检查 Bloom filter。
-
若 Bloom filter 显示 key 不存在,则不解析该分片 JSON body。
-
若 Bloom filter 显示 key 可能存在,则加载该分片 JSON body,并在对象中精确查找目标 Redirect。
-
这个设计有几个明显优点:
-
负查询速度快: Bloom filter 很快,且可调到极低假阳性率
-
分片可读性强: 分片就是 JSONL 文件,出问题时很容易 dump 出来逐项排查
-
实现风险低: JSON 解析与 Bloom filter 都是简单技术,可快速上线并收集真实生产数据
Link to heading正向命中时,JSON 解析成为瓶颈
我们一开始就怀疑 JSON 解析会成为瓶颈,dogfooding 结果也证实了这一点。当 Bloom filter 提示某条 Redirect 可能存在时,解析该分片的完整 JSON body 会消耗明显时间。高 CPU 负载下还出现了明显延迟尖峰,因为 JSON 解析本身是 CPU 密集型任务,会与节点上其他任务争抢资源。
减小分片大小有助于提升解析速度,但更小分片会提高基数(需要管理的分片数量)并增加缓存未命中率。这形成了典型权衡:大分片带来更高 JSON 解析 CPU 开销,小分片则带来更多 cache miss 导致的 I/O 延迟。我们需要一种无需解析整个分片就能取到单条值的数据格式。
Link to heading基于有序 key 的二分查找,避免整片解析
我们不再把 Redirect 存成一个 JSON blob,而是实现了基于 redirect path 的二分查找。每个分片中的 key 按序存储,因此可以对 key 做对数时间搜索。找到目标 key 后,只需解析该条 Redirect 对应的 JSON。这样就绕开了分片大小问题。查询成本不再随分片总数据量增长,因此我们可以把分片保持在有利于缓存命中的较大尺寸,同时不再承担整片 JSON 解析成本。
{"version":"bulk-redirects","bloom":{"n":3,"p":1e-7,"m":102,"k":23,"s":0}}"Mec7FxGVcJ0fHdj8HA==""/old-path"{"destination":"/new-path", ...}"/another-old-path"{"destination":"/another-new-path", ...}
有序 key 让我们无需解析整个分片即可完成二分查找
Link to heading延迟下降,尖峰消失
当正向查询路径不再把 JSON 解析放在热路径后,命中 Redirect 的请求不仅更快,延迟也更稳定。
最直观的改善是高 CPU 负载下延迟尖峰被消除。过去解析完整 JSON 分片时,Redirect 查询会和节点上的其他任务竞争 CPU;改为二分查找后,单请求 CPU 成本下降到足够低,资源争用不再构成主要问题。
Link to heading为“常见路径”而设计
Redirect 本身并不复杂。真正的挑战在于:把这个简单抽象放进“数据量巨大、命中率低(多数冷数据)且边缘侧延迟要求严格”的环境里。Routing rules 并不是这类问题的合适工具。
因此我们构建了 bulk redirects 的专用路径:
-
对 Redirect 数据做分片,保持每个数据块较小
-
使用 Bloom filter,让最常见的“no redirect”请求足够便宜
-
采用支持 key 二分查找的数据布局存储 Redirect
这轮开发再次强化了我们反复验证的一条原则:避免过早优化。先做简单、可调试的实现并打好观测,再让生产数据决定复杂度真正该落在哪些地方。
Link to heading开始使用 bulk redirects
Bulk redirects 已向 Pro 和 Enterprise 客户开放,可通过项目配置、dashboard、API 或 CLI 进行配置。当前限制为每个项目 100 万条 Redirect。如需更高容量,请联系我们。
Plan
Included redirects
Additional capacity
Pro
每个项目 1,000 条
每增加 25,000 条,$50/月
Enterprise
每个项目 10,000 条
每增加 25,000 条,$50/月
你可以用 bulk redirects 管理大规模迁移、修复失效链接、处理过期页面等。详见bulk redirects 文档或快速上手指南。
原文链接:https://vercel.com/blog/scaling-redirects-to-infinity-on-vercel

