本文记录了一次生产环境中遇到的性能问题排查和优化过程,包括Redis内存泄漏和数据库IO过高的诊断与解决。

问题背景

最近新开发的一个区块链数据同步和查询服务,主要负责从链上同步区块、交易等数据,并提供查询接口。服务运行一段时间后,监控系统开始告警:

  • 磁盘读取IO异常高
  • 磁盘写入IO持续
  • Redis内存持续增长

然后出现服务异常。

问题排查过程

第一步:监控数据分析

首先,我们分析了监控数据,发现:

  1. 读取IO远高于写入IO:说明主要问题在读取操作
  2. Redis内存增长曲线:呈线性增长,没有稳定趋势
  3. 问题出现时机:在历史区块同步和大量交易处理时尤为明显

第二步:代码审查

基于监控数据,我们重点审查了以下几个关键模块:

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

实施

  1. 定义缓存TTL常量(如5分钟或24小时,根据业务需求)
  2. 修改缓存函数,为单个数据缓存设置TTL
  3. 确保列表缓存和单个缓存都有合理的过期策略

预期效果

  • 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%

经验总结

技术层面

  1. 缓存设计原则
    • 所有缓存都应该设置合理的TTL
    • 定期审查缓存策略,避免内存泄漏
    • 建立缓存监控和告警机制
  2. 数据库优化原则
    • 优先使用批量操作替代循环中的单独操作
    • 对于存在性检查,使用批量查询
    • 合理使用数据库的批量操作特性(如 IN 查询、批量插入等)
  3. 性能监控
    • 建立完善的监控体系(IO、内存、查询性能等)
    • 设置合理的告警阈值
    • 定期分析监控数据,发现潜在问题

流程层面

  1. 代码审查
    • 代码审查时应关注性能问题
    • 特别关注缓存和数据库操作
    • 建立性能检查清单
  2. 测试策略
    • 进行压力测试,验证性能
    • 长期运行测试,发现内存泄漏等问题
    • 监控测试环境的资源使用情况
  3. 文档维护
    • 记录性能优化方案和效果
    • 维护性能监控文档
    • 分享经验教训

预防措施

  1. 代码规范
    • 所有缓存操作必须设置TTL
    • 批量操作优先于循环操作
    • 数据库查询优先使用批量查询
  2. 监控告警
    • Redis内存使用率告警(>80%)
    • 磁盘IO告警(>阈值)
    • 数据库慢查询告警
  3. 定期审查
    • 定期审查缓存使用情况
    • 定期审查数据库查询性能
    • 定期进行性能测试

希望这次经验能够帮助到遇到类似问题的开发者。性能优化是一个持续的过程,需要我们在日常开发中保持警惕,建立良好的编码习惯和监控机制。


孟斯特

声明:本作品采用署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0)进行许可,使用时请注明出处。
Author: mengbin
blog: mengbin
Github: mengbin92
腾讯云开发者社区:孟斯特