Envoy JWT AuthN 远程 JWKS 拉取 Span 采样修复:从“总是采样”到“继承父 Span 采样决策”
【免费下载链接】envoyCloud-native high-performance edge/middle/service proxy项目地址: https://gitcode.com/GitHub_Trending/en/envoy
导读
本文围绕 Envoy 中 JWT 认证(JWT AuthN)过滤器的一条 bug 修复展开:修复前,JWT Remote PubKey Fetch子 Span 由于异步客户端请求选项将采样决策留在了默认值true,导致远程 JWKS 拉取产生的跟踪 Span总是被采样;修复后,采样决策被显式置空(unset),使该子 Span继承父 Span 的采样决策。读完本文,你将理解 Envoy 异步 HTTP 客户端RequestOptions::sampled_的默认行为、子 Span 采样决策的传递机制,以及这条修复对应的源码与测试验证方式,可直接迁移到自己的过滤器或异步调用代码中排查同类“Span 过度采样”问题。
一、背景:JWT AuthN 过滤器与远程 JWKS 拉取
Envoy 的 JWT 认证过滤器(jwt_authn)负责在请求进入时验证 JWT(JSON Web Token)。当 JWT 的签发方(issuer)使用远程 JWKS(JSON Web Key Set)提供公钥时,Envoy 需要通过 HTTP 从远端拉取 JWKS 来验证签名。该能力由RemoteJwks配置定义,其核心字段在 api/envoy/extensions/filters/http/jwt_authn/v3/config.proto 中说明:
http_uri:拉取 JWKS 的 HTTP URI,包括uri、cluster与timeout,必填;cache_duration:缓存 JWKS 的过期时长,未指定时默认 10 分钟;async_fetch:是否在主线程异步预取 JWKS(监听器激活前),启用后各 worker 线程无需各自拉取。
一个典型的远程 JWKS 配置片段(来自 config.proto 的注释示例):
http_uri: uri: https://www.googleapis.com/oauth2/v1/certs cluster: jwt.www.googleapis.com|443 timeout: 1s拉取动作由JwksFetcherImpl执行,它通过 HTTP 异步客户端(Http::AsyncClient)向上述 URI 发起 GET 请求,其实现位于 source/extensions/filters/http/common/jwks_fetcher.cc。
二、问题本质:异步客户端默认“总是采样”
2.1 采样决策的默认值
在修复之前,JwksFetcherImpl::fetch()构造异步请求选项时没有设置采样字段,而异步客户端RequestOptions中sampled_的默认值是true。见 envoy/http/async_client.h:
std::optional<bool> sampled_{true};在 source/common/http/async_client_impl.cc 中,该选项被直接应用到子 Span 上:
// Span gets sampled by default, as sampled_ defaults to true. // If caller overrides sampled_ with empty value, sampling status of the parent is kept. if (options.sampled_.has_value()) { child_span_->setSampled(options.sampled_.value()); }这意味着:只要调用方不显式处理,所有通过异步客户端创建的子 Span 都会被强制采样,即使父 Span 本身并未被采样。对于每次远程 JWKS 拉取,Envoy 都会无条件产生一条JWT Remote PubKey Fetch跟踪数据,在采样率设置较低的生产环境中会显著放大 tracing 后端的数据量与成本,并污染采样统计。
2.2 修复前 JwksFetcher 的调用方式
修复前,source/extensions/filters/http/common/jwks_fetcher.cc 中的选项构造大致为:
auto options = Http::AsyncClient::RequestOptions() .setTimeout(...) .setParentSpan(parent_span) // 采样字段保持默认(true)——导致总是被采样 .setChildSpanName("JWT Remote PubKey Fetch");2.3 修复方案:显式置空采样选项
本次 bug 修复的核心改动正是这一行——显式调用.setSampled(std::nullopt),将采样决策从“默认 true”改为“无显式值”,从而让子 Span 继承父 Span 的采样决策:
auto options = Http::AsyncClient::RequestOptions() .setTimeout(std::chrono::milliseconds( DurationUtil::durationToMilliseconds(remote_jwks_.http_uri().timeout()))) .setParentSpan(parent_span) // Leave sampled unset so the JWKS fetch span honors the parent span's // sampling decision instead of always being sampled. .setSampled(std::nullopt) .setChildSpanName("JWT Remote PubKey Fetch");改动依据正是 async_client_impl.cc 中注释明确的行为契约:“If caller overrides sampled_ with empty value, sampling status of the parent is kept”——当sampled_为 nullopt 时,has_value()为 false,setSampled不会被调用,子 Span 保留spawnChild时从父 Span 继承而来的采样状态。
三、采样决策如何“继承”父 Span
从源码结构看,继承链条分两层:
- Span 创建:async_client_impl.cc 中,当
options.parent_span_非空时,通过parent_span_->spawnChild(Tracing::EgressConfig::get(), child_span_name, ...)创建子 Span。子 Span 在诞生时就带有父 Span 的采样属性(sampled 状态由父级传递)。 - 采样覆写:随后
sampled_是否有值决定是否覆写这一继承状态。修复后 JWKS 拉取路径不再覆写,因此子 Span 的采样与否完全跟随其父 Span——通常父 Span 就是 JWT 认证过滤器所在请求的跟踪 Span,其采样由全局采样策略(如 sampling 配置、trace 决策钩子等)决定。
由此,只有在父请求本身被采样时,JWT Remote PubKey Fetch子 Span 才会被采样,从而在低采样率场景下大幅减少无意义的 JWKS 拉取跟踪数据。
四、测试验证:测试代码如何锁定这一行为
该修复在测试中被显式断言,可通过测试用例验证:
JwksFetcher 单元测试:test/extensions/filters/http/common/jwks_fetcher_test.cc 中的
TestSpanPassedDown用例,通过拦截异步客户端的send_调用检查传入的RequestOptions:- 断言
options.parent_span_ == &parent_span_(父 Span 正确传入); - 断言
options.child_span_name_ == "JWT Remote PubKey Fetch"; - 关键断言
EXPECT_FALSE(options.sampled_.has_value())——采样选项必须保持无值,一旦将来有人重新引入默认采样或显式置 true,此测试会立即失败。
- 断言
JwtAuthn 集成测试:test/extensions/filters/http/jwt_authn/provider_verifier_test.cc 中同样断言了
"JWT Remote PubKey Fetch"子 Span 名称,验证该 Span 在 JWT 认证主流程中的命名约定。
这些测试为“继承父 Span 采样决策”提供了可复现的行为契约,防止回归。
五、实践启示:如何在自己的过滤器/异步调用中避免“总是采样”
这条修复是一个典型的 Envoy 扩展开发范式,可推广到任何使用Http::AsyncClient::RequestOptions发起带跟踪异步请求的过滤器或组件:
- 显式决定采样字段:不要依赖
sampled_{true}的默认值。若子 Span 应跟随请求的采样策略,请显式设置.setSampled(std::nullopt);若确需强制采样,再显式.setSampled(true)。 - 理解继承契约:
spawnChild创建的子 Span 已继承父级采样状态,sampled_仅是“覆写开关”,默认的true是易被忽略的陷阱。 - 用测试锁定行为:参考 jwks_fetcher_test.cc 的写法,在拦截
send_的Invoke回调中断言options.sampled_.has_value()的取值,使采样行为成为可验证的契约。 - 关注改动源头:本修复记录于 changelogs/current/bug_fixes/jwt_authn__jwks-fetch-span-honors-parent-sampling.rst,如需排查同类问题,可在该变更日志目录下检索其他与采样、跟踪相关的修复记录。
结语
本修复虽只改动一行,却纠正了一个影响面广泛的默认行为:远程 JWKS 拉取 Span 从“无条件被采样”变为“跟随父请求的采样决策”,既保证了请求级链路跟踪的完整性,又避免了低采样率环境下 tracing 数据的无效膨胀。理解RequestOptions::sampled_的默认值与async_client_impl的继承逻辑,是 Envoy 扩展开发中正确控制跟踪开销的关键一环。
【免费下载链接】envoyCloud-native high-performance edge/middle/service proxy项目地址: https://gitcode.com/GitHub_Trending/en/envoy
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考