目录

Redis中BigKey解决方案

什么是BigKey?

大key场景

大key问题

如果redis的key较长时,会产生什么样的影响呢?

如何查看Redis中的bigKey?

查看所有BigKey

查看单个Key

Redis 4.0之前的大key的发现与删除方法

Redis 4.0之后的大key的发现与删除方法

大key如何优化

拆分

本地缓存

热Key问题

热Key问题产生的原因

热点Key问题的危害

怎么发现热key

如何解决

有办法在项目运行过程中,自动发现热key,然后程序自动处理么?

饿了吗方案1

饿了吗方案2


Redis中BigKey解决方案

什么是BigKey?

BigKey指的是redis中一些key value值很大,这些key在序列化与反序列化过程中花费的时间很大! 操作bigkey的通常比较耗时,也就意味着阻塞Redis可能性越大!占用的流量同时也会变得很大!

大白话就是bigkey实际指一个key对应的value很大,占用的空间很大!

string长度大于10K,list长度大于10240认为是big bigkeys

大key场景

Redis使用者应该都遇到过大key相关的场景,比如:

1、热门话题下评论、答案排序场景。

2、大V的粉丝列表。

3、使用不恰当,或者对业务预估不准确、不及时进行处理垃圾数据等。

大key问题

由于Redis主线程为单线程模型,大key也会带来一些问题,如:

 1 内存不均:集群模式在slot分片均匀情况下,会出现数据和查询倾斜情况,部分有大key的Redis节点占用内存多,QPS高。

2 阻塞请求:redis为单线程,单value较大读写需要较长的处理时间,会阻塞后续的请求处理;大key相关的删除或者自动过期时,会出现qps突降或者突升的情况,极端情况下,会造成主从复制异常,Redis服务阻塞无法响应请求。大key的体积与删除耗时可参考下表:

key类型 field数量耗时

Hash~100万~1000ms

List~100万~1000ms

Set~100万~1000ms

Sorted Set~100万~1000ms

3 阻塞网络:单value较大时会占用服务器网卡较多带宽,可能会影响该服务器上的其他Redis实例或者应用。

如果redis的key较长时,会产生什么样的影响呢?

我们需要知道Redis是如何存储key和value的:

根结构为RedisServer,其中包含RedisDB(数据库)。而RedisDB实际上是使用Dict(字典)结构对Redis中的kv进行存储的。这里的key即字符串,value可以是string/hash/list/set/zset这五种对象之一。

Dict字典结构中,存储数据的主题为DictHt,即哈希表。而哈希表本质上是一个DictEntry(哈希表节点)的数组,并且使用链表法解决哈希冲突问题

所以在这里实际存储时,key和value都是存储在DictEntry中的。所以基本上来说,大key和大value带来的内存不均和网络IO压力都是一致的,只是key相较于value还多一个做hashcode和比较的过程(链表中进行遍历比较key),会有更多的内存相关开销。

结论:

大key和大value的危害是一致的:内存不均、阻塞请求、阻塞网络

key由于比value需要做更多的操作如hashcode、链表中比较等操作,所以会比value更多一些内存相关开销。

如何查看Redis中的bigKey?

我们以String为例:

存储几个String key进去,然后使用命令!

查看所有BigKey

redis-cli自带的一个命令(不需要连接Redis)。对整个redis进行扫描,寻找较大的key。

redis-cli -h 127.0.0.1 -p 6379 --bigkeys

Redis中有3个String , 可以看到发现一个BigKey key3

获取key3的值看看

该命令使用scan方式对key进行统计,所以使用时无需担心对redis造成阻塞。

输出大概分为两部分,summary之上的部分,只是显示了扫描的过程。

summary部分给出了每种数据结构中最大的Key。

统计出的最大key只有string类型是以字节长度为衡量标准的。

list,set,zset等都是以元素个数作为衡量标准,不能说明其占的内存就一定多。所以,如果你的Key主要以string类型存在,这种方法就比较适合。

我的hash中

 key3值

这种是查看所有的key 是否是bigKey

查看单个Key

debug object


其中serializedlength表示key对应的value序列化之后的字节数

注意:

debug object bigkey本身可能就会比较慢,它本身就会存在阻塞Redis的可能,可能会比较危险、而且不太准确(序列化后的长度);

Redis 4.0之前的大key的发现与删除方法

1、redis-rdb-tools工具。redis实例上执行bgsave,然后对dump出来的rdb文件进行分析,找到其中的大KEY。

2、redis-cli --bigkeys命令。可以找到某个实例5种数据类型(String、hash、list、set、zset)的最大key。

3、自定义的扫描脚本,以Python脚本居多,方法与redis-cli --bigkeys类似。

4、debug object key命令。可以查看某个key序列化后的长度,每次只能查找单个key的信息。官方不推荐。

redis-rdb-tools工具 

关于rdb工具的详细介绍请查看链接https://github.com/sripathikrishnan/redis-rdb-tools,在此只介绍内存相关的使用方法。基本的命令为 rdb -c memory dump.rdb (其中dump.rdb为Redis实例的rdb文件,可通过bgsave生成)。

输出结果如下:

database,type,key,size_in_bytes,encoding,num_elements,len_largest_element

0,hash,hello1,1050,ziplist,86,22,

0,hash,hello2,2517,ziplist,222,8,

0,hash,hello3,2523,ziplist,156,12,

0,hash,hello4,62020,hashtable,776,32,

0,hash,hello5,71420,hashtable,1168,12,

可以看到输出的信息包括数据类型,key、内存大小、编码类型等。Rdb工具优点在于获取的key信息详细、可选参数多、支持定制化需求,结果信息可选择json或csv格式,后续处理方便,其缺点是需要离线操作,获取结果时间较长。

redis-cli --bigkeys命令

Redis-cli --bigkeys是redis-cli自带的一个命令。它对整个redis进行扫描,寻找较大的key,并打印统计结果。

例如redis-cli -p 6379 --bigkeys

#Scanning the entire keyspace to find biggest keys as well as

#average sizes per key type.  You can use -i 0.1 to sleep 0.1 sec

#per 100 SCAN commands (not usually needed).

[00.72%] Biggest hash   found so far 'hello6' with 43 fields

[02.81%] Biggest string found so far 'hello7' with 31 bytes

[05.15%] Biggest string found so far 'hello8' with 32 bytes

[26.94%] Biggest hash   found so far 'hello9' with 1795 fields

[32.00%] Biggest hash   found so far 'hello10' with 4671 fields

[35.55%] Biggest string found so far 'hello11' with 36 bytes

-------- summary -------

Sampled 293070 keys in the keyspace!

Total key length in bytes is 8731143 (avg len 29.79)

Biggest string found 'hello11' has 36 bytes

Biggest   hash found 'hello10' has 4671 fields

238027 strings with 2300436 bytes (81.22% of keys, avg size 9.66)

0 lists with 0 items (00.00% of keys, avg size 0.00)

0 sets with 0 members (00.00% of keys, avg size 0.00)

55043 hashs with 289965 fields (18.78% of keys, avg size 5.27)

0 zsets with 0 members (00.00% of keys, avg size 0.00)

我们可以看到打印结果分为两部分,扫描过程部分,只显示了扫描到当前阶段里最大的key。summary部分给出了每种数据结构中最大的Key以及统计信息。

redis-cli --bigkeys的优点是可以在线扫描,不阻塞服务;缺点是信息较少,内容不够精确。扫描结果中只有string类型是以字节长度为衡量标准的。List、set、zset等都是以元素个数作为衡量标准,元素个数多不能说明占用内存就一定多。

自定义Python扫描脚本

通过strlen、hlen、scard等命令获取字节大小或者元素个数,扫描结果比redis-cli --keys更精细,但是缺点和redis-cli --keys一样,不赘述。

总之,之前的方法要么是用时较长离线解析,或者是不够详细的抽样扫描,离理想的以内存为维度的在线扫描获取详细信息有一定距离。由于在redis4.0前,没有lazy free机制;针对扫描出来的大key,DBA只能通过hscan、sscan、zscan方式渐进删除若干个元素;但面对过期删除键的场景,这种取巧的删除就无能为力。我们只能祈祷自动清理过期key刚好在系统低峰时,降低对业务的影响。

Redis 4.0之后的大key的发现与删除方法

Redis 4.0引入了memory usage命令和lazyfree机制,不管是对大key的发现,还是解决大key删除或者过期造成的阻塞问题都有明显的提升。

在某些业务场景下,Redis大key的问题是难以避免的,但是,memory usage命令和lazyfree机制分别提供了内存维度的抽样算法和异步删除优化功能,这些特性有助于我们在实际业务中更好的预防大key的产生和解决大key造成的阻塞。

下面我们从源码(摘自Redis 5.0.4版本)来理解memory usage和lazyfree的特点。

memory usage

{"memory",memoryCommand,-2,"rR",0,NULL,0,0,0,0,0}
(server.c 285⾏)

void memoryCommand(client c) {
/.../
/计算key大小是通过抽样部分field来估算总大小。/
else if (!strcasecmp(c->argv[1]->ptr,"usage") && c->argc >= 3) {
size_t usage = objectComputeSize(dictGetVal(de),samples);
/...*/
}
}
(object.c 1299⾏)

从上述源码看到memory usage是通过调用objectComputeSize来计算key的大小。我们来看objectComputeSize函数的逻辑。

define OBJ_COMPUTE_SIZE_DEF_SAMPLES 5 /* Default sample size. */
size_t objectComputeSize(robj o, size_t sample_size) {
/...代码对数据类型进行了分类,此处只取hash类型说明/
/.../
/循环抽样个field,累加获取抽样样本内存值,默认抽样样本为5/
while((de = dictNext(di)) != NULL && samples < sample_size) {
ele = dictGetKey(de);
ele2 = dictGetVal(de);
elesize += sdsAllocSize(ele) + sdsAllocSize(ele2);
elesize += sizeof(struct dictEntry);
samples++;
}
dictReleaseIterator(di);
/根据上一步计算的抽样样本内存值除以样本量,再乘以总的filed个数计算总内存值/
if (samples) asize += (double)elesize/samplesdictSize(d);
/.../
}
(object.c 779⾏)

由此,我们发现memory usage默认抽样5个field来循环累加计算整个key的内存大小,样本的数量决定了key的内存大小的准确性和计算成本,样本越大,循环次数越多,计算结果更精确,性能消耗也越多。

我们可以通过Python脚本在集群低峰时扫描Redis,用较小的代价去获取所有key的内存大小。以下为部分伪代码,可根据实际情况设置大key阈值进行预警。

for key in r.scan_iter(count=1000):
redis-cli = '/usr/bin/redis-cli'
configcmd = '%s -h %s -p %s memory usage %s' % (redis-cli, rip,rport,key)
keymemory = commands.getoutput(configcmd)

lazyfree机制

Lazyfree的原理是在删除的时候只进行逻辑删除,把key释放操作放在bio(Background I/O)单独的子线程处理中,减少删除大key对redis主线程的阻塞,有效地避免因删除大key带来的性能问题。在此提一下bio线程,很多人把Redis通常理解为单线程内存数据库, 其实不然。Redis将最主要的网络收发和执行命令等操作都放在了主工作线程,然而除此之外还有几个bio后台线程,从源码中可以看到有处理关闭文件和刷盘的后台线程,以及Redis4.0新增加的lazyfree线程。

/* Background job opcodes */

define BIO_LAZY_FREE 2 /* Deferred objects freeing. */
(bio.h 38⾏)

下面我们以unlink命令为例,来理解lazyfree的实现原理。

(bio.h 38⾏)

下面我们以unlink命令为例,来理解lazyfree的实现原理。

{"unlink",unlinkCommand,-2,"wF",0,NULL,1,-1,1,0,0},
(server.c 137⾏)

void unlinkCommand(client *c) {
delGenericCommand(c,1);
}
(db.c 490⾏)

通过这几段源码可以看出del命令和unlink命令都是调用delGenericCommand,唯一的差别在于第二个参数不一样。这个参数就是异步删除参数。

/* This command implements DEL and LAZYDEL. /
void delGenericCommand(client c, int lazy) {
/.../
int deleted = lazy ? dbAsyncDelete(c->db,c->argv[j]) :
dbSyncDelete(c->db,c->argv[j]);
/.../
}
(db.c 468⾏)

可以看到delGenericCommand函数根据lazy参数来决定是同步删除还是异步删除。当执行unlink命令时,传入lazy参数值1,调用异步删除函数dbAsyncDelete。否则执行del命令传入参数值0,调用同步删除函数dbSyncDelete。我们重点来看异步删除dbAsyncDelete的实现逻辑:

define LAZYFREE_THRESHOLD 64
/定义后台删除的阈值,key的元素大于该阈值时才真正丢给后台线程去删除/
int dbAsyncDelete(redisDb db, robj key) {
/.../
/lazyfreeGetFreeEffort来获取val对象所包含的元素个数/
size_t free_effort = lazyfreeGetFreeEffort(val);

    /* 对删除key进行判断,满足阈值条件时进行后台删除 */
    if (free_effort > LAZYFREE_THRESHOLD && val->refcount == 1) {
        atomicIncr(lazyfree_objects,1);
        bioCreateBackgroundJob(BIO_LAZY_FREE,val,NULL,NULL);
        /*将删除对象放入BIO_LAZY_FREE后台线程任务队列*/
        dictSetVal(db->dict,de,NULL);
        /*将第一步获取到的val值设置为null*/
    }
/*...*/
}
(lazyfree.c 53⾏)

上面提到了当删除key满足阈值条件时,会将key放入BIO_LAZY_FREE后台线程任务队列。接下来我们来看BIO_LAZY_FREE后台线程。

/.../
else if (type == BIO_LAZY_FREE) {
if (job->arg1)
/* 后台删除对象函数,调用decrRefCount减少key的引用计数,引用计数为0时会真正的释放资源 /
lazyfreeFreeObjectFromBioThread(job->arg1);
else if (job->arg2 && job->arg3)
/ 后台清空数据库字典,调用dictRelease循环遍历数据库字典删除所有key /
lazyfreeFreeDatabaseFromBioThread(job->arg2,job->arg3);
else if (job->arg3)
/ 后台删除key-slots映射表,在Redis集群模式下会用*/
lazyfreeFreeSlotsMapFromBioThread(job->arg3);
}
(bio.c 197⾏)

unlink命令的逻辑可以总结为:执行unlink调用delGenericCommand函数传入lazy参数值1,来调用异步删除函数dbAsyncDelete,将满足阈值的大key放入BIO_LAZY_FREE后台线程任务队列进行异步删除。类似的后台删除命令还有flushdb async、flushall async。它们的原理都是获取删除标识进行判断,然后调用异步删除函数emptyDbAsnyc来清空数据库。这些命令具体的实现逻辑可自行查看flushdbCommand部分源码,在此不做赘述。

除了主动的大key删除和数据库清空操作外,过期key驱逐引发的删除操作也会阻塞Redis服务。因此Redis4.0除了增加上述三个后台删除的命令外,还增加了4个后台删除配置项,分别为slave-lazy-flush、lazyfree-lazy-eviction、lazyfree-lazy-expire和lazyfree-lazy-server-del。

slave-lazy-flush:slave接收完RDB文件后清空数据选项。建议大家开启slave-lazy-flush,这样可减少slave节点flush操作时间,从而降低主从全量同步耗时的可能性。

lazyfree-lazy-eviction:内存用满逐出选项。若开启此选项可能导致淘汰key的内存释放不够及时,内存超用。

lazyfree-lazy-expire:过期key删除选项。建议开启。

lazyfree-lazy-server-del:内部删除选项,比如rename命令将oldkey修改为一个已存在的newkey时,会先将newkey删除掉。如果newkey是一个大key,可能会引起阻塞删除。建议开启。

上述四个后台删除相关的参数实现逻辑差异不大,都是通过参数选项进行判断,从而选择是否采用dbAsyncDelete或者emptyDbAsync进行异步删除。

大key如何优化

优化big key的原则就是string减少字符串长度,list、hash、set、zset等减少成员数;

string长度大于10K,list长度大于10240认为是big bigkeys

拆分

如果对象是整存争取

将对象拆分后才能多个小key-value,get不同的key或者批量获取stringRedisTemplate.opsForValue() .multiGet(keyList)

如果对象是部分更新获取数据

可以分拆成几个key-value,也可以存储在hash中,部分更新部分存取!

如果是hash ,set,zset ,list 等元素

固定一个桶的数量,比如1000,每次存取的时候,先在本地计算field的hash值,模除1000,确定该field落在哪个key上。

newHashKey = hashKey + (hash(field) % 1000);

hset(newHashKey, field, value);

hget(newHashKey, field)

set, zset, list 也可以类似上述做法!

本地缓存

减少访问redis次数,降低危害减少访问redis次数,降低危害! 当然本地开销也会变大!

热Key问题

热key问题就是,突然有几十万的请求去访问redis上的某个特定key。那么,这样会造成流量过于集中,达到物理网卡上限,从而导致这台redis的服务器宕机。

那接下来这个key的请求,就会直接怼到你的数据库上,导致你的服务不可用。

热Key问题产生的原因

1、用户消费的数据远大于生产的数据(热卖商品、热点新闻、热点评论、明星直播)。

在日常工作生活中一些突发的的事件,例如:双十一期间某些热门商品的降价促销,当这其中的某一件商品被数万次点击浏览或者购买时,会形成一个较大的需求量,这种情况下就会造成热点问题。

同理,被大量刊发、浏览的热点新闻、热点评论、明星直播等,这些典型的读多写少的场景也会产生热点问题。

2、请求分片集中,超过单 Server 的性能极限。

在服务端读数据进行访问时,往往会对数据进行分片切分,此过程中会在某一主机 Server 上对相应的 Key 进行访问,当访问超过 Server 极限时,就会导致热点 Key 问题的产生。

热点Key问题的危害

1、流量集中,达到物理网卡上限。

2、请求过多,缓存分片服务被打垮。

3、DB 击穿,引起业务雪崩。

如前文讲到的,当某一热点 Key 的请求在某一主机上超过该主机网卡上限时,由于流量的过度集中,会导致服务器中其它服务无法进行。

如果热点过于集中,热点 Key 的缓存过多,超过目前的缓存容量时,就会导致缓存分片服务被打垮现象的产生。

当缓存服务崩溃后,此时再有请求产生,会缓存到后台 DB 上,由于DB 本身性能较弱,在面临大请求时很容易发生请求穿透现象,会进一步导致雪崩现象,严重影响设备的性能。 

怎么发现热key

方法一:凭借业务经验,进行预估哪些是热key

其实这个方法还是挺有可行性的。比如某商品在做秒杀,那这个商品的key就可以判断出是热key。缺点很明显,并非所有业务都能预估出哪些key是热key。

方法二:在客户端进行收集

这个方式就是在操作redis之前,加入一行代码进行数据统计。那么这个数据统计的方式有很多种,也可以是给外部的通讯系统发送一个通知信息。缺点就是对客户端代码造成入侵。

方法三:在Proxy层做收集

有些集群架构是下面这样的,Proxy可以是Twemproxy,是统一的入口。可以在Proxy层做收集上报,但是缺点很明显,并非所有的redis集群架构都有proxy。


方法四:用redis自带命令定时扫描

(1)monitor命令,该命令可以实时抓取出redis服务器接收到的命令,然后写代码统计出热key是啥。当然,也有现成的分析工具可以给你使用,比如redis-faina。但是该命令在高并发的条件下,有内存增暴增的隐患,还会降低redis的性能。

(2)hotkeys参数,redis 4.0.3提供了redis-cli的热点key发现功能,执行redis-cli时加上–hotkeys选项即可。但是该参数在执行的时候,如果key比较多,执行起来比较慢。

方法五:自己抓包评估

Redis客户端使用TCP协议与服务端进行交互,通信协议采用的是RESP。自己写程序监听端口,按照RESP协议规则解析数据,进行分析。

该方案无需侵入现有的 SDK 或者 Proxy 中间件,开发维护成本可控,但也存在缺点的,具体是热 key 节点的网络流量和系统负载已经比较高了,抓包可能会情况进一步恶化。

以上五种方案,各有优缺点。根据自己业务场景进行抉择即可。那么发现热key后,如何解决呢?

如何解决

目前业内的方案有两种

(1)利用二级缓存

比如利用ehcache,或者一个HashMap都可以。在你发现热key以后,把热key加载到系统的JVM中。
针对这种热key请求,会直接从jvm中取,而不会走到redis层。

假设此时有十万个针对同一个key的请求过来,如果没有本地缓存,这十万个请求就直接怼到同一台redis上了。

现在假设,你的应用层有50台机器,OK,你也有jvm缓存了。这十万个请求平均分散开来,每个机器有2000个请求,会从JVM中取到value值,然后返回数据。避免了十万个请求怼到同一台redis上的情形。

(2)备份热key

这个方案也很简单。不要让key走到同一台redis上不就行了。我们把这个key,在多个redis上都存一份不就好了。接下来,有热key请求进来的时候,我们就在有备份的redis上随机选取一台,进行访问取值,返回数据。

假设redis的集群数量为N,步骤如下图所示


注:不一定是2N,你想取3N,4N都可以,看要求。
伪代码如下

const M = N * 2
//生成随机数
random = GenRandom(0, M)
//构造备份新key
bakHotKey = hotKey + “_” + random
data = redis.GET(bakHotKey)
if data == NULL {
    data = GetFromDB()
    redis.SET(bakHotKey, expireTime + GenRandom(0,5))
}

有办法在项目运行过程中,自动发现热key,然后程序自动处理么?

嗯,好问题,那我们来讲讲业内怎么做的。其实只有两步

(1)监控热key

(2)通知系统做处理

(1)监控热key

在监控热key方面,有赞用的是方式二:在客户端进行收集。

在《有赞透明多级缓存解决方案(TMC)》中有一句话提到

TMC 对原生jedis包的JedisPool和Jedis类做了改造,在JedisPool初始化过程中集成TMC“热点发现”+“本地缓存”功能Hermes-SDK包的初始化逻辑。

也就说人家改写了jedis原生的jar包,加入了Hermes-SDK包。

那Hermes-SDK包用来干嘛?

OK,就是做热点发现和本地缓存。

从监控的角度看,该包对于Jedis-Client的每次key值访问请求,Hermes-SDK 都会通过其通信模块将key访问事件异步上报给Hermes服务端集群,以便其根据上报数据进行“热点探测”。

当然,这只是其中一种方式,有的公司在监控方面用的是方式五:自己抓包评估。

具体是这么做的,先利用flink搭建一套流式计算系统。然后自己写一个抓包程序抓redis监听端口的数据,抓到数据后往kafka里丢。

接下来,流式计算系统消费kafka里的数据,进行数据统计即可,也能达到监控热key的目的。

(2)通知系统做处理

在这个角度,有赞用的是上面的解决方案一:

利用二级缓存进行处理。

有赞在监控到热key后,Hermes服务端集群会通过各种手段通知各业务系统里的Hermes-SDK,告诉他们:"老弟,这个key是热key,记得做本地缓存。"

于是Hermes-SDK就会将该key缓存在本地,对于后面的请求。Hermes-SDK发现这个是一个热key,直接从本地中拿,而不会去访问集群。

除了这种通知方式以外。我们也可以这么做,比如你的流式计算系统监控到热key了,往zookeeper里头的某个节点里写。然后你的业务系统监听该节点,发现节点数据变化了,就代表发现热key。最后往本地缓存里写,也是可以的。

通知方式各种各样,大家可以自由发挥。本文只是提供一个思路。

饿了吗方案1

由于在饿了么内部,所有的 Redis 请求都是经过透明代理 Samaritan[2] 的,并且该代理是由我们自己开发维护的,在代理层改造的成本完全受控,因此我们选择了方案二,即在代理层进行收集上报。

大的方向确定之后,需要考虑具体的细节,比如:

记录所有请求如何能够保证不占用过多的内存甚至 OOM ?

记录所有请求如何能够保证代理的性能, 请求耗时不会有明显的上升?

针对第 1 点,既然我们只关心热 key 而不是要统计所有 key 的 counter,那么就可以用 LFU 只保留访问频次最高的,第 2 点则需要结合代理具体的实现去考虑。

下图是代理内部的实现方案, 略去了一些无关的细节:

注:

每个 redis node 会创建一个与之对应的唯一的 client,其上的所有请求都采用 pipeline 执行

每个 client 内部都有自己的 Hotkey Collector,不同 Collector 间相互独立

Hotkey Collector 内部结构如下所示,包含 LFU Counter、Syncer 和 Etrace Client 三部分:

 

Etrace 是一个内部的应用监控平台,类似的开源产品是 CAT

基本的工作流程是,LFU Counter 负责记录 key 的访问频次,Syncer 会定期将统计数据通过 Etrace Client 发送给远端的服务器。另外,为了避免向服务端发送过多无效的数据,内部会预先设置一个阈值,超过阈值的才发送到服务端。

按照预先的设计,我们将会有一个实时计算的服务去拉取 Etrace 上的数据,进行聚合计算得到当前的热点 key。但不幸地是代理中间件改造上线后的很长一段时间内,这个实时计算服务的开发都未被提上日程,分析下来主要是 ROI 低和维护成本高,因此在业务上如果要查热 key 就只能在 Etrace 上手动戳 event 碰运气比如:

 由于使用起来很麻烦,用户在第一次体验之后基本就放弃了,不会再用第二次,甚至连我们自己都不愿意使用… 在当时我们急需要找到一种更好的方案去解决用户体验和系统复杂度的问题,让该特性能真正地赋能于业务。

饿了吗方案2

对前面方案进行优化的话,可以从以下两个方面入手:

如何在不增加实时计算组件提升成本的前提下高效地聚合数据?

如何提升用户体验,让用户方便地使用?

针对第一点,当时第一个想法是能不能把聚合逻辑放在代理进程内,这样的话就不用再依赖任何外部组件,可以降低整个系统的复杂度和维护成本。但这里会有个问题,之前设计外部聚合组件的初衷是为了聚合不同机器的数据,现在采用单机数据会不会有问题,逻辑是不是站得住脚?

仔细思考下来,逻辑上是成立的,因为到达业务的流量是负载均衡过的,不同实例上的流量是比较均匀的,差不太多的,基于这个局部可以代表整体的原则,那么单实例上的热 key 就可以代表全局的一个情况。

另外,就易用性和使用体验上来说,如果聚合的数据在进程内,我们可以提供 HOTKEY 类似的自定义命令,让用户通过 redis-cli 直接获取。

最终的方案如下,已略去无关细节:

 实现上来说,每个集群会有一个全局的 Hotkey Collector,每个 client 上有自己独立的 Counter,Counter 依旧采用前面提到的 LFU算法,Collector 会定时地去收集每个 Counter 的数据并进行聚合,聚合的时候不会使用真实的计数,而是使用概率计数,并且为了适应访问模式的变化 counter 的值会随着时间衰减,整体上与 redis lfu非常类似。

下面是一个生产环境的真实例子,展示了近一段时间内比较热的 key:

 注:

默认使用的 log factor 因子是 10,counter 值每分钟衰减一半

Collector 默认的容量是 32,只记录请求频次最高的 32 个 key

输出的结果与 redis-cli --hotkeys 非常类似,counter 具体含义可以参考 Using Redis as an LRU cache[7] 一文末尾表格
 

Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐