一次特别的性能问题排查

一次特别的性能问题排查
Photo by K. Mitch Hodge / Unsplash

最近几天遇到了一个令人头疼的问题:后端 API 接口响应越来越慢,有时甚至会出现假死状态,完全无法响应请求。唯一的临时解决方案是重启后端服务,但过不了多久问题又会重现。

初期症状:

  • API 响应时间从几十毫秒逐渐增长到几秒
  • 随着服务运行时间增长,性能持续下降
  • 最终会进入假死状态,必须重启才能恢复
  • 重启后短时间内运行正常,然后重蹈覆辙

排查过程

这种"越跑越慢"的症状让我首先怀疑是内存泄漏或资源未释放。我尝试了多种方向:

1. 优化缓存策略

面对性能问题,第一反应是减少不必要的计算和请求:

后端 Redis 缓存

  • 将频繁查询的数据加入 Redis 缓存
  • 对热点接口实施缓存层
  • 设置合理的缓存过期时间

前端静态资源优化

// 为静态文件添加版本号/随机码,实现持久化缓存
<script src="/app.js?v=a8f3c2d1"></script>
<link href="/style.css?v=b9e4d3f2" rel="stylesheet">

多层缓存控制

  • 服务器端:设置了合理的 Cache-Control 响应头
  • CDN 层:配置静态资源缓存策略,减轻源站压力
  • 浏览器端:利用强缓存和协商缓存机制
// 为静态资源添加缓存头
HttpResponse::Ok()
    .insert_header(("Cache-Control", "public, max-age=31536000, immutable"))
    .insert_header(("ETag", resource_hash))
    .body(content)

这些优化确实带来了改善,缓存命中率提高,响应时间减少。但问题依然存在:服务运行一段时间后,即使缓存命中率很高,整体性能还是会持续下降

2. 检查数据库连接

数据库往往是性能问题的常见瓶颈,我进行了全面检查:

连接池优化

  • 检查连接池配置,确认没有连接泄漏
  • 调整连接池大小,平衡并发和资源消耗

索引优化

-- 分析慢查询日志,为高频查询字段添加索引
CREATE INDEX idx_user_id ON downloads(user_id);
CREATE INDEX idx_created_at ON favorites(created_at);
CREATE INDEX idx_composite ON user_points(user_id, last_reset_date);

-- 使用 EXPLAIN 分析查询计划
EXPLAIN ANALYZE SELECT * FROM downloads WHERE user_id = ?;

查询性能监控

  • 监控数据库查询性能,未发现明显慢查询
  • 数据库服务器 CPU、内存资源充足
  • 连接数在正常范围内

索引优化后,相关查询速度确实提升了 30-50%,但整体性能下降的趋势并没有改变。

3. 分析内存使用

  • 观察进程内存占用,增长并不明显
  • 排除了明显的内存泄漏问题

4. 查看系统资源

  • CPU 使用率正常
  • 内存使用正常
  • 磁盘 I/O 出现异常高的写入量 ⚠️

这时我意识到,之前的优化都是"治标",真正的问题可能在其他地方。

真相大白

当我注意到磁盘 I/O 异常时,突然想到了一个被忽视的细节:日志文件

查看日志目录后发现:

  • 单个日志文件在几小时内增长到 几十 MB
  • 每次 API 调用都会产生 多条详细日志
  • 大量的 INFO 级别日志记录了每个操作的细节

问题根源找到了:过度的日志记录导致了严重的 I/O 瓶颈

为什么日志会导致性能问题?

  1. 频繁的磁盘写入:每个请求产生多条日志,高并发下磁盘 I/O 成为瓶颈
  2. 文件系统开销:日志文件过大后,文件系统的写入性能下降
  3. 锁竞争:多线程/协程写入同一日志文件可能产生锁竞争
  4. 缓冲区刷新:大量日志导致频繁的 buffer flush,影响性能

解决方案

临时方案

立即停止日志输出,让服务先稳定运行:

// 临时禁用详细日志
env_logger::Builder::from_default_env()
    .filter_level(log::LevelFilter::Error)
    .init();

效果立竿见影,API 响应恢复正常。

长期优化计划

  1. 调整日志级别
    • 生产环境使用 WARNERROR 级别
    • 只在关键路径记录必要信息
    • 开发环境可保留详细日志
  2. 实施日志轮转
    • 使用 tracing-appenderlog4rs 实现日志轮转
    • 按大小或时间切割日志文件
    • 自动压缩和清理历史日志
  3. 结构化日志
    • 使用 JSON 格式,方便后续分析
    • 减少冗余信息,提高可读性
  4. 集中式日志管理
    • 考虑使用 ELK 或 Loki 等日志系统
    • 将日志收集和分析从应用服务器分离

异步日志写入

// 使用异步日志,避免阻塞主线程
use tracing_subscriber::fmt::writer::MakeWriterExt;

let file_appender = tracing_appender::rolling::daily("/var/log", "app.log");
let (non_blocking, _guard) = tracing_appender::non_blocking(file_appender);

优化日志内容

// 之前:记录所有细节
log::info!("用户 {} 请求了资源 {}, 参数: {:?}", user_id, resource, params);

// 优化后:只记录关键信息
log::debug!("用户 {} 访问资源 {}", user_id, resource);

经验总结

教训

  1. 日志不是免费的:每一行日志都有性能开销
  2. 生产环境要谨慎:INFO 级别在开发时很好,但生产环境可能是灾难
  3. 监控要全面:不要只看 CPU 和内存,磁盘 I/O 同样重要
  4. 从小处着手:有时性能问题的根源可能是最不起眼的地方

最佳实践

  • 分级使用:ERROR 用于错误,WARN 用于警告,INFO 谨慎使用,DEBUG/TRACE 仅限开发
  • 配置化:日志级别应该可以通过配置文件或环境变量调整
  • 定期审查:定期检查日志输出,删除不必要的日志语句
  • 性能测试:压力测试时要关注日志对性能的影响

Rust 日志库推荐

  • tracing:功能强大的结构化日志框架
  • env_logger:简单轻量的日志实现
  • tracing-subscriber:灵活的日志订阅和过滤
  • tracing-appender:支持日志轮转和异步写入

这次经历让我意识到,性能优化不仅仅是算法和架构层面的事情,系统的每个组件都可能成为瓶颈。日志作为调试和监控的重要工具,使用不当反而会拖累系统性能。

在追求可观测性的同时,我们也要平衡性能开销。最好的日志是那些既能提供有价值信息,又不会影响系统性能的日志

Read more

間

春节回家,我又见到了我干爹家的三儿子。 他生下来就带着残疾,不能说话,手脚不协调,走路一瘸一拐,嘴角总是挂着口水。小时候干爹干娘怕别人欺负他,教他见人就笑。所以这么多年,不管走到哪,他都是笑着的。 左脚脚尖点地,左手弯着伸不直,走路习惯性靠在路的最右边,紧贴着路沿。我有时候担心他会踩进沟里,想想又觉得,也许他自己知道,这样不容易被人撞到。 那天下午我一个人在村东边路上走,他跟了上来。脸上沾着灰,鼻子里有一团鼻垢,我下意识想帮他弄掉,他偏过头,自己扣了下来,然后转过脸,把手里点着的烟举了举,冲我笑。 他的手指黄黄的,染得很深。后来我知道,小时候有人逗他,教他抽烟,就这么上了瘾,又没有能力自己戒。烟瘾越来越大,有烟就一口气抽完,多的时候一天三包。这两年逢年过节,大家口袋里都装着烟,见面互让,他也学会了凑过去。村里谁家办红白喜事,他都去帮着搬凳子搬椅子,人家给他几根烟,他就高兴。我那半包苏烟,后来进了他的口袋。

折叠时间

折叠时间

上次坐地铁的时候,我盯着手机看了一眼时间:20:37。等反应过来抬起头,已经是20:52了。十五分钟,就这么没了。 但1月牙疼去看牙医,在椅子上躺着等医生准备器械,那三分钟感觉比一个小时还长。 同样是时间,为什么有时候像沙子一样从指缝溜走,有时候又像琥珀一样凝固住每一秒? 不同的星球,不同的时钟 物理学告诉我们,引力会让时间变慢。在靠近黑洞的地方过一小时,地球上可能已经过了好几年。就像不同重量的球压在一张网上,越重的球把网面压得越深,时间在那里流逝得就越慢。 这个画面一直让我着迷。 后来我想,其实我们每个人的内心世界也像是不同的星球。有些事情对你来说很重要,它就像一颗大质量的星球,把你的时间网压出很深的凹陷。你围绕着它打转,时间在那里变得又浓又稠。 恋爱的时候,一天能想对方好几百次。每一次心跳都被放大,每一个眼神都值得回味。楼下等她的那段时间好像特别"漫长"。 但也有些日子,你就是在重复。起床、上班、吃饭、睡觉。一天天像复制粘贴一样过去了,回头看,好像什么都没留下。 大象和蚂蚁的一秒钟

思考

思考

在你阅读这篇文章之前,先问自己一个问题:你上一次真正深度思考是什么时候? 我所说的"深度思考",是指遇到一个具体而困难的问题,然后花费好几天时间专注于解决它的那种状态。 你的答案是什么? * a) 经常如此 * b) 从来没有 * c) 介于两者之间 如果你的答案是 (a) 或 (b),这篇文章可能不适合你。但如果像我一样,你的答案是 (c),那么这篇文章或许能引起你的共鸣,至少让你知道,你并不孤单。 首先声明:这篇文章没有答案,甚至没有建议。它只是我最近几个月内心感受的一次宣泄。 建造者与思考者 我相信我的性格建立在两个主要特质之上: 1. 建造者(渴望创造、交付和务实) 2. 思考者(需要深度、持久的智力挑战) 建造者这一面很容易理解,它追求速度和实用性。这是我渴望将"想法"转化为"现实&