dbaplus社群 11月12日 08:56
Golang 实现 Redis 大 Key 离线解析方案
index_new5.html
../../../zaker_core/zaker_tpl_static/wap/tpl_guoji1.html

 

本文作者刘宇分享了如何使用 Golang 编写一套 Redis 大 Key 离线解析工具。该方案旨在解决现有工具性能瓶颈和定制化不足的问题,通过解析 RDB 文件,完全不影响在线集群,并能获取全量键的精确内存数据。文章详细介绍了 Golang 实现的核心思路,包括准备工作(选择 RDB 解析库)、核心流程(从 RDB 文件到大 Key 数据)、性能优化(并发解析和减少内存占用),以及实际落地(编译成二进制文件、定时任务和集成到运维平台)。作者还分享了遇到的坑以及解决方案,并提出了后续的改进计划。

💡 **Golang 实现 RDB 离线解析的必要性与优势**:作者指出,在 Redis 运维中,大 Key 是影响集群稳定性的重要因素。现有的 `scan` 或 `redis-cli --bigkeys` 方案可能影响在线集群性能,而 Python 编写的 RDB 解析工具存在性能瓶颈(解析 GB 级 RDB 文件耗时过长)和定制化不足的问题。因此,作者选择使用 Golang 从零开发一套高性能、可定制、易集成的 RDB 离线解析工具,以实现快速解析并支持与内部运维平台对接。

⚙️ **Golang RDB 解析的核心技术实现**:该方案的核心流程包括准备工作(选择并魔改了 `HDT3213/rdb` 库以满足需求)、核心解析(通过 `NewDecoder` 读取 RDB 文件,并在 `Parse` 函数中定义结果处理器,实时将键值对数据通过 channel 输出,实现边解析边输出以减少内存占用)以及结果处理(将解析出的符合阈值的大 Key 数据结构化后入库)。代码示例展示了如何打开 RDB 文件、初始化解析器、通过 channel 传递数据以及将结果存储到数据库。

🚀 **性能优化与工程化落地**:为了应对超大 RDB 文件,文章强调了 Golang 的并发优势,通过协程实现 RDB 文件的并发解析,并采用“边解析边输出”的流式处理方式,将解析出的数据立即写入 channel,再由消费协程进行处理,有效避免了内存暴涨。在工程化方面,文章介绍了如何将 Golang 代码编译成跨平台的二进制文件,并通过定时任务实现自动化执行,最终将解析结果集成到 Grafana 等运维平台,实现可视化监控和灵活定制。

⚠️ **实战中的经验与挑战**:作者分享了在实践中遇到的两个主要问题:一是 RDB 文件格式不兼容老版本 Redis 的问题,解决方案是更换支持多版本的库或进行源码二次开发;二是解析超大 RDB 文件时内存溢出问题,解决方案是启用“流式解析”,即解析完一个数据库的数据就立即处理并释放,避免数据堆积。这些经验对于读者在类似场景下进行 RDB 解析具有重要的参考价值。

原创 刘宇 2025-11-12 07:15 广东

Golang 实战:基于 RDB 文件的 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统一接收,方便管理
funcMyFindBiggestKeys(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

阅读原文

跳转微信打开

Fish AI Reader

Fish AI Reader

AI辅助创作,多种专业模板,深度分析,高质量内容生成。从观点提取到深度思考,FishAI为您提供全方位的创作支持。新版本引入自定义参数,让您的创作更加个性化和精准。

FishAI

FishAI

鱼阅,AI 时代的下一个智能信息助手,助你摆脱信息焦虑

联系邮箱 441953276@qq.com

相关标签

Golang Redis 大 Key RDB 解析 性能优化 离线解析 运维工具 云原生 Golang Redis Large Key RDB Parsing Performance Optimization Offline Parsing O&M Tool Cloud Native
相关文章