一次特别的性能问题排查

一次特别的性能问题排查
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

心智难民

心智难民

心智,按照牛津词典的定义,是获取和运用知识的能力。 互联网是一场技术革命,给每个人提供了机会。社会是由阶层组成的,每一场技术革命都促使了不同阶层的重新洗牌,或者说阶层分化。网络世界的阶层分化是什么样的呢?大概可以分为两个大的阶层:一类是接受高质量信息的精英阶层,另外一类是消费网络上的垃圾信息、接受劣质信息的乌合之众。 当然,这里说的“免费”是打引号的。因为它不仅不免费,而且一点也不便宜。 人们喜欢免费的东西。但是世界上除了阳光和空气,没什么是真正免费的东西,只是支付的方式不一样——有的直接用钱付,有的间接用钱付;有些用生活质量付,有些用人生的潜力和机会付。 You must pay for everything in this world, one way or another. Nothing is free. 你终究会以不同的方式付费,天下没有免费的午餐。 如果一个人只接受网上“免费”的信息,就像是只吃劣质食品一样,结果就是精神世界的劣质化。因为接受信息质量的差异,

By 王圆圆
Crazy World

Crazy World

by Jeff Daniels 译文 我看见一个年轻女孩笑了, 因为他刚说的话。 我看着他坠入她那双美丽的眼睛里, 脸红的像玫瑰。 我看见一位老人在走路, 妻子陪在他身旁。 我看着他俯身握住她的手, 天啊,我竟然哭了。 这疯狂的世界越来越疯狂, 我有什么资格评判呢? 但值得庆幸的是, 在这个充满仇恨的世界里, 还有人在用心相爱着。 我看见狗摇着尾巴, 看见孩子在奔跑。 我也曾在无数个日落里, 对着夕阳唱着歌。 我看见有人为别人扶着门, 看见陌生人握手寒暄。 我看见她和那个曾经错过的旧情人拥吻, 时间比计划中的更长了一些。 这个疯狂的世界继续疯狂着, 但我能说什么? 好在这个充满恨的世界里, 还有人在用心相爱着。 我看见祈祷被回应, 看见了六月里的新娘。 我骄傲地说,我当时见到了银河, 对着月光下的人们闪烁。 我看见送出的一打玫瑰, 见过她满心的欢喜藏不住, 我见过的已经足够, 让我明白我所知道的, 也坚信我依然相信的。 这疯狂的世界越来越疯狂, 我能说什么? 但值得庆幸的是, 在这个充满仇恨的世界里, 还有人相爱着。 原文 I’ve seen a

By 王圆圆
人是能被改变的吗?

人是能被改变的吗?

想改变别人基本上是在浪费时间。这个话题听起来简单,但仔细想想,我们生活中有太多时候都在做这种徒劳的事。 生活中的人大概可以分成三类: 喜欢的人 - 这些人即使有缺点你也能接受。你们相处舒服,他们做什么你都能理解,就算偶尔看不惯,也不会想着要去改造他们。 无所谓的人 - 占了我们生活中的大多数。同事、路人、网上的陌生人,他们怎么生活、怎么思考,其实跟你一点关系都没有。 讨厌的人 - 那些让你感到不舒服的人。可能是价值观完全相反,可能是行为方式你无法忍受。 既然人际关系本来就是这样,为什么还要费劲去改变谁呢?尤其是那些无所谓的人和讨厌的人,你花时间去说服他们、纠正他们,最后累的是自己。有这个功夫,不如多看两本书,学点新东西,改变一下自己。 美国人教小孩一个词:Walk Away。意思就是遇到麻烦的人、不讲理的人,转身走就完了,不用纠缠。 这听起来好像是逃避,但其实是一种很成熟的处理方式。你不是害怕对方,而是知道跟这种人浪费时间没有意义。 有个作家Charles Portis说过一句话挺有意思的:"

By 王圆圆
留守的代价

留守的代价

我有一个90后的朋友,她的故事让我久久无法平静。 她13岁那年,初中还没读完就辍学了,跟着同乡去了南方打工。六年后,在家人的安排下,她嫁给了邻村一个老实人家的儿子。没有恋爱,没有了解,只有两个家庭觉得"差不多,能过"的判断。 婚后他们一起在宁波工作,陆续有了两个女儿。按理说,一家四口,日子虽苦但也算完整。但我们那个地方,重男轻女的观念像一只看不见的手,推着她生下了第三个孩子——终于是个儿子。 三个孩子陆续到了上学的年龄,他们却一直在外打工。孩子成了留守儿童,跟着爷爷奶奶在老家,一年见父母一两次。视频通话里,孩子越来越沉默,成绩越来越差,老师反映性格也出现了问题。 她做了一个决定:回家照顾孩子。 他继续在外地送快递。从此,这个家庭被一分为二——一边是她独自面对三个问题儿童的混乱和辛苦,一边是他在城市里每天十几个小时的奔波劳累。 本来就没什么感情基础的两个人,在这种分离中,最后那点维系也消磨殆尽了。 最近两年,他给家里的生活费越来越少。后来她才知道,他在外面有了别人,赚的钱不多,都花在了新欢身上。

By 王圆圆