本文记录了一次生产环境中遇到的性能问题排查和优化过程,包括Redis内存泄漏和数据库IO过高的诊断与解决。
问题背景
最近新开发的一个区块链数据同步和查询服务,主要负责从链上同步区块、交易等数据,并提供查询接口。服务运行一段时间后,监控系统开始告警:
- 磁盘读取IO异常高
- 磁盘写入IO持续
- Redis内存持续增长
然后出现服务异常。
问题排查过程
第一步:监控数据分析
首先,我们分析了监控数据,发现:
- 读取IO远高于写入IO:说明主要问题在读取操作
- Redis内存增长曲线:呈线性增长,没有稳定趋势
- 问题出现时机:在历史区块同步和大量交易处理时尤为明显
第二步:代码审查
基于监控数据,我们重点审查了以下几个关键模块:
1. Redis缓存策略
检查缓存相关的代码,发现了一个严重问题:
- 列表缓存:通过
LTrim限制了大小(保留最近100条),这部分正常 - 单个数据缓存:通过唯一标识(如区块高度、交易哈希)作为key缓存,但没有设置过期时间
这意味着每个区块和交易一旦被缓存,就会永久保存在Redis中,永远不会被清理。
数据量估算:
- 假设每天产生1万个区块,5万笔交易
- 每个区块约10KB,每笔交易约5KB
- 30天后:区块缓存约3GB,交易缓存约7.5GB,总计超过10GB且持续增长
2. 数据库查询模式
检查数据库操作代码,发现了几个低效模式:
问题1:大量单独的存在性检查
- 每个区块处理前都要查询一次数据库检查是否存在
- 每个交易处理前都要查询一次数据库检查是否存在
- 历史区块同步时,如果有1000个区块要处理,就是1000次查询
问题2:循环中的单独查询
- 处理交易时,涉及的每个账户都要单独查询一次
- 一个交易可能涉及多个账户,查询次数成倍增加
- 账户余额更新时,每个余额都是单独的查询+更新操作
问题3:缺少批量操作
- 所有操作都是单个处理,没有利用批量查询的优势
- 历史区块同步虽然有并发处理,但每个区块仍然是单独查询
第三步:根因分析
通过代码审查,我们确定了问题的根本原因:
Redis内存泄漏
- 原因:缓存条目没有设置TTL(Time To Live),导致永久保存
- 影响:内存持续增长,最终可能导致Redis OOM
- 严重程度:高
数据库IO过高
- 原因1:大量单独的存在性检查查询,没有使用批量查询
- 原因2:循环中的单独查询,没有批量获取
- 原因3:账户余额更新使用循环中的单独操作,效率低下
- 影响:读取IO达到370MB/s,写入IO持续9MB/s
- 严重程度:高
解决方案设计
方案1:修复Redis内存泄漏
思路:为所有缓存条目设置合理的TTL
实施:
- 定义缓存TTL常量(如5分钟或24小时,根据业务需求)
- 修改缓存函数,为单个数据缓存设置TTL
- 确保列表缓存和单个缓存都有合理的过期策略
预期效果:
- Redis内存使用稳定,不再无限增长
- 内存使用降低70-80%
方案2:优化数据库查询
思路:使用批量操作替代单独操作
2.1 批量存在性检查
问题场景:历史区块同步时,需要检查大量区块是否存在
优化方案:
- 实现批量存在性检查函数,一次查询检查多个区块
- 在同步前先批量检查,只处理不存在的区块
- 将 N 次查询减少到 1 次批量查询
预期效果:
- 查询次数减少80-90%
- 读取IO显著降低
2.2 批量账户查询
问题场景:处理交易时,需要查询多个账户信息
优化方案:
- 实现批量账户查询函数
- 收集所有需要查询的账户,一次性批量查询
- 将 N 次查询减少到 1 次批量查询
预期效果:
- 查询次数减少70-80%
- 读取IO进一步降低
2.3 优化账户余额更新
问题场景:更新账户余额时,每个余额都是单独的查询+更新
优化方案:
- 使用批量upsert操作
- 在单个事务中处理所有余额更新
- 使用数据库的
ON DUPLICATE KEY UPDATE或类似机制
预期效果:
- 操作次数从 2N 减少到 N
- 写入IO降低20-30%
经验总结
技术层面
- 缓存设计原则
- 所有缓存都应该设置合理的TTL
- 定期审查缓存策略,避免内存泄漏
- 建立缓存监控和告警机制
- 数据库优化原则
- 优先使用批量操作替代循环中的单独操作
- 对于存在性检查,使用批量查询
- 合理使用数据库的批量操作特性(如
IN查询、批量插入等)
- 性能监控
- 建立完善的监控体系(IO、内存、查询性能等)
- 设置合理的告警阈值
- 定期分析监控数据,发现潜在问题
流程层面
- 代码审查
- 代码审查时应关注性能问题
- 特别关注缓存和数据库操作
- 建立性能检查清单
- 测试策略
- 进行压力测试,验证性能
- 长期运行测试,发现内存泄漏等问题
- 监控测试环境的资源使用情况
- 文档维护
- 记录性能优化方案和效果
- 维护性能监控文档
- 分享经验教训
预防措施
- 代码规范
- 所有缓存操作必须设置TTL
- 批量操作优先于循环操作
- 数据库查询优先使用批量查询
- 监控告警
- Redis内存使用率告警(>80%)
- 磁盘IO告警(>阈值)
- 数据库慢查询告警
- 定期审查
- 定期审查缓存使用情况
- 定期审查数据库查询性能
- 定期进行性能测试
希望这次经验能够帮助到遇到类似问题的开发者。性能优化是一个持续的过程,需要我们在日常开发中保持警惕,建立良好的编码习惯和监控机制。
声明:本作品采用署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0)进行许可,使用时请注明出处。
Author: mengbin
blog: mengbin
Github: mengbin92
腾讯云开发者社区:孟斯特
—