在Redis的开发和运维过程中,由于对于Redis的某些特性没有真正合理地使用,会遇到一些棘手的问题,本章将对一些典型的“陷阱”进行逐一分析并提出解决方案,主要内容包括:
- Linux配置优化要点。
- flushall/flushdb误操作快速恢复方法。
- 安全的Redis如何设计。
- 处理bigkey的方案与最佳实践。
- 寻找热点key。
12.1 Linux配置优化
12.1.1 内存分配控制
1.vm.overcommit_memory
Redis在启动时可能会出现这样的日志:
WARNING overcommit_memory is set to 0! Background save may fail under low memory condition. To fix this issue add 'vm.overcommit_memory = 1' to /etc/sysctl.conf and then reboot or run the command 'sysctl vm.overcommit_memory=1' for this to take effect.
在分析这个问题之前,首先要弄清楚什么是overcommit?Linux操作系统对大部分申请内存的请求都回复yes,以便能运行更多的程序。因为申请内存后,并不会马上使用内存,这种技术叫做overcommit。如果Redis在启动时有上面的日志,说明vm.overcommit_memory=0,Redis提示把它设置为1。
vm.overcommit_memory用来设置内存分配策略,有三个可选值,如表12-1所示。
本节的可用内存代表物理内存与swap之和。
日志中的Background save代表的是bgsave和bgrewriteaof,如果当前可用内存不足,操作系统应该如何处理fork操作。如果vm.overcommit_memory=0,代表如果没有可用内存,就申请内存失败,对应到Redis就是执行fork失败,在Redis的日志会出现:
Cannot allocate memory
Redis建议把这个值设置为1,是为了让fork操作能够在低内存下也执行成功。
2.查看及设置方法
##查看
cat /proc/sys/vm/overcommit_memory
##设置
echo "vm.overcommit_memory=1" >> /etc/sysctl.conf
sysctl vm.overcommit_memory=1
3. 最佳实践
- Redis设置合理的maxmemory,保证机器有20%~30%的闲置内存。
- 集中化管理AOF重写和RDB的bgsave。
- 设置vm.overcommit_memory=1,防止极端情况下会造成fork失败。
12.1.2 swappiness
1. 参数说明
swap对于操作系统来比较重要,当物理内存不足时,可以将一部分内存页进行swap操作,已解燃眉之急。但世界上没有免费午餐,swap空间由硬盘提供,对于需要高并发、高吞吐的应用来说,磁盘IO通常会成为系统瓶颈。在Linux中,并不是要等到所有物理内存都使用完才会使用到swap,系统参数swppiness会决定操作系统使用swap的倾向程度。swappiness的取值范围是0~100,swappiness的值越大,说明操作系统可能使用swap的概率越高,swappiness值越低,表示操作系统更加倾向于使用物理内存。swap的默认值是60,了解这个值的含义后,有利于Redis的性能优化。表12-2对swappiness的重要值进行了说明
表12-2 swapniess重要值策略说明OOM(Out Of Memory)killer机制是指Linux操作系统发现可用内存不足时,强制杀死一些用户进程(非内核进程),来保证系统有足够的可用内存进行分配。从表12-2中可以看出,swappiness参数在Linux3.5版本前后的表现并不完全相同,Redis运维人员在设置这个值需要关注当前操作系统的内核版本。
2. 设置方法
swappiness设置方法如下:
echo {bestvalue} > /proc/sys/vm/swappiness
但是上述方法在系统重启后就会失效,为了让配置在重启Linux操作系统后立即生效,只需要在/etc/sysctl.conf追加vm.swappiness={bestvalue}即可。
echo vm.swappiness={bestvalue} >> /etc/sysctl.conf
如果Linux>3.5,vm.swapniess=1,否则vm.swapniess=0,从而实现如下两个目标:
- 物理内存充足时候,使Redis足够快。
- 物理内存不足时候,避免Redis死掉(如果当前Redis为高可用,死掉比阻塞更好)。
12.1.3 THP
Redis在启动时可能会看到如下日志:
WARNING you have Transparent Huge Pages (THP) support enabled in your kernel. This will create latency and memory usage issues with Redis. To fix this issue run the command 'echo never > /sys/kernel/mm/transparent_hugepage/enabled' as root,and add it to your /etc/rc.local in order to retain the setting after a reboot.Redis must be restarted after THP is disabled.
从提示看Redis建议修改Transparent Huge Pages(THP)的相关配置,Linux kernel在2.6.38内核增加了THP特性,支持大内存页(2MB)分配,默认开启。当开启时可以降低fork子进程的速度,但fork操作之后,每个内存页从原来4KB变为2MB,会大幅增加重写期间父进程内存消耗。同时每次写命令引起的复制内存页单位放大了512倍,会拖慢写操作的执行时间,导致大量写操作慢查询,例如简单的incr命令也会出现在慢查询中。因此Redis日志中建议将此特性进行禁用,禁用方法如下:
echo never > /sys/kernel/mm/transparent_hugepage/enabled
为了使机器重启后THP配置依然生效,可以在/etc/rc.local中追加echo never>/sys/kernel/mm/transparent_hugepage/enabled。
12.1.4 OOM killer
OOM killer会在可用内存不足时选择性地杀掉用户进程,它的运行规则是怎样的,会选择哪些用户进程“下手”呢?OOM killer进程会为每个用户进程设置一个权值,这个权值越高,被“下手”的概率就越高,反之概率越低。每个进程的权值存放在/proc/{progress_id}/oom_score中,这个值是受/proc/{progress_id}/oom_adj的控制,oom_adj在不同的Linux版本中最小值不同,可以参考Linux源码中oom.h(从-15到-17)。当oom_adj设置为最小值时,该进程将不会被OOM killer杀掉,设置方法如下。
echo {value} > /proc/${process_id}/oom_adj
对于Redis所在的服务器来说,可以将所有Redis的oom_adj设置为最低值或者稍小的值,降低被OOM killer杀掉的概率.
- 笔者认为oom_adj参数只能起到辅助作用,合理地规划内存更为重要。
- 通常在高可用情况下,被杀掉比僵死更好,因此不要过多依赖oom_ad j配置。
12.1.5 使用NTP
NTP(Network Time Protocol,网络时间协议)是一种保证不同机器时钟一致性的服务。我们知道像Redis Sentinel和Redis Cluster这两种功能需要多个Redis节点的类型,可能会涉及多台服务器。虽然Redis并没有对多个服务器的时钟有严格要求,但是假如多个Redis实例所在的服务器时钟不一致,对于一些异常情况的日志排查是非常困难的,例如Redis Cluster的故障转移,如果日志时间不一致,对于我们排查问题带来很大的困扰(注:但不会影响集群功能,集群节点依赖各自时钟)。一般公司里都会有NTP服务用来提供标准时间服务,从而达到纠正时钟的效果,为此我们可以每天定时去同步一次系统时间,从而使得集群中的时间保持统一。
12.1.6 ulimit
在Linux中,可以通过ulimit查看和设置系统当前用户进程的资源数。其中ulimit-a命令包含的open files参数,是单个用户同时打开的最大文件个数。
Redis允许同时有多个客户端通过网络进行连接,可以通过配置maxclients来限制最大客户端连接数。对Linux操作系统来说,这些网络连接都是文件句柄。
这里建议把open files 调大,比如65535.
ulimit –Sn {max-open-files}
12.1.7 TCP backlog
Redis默认的tcp-backlog值为511,可以通过修改配置tcp-backlog进行调整,如果Linux的tcp-backlog小于Redis设置的tcp-backlog,那么在Redis启动时会看到如下日志:
# WARNING: The TCP backlog setting of 511 cannot be enforced because /proc/sys/ net/core/somaxconn is set to the lower value of 128.
查看方法:
# cat /proc/sys/net/core/somaxconn
128
修改方法:
echo 511 > /proc/sys/net/core/somaxconn
12.2 flushall/flushdb误操作
Redis的flushall/flushdb命令可以做数据清除,对于Redis的开发和运维人员有一定帮助,然而一旦误操作,它的破坏性也是很明显的。怎么才能快速恢复数据,让损失达到最小呢?本节我们将结合之前学习的Redis相关知识进行分析,最后给出一个合理的方案。
为了方便说明,下文中除了AOF文件中的flushall/flushdb以外,其他所有的flushall/flushdb都用flush代替。
假设进行flush操作的Redis是一对主从结构的主节点,其中键值对的个数是100万,每秒写入量是1000。
12.2.1 缓存与存储
被误操作flush后,根据当前Redis是缓存还是存储使用策略有所不同:
- 缓存:对于业务数据的正确性可能造成损失还小一点,因为缓存中的数据可以从数据源重新进行构建,但是在第11章介绍了缓存雪崩和缓存穿透的相关知识,当前场景也有类似的地方,如果业务方并发量很大,可能会对后端数据源造成一定的负载压力,这个问题也是不容忽视。
- 存储:对业务方可能会造成巨大的影响,也许flush操作后的数据是重要配置,也可能是一些基础数据,也可能是业务上的重要一环,如果没有提前做业务降级操作,那么最终反馈到用户的应用可能就是报错或者空白页面等,其后果不堪设想。即使做了相应的降级或者容错处理,对于用户体验也有一定的影响。
所以Redis无论作为缓存还是作为存储,如何能在flush操作后快速恢复数据才是至关重要的。持久化文件肯定是恢复数据的媒介,下面两个小节将对AOF和RDB文件进行分析。
12.2.2 借助AOF机制恢复
Redis执行了flush操作后,AOF持久化文件会受到什么影响呢?如下所示:
- appendonly no:对AOF持久化没有任何影响,因为根本就不存在AOF文件。
- appendonly yes:只不过是在AOF文件中追加了一条记录,例如下面就是AOF文件中的flush操作记录:
*1
$8
flushall
虽然Redis中的数据被清除掉了,但是AOF文件还保存着flush操作之前完整的数据,这对恢复数据是很有帮助的。注意问题如下:
1)如果发生了AOF重写,Redis遍历所有数据库重新生成AOF文件,并会覆盖之前的AOF文件。所以如果AOF重写发生了,也就意味着之前的数据就丢掉了,那么利用AOF文件来恢复的办法就失效了。所以当误操作后,需要考虑如下两件事。
- 调大AOF重写参数auto-aof-rewrite-percentage和auto-aof-rewrite-minsize,让Redis不能产生AOF自动重写。
- 拒绝手动bgrewriteaof。
2)如果要用AOF文件进行数据恢复,那么必须要将AOF文件中的flushall相关操作去掉,为了更加安全,可以在去掉之后使用redis-check-aof这个工具去检验和修复一下AOF文件,确保AOF文件格式正确,保证数据恢复正常。
12.2.3 RDB有什么变化
Redis执行了flushall操作后,RDB持久化文件会受到什么影响呢?
1)如果没有开启RDB的自动策略,也就是配置文件中没有类似如下配置:
save 900 1
save 300 10
save 60 10000
那么除非手动执行过save、bgsave或者发生了主从的全量复制,否则RDB文件也会保存flush操作之前的数据,可以作为恢复数据的数据源。注意问题如下:
- 防止手动执行save、bgsave,如果此时执行save、bgsave,新的RDB文件就不会包含flush操作之前的数据,被老的RDB文件进行覆盖。
- RDB文件中的数据可能没有AOF实时性高,也就是说,RDB文件很可能很久以前主从全量复制生成的,或者之前用save、bgsave备份的。
2)如果开启了RDB的自动策略,由于flush涉及键值数量较多,RDB文件会被清除,意味着使用RDB恢复基本无望。
综上所述,如果AOF已经开启了,那么用AOF来恢复是比较合理的方式,但是如果AOF关闭了,那么RDB虽然数据不是很实时,但是也能恢复部分数据,完全取决于RDB是什么时候备份的。当然RDB并不是一无是处,它的恢复速度要比AOF快很多,但是总体来说对于flush操作之后不是最好的恢复数据源。
12.2.4 从节点有什么变化
Redis从节点同步了主节点的flush命令,所以从节点的数据也是被清除了,从节点的RDB和AOF的变化与主节点没有任何区别。
12.2.5 快速恢复数据
下面使用AOF作为数据源进行恢复演练。
- 防止AOF重写。快速修改Redis主从的auto-aof-rewrite-percentage和auto-aof-rewrite-min-size变为一个很大的值,从而防止了AOF重写的发生,例如:
config set auto-aof-rewrite-percentage 1000
config set auto-aof-rewrite-min-size 100000000000
- 去掉主从AOF文件中的flush相关内容:
*1
$8
flushall
- 重启Redis主节点服务器,恢复数据。
本节通过flush误操作的数据恢复,重新梳理了持久化、复制的相关知识,这里建议运维人员提前准备shell脚本或者其他自动化的方式处理,因为故障不等人,对于flush这样的危险操作,应该通过有效的方式进行规避,下节将介绍具体的方法。
12.3 安全的Redis
数据丢失对于很多Redis的开发者来说是致命的,经过相关机构的调查发现,被攻击的Redis有如下特点:
- Redis所在的机器有外网IP。
- Redis以默认端口6379为启动端口,并且是对外网开放的。
- Redis是以root用户启动的。
- Redis没有设置密码。
- Redis的bind设置为0.0.0.0或者""。
攻击者充分利用Redis的dir和dbfilename两个配置可以使用config set动态设置,以及RDB持久化的特性,将自己的公钥写入到目标机器的/root/.ssh/authotrized_keys文件中,从而实现了对目标机器的攻陷。攻击过程如图12-2所示。
这里不做演示攻击的过程,可以自行百度,攻击的过程非常简单。
Redis的设计目标是一个在内网运行的轻量级高性能键值服务,因为是在内网运行,所以对于安全方面没有做太多的工作,Redis只提供了简单的密码机制,并且没有做用户权限的相关划分。那么,在日常对于Redis的开发和运维中要注意哪些方面才能让Redis服务不仅能提供高效稳定的服务,还能保证在一个足够安全的网络环境下运行呢?下面将从7个方面进行介绍。
12.3.1 Redis密码机制
1. 简单的密码机制
Redis提供了requirepass配置为Redis提供密码功能,如果添加这个配置,客户端就不能通过redis-cli -h{ip} -p{port}来执行命令。
Redis提供了两种方式访问配置了密码的Redis:
- redis-cli-a参数。使用redis-cli连接Redis时,添加-a加密码的参数,如果密码正确就可以正常访问Redis了.
- auth命令。通过redis-cli连接后,执行auth加密码命令,如果密码正确就可以正常访问访问Redis了.
2. 运维建议
这种密码机制能在一定程度上保护Redis的安全,但是在使用requirepass时候要注意一下几点:
- 密码要足够复杂(64个字节以上),因为Redis的性能很高,如果密码比较简单,完全是可以在一段时间内通过暴力破解来破译密码。
- 如果是主从结构的Redis,不要忘记在从节点的配置中加入masterauth(master的密码)配置,否则会造成主从节点同步失效。
- auth是通过明文进行传输的,所以也不是100%可靠,如果被攻击者劫持也相当危险。
12.3.2 伪装危险命令
1. 引入rename-command
Redis中包含了很多“危险”的命令,一旦错误使用或者误操作,后果不堪设想,例如如下命令:
- keys:如果键值较多,存在阻塞Redis的可能性。
- flushall/flushdb:数据全部被清除。
- save:如果键值较多,存在阻塞Redis的可能性。
- debug:例如debug reload会重启Redis。
- config:config应该交给管理员使用。
- shutdown:停止Redis。
理论上这些命令不应该给普通开发人员使用,那有没有什么好的方法能够防止这些危险的命令被随意执行呢?Redis提供了rename-command配置解决这个问题。下面直接用一个例子说明rename-command的作用。例如当前Redis包含10000个键值对,现使用flushall将全部数据清除:
127.0.0.1:6379> flushall
OK
例如Redis添加如下配置:
rename-command flushall jlikfjalijl3i4jl3jql34j
那么再执行flushall命令的话,会收到Redis不认识flushall的错误提示,说明我们成功地用rename-command对flushall命令做了伪装:
127.0.0.1:6379> flushall
(error) ERR unknown command ‘flushall’
而如果执行jlikfjalijl3i4jl3jql34(随机字符串),那么就可以实现flushall的功能了,这就是rename-command的作用,管理员可以对认为比较危险的命令做rename-command处理:
127.0.0.1:6379> jlikfjalijl3i4jl3jql34j
OK
2. 没有免费的午餐
rename-command虽然对Redis的安全有一定帮助,但是天下并没有免费的午餐。使用了rename-command时可能会带来如下麻烦:
- 管理员要对自己的客户端进行修改,例如jedis.flushall()操作内部使用的是flushall命令,如果用rename-command后需要修改为新的命令,有一定的开发和维护成本。
- rename-command配置不支持config set,所以在启动前一定要确定哪些命令需要使用rename-command。
- 如果AOF和RDB文件包含了rename-command之前的命令,Redis将无法启动,因为此时它识别不了rename-command之前的命令。
- Redis源码中有一些命令是写死的,rename-command可能造成Redis无法正常工作。例如Sentinel节点在修改配置时直接使用了config命令,如果对config使用rename-command,会造成Redis Sentinel无法正常工作。
3. 最佳实践
在使用rename-command的相关配置时,需要注意以下几点:
- 对于一些危险的命令(例如flushall),不管是内网还是外网,一律使用rename-command配置
- 建议第一次配置Redis时,就应该配置rename-command,因为rename-command不支持config set。
- 如果涉及主从关系,一定要保持主从节点配置的一致性,否则存在主从数据不一致的可能性。
12.3.3 防火墙
可以使用防火墙限制输入和输出的IP或者IP范围、端口或者端口范围,在比较成熟的公司都会对有外网IP的服务器做一些端口的限制,例如只允许80端口对外开放。因为一般来说,开放外网IP的服务器中Web服务器比较多,但通常存储服务器的端口无需对外开放,防火墙是一个限制外网访问Redis的必杀技。
12.3.4 bind
bind指定Redis与哪一块网卡相绑定,在多网卡的情况下,我们建议:
- 如果机器有外网IP,但部署的Redis是给内部使用的,建议去掉外网网卡或者使用bind配置限制流量从外网进入。
- 如果客户端和Redis部署在一台服务器上,可以使用回环地址(127.0.0.1)。
- bind配置不支持config set,所以尽可能在第一次启动前配置好。
Redis3.2提供了protected-mode配置(默认开启),如果当前Redis没有配置密码,没有配置bind,那么只允许来自本机的访问,也就是相当于配置了bind 127.0.0.1。
12.3.5 定期备份数据
天有不测风云,假如有一天Redis真的被攻击了(清理了数据,关闭了进程),那么定期备份的数据能够在一定程度挽回一些损失,定期备份持久化数据是一个比较好的习惯。
12.3.6 不使用默认端口
Redis的默认端口是6379,不使用默认端口从一定程度上可降低被入侵者发现的可能性,因为入侵者通常本身也是一些攻击程序,对目标服务器进行端口扫描,例如MySQL的默认端口3306、Memcache的默认端口11211、Jetty的默认端口8080等都会被设置成攻击目标,Redis作为一款较为知名的NoSQL服务,6379必然也在端口扫描的列表中,虽然不设置默认端口还是有可能被攻击者入侵,但是能够在一定程度上降低被攻击的概率。
12.3.7 使用非root用户启动
root用户作为管理员,权限非常大。如果被入侵者获取root权限后,就可以在这台机器以及相关机器上“为所欲为”了。笔者建议在启动Redis服务的时候使用非root用户启动。事实上许多服务,例如Resin、Jetty、HBase、Hadoop都建议使用非root启动。
12.4 处理bigkey
bigkey是指key对应的value所占的内存空间比较大,例如一个字符串类型的value可以最大存到512MB,一个列表类型的value最多可以存储232-1个元素。如果按照数据结构来细分的话,一般分为字符串类型bigkey和非字符串类型bigkey。
- 字符串类型:体现在单个value值很大,一般认为超过10KB就是bigkey,但这个值和具体的OPS相关。
- 非字符串类型:哈希、列表、集合、有序集合,体现在元素个数过多。
bigkey无论是空间复杂度和时间复杂度都不太友好,下面我们将介绍它的危害。
12.4.1 bigkey的危害
bigkey的危害体现在三个方面:
- 内存空间不均匀(平衡):例如在Redis Cluster中,bigkey会造成节点的内存空间使用不均匀。
- 超时阻塞:由于Redis单线程的特性,操作bigkey比较耗时,也就意味着阻塞Redis可能性增大。
- 网络拥塞:每次获取bigkey产生的网络流量较大,假设一个bigkey为1MB,每秒访问量为1000,那么每秒产生1000MB的流量,对于普通的千兆网卡(按照字节算是128MB/s)的服务器来说简直是灭顶之灾,而且一般服务器会采用单机多实例的方式来部署,也就是说一个bigkey可能会对其他实例造成影响,其后果不堪设想。
bigkey的存在并不是完全致命的,如果这个bigkey存在但是几乎不被访问,那么只有内存空间不均匀的问题存在,相对于另外两个问题没有那么重要紧急,但是如果bigkey是一个热点key(频繁访问),那么其带来的危害不可想象,所以在实际开发和运维时一定要密切关注bigkey的存在。
12.4.2 如何发现
redis-cli --bigkeys可以命令统计bigkey的分布,但是在生产环境中,开发和运维人员更希望自己可以定义bigkey的大小,而且更希望找到真正的bigkey都有哪些,这样才可以去定位、解决、优化问题。判断一个key是否为bigkey,只需要执行debug object key查看serializedlength属性即可,它表示key对应的value序列化之后的字节数,例如我们执行如下操作:
127.0.0.1:6379> debug object key
Value at:0x7fc06c1b1430 refcount:1 encoding:raw serializedlength:1256350 lru:11686193 lru_seconds_idle:20
可以发现serializedlength=11686193字节,约为1M,同时可以看到encoding是raw,也就是字符串类型,那么可以通过strlen来看一下字符串的字节数为2247394字节,约为2MB:
127.0.0.1:6379> strlen key
(integer) 2247394
serializedlength不代表真实的字节大小,它返回对象使用RDB编码序列化后的长度,值会偏小,但是对于排查bigkey有一定辅助作用,因为不是每种数据结构都有类似strlen这样的方法。
在实际生产环境中发现bigkey的两种方式如下:
-
被动收集:许多开发人员确实可能对bigkey不了解或重视程度不够,但是这种bigkey一旦大量访问,很可能就会带来命令慢查询和网卡跑满问题,开发人员通过对异常的分析通常能找到异常原因可能是bigkey,这种方式虽然不是被笔者推荐的,但是在实际生产环境中却大量存在,建议修改Redis客户端,当抛出异常时打印出所操作的key,方便排查bigkey问题。
-
主动检测:scan+debug object:如果怀疑存在bigkey,可以使用scan命令渐进的扫描出所有的key,分别计算每个key的serializedlength,找到对应bigkey进行相应的处理和报警,这种方式是比较推荐的方式。
-
如果键值个数比较多,scan+debug object会比较慢,可以利用Pipeline机制完成。
-
对于元素个数较多的数据结构,debug object执行速度比较慢,存在阻塞Redis的可能。
-
如果有从节点,可以考虑在从节点上执行。
12.4.3 如何删除
当发现Redis中有bigkey并且确认要删除时,如何优雅地删除bigkey?无论是什么数据结构,del命令都将其删除。但是相信通过上面的分析后你一定不会这么做,因为删除bigkey通常来说会阻塞Redis服务。下面给出一组测试数据分别对string、hash、list、set、sorted set五种数据结构的bigkey进行删除,bigkey的元素个数和每个元素的大小不尽相同。
下面测试和服务器硬件、Redis版本比较相关,可能在不同的服务器上执行速度不太相同,但是能提供一定的参考价值
表12-3展示了删除512KB~10MB的字符串类型数据所花费的时间,总体来说由于字符串类型结构相对简单,删除速度比较快,但是随着value值的不断增大,删除速度也逐渐变慢。
表12-4展示了非字符串类型的数据结构在不同数量级、不同元素大小下对bigkey执行del命令的时间,总体上看元素个数越多、元素越大,删除时间越长,相对于字符串类型,这种删除速度已经足够可以阻塞Redis。
表12-4 删除hash、list、set、sorted set四种数据结构不同数量不同元素大小的耗时从上分析可见,除了string类型,其他四种数据结构删除的速度有可能很慢,这样增大了阻塞Redis的可能性。既然不能用del命令,那有没有比较优雅的方式进行删除呢,这时候就需要将第2章介绍的scan命令的若干类似命令拿出来:sscan、hscan、zscan。
1. string
对于string类型使用del命令一般不会产生阻塞:
del bigkey
2. hash、list、set、sorted set
以hash为例子,使用hscan命令,每次获取部分(例如100个)field value,再利用hdel删除每个field(为了快速可以使用Pipeline).
请勿忘记每次执行到最后执行del key操作。
12.4.4 最佳实践思路
由于开发人员对Redis的理解程度不同,在实际开发中出现bigkey在所难免,重要的是,能通过合理的检测机制及时找到它们,进行处理。作为开发人员在业务开发时应注意不能将Redis简单暴力的使用,应该在数据结构的选择和设计上更加合理,例如出现了bigkey,要思考一下可不可以做一些优化(例如拆分数据结构)尽量让这些bigkey消失在业务中,如果bigkey不可避免,也要思考一下要不要每次把所有元素都取出来(例如有时候仅仅需要hmget,而不是hgetall)。最后,可喜的是,Redis将在4.0版本支持lazy delete free的模式,那时删除bigkey不会阻Redis。
12.5 寻找热点key
热门新闻事件或商品通常会给系统带来巨大的流量,对存储这类信息的Redis来说却是一个巨大的挑战。以Redis Cluster为例,它会造成整体流量的不均衡,个别节点出现OPS过大的情况,极端情况下热点key甚至会超过Redis本身能够承受的OPS,因此寻找热点key对于开发和运维人员非常重要。下面就从四个方面来分析热点key。
1. 客户端
客户端其实是距离key“最近”的地方,因为Redis命令就是从客户端发出的,例如在客户端设置全局字典(key和调用次数),每次调用Redis命令时,使用这个字典进行记录.
使用客户端进行热点key的统计非常容易实现,但是同时问题也非常多:
- 无法预知key的个数,存在内存泄露的危险。
- 对于客户端代码有侵入,各个语言的客户端都需要维护此逻辑,维护成本较高。
- 只能了解当前客户端的热点key,无法实现规模化运维统计。
当然除了使用本地字典计数外,还可以使用其他存储来完成异步计数,从而解决本地内存泄露问题。但是另两个问题还是不好解决。
2. 代理端
像Twemproxy、Codis这些基于代理的Redis分布式架构,所有客户端的请求都是通过代理端完成的,如图12-5所示。此架构是最适合做热点key统计的,因为代理是所有Redis客户端和服务端的桥梁。但并不是所有Redis都是采用此种架构。
3. Redis服务端
使用monitor命令统计热点key是很多开发和运维人员首先想到,monitor命令可以监控到Redis执行的所有命令。利用monitor命令的结果就可以统计出一段时间内的热点key排行榜、命令排行榜、客户端分布等数据。
此种方法会有两个问题:
- 本书多次强调monitor命令在高并发条件下,会存在内存暴增和影响Redis性能的隐患,所以此种方法适合在短时间内使用。
- 只能统计一个Redis节点的热点key,对于Redis集群需要进行汇总统计。
4. 机器
4.1节我们介绍过,Redis客户端使用TCP协议与服务端进行交互,通信协议采用的是RESP。如果站在机器的角度,可以通过对机器上所有Redis端口的TCP数据包进行抓取完成热点key的统计。此种方法对于Redis客户端和服务端来说毫无侵入,是比较完美的方案,但是依然存在两个问题:
- 需要一定的开发成本,但是一些开源方案实现了该功能,例如ELK(ElasticSearch Logstash Kibana)体系下的packetbeat[2]插件,可以实现对Redis、MySQL等众多主流服务的数据包抓取、分析、报表展示。
- 由于是以机器为单位进行统计,要想了解一个集群的热点key,需要进行后期汇总。
最后我们总结出解决热点key问题的三种方案。选用哪种要根据具体业务场景来决定。下面是三种方案的思路。
- 拆分复杂数据结构:如果当前key的类型是一个二级数据结构,例如哈希类型。如果该哈希元素个数较多,可以考虑将当前hash进行拆分,这样该热点key可以拆分为若干个新的key分布到不同Redis节点上,从而减轻压力。
- 迁移热点key:以Redis Cluster为例,可以将热点key所在的slot单独迁移到一个新的Redis节点上,但此操作会增加运维成本。
- 本地缓存加通知机制:可以将热点key放在业务端的本地缓存中,因为是在业务端的本地内存中,处理能力要高出Redis数十倍,但当数据更新时,此种模式会造成各个业务端和Redis数据不一致,通常会使用发布订阅机制来解决类似问题。
网友评论