所有权问题的持续观察
Rust 的借用检查发生在编译期,服务上线后不会突然弹出一条“这里的所有权设计有问题”。运行时能看到的,是设计选择留下的痕迹:为了绕过生命周期而增加的复制、被长时间持有的Arc、锁等待,以及请求结束后没有及时释放的资源。
所以我不会给“所有权问题”单独做一个含义模糊的监控项,而是从对象的创建、共享和销毁过程入手。缓存有多少条目,任务队列里还有多少待处理项,某类连接何时建立和归还,这些状态更容易验证,也能与具体代码对应。
tracing::debug!(cache_entries = cache.len(), "cache snapshot");这里记录的是聚合数量,不是缓存键和值。日志若包含用户输入,即使排障更方便,也扩大了不必要的数据暴露。对象标识同样要谨慎:能用随机请求号串起一次操作,就不要把账号、文件名或业务主键直接写进去。
先画清对象的生命周期
遇到对象数持续增加,我会先回答几个具体问题:对象在哪个入口创建,谁持有它,正常结束和取消分别经过哪条释放路径,后台任务是否还保留引用。代码评审时把这些关系画成一条短时间线,通常比盯着内存曲线猜原因更快。
Arc的强引用计数可以作为局部诊断线索,但不适合长期在高频路径里大量打印。需要观察时只在测试环境或采样条件下记录,并结合任务状态判断。计数高不一定是泄漏,可能只是并发请求尚未结束;计数恢复也不代表没有峰值复制。单个数字只能帮助缩小范围。
把复制和锁等待分开看
有些性能下降并不是对象没有释放,而是所有权边界不合适。为了让数据跨任务使用,代码可能频繁clone大块内容;为了共享可变状态,又可能把过多逻辑放进同一把锁。两者在外部都表现为变慢,但排查方法不同。
复制问题应通过分配剖析和输入规模复现,确认复制发生在哪个调用点。锁问题则看等待时间、临界区长度和竞争任务,不能从 CPU 使用率直接下结论。修改时一次只处理一个因素:先缩小克隆范围,或先把锁内的 I/O 移出去,然后用相同负载复查。否则即使曲线好转,也很难知道是哪项改动起作用。
取消路径常常暴露隐藏持有
正常请求会走到函数末尾,取消和超时却可能提前返回。若子任务没有收到取消信号,或者 channel 的发送端仍被某个结构体持有,资源就会比预期活得更久。测试时我会主动取消任务,确认子任务停止、连接归还、临时文件清理,队列计数也回到合理状态。
当观测发现异常,下一步应是构造最小生命周期复现,再检查无意的Arc持有、channel 克隆或锁范围。日志和指标负责指出“哪一段行为不同”,profile 与代码审阅才用于确认原因。把一次相关变化直接写成内存泄漏,既不准确,也容易把修复带到错误方向。