reload 与运行时改级别
主线位置:事件主线的第二、三道门。
LevelFilter::current()(全局MAX_LEVEL)和调用点里缓存的Interest,都是按装上时的 filter 算出来的。运行时换掉 filter,这两个缓存不会自己更新;reload的Handle在每次修改之后调用callsite::rebuild_interest_cache,把它们按新的 filter 重算一遍。
flowchart TB modify["Handle::modify(f)"] --> up["Weak::upgrade"] up -->|失败| gone["Error:subscriber 已没了"] up --> wl["inner.write()"] wl --> fcall["f(&mut L)"] fcall --> dl["drop 写锁"] dl --> rb["rebuild_interest_cache"] rb --> rc["每个调用点重问 register_callsite"] rb --> ml["重算 MAX_LEVEL"] ml --> logmax["log::set_max_level"]
tracing_subscriber::fmt().init() 装上的 subscriber 是一棵类型固定的 Layered<...>,全局默认又只能设一次(见 Dispatch:全局与线程局部)。程序跑起来以后想把级别从 info 调到 debug,既不能重装 subscriber,也不能改它的类型,只能在类型里预留一个「可以换内容的格子」。reload.rs 一共 394 行,提供的就是这个格子。Layer / Filter 两个 trait 见 Layer 与 Layered 和 过滤:全局与每层,Interest 缓存和重算见 Callsite 与 Interest 缓存。
本页的实验都在 M2 Max、rustc 1.99.0 上跑,依赖:
tracing = "=0.1.44"
tracing-subscriber = { version = "=0.3.23", features = ["env-filter"] }
env-filter 只有第一个实验要用。
reload::Layer 与 Handle
两个类型,共享同一个 RwLock(Layer、Handle):
pub struct Layer<L, S> {
// TODO(eliza): this once used a `crossbeam_util::ShardedRwLock`. We may
// eventually wish to replace it with a sharded lock implementation on top
// of our internal `RwLock` wrapper type. If possible, we should profile
// this first to determine if it's necessary.
inner: Arc<RwLock<L>>,
_s: PhantomData<fn(S)>,
}
pub struct Handle<L, S> {
inner: Weak<RwLock<L>>,
_s: PhantomData<fn(S)>,
}
Layer::new 把 inner 放进 Arc<RwLock<L>>,同时 Arc::downgrade 出一个 Handle 一起返回。reload::Layer 进了 subscriber,Handle 留在你手里,可以 clone 后交给别的线程(比如一个管理接口的 handler)。Handle 拿的是 Weak,不让 subscriber 的生命周期被它延长。
RwLock 默认是 std::sync::RwLock;开了 parking_lot feature 则换成 parking_lot 的实现,由 sync.rs 包成同样的接口。S 只出现在 PhantomData<fn(S)> 里,它是这层所在的 subscriber 类型,Layer<S> 的实现需要它。
reload::Layer 实现了两个 trait,每个方法都是「拿读锁,转给里面的值」:
impl Layer<S> for reload::Layer<L, S>,要求L: Layer<S>。可以包一个全局过滤器(EnvFilter、LevelFilter都实现了Layer),也可以包一个输出层。impl Filter<S> for reload::Layer<L, S>,要求L: Filter<S>,可以放进layer.with_filter(...)当每层过滤器。
转发时用的是 try_lock! 宏:
macro_rules! try_lock {
($lock:expr) => {
try_lock!($lock, else return)
};
($lock:expr, else $els:expr) => {
if let ::core::result::Result::Ok(l) = $lock {
l
} else if std::thread::panicking() {
$els
} else {
panic!("lock poisoned")
}
};
}
锁被毒化(某个线程拿着写锁时 panic 了)时,如果当前线程本身也在 panic,就退回一个保守的默认值,避免 panic 中再 panic;否则直接 panic。各方法的退路:register_callsite 退回 Interest::sometimes(),enabled 和 event_enabled 退回 false,max_level_hint 退回 None,on_event 这类回调直接 return。
每个回调都要拿一次读锁,这就是模块文档说的「a (relatively small) amount of overhead, and should thus only be used as needed」。不改级别的程序不必套 reload。
运行时把 info 调成 debug
use tracing::{debug, info, level_filters::LevelFilter};
use tracing_subscriber::{EnvFilter, fmt, prelude::*, reload};
fn tick(round: u32) {
debug!(round, "debug event");
info!(round, "info event");
}
fn levels() {
println!("tracing MAX_LEVEL = {}, log::max_level() = {}", LevelFilter::current(), log::max_level());
}
fn main() {
let (filter, handle) = reload::Layer::new(EnvFilter::new("info"));
tracing_subscriber::registry()
.with(filter)
.with(fmt::layer().with_ansi(false).without_time())
.init();
tick(1);
levels();
handle.reload(EnvFilter::new("debug")).unwrap();
levels();
tick(2);
}
(另加依赖 log = "0.4",只为打印 log::max_level()。)
INFO reload_level: info event round=1
tracing MAX_LEVEL = info, log::max_level() = INFO
tracing MAX_LEVEL = debug, log::max_level() = DEBUG
DEBUG reload_level: debug event round=2
INFO reload_level: info event round=2
reload 之后,全局 MAX_LEVEL 和 log 的最大级别都变成了 debug,第一轮被挡掉的 debug! 调用点在第二轮输出了。reload::Layer 放在 .with(filter) 的位置,是全局过滤:它在 fmt::Layer 内侧,Layered 问到它说「不」,外面的 fmt 就看不到。
fmt() builder 也有现成的写法:with_filter_reloading 把 builder 里的 EnvFilter 包进 reload::Layer,init 之前用 reload_handle 取出 Handle:
pub fn with_filter_reloading(
self,
) -> SubscriberBuilder<N, E, crate::reload::Layer<crate::EnvFilter, Formatter<N, E, W>>, W>
{
let (filter, _) = crate::reload::Layer::new(self.filter);
SubscriberBuilder {
filter,
inner: self.inner,
}
}
new 返回的 Handle 被 _ 丢掉了,没关系:reload_handle 随时可以用 self.filter.handle() 再降级出一个。
modify:改完一定重建缓存
Handle::reload 只是 modify(|layer| *layer = new_value.into())。全部逻辑在 modify:
pub fn modify(&self, f: impl FnOnce(&mut L)) -> Result<(), Error> {
let inner = self.inner.upgrade().ok_or(Error {
kind: ErrorKind::SubscriberGone,
})?;
let mut lock = try_lock!(inner.write(), else return Err(Error::poisoned()));
f(&mut *lock);
// Release the lock before rebuilding the interest cache, as that
// function will lock the new layer.
drop(lock);
callsite::rebuild_interest_cache();
// If the `log` crate compatibility feature is in use, set `log`'s max
// level as well, in case the max `tracing` level changed. We do this
// *after* rebuilding the interest cache, as that's when the `tracing`
// max level filter is re-computed.
#[cfg(feature = "tracing-log")]
tracing_log::log::set_max_level(tracing_log::AsLog::as_log(
&crate::filter::LevelFilter::current(),
));
Ok(())
}
- 升级弱引用。subscriber 已经被 drop,返回
ErrorKind::SubscriberGone。 - 拿写锁、改值。写锁期间所有线程上经过这一层的回调都在读锁上等着,
f应当很快。 - 先放锁,再重建。注释说得明白:
rebuild_interest_cache会遍历所有调用点,对每个调用点问register_callsite,问到这一层时要拿读锁。std::sync::RwLock不可重入,拿着写锁去重建,同一线程会自己等自己。 - 重建缓存。
callsite::rebuild_interest_cache走到Callsites::rebuild_interest:重算每个调用点的Interest,再用所有 dispatcher 的max_level_hint重算MAX_LEVEL。它是全局的,进程里所有调用点、所有 dispatcher 都要过一遍,代价和调用点数量成正比。 - 同步
log的最大级别。开了tracing-log时,把新的LevelFilter::current()写进log::set_max_level。这一步来自 #1270(「If reloading the subscriber caused the maxtracinglevel to change, this ensures that the change is propagated to thelogcrate as well」),它 2021 年合进master,但 0.1 线上直到 tracing-subscriber 0.3.19 才带上,CHANGELOG 记作「Setlogmax level when reloading」。在那之前,调高级别后log::debug!这类记录会在log自己的级别检查处被挡掉。
Error 只有两种:is_dropped()(subscriber 没了)和 is_poisoned()(锁被毒化)。前者的样子:
use tracing_subscriber::{filter::LevelFilter, prelude::*, reload};
fn main() {
let (filter, handle) = reload::Layer::new(LevelFilter::INFO);
let subscriber = tracing_subscriber::registry().with(filter);
drop(subscriber);
let err = handle.reload(LevelFilter::DEBUG).unwrap_err();
println!("{err} (is_dropped = {})", err.is_dropped());
}
subscriber no longer exists (is_dropped = true)
读当前值用 with_current(借用)或 clone_current(要求 L: Clone),它们只拿读锁,不触发重建。
为什么必须重建:两道门都记着旧答案
EnvFilter 的静态指令在 register_callsite 时就给出 always / never,max_level_hint 给出一个级别(见 EnvFilter)。这两个回答分别被缓存在调用点的原子变量和全局 MAX_LEVEL 里,之后的事件直接读缓存,不再问 filter(见 Callsite 与 Interest 缓存)。只改 filter 不重建,会发生什么?
reload 总是会重建,没法用它演示。下面写一个过滤层,级别放在一个原子变量里,回答方式和 LevelFilter 当 Layer 用时一样(register_callsite 给 always / never,max_level_hint 给自己的级别)。改原子变量不会触发任何重算,重建由我们手动调用:
use std::sync::atomic::{AtomicUsize, Ordering::Relaxed};
use tracing::{Level, Metadata, Subscriber, debug, info, level_filters::LevelFilter};
use tracing_core::{Interest, callsite};
use tracing_subscriber::layer::{Context, Layer};
use tracing_subscriber::{fmt, prelude::*};
// 0 = WARN, 1 = INFO, 2 = DEBUG:改它不会触发任何重算
static LEVEL: AtomicUsize = AtomicUsize::new(1);
fn level() -> Level {
[Level::WARN, Level::INFO, Level::DEBUG][LEVEL.load(Relaxed)]
}
// 和 LevelFilter 当全局过滤层时的回答方式一样,只是级别放在一个原子变量里
struct KnobFilter;
impl<S: Subscriber> Layer<S> for KnobFilter {
fn register_callsite(&self, meta: &'static Metadata<'static>) -> Interest {
if *meta.level() <= level() { Interest::always() } else { Interest::never() }
}
fn enabled(&self, meta: &Metadata<'_>, _: Context<'_, S>) -> bool {
*meta.level() <= level()
}
fn max_level_hint(&self) -> Option<LevelFilter> {
Some(LevelFilter::from_level(level()))
}
}
fn tick(step: &str) {
debug!("debug event");
info!("info event");
println!("-- {step}: knob={} MAX_LEVEL={}", level(), LevelFilter::current());
}
fn main() {
tracing_subscriber::registry()
.with(KnobFilter)
.with(fmt::layer().with_ansi(false).without_time().with_target(false))
.init();
tick("A start");
LEVEL.store(2, Relaxed);
tick("B knob to DEBUG, no rebuild");
callsite::rebuild_interest_cache();
tick("C after rebuild");
LEVEL.store(0, Relaxed);
tick("D knob to WARN, no rebuild");
callsite::rebuild_interest_cache();
tick("E after rebuild");
}
(另加依赖 tracing-core = "=0.1.36",为了直接调 callsite::rebuild_interest_cache。)前三步的输出:
INFO info event
-- A start: knob=INFO MAX_LEVEL=info
INFO info event
-- B knob to DEBUG, no rebuild: knob=DEBUG MAX_LEVEL=info
DEBUG debug event
INFO info event
-- C after rebuild: knob=DEBUG MAX_LEVEL=debug
B 步 filter 已经愿意放行 DEBUG,debug! 还是没有输出。它根本走不到 filter:
- 第二道门:
MAX_LEVEL还是info,debug!展开后的level_enabled!比较一下就返回了。 - 第三道门:即使绕过第二道门,
debug!调用点里缓存的Interest是 A 步之前注册时得到的never,也会直接返回。
C 步手动重建以后,两道门都按新答案放行。Handle::modify 里那行 callsite::rebuild_interest_cache() 做的就是 C 步。
接着上面的程序跑。D 步之前 debug! 和 info! 两个调用点缓存的都是 C 步重建时得到的 always,MAX_LEVEL 是 debug。
方向反过来(调低级别)时,不重建的后果不是「看不到」而是「关不掉」,下面的答案里有实测。max_level_hint 也属于要转发的回答:reload::Layer 曾经没有转发它。
0.3.14 及以前,reload::Layer 没有实现 max_level_hint,用的是 Layer trait 的默认实现,返回 None。#2204 的说明:「When using a reload layer, the fast-path current level check doesn't work, as the max_level_hint is just None, which rebuild_interest interprets as TRACE」。也就是说,只要用了 reload 包过滤器,MAX_LEVEL 就被算成 TRACE,所有被关掉的 trace! / debug! 都得走到第三道门才被挡下。改法是「Pass through to the underlying layer/filter. On poisons, when already panicking, just return None」,就是现在的 max_level_hint 里那句 try_lock!(self.inner.read(), else return None).max_level_hint()。PR 合进的是 master,0.1 线上第一个带它的是 tracing-subscriber 0.3.15,这个版本只修了这一处,CHANGELOG 写的是「fixes a bug where the reload layer would fail to pass through max_level_hint to the underlying layer, potentially breaking filtering」。上面实验里 MAX_LEVEL=info 能随 reload 变成 debug,靠的就是它。
只能换值,不能换类型
Handle<L, S>::reload 接受 impl Into<L>,L 在 reload::Layer::new 时就定死了。想把文本输出换成 JSON:
use tracing_subscriber::{fmt, prelude::*, reload};
fn main() {
let (layer, handle) = reload::Layer::new(fmt::layer());
tracing_subscriber::registry().with(layer).init();
handle.reload(fmt::layer().json()).unwrap();
}
rustc 1.99.0 拒绝(json feature 已开):
error[E0277]: the trait bound `Layer<Registry>: From<Layer<_, JsonFields, Format<Json>>>` is not satisfied
--> src/bin/reload_type.rs:6:17
|
6 | handle.reload(fmt::layer().json()).unwrap();
| ------ ^^^^^^^^^^^^^^^^^^^ unsatisfied trait bound
| |
| required by a bound introduced by this call
|
= help: the trait `From<tracing_subscriber::fmt::Layer<_, JsonFields, Format<Json>>>` is not implemented for `tracing_subscriber::fmt::Layer<Registry>`
= note: required for `tracing_subscriber::fmt::Layer<_, JsonFields, Format<Json>>` to implement `Into<tracing_subscriber::fmt::Layer<Registry>>`
note: required by a bound in `Handle::<L, S>::reload`
--> ~/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tracing-subscriber-0.3.23/src/reload.rs:300:42
|
300 | pub fn reload(&self, new_value: impl Into<L>) -> Result<(), Error> {
| ^^^^^^^ required by this bound in `Handle::<L, S>::reload`
= note: the full name for the type has been written to '.../reload_type-583941e9eeb06ed5.long-type-17110344460797409915.txt'
= note: consider using `--verbose` to print the full type name to the console
fmt::layer() 和 fmt::layer().json() 的 N、E 两个类型参数都不同(见 fmt),是两个类型。同一个类型内部的配置也未必能改:fmt::Layer 的格式开关大多是消费 self 的 builder 方法,只有 set_ansi、set_span_events 等少数几个 &mut self 方法能在 modify 里用。
把 L 定成 Box<dyn Layer<Registry> + Send + Sync> 可以绕开类型限制,类型擦除以后什么 layer 都能换进去:
use tracing::info;
use tracing_subscriber::{Layer, Registry, fmt, prelude::*, reload};
type BoxedLayer = Box<dyn Layer<Registry> + Send + Sync>;
fn main() {
let text: BoxedLayer = fmt::layer().with_ansi(false).without_time().boxed();
let (layer, handle) = reload::Layer::new(text);
tracing_subscriber::registry().with(layer).init();
info!(n = 1, "as text");
let json: BoxedLayer = fmt::layer().json().without_time().boxed();
handle.reload(json).unwrap();
info!(n = 2, "as json");
}
INFO reload_boxed: as text n=1
{"level":"INFO","fields":{"message":"as json","n":2},"target":"reload_boxed"}
但新换进来的 layer 没有见过换之前创建的 span。fmt 的 JSON 格式要在 Extensions 里找 FormattedFields<JsonFields>,那是 JSON 层自己的 on_new_span 才会写的。在换之前就进入了的 span 里打一条日志:
let span = info_span!("conn", peer = "10.0.0.7");
let _g = span.enter();
info!("as text");
let json: BoxedLayer = fmt::layer().json().without_time().boxed();
handle.reload(json).unwrap();
info!("as json, inside a span created before reload");
INFO conn{peer="10.0.0.7"}: reload_boxed_span: as text
thread 'main' (1419966) panicked at ~/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tracing-subscriber-0.3.23/src/fmt/format/json.rs:168:14:
Unable to find FormattedFields in extensions; this is a bug
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
release 构建也 panic,这里是 SerializableSpan::serialize 里的一个 expect。同一类问题还有 #3529(open):span 进入时开着 FmtSpan::FULL,用 modify 关掉 span 事件后退出、再打开后关闭,Timings 的进入计数没跟上,debug 构建在 on_close 的 debug_assert_eq!(entered_count, 0) 处 panic。共同的原因是:layer 把状态存在每个 span 的 Extensions 里,状态是旧配置写的,换了配置的 layer 读它时前提已经不成立。所以 reload 最适合换过滤器这类不往 span 里存东西的值;换输出层,要能接受已有 span 上的错乱。
和每层过滤一起用
把一个 Filtered(layer.with_filter(...) 的产物)整个包进 reload,再 reload 一个新的 Filtered,是 Handle::reload 文档明说不能做的事(「cannot be used with the Filtered layer; use Handle::modify instead」),对应 #1629,至今 open。
原因在 FilterId。每个 Filtered 在 on_layer 里向 Registry 要一个 FilterId,用来在每层过滤的位图里占一位(见 过滤:全局与每层)。on_layer 只在 .with(...) 组装 subscriber 时调用一次。reload 换进来的新 Filtered 没经过这一步,它的 id 还是构造时的 FilterId::disabled(),也就是 u64::MAX。
use tracing::info;
use tracing_subscriber::{Layer, filter::LevelFilter, fmt, prelude::*, reload};
fn main() {
let make = || fmt::layer().with_ansi(false).without_time().with_filter(LevelFilter::TRACE);
let (layer, handle) = reload::Layer::new(make());
tracing_subscriber::registry().with(layer).init();
info!("before reload");
handle.reload(make()).unwrap();
info!("after reload");
println!("main returns");
}
debug 构建,panic 在 Filtered::id 的 debug_assert!:
INFO reload_filtered: before reload
thread 'main' (1410933) panicked at ~/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tracing-subscriber-0.3.23/src/filter/layer_filters/mod.rs:633:9:
a `Filtered` layer was used, but it had no `FilterId`; was it registered with the subscriber?
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
release 构建没有这个断言,程序不 panic,但结果是错的。FilterMap::set 遇到 u64::MAX 直接跳过,is_enabled 拿 u64::MAX 当掩码,只有所有每层过滤器都放行时才为真。也就是说,这一层不再按自己的过滤器决定,而是跟着别的层走。两个输出层,console 被 reload 成 WARN,file 是 DEBUG:
let (console_layer, handle) = reload::Layer::new(console(LevelFilter::INFO));
let file_layer = fmt::layer()
// ...
.with_filter(LevelFilter::DEBUG);
tracing_subscriber::registry().with(console_layer).with(file_layer).init();
tick("console INFO, file DEBUG");
handle.reload(console(LevelFilter::WARN)).unwrap();
tick("reload console to WARN");
(console(level) 返回带 [console] 前缀 writer 的 fmt::layer().with_filter(level);tick 依次打 debug!、info!、warn!。)release 构建,跑 3 次输出相同:
[file] DEBUG debug event
[console] INFO info event
[file] INFO info event
[console] WARN warn event
[file] WARN warn event
-- console INFO, file DEBUG
[console] DEBUG debug event
[file] DEBUG debug event
[console] INFO info event
[file] INFO info event
[console] WARN warn event
[file] WARN warn event
-- reload console to WARN
本想把 console 调严,它反而开始输出 DEBUG:file 放行的它都放行。
还有一层原因让 reload 和每层过滤合不来:Layered 靠 downcast_raw 探测内层有没有每层过滤器(layer_has_plf),结果决定 pick_interest 怎么合并两层的 Interest。而 reload::Layer::downcast_raw 除了 NoneLayerMarker 一律返回 None,注释给的理由是「it is generally unsafe to downcast through a reload, because the pointer can be invalidated after the lock is dropped」。包在 reload 里的 Filtered 因此被当成普通 layer,Interest 的合并按全局过滤的规则走。open 的 #3345 报的「tracing::enabled! false positives when using Reload layer」就出现在这种组合下。
正确的用法有两种。一是只用 modify 改已有 Filtered 里面的过滤器,FilterId 不变:
handle.modify(|l| *l.filter_mut() = LevelFilter::WARN).unwrap();
同一个两层程序换成这一行,console 只剩 WARN:
[file] DEBUG debug event
[file] INFO info event
[console] WARN warn event
[file] WARN warn event
-- modify console filter to WARN
二是不包 Filtered,只包过滤器本身。reload::Layer 实现了 Filter(0.3.12 起,#2159),可以直接塞进 with_filter。模块文档也这么建议:「If you only want to be able to dynamically change the Filter on a layer, prefer wrapping that Filter in the reload::Layer.」
// 只把 console 的 Filter 包进 reload,Filtered 本身不动
let (console_filter, handle) = reload::Layer::new(LevelFilter::INFO);
let console = fmt::layer()
// ...
.with_filter(console_filter);
// file 层同上,LevelFilter::DEBUG
tracing_subscriber::registry().with(console).with(file).init();
tick("console INFO, file DEBUG");
handle.reload(LevelFilter::WARN).unwrap();
tick("reload console filter to WARN");
debug、release 两种构建输出相同:
[file] DEBUG debug event
[console] INFO info event
[file] INFO info event
[console] WARN warn event
[file] WARN warn event
-- console INFO, file DEBUG
[file] DEBUG debug event
[file] INFO info event
[console] WARN warn event
[file] WARN warn event
-- reload console filter to WARN
Filtered 和它的 FilterId 从头到尾都是装上时那一个,被换掉的只是锁里的 LevelFilter。
动脑筋答案
no_rebuild 的完整输出,跑 3 次相同:
INFO info event
-- A start: knob=INFO MAX_LEVEL=info
INFO info event
-- B knob to DEBUG, no rebuild: knob=DEBUG MAX_LEVEL=info
DEBUG debug event
INFO info event
-- C after rebuild: knob=DEBUG MAX_LEVEL=debug
DEBUG debug event
INFO info event
-- D knob to WARN, no rebuild: knob=WARN MAX_LEVEL=debug
-- E after rebuild: knob=WARN MAX_LEVEL=warn
- D 步输出 2 行,
DEBUG和INFO都出来了,尽管 filter 此时只想要WARN。MAX_LEVEL还是debug,第二道门放行;两个调用点缓存的是always,第三道门不但放行,还跳过了enabled。KnobFilter::enabled里那句按当前级别判断的代码根本没被调用。 - E 步输出 0 行。重建时
register_callsite按WARN给两个调用点都回答never,MAX_LEVEL变成warn,debug!和info!在第二道门就停了。
所以调低级别不重建,被关掉的日志照样输出,而且一直输出到下一次有人重建为止(比如新建一个 Dispatch)。
小结
reload::Layer<L, S>是Arc<RwLock<L>>,每个回调拿读锁转发;Handle拿Weak,subscriber 没了返回is_dropped()的错误。它同时实现Layer和Filter。Handle::modify拿写锁改值、先放锁、再rebuild_interest_cache,最后(开tracing-log时)同步log::set_max_level;reload就是modify整体赋值。- 必须重建,是因为调用点的
Interest和全局MAX_LEVEL都缓存着旧 filter 的回答:调高级别不重建,新放行的日志过不了门;调低级别不重建,always的调用点连enabled都不问,关不掉。 - 只能换成同一个类型
L的值。Box<dyn Layer>能绕开,但换进来的 layer 没见过已有 span,读Extensions时可能 panic(JSON 格式的FormattedFields、#3529 的Timings)。 - 不要
reload一整个Filtered:新值没有FilterId,debug 构建 panic,release 构建跟着别的层放行(#1629)。用modify改filter_mut(),或者只把Filter包进reload::Layer。