一个 Golang 编写的在线 Redis 内存分析工具 - V2EX
V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
winjeg
V2EX    问与答

一个 Golang 编写的在线 Redis 内存分析工具

  •  
  •   winjeg 2018-12-16 16:26:14 +08:00 383 次点击
    这是一个创建于 2501 天前的主题,其中的信息可能已经有所发展或是发生改变。

    redis 内存分析工具 rma4go

    redis 是一个很有名的内存型数据库,这里不做详细介绍。而rma4go (redis memory analyzer for golang) 是一个 redis 的内存分析工具,这个工具的主要作用是针对运行时期的 redis 进行内存的分析,统计 redis 中 key 的分布情况, 各种数据类型的使用情况,key 的 size,大 key 的数量及分布, key 的过期状况分布等一些有助于定位 redis 使用问题的工具,希望这能够给应用开发者提供便利排查生产中所遇到的实际问题。

    rma4go的应用场景

    redis 是目前很流行的一个内存型数据库,很多企业都在使用。 但由于业界并没有很多对于 redis 使用上的规范,或者是有一些规范并没有被很好的遵循, 存在很多 redis 使用上的问题,我这边就列举一些例子:

    1. redis 存用满了, 不知道 key 的分布情况,不知道来源于那个应用
    2. redis 被 block 了,不知道什么原因导致的 block,是哪个应用里的什么 key 的操作导致的
    3. 想迁移 redis 数据,或者调整一些设置,但不知道要不要对 redis 里的数据进行保留,以及不知道什么业务在使用等
    4. redis 的 key 的过期情况不明朗, 不知道哪些东西可以删除或者调整 其实上面的一些问题是我随便列举出来的一些,并不是所有的存在的问题,相信也有很多其他场景同样会用到这样的一个 redis 内存分析工具rma4go

    rma4go 的具体功能

    数据维度

    对于 key 的分析我们这个工具会提供如下几个维度的数据:

    • key 的数量分布维度
    • key 的过期分布维度
    • key 的类型分布维度
    • key 对应的的数据的大小分布维度
    • key 的前缀分布维度
    • 慢 key 与大 key 的维度

    当然以后如果发现有更好的纬度也会添加进去,目前先以这几个纬度为主

    数据类型设计

    type RedisStat struct { All KeyStat `json:"all"` String KeyStat `json:"string"` Hash KeyStat `json:"hash"` Set KeyStat `json:"set"` List KeyStat `json:"list"` ZSet KeyStat `json:"zset"` Other KeyStat `json:"other"` BigKeys KeyStat `json:"bigKeys"` } // distributions of keys of all prefixes type Distribution struct { KeyPattern string `json:"pattern"` Metrics } // basic metrics of a group of key type Metrics struct { KeyCount int64 `json:"keyCount"` KeySize int64 `json:"keySize"` DataSize int64 `json:"dataSize"` KeyNeverExpire int64 `json:"neverExpire"` ExpireInHour int64 `json:"expireInHour"` // >= 0h < 1h ExpireInDay int64 `json:"expireInDay"` // >= 1h < 24h ExpireInWeek int64 `json:"expireInWeek"` // >= 1d < 7d ExpireOutWeek int64 `json:"expireOutWeek"` // >= 7d } 

    实现细节

    key 元信息

    type KeyMeta struct { Key string KeySize int64 DataSize int64 Ttl int64 Type string } 

    众所周知,redis 里的所有的数据基本都是由 key 的, 也是根据 key 进行操作的,那么对 redis 里的 key 进行分析我们必须要记录下来这个 key 的信息才可以做到, 我们能记录的信息正如以上结构中的一样,key 本身,key 的大小, 数据的大小, 过期时间以及 key 的类型。这些信息是我们对 key 进行分析的一个基础信息,都可以通过一些简单的 redis 命令就可以取到。

    遍历 redis 所有 key

    要对一个 redis 进行完整的 key 分析, 我们就需要有办法能够访问到所有 key 的源信息, 所幸 redis 提供了 scan这么一种方式可以比较轻量的遍历所有的 key,访问到相应的 key 的元信息。 这样对于 redis 而言, 进行在线 key 分析的时候造成的压力也不会非常大,当然 key 分析不能再 QPS 高峰期进行, 需要在 redis 资源余量允许的情况下进行分析。

    另外由于 redis 本身的一个内存清理机制,有 25%的过期占用可以在分析 key 的时候被清理掉, 因此这个分析工具同时兼具了清理一部分内存的作用, 如果 redis 里面存在过期的而且存在于内存里面的 key 的话。

    对记录的信息进行分析与汇总

    有了遍历所有 key 的方法, 又有了元数据, 剩下的事情就是把这些数据进行聚合汇总, 这个主要是一个算法上的工作, 最难的部分要数这个 key 聚合的部分了, 这里面有很多取舍, 由于作者我本人不是专攻算法的, 而且没有找到合适的库, 因此只能动手自己想了一种方式。 基本的思路是:

    压缩的算法
    1. 对于每个新的 key 的元信息, 添加到老的 key 分析对象里去
    2. 对这个 key 从后往前缩短, 去除尾部,看是否已经包含这个 key 的统计信息,如果包含, 则把 key 的信息累加上去, 如果不包含则创建一个新的纪录。
    3. 当记录的个数添加到一定数量的时候, 对对象的个数进行一次压缩
      • 压缩的算法也是从字符串的末尾往字符串首部进行压缩
      • 当压缩不能增加这个 pattern 的 key 的个数的时候使用原来的 key (压缩前的 key )
      • 当压缩可以增加这个 pattern 的 key 的个数的时候,进行 key 的合并,把 pattern 设置成压缩后的 pattern
      • 当记录的条数超过指定的条数就循环往复,直到压缩到小于指定的条数为止
      • 如果对于 key 的最小长度(就算再压缩也要保留一两位)有要求, 有一些压缩到字符串的最小长度的参数可以进行调整与设置, 进行一定的取舍。
    4. 直到 scan 完毕
    代码如下
    const ( defaultSize = 128 compactNum = 30 maxLeftNum = 150 minKeyLenLower = 2 minKeyLen = 5 ) func (stat *KeyStat) compact() { distMap := stat.Distribution tmpMap := make(map[string][]string, defaultSize) shrinkTo := compactNum for k := range distMap { compactedKey := k if orgks, ok := tmpMap[compactedKey]; ok { orgks = append(orgks, k) tmpMap[compactedKey] = orgks } else { ks := make([]string, 0, defaultSize) ks = append(ks, k) tmpMap[compactedKey] = ks } } shrinkTo-- for (len(tmpMap) > compactNum && shrinkTo >= minKeyLen) || (len(tmpMap) > maxLeftNum && shrinkTo >= minKeyLenLower) { tnMap := make(map[string][]string, defaultSize) for k := range tmpMap { // shrink if len(k) > shrinkTo { compactedKey := k[0:shrinkTo] if oik, ok := tnMap[compactedKey]; ok { oik = append(oik, tmpMap[k]...) tnMap[compactedKey] = oik } else { ks := make([]string, 0, defaultSize) ks = append(ks, tmpMap[k]...) tnMap[compactedKey] = ks } } else { tnMap[k] = tmpMap[k] } } // 如果此次 shrink 没有使得这个集合的元素数量增加, 就使用原来的 key for k := range tmpMap { if len(k) > shrinkTo { ck := k[0:shrinkTo] if len(tnMap[ck]) == len(tmpMap[k]) && len(tnMap[ck]) > 1 { x := make([]string, 0, defaultSize) tnMap[k] = append(x, tnMap[ck]...) delete(tnMap, ck) } } } tmpMap = tnMap shrinkTo -- } dists := make(map[string]Distribution, defaultSize) for k, v := range tmpMap { if len(v) > 1 { var nd Distribution for _, dk := range v { d := distMap[dk] nd.KeyPattern = k + "*" nd.KeyCount += d.KeyCount nd.KeySize += d.KeySize nd.DataSize += d.DataSize nd.ExpireInHour += d.ExpireInHour nd.ExpireInWeek += d.ExpireInWeek nd.ExpireInDay += d.ExpireInDay nd.ExpireOutWeek += d.ExpireOutWeek nd.KeyNeverExpire += d.KeyNeverExpire } dists[k] = nd } else { for _, dk := range v { nd := distMap[dk] nd.KeyPattern = dk + "*" dists[dk] = nd } } } stat.Distribution = dists } 

    在线 key 分析的 github 项目

    rma4go 这是一个我已经写好的项目, 它使用起来非常简单

    构建方法

    1. 构建之前请确保 golang sdk 已经安装, 并且版本 >=1.11.0
    2. 请确保已经具备翻墙的环境, 因为它要下载一些依赖,可能来自墙外 翻墙方法如下
    // linux/osx export http_proxy=somehost:port export https_proxy=somehost:port // windows set http_proxy=somehost:port set https_proxy=somehost:port 
    1. 构建
    git clone [email protected]:winjeg/rma4go.git cd rma4go go build . 

    使用方法

    用法如下:rma4go -h

    rma4go usage: rma4go -r some_host -p 6379 -a password -d 0 ====================================================== -H string address of a redis (default "localhost") -a string password/auth of the redis -d int db of the redis to analyze -h help content -p int port of the redis (default 6379) -r string address of a redis (default "localhost") 

    示例输出

    all keys statistics | PATTERN | KEY NUM | KEY SIZE | DATA SIZE | EXPIRE IN HOUR | EXPIRE IN DAY | EXPIRE IN WEEK | EXPIRE OUT WEEK | NEVER EXPIRE | |---------|---------|----------|-----------|----------------|---------------|----------------|-----------------|--------------| | total | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | string keys statistics | PATTERN | KEY NUM | KEY SIZE | DATA SIZE | EXPIRE IN HOUR | EXPIRE IN DAY | EXPIRE IN WEEK | EXPIRE OUT WEEK | NEVER EXPIRE | |---------|---------|----------|-----------|----------------|---------------|----------------|-----------------|--------------| | total | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | list keys statistics | PATTERN | KEY NUM | KEY SIZE | DATA SIZE | EXPIRE IN HOUR | EXPIRE IN DAY | EXPIRE IN WEEK | EXPIRE OUT WEEK | NEVER EXPIRE | |---------|---------|----------|-----------|----------------|--------------|----------------|-----------------|--------------| | total | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | hash keys statistics | PATTERN | KEY NUM | KEY SIZE | DATA SIZE | EXPIRE IN HOUR | EXPIRE IN DAY | EXPIRE IN WEEK | EXPIRE OUT WEEK | NEVER EXPIRE | |---------|---------|----------|-----------|----------------|---------------|----------------|-----------------|--------------| | total | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | set keys statistics | PATTERN | KEY NUM | KEY SIZE | DATA SIZE | EXPIRE IN HOUR | EXPIRE IN DAY | EXPIRE IN WEEK | EXPIRE OUT WEEK | NEVER EXPIRE | |---------|---------|----------|-----------|----------------|---------------|----------------|-----------------|--------------| | total | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | zset keys statistics | PATTERN | KEY NUM | KEY SIZE | DATA SIZE | EXPIRE IN HOUR | EXPIRE IN DAY | EXPIRE IN WEEK | EXPIRE OUT WEEK | NEVER EXPIRE | |---------|---------|----------|-----------|----------------|---------------|----------------|-----------------|--------------| | total | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | other keys statistics | PATTERN | KEY NUM | KEY SIZE | DATA SIZE | EXPIRE IN HOUR | EXPIRE IN DAY | EXPIRE IN WEEK | EXPIRE OUT WEEK | NEVER EXPIRE | |---------|---------|----------|-----------|----------------|---------------|----------------|-----------------|--------------| | total | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | big keys statistics | PATTERN | KEY NUM | KEY SIZE | DATA SIZE | EXPIRE IN HOUR | EXPIRE IN DAY | EXPIRE IN WEEK | EXPIRE OUT WEEK | NEVER EXPIRE | |---------|---------|----------|-----------|----------------|---------------|----------------|-----------------|--------------| | total | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 

    数据是有的, 由于平台的限制, 大家可以去我 github 项目的 readme 去看

    作为依赖使用

    获取方法如下:

    go get github.com/winjeg/rma4go 

    使用方法如下:

    func testFunc() { h := "localhost" a := "" p := 6379 cli := client.BuildRedisClient(client.ConnInfo{ Host: h, Auth: a, Port: p, }, cmder.GetDb()) stat := analyzer.ScanAllKeys(cli) // print in command line stat.Print() // the object is ready to use } 

    github 维护(主要阵地)

    1. 欢迎其他开发者加入
    2. 欢迎提 issue 反馈问题
    3. 欢迎任何有意义的建议
    4. 另外欢迎 star,不建议 fork,建议直接提交 PR ; )
    目前尚无回复
    关于     帮助文档     自助推广系统     博客     API     FAQ     Solana     922 人在线   最高记录 6679       Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 25ms UTC 21:15 PVG 05:15 LAX 14:15 JFK 17:15
    Do have faith in what you're doing.
    ubao msn snddm index pchome yahoo rakuten mypaper meadowduck bidyahoo youbao zxmzxm asda bnvcg cvbfg dfscv mmhjk xxddc yybgb zznbn ccubao uaitu acv GXCV ET GDG YH FG BCVB FJFH CBRE CBC GDG ET54 WRWR RWER WREW WRWER RWER SDG EW SF DSFSF fbbs ubao fhd dfg ewr dg df ewwr ewwr et ruyut utut dfg fgd gdfgt etg dfgt dfgd ert4 gd fgg wr 235 wer3 we vsdf sdf gdf ert xcv sdf rwer hfd dfg cvb rwf afb dfh jgh bmn lgh rty gfds cxv xcv xcs vdas fdf fgd cv sdf tert sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf shasha9178 shasha9178 shasha9178 shasha9178 shasha9178 liflif2 liflif2 liflif2 liflif2 liflif2 liblib3 liblib3 liblib3 liblib3 liblib3 zhazha444 zhazha444 zhazha444 zhazha444 zhazha444 dende5 dende denden denden2 denden21 fenfen9 fenf619 fen619 fenfe9 fe619 sdf sdf sdf sdf sdf zhazh90 zhazh0 zhaa50 zha90 zh590 zho zhoz zhozh zhozho zhozho2 lislis lls95 lili95 lils5 liss9 sdf0ty987 sdft876 sdft9876 sdf09876 sd0t9876 sdf0ty98 sdf0976 sdf0ty986 sdf0ty96 sdf0t76 sdf0876 df0ty98 sf0t876 sd0ty76 sdy76 sdf76 sdf0t76 sdf0ty9 sdf0ty98 sdf0ty987 sdf0ty98 sdf6676 sdf876 sd876 sd876 sdf6 sdf6 sdf9876 sdf0t sdf06 sdf0ty9776 sdf0ty9776 sdf0ty76 sdf8876 sdf0t sd6 sdf06 s688876 sd688 sdf86