DC娱乐网

这个主流大法竟是大坑!Redis 大 Key 问题怎么破?

作者介绍刘宇,翼支付云原生存储领域资深专家,深耕有状态服务云原生化全链路实践,聚焦分布式数据库核心技术攻坚与开发运维一体

作者介绍

刘宇,翼支付云原生存储领域资深专家,深耕有状态服务云原生化全链路实践,聚焦分布式数据库核心技术攻坚与开发运维一体化体系的构建。

在 Redis 运维中 ,大 Key 始终是威胁集群稳定性的 “ 隐形炸弹”。我梳理 Redis 大 Key 解析系统时 ,在比较了scan查找大key方案 ,以及使用官方的redis-cli --bigkeys方案后 ,选择 “基于 RDB的离线解析” 方案 —— 它完全不影响在线集群 ,还能获取全量键的精确内存数据 ,特别适合非实时性的大 Key 排查场景(实时的可以用慢查询系统 ,这个以前曾在dbaplus分享过)。最近我用 Golang 实现了这套方案 ,今天就从场景需求、技术实现到实际落地 ,和大家详细分享整个过程。

一、为什么要造这个 “轮子”?

市面上其实有现成的 RDB 解析工具( 比如 Python 写的 redis-rdb-tools ),但在实际运维中,我遇到了两个关键问题:

性能瓶颈:面对 GB 级甚至更大的 RDB 文件 ,Python 工具解析耗时过长( 曾试过解析20GB 的 RDB ,跑了近 1 小时),而运维场景中常需要批量处理多集群的 RDB 文件 ,效率亟待提升;定制化不足:现有工具输出的结果多是通用格式 ,无法直接对接我们内部的运维平台( 比如按业务线分类大 Key、 自动同步结果到 Grafana 面板),需要额外写脚本二次处理。

考虑到 Golang 的并发优势和高性能特性 ,以及能直接编译成二进制文件(方便在不同服务器部署),我决定用 Golang 从零实现—套 RDB 离线解析工具 ,核心目标是:快解析、可定制、易集成。

二、Golang 实现 RDB 解析的核心思路

要解析 RDB 文件 ,首先得理解其二进制格式 ——Redis 的 RDB 文件是按特定协议存储的二进制数据 ,包含数据库选择、键值对数据、过期时间等信息。整个实现过程可以拆成 3 个核心步骤:

1. 准备工作:选择合适的 RDB 解析库

自己手写 RDB 格式解析会耗费大量时间(要处理各种数据类型、压缩格式),Golang 生态中有成熟的 RDB 解析库可以复用, 选择了 https://github.com/HDT3213/rdb (轻量、文档清晰,支持 Redis 6.0 + 的 RDB 格式),本来打算导入这个库的解析能力扩展 ,但研究发现这个库不支持外部导出为内部属性 ,不能完全满足需求 ,考虑再三 ,最后决定通过对项目进行部分魔改 ,通过replace依赖模块指向魔改版本来处理。

2. 核心流程:从 RDB 文件到大 Key 数据

整个解析大key的流程很清晰:备份 RDB 文件 → 读取 RDB 文件 → 解析键值对与内存信息 →筛选大 Key → 输出结果 ,下面—步步拆解关键代码。

步骤 1 :读取 RDB 文件并初始化解析器

首先要打开 RDB 文件 ,然后用 NewDecoder 初始化解析器, 同时定义—个 “结果处理器”(用来接收解析出的每—个键值对数据)。

// 通过一个channel返回解析好的数据,因为有多个rdb要解析,这里传入一个channel统一接收,方便管理 func MyFindBiggestKeys(rdbFilename string, output chan<- RedisData, options ...interface{}) error { var err error if rdbFilename == "" { return errors.New("src file path is required") } rdbFile, err := os.Open(rdbFilename) if err != nil { return fmt.Errorf("open rdb %s failed, %v", rdbFilename, err) } defer func() { _ = rdbFile.Close() }() var dec decoder = core.NewDecoder(rdbFile) if dec, err = wrapDecoder(dec, options...); err != nil { return err } err = dec.Parse(func(object model.RedisObject) bool { data := RedisData{ Data: object, Err: nil, } select { case output <- data: return true case <-time.After(5 * time.Second): err = errors.New("send to output channel timeout") return false } }) if err != nil { return fmt.Errorf("parse rdb failed: %w", err) } return nil }

步骤 2:解析键值对 ,判断是否为大 Key

这是最核心的部分 ,提取对应的内存大小 ,和task预设阈值对比 ,判断是否为大 Key。

func (b *biz) ExecuteSingleTask(ctx context.Context, task *models.Task) error { // 1. 提取任务参数 pwd := task.Dir jobID := task.JobID // 任务指定的大key阈值,由每个task任务传递 size := task.Size // 2. 路径处理 path, err := mypath.GetLastDirAndFiles(pwd) if err != nil { return err } redisName := path.LastDirName files := path.Files slog.Info("process task", "taskID", task.ID, "redisName", redisName, "filesCount", len(files)) // 3. Redis 数据处理 ch := make(chan helper.RedisData, 1000) var wg sync.WaitGroup // 3.1 启动生产者协程(读取文件并发送到 channel) for _, file := range files { currentFile := pwd + "/" + file wg.Add(1) go func(filePath string) { defer wg.Done() if err := helper.MyFindBiggestKeys(filePath, ch); err != nil { slog.Error("producer process file failed", "file", filePath, "err", err.Error()) } }(currentFile) } // 3.2 启动协程:等待生产者完成后关闭 channel go func() { wg.Wait() close(ch) slog.Info("task producer done, channel closed", "taskID", task.ID) }() // 3.3 消费 channel 数据并存储结果( for data := range ch { if data.Err != nil { slog.Error("data error", "error", data.Err) continue } if uint64(data.Data.GetSize()) <= *size { continue }

...

步骤 3: 输出大 Key 结果。

解析完成后 ,需要将大 Key 结果以易读的格式输出。我这里实现了数据入库(方便后续分析或接入运维平台)。

// 构造结构体 rediskey := &models.RedisKey{ JobID: jobID, RedisName: redisName, Key: data.Data.GetKey(), Type: data.Data.GetType(), Size: int64(data.Data.GetSize()), CreatedAt: time.Now(), } if err := b.ResultV1().CreateTaskResult(ctx, rediskey); err != nil { slog.Error("operation failed", "err", err, "key", data.Data.GetKey(), ) } slog.Info("received data", "key", data.Data.GetKey(), "type", data.Data.GetType(), "size", data.Data.GetSize(), ) } return nil }

3. 性能优化:让解析更快

Golang 本身性能已经很好 ,但面对超大 RDB 文件( 比如 20GB 以上),还是需要做—些优化,主要从以下两点入手:

优化 1 :并发解析(利用 Golang 的协程)

RDB 文件是按Redis分片集群来备份导出的 ,核心思路是将不同RDB的解析任务分配到不同协程中 ,提升并行处理效率。具体代码这里就不展开了 ,需要注意协程安全 ,用 sync.WaitGroup 等待所有协程完成。

优化 2:减少内存占用

解析大 RDB 文件时 ,容易出现内存暴涨( 比如解析 20GB 的 RDB ,可能需要占用几十 GB 内存)。可以通过 “边解析边输出” 的方式优化:解析出—个大 Key 后 ,立即写入channel ,而不是先存在切片中(避免大量数据堆积在内存),入库的协程读取channel进行插入数据库。

修改思路:将key ,parse过程中判断是大 Key 后 ,直接写入channel文件 ,无需存储到切片。

三、实际落地:从代码到运维工具

代码写完后 ,还需要做—些 “工程化” 处理 ,让它成为真正能用的运维工具:

1. 编译成二进制文件

Golang 可以跨平台编译 ,通过 Makefile可以快速支持编译各种平台的二进制文件 ,并且快速启动调试:

# 运行程序(用于开发调试) run: @echo "运行程序 ..." go run $(MAIN_FILE) -c configs/rdb-server.yaml # 交叉编译:生成Linux-amd64架构的可执行文件 build-linux: @echo "编译Linux-amd64架构程序 ..." mkdir -p $(OUTPUT_DIR) GOOS=linux GOARCH=amd64 go build $(GO_BUILD_FLAGS) -o $(OUTPUT_DIR)/$(BINARY_NAME)-linux-amd64 $(MAIN_FILE) @echo "Linux版本编译完成:$(OUTPUT_DIR)/$(BINARY_NAME)-linux-amd64" # 交叉编译:生成Windows-amd64架构的可执行文件 build-windows: @echo "编译Windows-amd64架构程序 ..." mkdir -p $(OUTPUT_DIR) GOOS=windows GOARCH=amd64 go build $(GO_BUILD_FLAGS) -o $(OUTPUT_DIR)/$(BINARY_NAME)-windows-amd64.exe $(MAIN_FILE) @echo "Windows版本编译完成:$(OUTPUT_DIR)/$(BINARY_NAME)-windows-amd64.exe"

将二进制文件放到服务器上 ,直接运行即可:

./rdb-bigkey-linux-amd64 -c configs/rdb-server.yaml

2. 定时任务( 自动执行)

通过定时任务 ,设置每天低峰期自动执行按集群列表执行 RDB 备份任务 ,备份任务完成后将结果入task表 ,任务自动触发大key解析处理。

3. 集成到运维平台

这里没有开发前端 ,而是基于Grafana ,将大 Key 从数据库(如 MySQL)读取 ,再通过Grafana 配置面板 ,展⽰ “每日大 Key 数量趋势”“各业务线大 Key 分布” 等图表 ,实现可视化监控。公司如果有其他运维平台 ,也可以集成进去 ,定制更灵活。

四、踩过的坑与解决方案

在实际测试和使用中 ,我遇到了几个问题 ,这里分享给大家 ,避免踩坑:

坑 1 :RDB 文件格式不兼容

问题:解析某些老版本 Redis(如 Redis 4.0) 的 RDB 文件时 ,出现错误。

原因:采用的库默认支持 Redis 6.0+ ,对老版本个别编码不兼容。

解决方案:更换为支持多版本的库 ,或者对解析程序源码进行二次开发。

坑 2:解析超大 RDB 文件时内存溢出

问题:解析 20GB 的 RDB 文件时 ,程序内存占用超过 40GB ,被系统 kill。

原因:默认情况下 ,解析器会将整个 RDB 文件的键值对加载到内存 ,导致内存暴涨。

解决方案:启用 “流式解析” ,边解析边释放内存 —— 在 Decode 函数中 ,每解析完—个数据库的键值对 ,就立即处理并释放该数据库的数据 ,避免堆积。

五、总结与后续计划

这套 Golang 实现的 RDB 大 Key 解析工具 , 目前已经在我们公司的 Redis 集群中稳定运行了 2个月 ,相比之前的 Python 工具 ,解析速度提升了 3-5 倍(解析 20GB RDB 文件从 1 小时缩短到15 分钟左右),而且支持自定义阈值和结果输出格式 ,非常灵活。

后续我计划在现有基础上增加两个功能:

业务线自动分类:根据键名前缀(如 user:info: 属于用戶业务 , order:detail: 属于订单业务), 自动给大 Key 打上业务标签 ,方便定位责任团队;大 Key 增长趋势分析:将每天的大 Key 结果存入数据库 ,对比分析 “某键是否连续 3 天为大Key”“内存是否持续增长” ,提前预警潜在风险。

如果你也在做 Redis 大 Key 治理 ,希望这篇实战分享能给你带来帮助。如果有更好的优化思路或问题 ,欢迎交流!

dbaplus社群欢迎广大技术人员投稿,投稿邮箱:editor@dbaplus.cn