rma4go
redis 是一个很有名的内存型数据库,这里不做详细介绍。而rma4go
(redis memory analyzer for golang) 是一个 redis 的内存分析工具,这个工具的主要作用是针对运行时期的 redis 进行内存的分析,统计 redis 中 key 的分布情况, 各种数据类型的使用情况,key 的 size,大 key 的数量及分布, key 的过期状况分布等一些有助于定位 redis 使用问题的工具,希望这能够给应用开发者提供便利排查生产中所遇到的实际问题。
rma4go
的应用场景redis 是目前很流行的一个内存型数据库,很多企业都在使用。 但由于业界并没有很多对于 redis 使用上的规范,或者是有一些规范并没有被很好的遵循, 存在很多 redis 使用上的问题,我这边就列举一些例子:
rma4go
对于 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
}
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 分析, 我们就需要有办法能够访问到所有 key 的源信息, 所幸 redis 提供了 scan
这么一种方式可以比较轻量的遍历所有的 key,访问到相应的 key 的元信息。
这样对于 redis 而言, 进行在线 key 分析的时候造成的压力也不会非常大,当然 key 分析不能再 QPS 高峰期进行, 需要在 redis 资源余量允许的情况下进行分析。
另外由于 redis 本身的一个内存清理机制,有 25%的过期占用可以在分析 key 的时候被清理掉, 因此这个分析工具同时兼具了清理一部分内存的作用, 如果 redis 里面存在过期的而且存在于内存里面的 key 的话。
有了遍历所有 key 的方法, 又有了元数据, 剩下的事情就是把这些数据进行聚合汇总, 这个主要是一个算法上的工作, 最难的部分要数这个 key 聚合的部分了, 这里面有很多取舍, 由于作者我本人不是专攻算法的, 而且没有找到合适的库, 因此只能动手自己想了一种方式。 基本的思路是:
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
}
rma4go 这是一个我已经写好的项目, 它使用起来非常简单
// linux/osx
export http_proxy=somehost:port
export https_proxy=somehost:port
// windows
set http_proxy=somehost:port
set https_proxy=somehost:port
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
}