Uber 如何用原生 gRPC 加速 OpenSearch:从 REST/JSON 瓶颈到搜索与写入性能优化

Uber 如何用原生 gRPC 加速 OpenSearch:从 REST/JSON 瓶颈到搜索与写入性能优化

本文是对 Accelerating Search and Inestion with High-Performance gRPC™ in OpenSearch™ 的整理与翻译。


内容结构概览

本文会围绕 Uber 如何在 OpenSearch 中引入原生 gRPC 传输层展开,主要包括:

  1. 为什么搜索系统在 Uber 几乎无处不在
  2. REST/JSON 在高吞吐、低延迟、大 payload 场景下为什么成为瓶颈
  3. Uber 内部基础设施为什么更偏向 gRPC + Protobuf
  4. OpenSearch 原本只有 REST/JSON API 带来了什么集成成本
  5. 为什么 Uber 选择在 OpenSearch 内部实现原生 gRPC,而不是长期维护适配层
  6. gRPC transport 如何作为 OpenSearch module 与 REST transport 并存
  7. REST 和 gRPC 如何共享 OpenSearch 内部逻辑
  8. 为什么优先覆盖 Search 和 Bulk API
  9. JSON/OpenAPI specification 如何自动转换为 Protobuf
  10. REST API 和 Protobuf API 在语义与演进方式上的差异
  11. 三阶段转换流水线:preprocessing、core conversion、postprocessing
  12. Search Gateway 如何从 Proto ↔ JSON 适配层迁移到 gRPC pass-through
  13. Bulk 写入场景下的性能收益:M3 p99 写入延迟下降约 60%
  14. M3 Indexer 最大索引延迟为什么下降 20% 到 35%
  15. Spark 批量索引任务为什么也能缩短 20% 到 35%
  16. 向量搜索为什么特别适合 gRPC + Protobuf
  17. Uber Eats 推荐场景中向量搜索延迟如何下降
  18. 高维向量为什么在 JSON 下特别浪费空间
  19. 文档二进制格式:CBOR、SMILE 与 gRPC 的组合
  20. gRPC SMILE 为什么比 REST JSON、gRPC JSON、REST SMILE 都更快
  21. 这次改造的核心结论:API 表示形式不是表层选择,而是系统演进能力的一部分
  22. 对后端工程师的启发:不要把协议层当成无关紧要的外壳

一、搜索在 Uber 不是一个边缘系统

很多人一听到 OpenSearch,会先想到日志检索、站内搜索、全文搜索。

但在 Uber 这种公司里,“搜索”远不只是用户在搜索框里输入关键词。

原文开头就强调,搜索支撑了 Uber 内部大量实时体验,包括:

  • 乘客和司机匹配
  • 欺诈检测
  • Uber Eats 推荐
  • 检索和排序
  • 近实时索引
  • 向量搜索
  • 大规模搜索平台能力

也就是说,搜索系统已经成为 Uber 实时决策系统的一部分。

如果搜索链路延迟变高,影响的可能不是一个后台管理页面,而是用户匹配、推荐结果、风控判断、商家/商品召回、指标索引延迟这些核心体验。

在这种背景下,搜索平台的协议层就不再是一个“随便用 REST 就行”的问题。

当系统规模、请求体大小、吞吐量和延迟敏感度都上来之后,REST/JSON 的成本会变得越来越明显。


二、REST/JSON 一开始很方便,后来变成性能和演进瓶颈

OpenSearch 历史上主要暴露 REST/JSON API。

这对生态兼容很友好。

REST 好理解,JSON 可读性强,调试方便,curl 一下就能验证,客户端也容易写。对通用搜索系统来说,这些优势非常重要。

但 Uber 内部大量基础设施已经使用 gRPC 和 Protobuf 通信。

这套系统依赖的是:

  • 强类型接口契约
  • Protobuf 的二进制序列化
  • HTTP/2 之上的传输能力
  • 更适合 streaming 的通信模型
  • 更低的编码/解码开销
  • 更稳定的跨语言接口演进方式

于是问题出现了。

Uber 内部调用链上,客户端可能已经在用 Protobuf 构造请求,但 OpenSearch 只接受 JSON。
这就必须在中间做转换:

Protobuf request ↓ 转换成 JSON ↓ 发送到 OpenSearch REST endpoint ↓ OpenSearch 返回 JSON ↓ 转换回 Protobuf response

这个适配层会带来三个问题。

第一,增加延迟。
每一次请求都要做 Protobuf 到 JSON,再从 JSON 到 Protobuf 的转换。

第二,增加 CPU 和内存开销。
尤其是大 payload、向量、批量写入场景,JSON 序列化和反序列化并不便宜。

第三,增加系统复杂度和运维风险。
适配层本身也要维护、升级、监控、处理兼容性问题。一旦 OpenSearch REST API 变化,适配层也可能需要调整。

所以原文的核心判断是:

REST/JSON 不只是“慢一点”,它开始限制 Uber 搜索平台未来如何演进。


三、为什么不继续维护适配层?

面对 REST/JSON 和 gRPC/Protobuf 的不匹配,最自然的办法是继续做 gateway 或 adaptor。

也就是外部仍然暴露 gRPC,内部转换成 OpenSearch REST。

这条路短期最简单,因为不用改 OpenSearch。

但 Uber 最终没有把它当成长期方案。

原因很现实:

适配层一旦成为永久架构,性能开销和复杂度也会永久存在。

如果只是少数低频请求,适配层没什么问题。
但如果是高吞吐、低延迟、海量搜索和写入请求,转换层就会成为每个请求都要经过的税。

而且 Uber 不希望长期维护自己的 OpenSearch fork,也不想在 OpenSearch 外面堆一个越来越复杂、越来越难删的翻译系统。

因此他们选择了更根本的方向:

在 OpenSearch 内部实现原生 gRPC endpoints,让 gRPC 成为和 REST 并存的一等 transport。

这也是原文标题里的重点:不是“用 gRPC 包一层 OpenSearch”,而是让 OpenSearch 自己支持高性能 gRPC。


四、整体架构:REST 和 gRPC 并存

Uber 的设计不是用 gRPC 取代 REST,而是让二者并存。

这点非常关键。

OpenSearch 的 REST API 有大量现有用户、工具和生态。
如果直接替换成 gRPC,会破坏兼容性,也会让迁移风险非常大。

所以他们选择:

REST transport 继续存在 gRPC transport 作为 module 引入 二者运行在不同端口 内部 OpenSearch node-to-node 逻辑尽量共享

也就是说,差异主要发生在 client-server transport 层。

从外部看:

REST/JSON client -> REST transport -> OpenSearch internal logic gRPC/Proto client -> gRPC transport -> OpenSearch internal logic

内部执行逻辑尽量不复制。

这是一种很稳妥的架构。

它保留了 REST 的兼容性和运维便利,同时给性能敏感、高吞吐、大 payload 的业务提供 gRPC 路径。

迁移也可以逐步完成。
不是所有服务一夜之间切到 gRPC,而是先让最适合的 Search 和 Bulk 工作负载使用 gRPC,其他场景仍然保留 REST。


五、gRPC Server:作为 OpenSearch module 提供

原生 gRPC transport 的核心组件是 OpenSearch 内部的 gRPC server implementation。

原文提到,为了提供灵活性,gRPC transport 以 module 的形式提供。

这意味着:

  • 可以和 REST transport 同时启用
  • 可以运行在不同端口
  • 可以让不同客户端选择不同协议
  • 可以避免把 gRPC 逻辑硬编码成唯一入口
  • 可以让未来更多插件扩展 gRPC 能力

在图 1 中,REST 和 gRPC 在 client-server 层不同,但进入 OpenSearch 内部后,很多核心逻辑是共享的。

这一点对长期维护非常重要。

如果 REST 和 gRPC 后面各走一套完全不同的内部逻辑,就会出现两个问题:

第一,行为可能不一致。
同一个 search request,经 REST 和 gRPC 可能出现不同结果或不同错误行为。

第二,维护成本翻倍。
每次 OpenSearch 功能演进,都要分别维护 REST 路径和 gRPC 路径。

所以正确做法是:

transport 层可以不同,但 query execution、core logic、handler 这些内部逻辑要尽量复用。


六、插件扩展:为什么需要 SPI?

OpenSearch 的生态里有大量插件。

比如 k-NN plugin 就是向量搜索的重要组成部分。

如果 gRPC transport 只能支持 OpenSearch core 里的少数 API,而插件没法扩展,那它很快会变成一个封闭能力,无法覆盖真实生产场景。

所以 Uber 的设计里,gRPC transport 发布了一个 SPI,也就是 Service Provider Interface。

这个 SPI 允许外部插件扩展 gRPC transport。

原文提到,像 k-NN plugin 这样的插件,可以扩展这个接口,提供自己的 Proto-to-POJO conversion utility methods。

这里的 POJO 可以理解成 OpenSearch Java 内部使用的普通对象模型。
也就是说,插件可以告诉 gRPC transport:

这个 Protobuf request 应该如何转换成我插件内部需要的 Java 对象

此外,插件还可以实现并暴露自己的 gRPC services,并接入主 transport-gRPC module。

这解决了一个长期演进问题:

gRPC 不能只服务 core API,它必须能随 OpenSearch 插件生态一起扩展。

否则很多高级搜索能力,比如向量检索、专用查询类型、插件自定义 API,都无法自然迁移到 gRPC。


七、优先覆盖哪些 API?Search 和 Bulk

OpenSearch API 很多,但 Uber 没有一开始就全量支持所有 gRPC API。

他们优先做了两个最重要的方向:

Search Bulk

原因也很直接。

对 Uber 和许多 OpenSearch 用户来说,最延迟敏感、最吞吐敏感的 API,往往就是搜索和写入。

Search 关系到用户请求的在线延迟。
Bulk 关系到索引写入吞吐、数据新鲜度和后台作业效率。

这两个 API 最能体现 gRPC 的优势:

  • 大 payload
  • 高频请求
  • 强类型 schema
  • 二进制编码
  • 更低序列化成本
  • 更少适配层转换
  • 更适合后续 streaming 扩展

所以 Uber 没有先追求“覆盖面最大”,而是先追求“收益最大”。

这是一个很典型的工程优先级选择:

不要平均用力改造所有接口,先改最热、最痛、最能证明价值的路径。


八、Protobuf 数据模型:gRPC transport 的根基

gRPC 不只是换了一个 HTTP 协议入口。

它的核心在于数据模型。

REST/JSON API 很多时候依赖 OpenAPI specification、JSON schema、path、query parameters、status code 等约定。

而 Protobuf 要求更明确的消息结构。

比如 REST 里可能是:

POST /index/_search?q=xxx body: JSON

到了 gRPC/Protobuf 里,就要变成更明确的 RPC 方法和 message:

service SearchService { rpc Search(SearchRequest) returns (SearchResponse); } message SearchRequest { string index = 1; Query query = 2; ... }

这只是示意,不是原文的完整 schema。

重点是:
REST API 里很多隐式语义,需要在 Protobuf 里变成显式字段和类型。

如果人工维护这一切,非常容易出错。

OpenSearch REST API 在演进,Protobuf API 也要跟着演进。
如果两边手写,很快就会 drift,也就是漂移。

一边加了字段,另一边忘了;
一边修改了语义,另一边没同步;
一边兼容旧客户端,另一边破坏了 wire compatibility。

所以 Uber 需要一条自动化流水线。


九、从 JSON/OpenAPI spec 自动生成 Protobuf

原文的图 2 展示了一个简化示例:如何把 Search 的 JSON specification 转成 Search RPC endpoint 和 Protobuf schema。

但这件事比“机械转换字段”复杂得多。

REST 和 Protobuf 的设计哲学并不一样。

REST 里,很多信息来自:

  • HTTP method
  • URL path
  • query parameter
  • request body
  • response body
  • status code
  • header
  • endpoint convention

Protobuf 里,很多信息必须变成:

  • service
  • rpc method
  • request message
  • response message
  • field type
  • field number
  • repeated field
  • enum
  • nested message
  • backward-compatible schema evolution

这意味着转换过程不仅是语法转换,而是语义转换。

Uber 建立了一条端到端自动化 pipeline,分成三个阶段:

preprocessing core conversion postprocessing

十、第一阶段:Preprocessing,先处理 REST 和 Protobuf 的语义差异

预处理阶段要解决的问题是:

REST specification 里的很多隐式语义,不能直接丢给 Protobuf 生成器。

比如 REST API 常见这些模式:

GET /index/_search POST /index/_search query parameter path parameter status-code-driven behavior JSON request body JSON response body

这些东西在 REST 世界里很自然。
但在 Protobuf 世界里,你需要显式定义 request 和 response message。

所以 preprocessing 阶段会做几类处理:

  • 过滤不需要或暂时不支持的 API
  • sanitize specification
  • 修改 schema
  • 处理 vendor extensions
  • 统一命名规则
  • 把 REST-specific 行为转换成更明确的 Protobuf-friendly 结构
  • 让隐式 request/response 语义显式化

可以把这一步理解为:

把 REST 世界里“大家心照不宣”的东西,整理成 Protobuf 能安全表达的东西。

如果没有这一步,自动生成出来的 Protobuf 要么很丑,要么不稳定,要么很难长期兼容。


十一、第二阶段:Core Conversion,真正生成 Protobuf

预处理之后,就进入核心转换阶段。

原文提到,OpenAPI Generator 对很多 JSON-based clients 和 servers 有成熟支持,但没有等价的、健壮的 JSON-to-Protobuf conversion 能力。

所以 Uber 做了一组转换规则,并贡献给 OpenAPI Generator。

这点很重要。

它说明这不是一个 Uber 内部临时脚本,而是试图把这套能力推回更广泛的工具生态。

转换阶段要把 OpenAPI specification 变成 Protobuf artifacts。

包括:

  • .protoschema
  • service definitions
  • request/response messages
  • 字段类型映射
  • repeated / map / enum 处理
  • nullable / optional / required 的表达
  • JSON schema 到 Protobuf schema 的结构映射

这一步决定了 gRPC API 和 REST API 的结构是否能长期保持一致。

如果生成规则不稳定,后续 OpenSearch API 每演进一次,都可能引入大量不必要 diff。

所以自动生成不仅要“能生成”,还要“可重复、可维护、可演进”。


十二、第三阶段:Postprocessing,守住 Protobuf wire compatibility

Protobuf 和 JSON 最大的不同之一,是字段编号。

在 Protobuf 中,字段号是 wire format 的一部分。
一旦某个字段号被发布出去,后续就不能随便改。

比如:

message SearchRequest { string index = 1; Query query = 2; }

如果以后把index的字段号从 1 改成 3,就可能破坏旧客户端。
客户端发送的二进制数据仍然带着 field number 1,服务端如果不再按原语义解析,就会出问题。

JSON 没有这种字段编号问题。
JSON 里字段名才是主要标识。

所以 Protobuf API 演进要更谨慎。

Postprocessing 阶段负责做长期 wire compatibility 检查:

  • 检查字段号有没有被重新编号
  • 检查旧字段有没有被错误复用
  • 检查新生成 schema 是否破坏旧客户端
  • 对比之前生成的 Protobuf
  • 应用兼容性保护规则

这一步非常关键。

因为 Uber 的目标不是做一个“一次性 gRPC demo”,而是让 gRPC 成为 OpenSearch 的长期一等 API surface。

长期 API 最怕的不是第一次生成麻烦,而是后续演进过程中慢慢破坏兼容性。


十三、为什么这条生成流水线很重要?

这条 pipeline 解决的是一个更深的问题:

如何让 REST 和 gRPC API 长期并存,而不让它们变成两套互相漂移的接口?

如果手工维护,会很快失控。

因为 OpenSearch API 会变化。
插件 API 会变化。
查询类型会增加。
字段会增加。
旧字段会 deprecated。
客户端会升级不一致。

如果 REST 是一套规范,gRPC 是另一套手写规范,它们迟早会不一致。

自动化转换和兼容性检查让 Uber 能做到:

REST API specification 继续作为基础 ↓ 规范化和预处理 ↓ 生成 Protobuf ↓ 兼容性检查 ↓ 生成代码 ↓ 通过 PR 合并到 protos/generated

这让 gRPC API 能跟随 OpenSearch REST API 演进,而不是变成一个很快过时的旁路接口。


十四、Uber 集成案例:Search Gateway

原文给出的 Uber 内部集成例子是 OpenSearch Gateway。

这个 Gateway 的职责是代理 Uber 内部所有 OpenSearch customer 的 search 和 ingest 请求,然后再转发到 OpenSearch cluster。

它提供了一些平台能力:

  • 安全控制
  • 可观测性
  • rate limiting
  • auditing
  • 统一入口
  • 集群访问控制
  • 请求代理

在原生 gRPC 之前,Search Gateway 面向内部客户可能已经使用 gRPC/Protobuf,但底层 OpenSearch 只支持 REST/JSON。

所以 Gateway 内部必须维护一个 adaptor。

这个 adaptor 做两件事:

ProtoToJsonMapper JsonToProtoMapper

请求路径大概是:

内部客户端 ↓ gRPC / Protobuf Search Gateway ↓ Proto 转 JSON Adaptor ↓ REST / JSON OpenSearch ↓ JSON response Adaptor ↓ JSON 转 Proto Search Gateway ↓ Protobuf response 内部客户端

这层 adaptor 是典型技术债。

它存在的原因不是业务需要,而是因为两边协议不一致。

引入 OpenSearch 原生 gRPC endpoint 之后,Search Gateway 可以把客户的 Protobuf request 直接 pass-through 到 OpenSearch gRPC endpoint。

新的路径变成:

内部客户端 ↓ gRPC / Protobuf Search Gateway ↓ pass-through OpenSearch gRPC endpoint

这样就去掉了中间的 Proto/JSON 双向转换。

收益包括:

  • 延迟下降
  • CPU 开销下降
  • Gateway 逻辑变简单
  • 减少自研 adaptor 维护成本
  • 减少转换错误和兼容性风险
  • 让内部 gRPC 生态和 OpenSearch 更自然对齐

这也是整篇文章最核心的架构收益之一。


十五、性能收益之一:Bulk 写入

引入 gRPC 的重要驱动力之一是性能。

原文首先讲 Bulk,也就是批量写入。

Bulk 写入在搜索系统里非常关键。
无论是日志、指标、文档、离线任务输出,还是业务数据索引,批量写入都是 OpenSearch 的核心路径。

Uber 的 M3 是内部指标系统。
M3 通过 OpenSearch 相关能力做索引写入。

迁移到 gRPC 后,M3 在生产中看到:

p99 write latency 从 34.1ms 降到 13.6ms 大约下降 60%

p50 也有明显改善:

p50 从 15.8ms 降到 10.5ms 大约下降 34%

这说明收益不是只体现在尾部延迟。
中位数和 p99 都有下降,只是 p99 下降更明显。

为什么 Bulk 写入适合 gRPC?

因为 Bulk 请求往往 payload 大,字段多,写入频率高。
JSON 解析、字符串编码、字段名重复、数字文本化都会造成开销。
Protobuf 的二进制格式可以减少这些成本。

同时,去掉 Gateway 中的 Proto/JSON 转换层,也会直接减少每次写入的 CPU 和延迟。


十六、M3 Indexer:最大索引延迟下降 20% 到 35%

M3 Indexer 负责把数百万数据点每秒写入 M3。

这里一个关键指标是 maximum indexing delay。

可以理解为:

一个指标数据点从产生到被索引进 M3,最长要等多久。

这个指标非常重要,尤其在 failover 时。

如果索引延迟太高,用户查询到的数据就不够新,告警、监控和分析都会受影响。

原文提到,在 M3 short index 上,使用 gRPC 后,maximum indexing delay 降低了 20% 到 35%。

在更高 RPS 下,gRPC 的优势更明显。

图 7 中,REST 和 gRPC 在较低 RPS 时差距没那么大,但当 RPS 上升到 600、800 时,REST 的 delay 增长更快,而 gRPC 保持得更好。

这说明 gRPC 的价值不只是单请求更快,还包括:

在更高吞吐压力下,系统退化更慢。

这对平台系统非常重要。

低负载下谁都能跑得不错。
真正关键的是高峰流量、failover、回放、批量补数据这些压力场景。


十七、Spark 批量索引任务:作业时间下降 20% 到 35%

除了在线系统,Uber 还有大量 Apache Spark 批处理任务,会把数据批量索引到 OpenSearch cluster。

这些任务通常不只是单次请求延迟问题,而是整个 job runtime 问题。

如果底层 Bulk API 更快,Spark 作业整体完成时间也会缩短。

原文提到,将这些 batch ingestion jobs 底层迁移到 gRPC Bulk API 后,作业运行时间减少了 20% 到 35%。

这很有意义。

因为批处理作业时间缩短,会带来多方面收益:

  • 数据更快可查
  • 计算资源占用时间更短
  • 作业排队压力降低
  • 集群吞吐提升
  • failover 或补数恢复更快
  • 成本下降

这也说明协议层优化不仅影响在线 latency,也影响离线数据管道吞吐。


十八、性能收益之二:Search,尤其是向量搜索

除了 Bulk,另一个收益明显的领域是 Search。

原文特别强调:最大收益出现在 vector search queries。

原因很容易理解。

向量搜索请求通常包含一个大向量,比如:

[0.123, -0.456, 0.789, ...]

如果用 JSON 表示,每个 float 都要变成文本:

[0.123,-0.456,0.789]

这里会有很多额外开销:

  • 数字转字符串
  • 字符串解析回 float
  • 逗号、括号、空格等结构符号
  • 小数文本表示比二进制 float 更长
  • JSON parser 需要处理通用语法
  • 大向量请求体非常膨胀

而 Protobuf 对 repeated float 可以使用 packed encoding。

也就是说,多个 float 可以紧凑地按二进制格式连续编码。

这对向量搜索非常适合。

所以向量维度越高,gRPC/Protobuf 相比 REST/JSON 的收益通常越大。


十九、Uber Eats 推荐场景:搜索延迟明显下降

原文给出的具体案例是 Delivery shopping lists。

这个能力用于 Uber Eats 上 grocery stores 的推荐。

迁移到 gRPC 后,搜索延迟变化如下:

p50: 83ms -> 38ms,下降约 53% p95: 114ms -> 64ms,下降约 43% p99: 205ms -> 176ms,下降约 14%

这里有一个值得注意的现象:

p50 和 p95 降得非常明显,但 p99 降幅较小。

原文解释,p99 受到 long-tail large queries 影响。

这说明 gRPC 解决了很多常规请求的序列化和传输开销,但最尾部的大查询可能还有其他瓶颈,比如:

  • 查询本身计算复杂
  • 向量维度更高
  • 返回结果更多
  • 后端执行耗时
  • shard 分布不均
  • cache miss
  • cluster 负载波动
  • 下游资源竞争

这也是性能优化里常见的情况:

协议优化能显著降低整体延迟,但不能自动消灭所有尾延迟。


二十、高维向量下,请求体大小差距非常大

原文图 9 对比了不同向量维度下 REST/JSON 和 gRPC/Protobuf 的 request size。

三个维度分别是:

1572 512 256

对应数据大致是:

向量维度REST/JSON 请求大小gRPC/Protobuf 请求大小节省比例
157240523 bytes4590 bytes88.7%
51214531 bytes2500 bytes82.8%
2567954 bytes1500 bytes81.1%

这个表非常能说明问题。

1572 维向量里,REST/JSON 请求体约 40KB,而 gRPC/Protobuf 只有约 4.6KB。
大小差距接近一个数量级。

为什么差这么多?

因为 JSON 对浮点数组非常不友好。

假设一个 float32 在二进制里只需要 4 字节。
但在 JSON 里,一个浮点数可能写成:

0.12345678

这就已经 10 个字符左右。
再加逗号、括号、字段名、引号等,整体很容易膨胀。

而 Protobuf packed repeated float 接近原始二进制表示。

所以向量越大,JSON 的文本膨胀越严重,gRPC 的优势越明显。


二十一、文档格式:不只是 JSON vs Protobuf,还有 CBOR / SMILE

OpenSearch REST 用户主要使用 JSON 表示文档。

但在 gRPC 场景下,使用二进制文档格式会更自然,比如:

  • CBOR
  • SMILE

SMILE 可以理解成一种 JSON 的二进制表示格式。
它保留了类似 JSON 的数据模型,但用二进制编码减少体积和解析成本。

原文特别比较了几种组合:

REST JSON gRPC JSON REST SMILE gRPC SMILE

结果显示,gRPC SMILE search 最快。

具体来说,gRPC SMILE search:

比 REST JSON 快 30% 比 gRPC JSON 快 45% 比 REST SMILE 快 47%

这个结果很有意思,因为它说明性能不只取决于 transport,也取决于 payload representation。

也就是说:

REST vs gRPC JSON vs SMILE

这两个维度都重要。

如果你只把 JSON 放进 gRPC 里,仍然要承担 JSON 编解码成本。
如果你只用 REST 传 SMILE,也仍然保留 REST 路径上的一些限制。

最好的组合是:

高效 transport + 高效 payload format

也就是 gRPC + SMILE。


二十二、性能总结:gRPC 适合哪些场景?

原文对性能收益做了总结。

gRPC 更适合这些工作负载:

大请求体 大 RPS 下的高吞吐 二进制文档格式

展开说就是:

第一,大 request size

请求越大,JSON 文本格式的劣势越明显。

例如向量搜索、Bulk 写入、大文档查询、大批量索引,都可能受益。

第二,高吞吐和高 RPS

低 RPS 下,协议差异可能被其他因素淹没。
但高 RPS 下,序列化、反序列化、适配层转换、网络传输大小都会变成累计成本。

每个请求省一点,乘以海量请求,就变成明显资源收益。

第三,二进制文档格式

如果 payload 本身可以用 CBOR、SMILE 或其他二进制格式表达,再配合 gRPC transport,效果会更好。

第四,强类型内部系统

如果你的公司内部已经广泛使用 Protobuf,OpenSearch 这类系统继续只暴露 JSON,会导致大量转换层。

原生 gRPC 可以减少架构摩擦。


二十三、这次改造最大的意义:不是“gRPC 更快”这么简单

很多人读这类文章,容易得出一个简单结论:

gRPC 比 REST 快。

这个说法太粗糙。

原文真正想强调的是:

API representation 不是表面选择。到了足够大的规模,它会影响系统演进、性能上限和开发效率。

REST/JSON 当然有它的价值。
兼容、可读、调试方便、生态成熟,这些都是优势。

但在 Uber 的搜索平台里,问题是:

  • 内部生态已经是 Protobuf/gRPC
  • OpenSearch 只有 REST/JSON
  • Search Gateway 需要维护转换层
  • 向量搜索请求越来越大
  • Bulk 写入吞吐越来越高
  • ML-driven queries 越来越多
  • streaming 和强类型接口越来越重要

在这个上下文里,REST/JSON 不再只是一个“入口格式”,而开始塑造系统上限。

它影响:

  • 请求体大小
  • 编解码成本
  • Gateway 架构
  • 插件扩展方式
  • API 兼容性模型
  • 客户端开发体验
  • 未来 streaming 能力
  • 搜索平台和内部基础设施的对齐程度

所以这次改造本质上是:

让 OpenSearch 和 Uber 内部服务通信方式对齐。


二十四、为什么保留 REST 仍然重要?

虽然 gRPC 带来明显收益,但 Uber 没有删除 REST。

这是非常正确的工程取舍。

REST 仍然有不可替代的价值:

  • 生态兼容
  • 运维调试方便
  • curl 可测试
  • 现有客户端无需迁移
  • 小流量、低性能敏感场景足够好
  • 外部用户熟悉
  • API 文档和工具链成熟

如果强行只保留 gRPC,就会制造巨大迁移成本。

所以更合理的架构是:

REST 作为兼容和通用入口继续存在 gRPC 作为高性能、一等 transport 提供给性能敏感场景

这也是很多大型系统演进的正确方式。

不是“新技术替换旧技术”,而是“新路径和旧路径并存,让业务按收益逐步迁移”。


二十五、这件事对 OpenSearch 社区有什么意义?

原文强调,这项工作不仅服务 Uber 内部,也回馈给 OpenSearch 开源生态。

因为 Uber 的目标不是长期维护自己的 fork,而是把原生 gRPC transport 推进 OpenSearch。

这对社区有几个意义。

第一,OpenSearch 获得了新的 first-class transport。
用户不再只能通过 REST/JSON 使用性能敏感 API。

第二,插件可以通过 SPI 扩展 gRPC 服务。
这让向量搜索、k-NN 等插件生态可以自然接入。

第三,OpenAPI 到 Protobuf 的转换规则和工具可以帮助长期维护 API parity。
这对 OpenSearch 这种 API surface 很大的项目非常重要。

第四,更多使用 Protobuf/gRPC 的公司可以减少集成适配层。
尤其是那些内部服务已经标准化 gRPC 的企业。

第五,为未来 streaming、高吞吐、大 payload、ML 查询场景打基础。

换句话说,这不是 Uber 内部做了一个“快一点的客户端”,而是在推动 OpenSearch 的 API 形态演进。


二十六、下一步:扩大覆盖、加强 streaming、安全和可观测性

原文最后提到几个下一步方向:

expand gRPC API coverage deepen streaming support continue improving security and observability

逐个理解。

1. 扩大 gRPC API 覆盖

当前优先覆盖的是 Search 和 Bulk。
未来可以覆盖更多 OpenSearch API。

但覆盖面扩大要非常谨慎,因为每个 API 都涉及 schema、兼容性、插件扩展、错误语义和客户端生成。

2. 加强 streaming support

gRPC 的一个优势是更适合 streaming。

对于大规模搜索、批量写入、长连接数据传输、持续导入导出,streaming 可能会带来更明显的架构收益。

3. 改进 security

一旦 gRPC 成为一等 transport,安全能力也要对齐 REST。

包括认证、授权、审计、加密、访问控制、插件安全边界等。

4. 改进 observability

生产系统里,新的 transport 不能只是能跑。

还要有:

  • metrics
  • tracing
  • logging
  • error classification
  • latency breakdown
  • request size distribution
  • method-level visibility
  • retry / timeout / backpressure 观测

否则迁移到 gRPC 之后,排障能力反而下降。

Uber 原本的 Gateway 就承担了 observability、rate limiting、auditing 等责任,所以 gRPC transport 也必须纳入这些平台能力。


二十七、原文致谢与背景

原文由 Uber 工程团队多位成员撰写,包括 Karen Xu、Xi Lu、Sam Akrah、Shuyi Zhang、Michael Froh 等。

文章最后感谢了 Uber 领导层的指导,也感谢 AWS 工程师参与构建 transport、query converters 和 Protobuf tooling。同时还感谢 Uber 内部 SIA、LucenePlus 和客户团队参与迁移与测试。

这也说明这不是单个工程师的局部优化,而是一个跨团队合作项目。

它涉及:

  • Uber Search Platform
  • OpenSearch
  • AWS 工程师
  • Protobuf tooling
  • transport implementation
  • query converters
  • 客户团队迁移
  • 性能测试
  • 生产验证

这种项目的难度并不只在“写 gRPC server”,而在于让它进入一个真实的大规模搜索平台,并且能长期维护、兼容、观测和推广。


二十八、从后端工程师角度看:这篇文章真正讲了什么?

如果只看标题,这篇文章像是在讲:

gRPC 比 REST 快。

但真正值得学习的是更深的几个点。

第一,协议不是外壳,而是架构的一部分

很多系统早期会认为协议层只是入口:

反正进来之后都转成内部对象,REST 也行,gRPC 也行。

小规模时确实如此。

但当请求量、payload 大小、延迟敏感度、内部生态标准化程度上来后,协议层会影响整个系统。

Uber 的 Search Gateway 例子就是这样。
因为 OpenSearch 只有 REST,Gateway 必须维护 adaptor。
这不是业务逻辑,而是协议不匹配导致的基础设施复杂度。

第二,性能优化要找最热路径

Uber 没有全量改造所有 OpenSearch API,而是先做 Search 和 Bulk。

因为它们最热、最敏感、最能体现收益。

这比“为了架构完整性一次做完所有 API”更实际。

第三,兼容性比性能更难

新增 gRPC endpoint 不难。
难的是让 REST 和 gRPC 长期保持一致,让 Protobuf wire compatibility 不被破坏,让插件也能扩展,让客户可以逐步迁移。

这就是为什么文章花了很多篇幅讲 JSON-to-Protobuf conversion pipeline。

第四,性能数据要按 workload 拆开看

Bulk 有 Bulk 的收益。
Vector search 有 vector search 的收益。
SMILE binary document 有 SMILE 的收益。
Spark batch ingestion 有作业时间收益。
M3 indexer 有最大索引延迟收益。

不能只给一个“整体提升百分比”。

真正的大规模优化,都要按 workload 拆开验证。

第五,开源项目演进要避免私有 fork

Uber 没有满足于内部 patch,而是将能力贡献回 OpenSearch 生态。

这对长期维护非常重要。

私有 fork 短期快,长期贵。
原生支持短期难,长期收益更大。


二十九、对普通团队有什么启发?

你可能会问:

我们公司没有 Uber 这么大,也没有 OpenSearch maintainer,要不要学这个?

可以学,但不要机械照搬。

1. 先看瓶颈是不是协议层

如果你的系统慢在数据库查询、业务计算、锁竞争、GC、缓存 miss,那么换 gRPC 不会神奇解决。

先 profile,再决定。

2. 大 payload 更值得关注

如果你的请求体很小,比如几个字段的 CRUD,REST/JSON 足够好。

但如果你传的是:

  • 大数组
  • 高维向量
  • 大批量写入
  • 大文档
  • 二进制内容
  • 高频实时请求

那 Protobuf/gRPC 的收益可能更明显。

3. 内部系统统一契约很重要

如果公司内部已经标准化 Protobuf/gRPC,但某个核心系统只支持 REST/JSON,那么中间适配层迟早会变成技术债。

这时可以考虑让核心系统支持内部标准协议。

4. 不要只关注“传输协议”

gRPC + JSON 不一定最优。
REST + SMILE 也不一定最优。
真正要看组合:

transport serialization format data model schema evolution client generation observability security migration path

5. 迁移要保留兼容路径

不要一上来砍掉 REST。
更稳妥的是让新旧协议并存,先迁移高收益 workload。

6. 自动化 schema 生成和兼容性检查很重要

如果你同时维护 REST 和 gRPC API,手工同步迟早出错。
要尽早建立自动化转换和 compatibility check。


三十、我的理解:这是一篇关于“表示形式”的系统工程文章

我觉得这篇文章最核心的一句话是:

API representation is not a surface-level choice.

翻译一下就是:

API 的表示形式不是表层选择。

这句话很值得后端工程师记住。

很多时候,我们把 JSON、Protobuf、REST、gRPC 看成“接口格式偏好”。

喜欢可读性就 JSON。
喜欢性能就 Protobuf。
喜欢通用就 REST。
喜欢强类型就 gRPC。

但到了 Uber 这种规模,API 表示形式会直接影响:

  • 系统延迟
  • 请求体大小
  • CPU 消耗
  • Gateway 复杂度
  • 适配层技术债
  • 客户端生成
  • API 演进
  • 插件扩展
  • 流式能力
  • 观测和安全接入
  • 平台未来上限

所以这不是“换个协议”的小改动,而是搜索平台的一次接口层重构。

尤其是向量搜索和 ML-driven queries 越来越多之后,JSON 的低效会被进一步放大。

过去很多搜索请求可能只是关键词、过滤条件、排序字段。
现在搜索请求可能包含几百维、上千维向量。
这时再用 JSON 文本表达 float 数组,成本就非常明显。

所以 Uber 的这次改造也代表一个趋势:

随着搜索系统从文本搜索走向向量搜索和机器学习驱动的检索,底层 API 也需要从“人类可读优先”逐步支持“机器高效传输优先”。

REST 仍然重要。
但高性能路径需要更合适的表示形式。


三十一、总结

Uber 这篇文章讲的是他们如何在 OpenSearch 中引入原生 gRPC endpoints,从而加速搜索和写入工作负载。

问题背景是:Uber 内部大量基础设施使用 gRPC 和 Protobuf,而 OpenSearch 历史上主要暴露 REST/JSON API。为了连接这两个世界,Uber 过去需要在 Search Gateway 中维护 Proto ↔ JSON 的适配层。这层转换增加了延迟、CPU 开销和系统复杂度。

Uber 最终选择在 OpenSearch 内部实现原生 gRPC transport,并让它和 REST transport 并存。gRPC transport 作为 module 运行在不同端口,REST 和 gRPC 在 client-server 层不同,但尽量复用 OpenSearch 内部执行逻辑。为了支持插件生态,他们还提供 SPI,让像 k-NN 这样的插件可以扩展 Proto-to-POJO 转换和自定义 gRPC services。

API 覆盖上,他们优先做 Search 和 Bulk,因为这是最延迟敏感、吞吐敏感、收益最明显的路径。为了让 REST API 和 gRPC API 长期保持一致,Uber 建立了 JSON/OpenAPI specification 到 Protobuf 的自动转换流水线,包括 preprocessing、core conversion、postprocessing 三个阶段,并通过兼容性检查保护 Protobuf wire compatibility。

生产效果非常明显:

M3 Bulk 写入 p99 延迟从 34.1ms 降到 13.6ms,下降约 60% M3 Bulk 写入 p50 从 15.8ms 降到 10.5ms,下降约 34% M3 Indexer 最大索引延迟降低 20% 到 35% Spark 批量索引任务运行时间降低 20% 到 35% Uber Eats 推荐场景的向量搜索 p50 从 83ms 降到 38ms,下降约 53% p95 从 114ms 降到 64ms,下降约 43% p99 从 205ms 降到 176ms,下降约 14%

向量搜索收益尤其明显,因为高维 float 向量在 JSON 中非常低效,而 Protobuf packed repeated float 可以大幅减少请求体大小。1572 维向量场景中,REST/JSON 请求约 40523 bytes,而 gRPC/Protobuf 约 4590 bytes,节省约 88.7%。

此外,gRPC 和二进制文档格式结合也有显著收益。gRPC SMILE search 比 REST JSON 快 30%,比 gRPC JSON 快 45%,比 REST SMILE 快 47%。

这篇文章真正的启发是:

在大规模系统里,API 表示形式不是无关紧要的外壳。它会影响性能上限、系统复杂度、开发效率和长期演进能力。

REST/JSON 仍然适合兼容、调试和通用访问。
但在高吞吐、大 payload、向量搜索、批量写入、内部强类型服务生态中,gRPC + Protobuf 可以显著降低系统成本。

Uber 的做法不是简单“用 gRPC 替换 REST”,而是让 REST 和 gRPC 并存,让高收益 workload 逐步迁移,同时把能力贡献回 OpenSearch 社区。

这才是这篇文章最值得学习的地方:
协议层优化不是单点性能技巧,而是平台架构演进的一部分。


参考资料

  • Accelerating Search and Ingestion with High-Performance gRPC™ in OpenSearch™