Envoy gRPC HTTP/1.1 Reverse Bridge 过滤器大响应缓冲 SEGFAULT 修复深度解析
【免费下载链接】envoyCloud-native high-performance edge/middle/service proxy项目地址: https://gitcode.com/GitHub_Trending/en/envoy
导读
本文围绕 Envoy 当前仓库中grpc_http1_reverse_bridge过滤器的一则崩溃修复(changelog 条目 grpc_http1_reverse_bridge__fixed-segfault-when-buffering-large-response.rst)展开:该过滤器在withhold_grpc_frames开启且未配置response_size_header时,如果上游响应体超过下游 HTTP/2 流控窗口,会因一次性缓冲并释放全部数据而触发 SEGFAULT。修复后的过滤器改为优先利用上游Content-Length头增量流式传输响应。读完本文,你将掌握该过滤器的完整工作原理、崩溃产生的根因、基于Content-Length的新流式路径在 filter.cc 中的实现细节,以及如何通过配置规避类似问题。
过滤器背景:把 gRPC 请求降级为 HTTP/1.1
grpc_http1_reverse_bridge是 Envoy 的一个 HTTP 流过滤器(stream filter),其核心能力正如 filter.h 中的注释所述:将一个入站的 gRPC HTTP 请求降级为 h/1.1 请求,从而让一个不理解 HTTP/2、HTTP/3 或 gRPC 语义的上游服务也能处理 gRPC 流量。官方文档 grpc_http1_reverse_bridge_filter.rst 将其工作流程总结为五步:
- 检查入站请求的 content-type,若为 gRPC 请求则启用过滤器;
- 将 content-type 改写为可配置的值(配置为
application/grpc时可视为 noop); - 可选地从请求体中剥离 gRPC frame header,并相应调整 Content-Length;
- 收到响应后,校验响应 content-type,并把 HTTP 状态码映射为 grpc-status 写入响应 trailers;
- 可选地在响应体前补上 gRPC frame header,同样需要调整 Content-Length。
由于最终映射到 HTTP/1.1,该过滤器只支持 unary(一元)gRPC 调用。
过滤器的配置定义在 v3/config.proto,包含三个核心字段:
| 字段 | 类型 | 说明 |
|---|---|---|
content_type | string(必填,min_len: 1) | 转发给上游的 content-type,同时用于校验上游响应是否带相同 content-type |
withhold_grpc_frames | bool | 为 true 时,过滤器假定上游不理解 gRPC 帧,负责剥掉请求中的 gRPC 帧头、在响应中加回帧头,向上游隐藏 gRPC 语义 |
response_size_header | string(须为合法 HTTP 头名) | 配合withhold_grpc_frames使用:指定上游某个响应头来告知响应大小,从而无需缓冲即可流式转发;若该头缺失或与实际响应体大小不符,过滤器将把上游响应视为错误 |
此外还有 per-route 配置FilterConfigPerRoute,仅含一个disabled字段,用于在特定 virtualhost/route/weighted-cluster 上关闭该过滤器。
崩溃场景:缓冲路径撞上下游 H2 流控窗口
在修复之前,当withhold_grpc_frames = true且没有配置response_size_header时,过滤器无法预先得知响应体大小,只能走"全量缓冲"路径。旧逻辑在 filter.cc 中表现为:每个非终止的encodeData调用都把数据buffer_.move(buffer)移入自有缓冲,返回StopIterationAndBuffer;直到end_stream才一次性把缓冲的数据 prepend 上 gRPC 帧头、整体释放给下游。
这带来的问题正是 changelog 条目所描述的:
Fixed a crash (SEGFAULT) in the
grpc_http1_reverse_bridgefilter whenwithhold_grpc_framesis enabled withoutresponse_size_headerand the upstream response body exceeds the downstream HTTP/2 stream flow control window. The filter now uses the upstreamContent-Lengthheader to stream the response incrementally instead of buffering and releasing it all at once.
即:当上游响应体大小超过下游 HTTP/2 流控窗口时,把整个响应在单次encodeData中一次性释放出去,会让 H2 codec 一次需要排入过多帧("overwhelm the H2 codec with too many frames at once",见 filter.cc 的注释),从而触发进程崩溃(SEGFAULT)。
从 reverse_bridge_test.cc 的测试注释可以印证这一点:该测试专门验证"当withhold_grpc_frames为 true 且未配置response_size_header时,过滤器使用上游响应头中的 Content-Length 来流式传输响应,而不是缓冲整个响应体,以防止大响应一次性产生过多帧压垮 H2 codec"。
修复方案:借上游 Content-Length 实现增量流式传输
修复的核心思路很直接:既然无法从response_size_header得知响应大小,那就读上游响应的Content-Length头。gRPC 反向桥接通常面对的是上游返回单个序列化 protobuf 消息(unary 场景),绝大多数上游会设置 Content-Length,这为流式化提供了可靠的依据。
修复在encodeHeaders阶段落地(filter.cc),新增了content_length_from_header_与response_message_length_两个状态字段(定义见 filter.h):
- 读取上游响应的
Content-Length,若存在且为正数:- 先做合法性校验:单个 protobuf 消息不能超过 2GB(
uint32_t上限),超过则直接以本地回复报错(grpc_bridge_content_length_wrong); - 记录
response_message_length_,置位content_length_from_header_ = true; - 把响应头中的 Content-Length 调整为
响应体长度 + Grpc::GRPC_FRAME_HEADER_SIZE(gRPC 帧头固定 5 字节),保证下游看到的长度与实际下发的数据一致;
- 先做合法性校验:单个 protobuf 消息不能超过 2GB(
- 若没有可用的 Content-Length,则退回到原有的缓冲路径(
adjustContentLength加 5 字节帧头后交给encodeData缓冲)。
encodeData侧随之进入"已知大小、直接流式"的分支(filter.cc):
- 第一段数据到达时,用
buildGrpcFrameHeader在数据前补上 5 字节 gRPC 帧头(帧内长度字段填response_message_length_),随后frame_header_added_ = true; - 中间的数据段原样透传,不再进入自有缓冲,直接返回
Continue; - 数据流结束(
end_stream)时,校验upstream_response_bytes_(累计收到的上游响应字节数)是否等于response_message_length_,不一致则以本地回复报错(grpc_bridge_content_length_wrong);同时写入携带 grpc-status 的 trailers。
由此,整个响应体被拆成多个小的encodeData调用逐步下发给 H2 codec,单次释放的数据量受控,不再触发流控窗口相关的崩溃。
三类响应路径的完整对照
结合 filter.cc 的实现,withhold_grpc_frames = true时响应处理实际上分三种路径:
| 场景 | encodeHeaders 行为 | encodeData 行为 | 备注 |
|---|---|---|---|
配置了response_size_header | 从指定头取值计算大小,头缺失即报错 | 已知大小,直接流式,帧头加在第一段数据前 | 原有流式路径 |
未配置response_size_header,但上游有合法Content-Length | 置content_length_from_header_,按长度+5调整 Content-Length | 已知大小,直接流式(本次修复新增) | 校验最终字节数与声明值一致 |
未配置response_size_header且上游无Content-Length | 仅调整 Content-Length(+5) | 全量缓冲到buffer_,结束时 prepend 帧头整体下发 | 旧缓冲路径,仅作为兜底保留 |
三种路径在encodeTrailers中还会做最后的兜底:当既没有response_size_header也没有content_length_from_header_(即走了缓冲路径)时,在 trailers 阶段把缓冲内容补上帧头后一次性addEncodedData(filter.cc)。
配置示例与 per-route 关闭
仓库自带的完整示例见 grpc-reverse-bridge-filter.yaml,核心配置如下:
http_filters: - name: envoy.filters.http.grpc_http1_reverse_bridge typed_config: "@type": type.googleapis.com/envoy.extensions.filters.http.grpc_http1_reverse_bridge.v3.FilterConfig content_type: application/grpc+proto withhold_grpc_frames: true - name: envoy.filters.http.router typed_config: "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router如果某些路由不希望启用该过滤器,可通过路由级typed_per_filter_config指定FilterConfigPerRoute.disabled: true将其关闭:
routes: - match: prefix: "/route-with-filter-disabled" route: host_rewrite_literal: localhost cluster: grpc timeout: 5.00s # per_filter_config disables the filter for this route typed_per_filter_config: envoy.filters.http.grpc_http1_reverse_bridge: "@type": type.googleapis.com/envoy.extensions.filters.http.grpc_http1_reverse_bridge.v3.FilterConfigPerRoute disabled: true该逻辑在 filter.cc 中通过resolveMostSpecificPerFilterConfig解析,最具体的 per-filter-config 生效;过滤器在 config.cc 中以envoy.filters.http.grpc_http1_reverse_bridge名称静态注册为NamedHttpFilterConfigFactory。
需要提醒的是:过滤器在请求路径上会剥离 gRPC 帧头并调整请求 Content-Length(filter.cc),若请求体不足 5 字节(不可能包含完整 gRPC 帧),会直接返回本地错误grpc_bridge_data_too_small;而上游返回的 content-type 若与配置的content_type不一致,也会以grpc_bridge_content_type_wrong直接终止响应(filter.cc)。这些细节在排查"修复后仍然报错"的场景时非常有用。
测试验证:三条针对性的单测
本次修复在 reverse_bridge_test.cc 中配套了三条针对性测试:
WithholdGrpcFramesStreamsWithContentLength(约 L1009):上游返回Content-Length: 12的响应,验证头调整后为 17(12 + 5 字节帧头),第一段数据被补上 5 字节帧头变为 9 字节,中间段原样 4 字节透传,最终段校验并写入grpc-status: 0trailers——完整覆盖新流式路径;WithholdGrpcFramesContentLengthMismatch(约 L1068):上游声明Content-Length: 100但实际只发 8 字节,验证end_stream时以本地回复报错 "envoy reverse bridge: upstream set incorrect content length"(grpc-status Internal),确认流式路径的完整性校验生效;WithholdGrpcFramesContentLengthTooLarge(约 L1117):上游声明超过uint32_t上限的 Content-Length,验证在encodeHeaders阶段即被拒绝,防止构造出非法 gRPC 帧。
这三条测试恰好从"正常流式、声明不符、超限拒绝"三个角度锁定了新逻辑的行为边界。从源码结构看,过滤器通过upstream_response_bytes_在每次encodeData累加(filter.cc),这一计数是实现流式路径字节数校验的基础。
局限与最佳实践
- 依赖上游 Content-Length:修复后的流式路径依赖上游如实设置
Content-Length。若上游用 chunked 传输或根本不设 Content-Length,过滤器只能退回缓冲路径,崩溃风险场景并未完全消除。对于已知会产生大响应的服务,建议上游总是设置准确的 Content-Length,或直接配置response_size_header走确定性更强的流式路径。 - unary 限制:由于映射到 HTTP/1.1,仅支持一元 gRPC 调用,流式调用不在本过滤器支持范围内。
- 单消息 2GB 上限:单个 protobuf 消息不能超过 2GB,这是 gRPC 帧长度字段(uint32)的硬限制,超限会直接报错而非崩溃。
- 升级验证:该修复属于 bug_fixes 类别(见 changelogs/current/bug_fixes),部署到生产前建议先运行上述单测与集成测试 reverse_bridge_integration_test.cc 确认行为符合预期。
【免费下载链接】envoyCloud-native high-performance edge/middle/service proxy项目地址: https://gitcode.com/GitHub_Trending/en/envoy
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考