一、flushall/flushdb误操作的处理
假设进行flush操作的Redis是一对主从结构的主节点,其中键值对的个数是100万,每秒写入量是1000。
1.缓存与存储
被误操作flush后,根据当前Redis是缓存还是存储使用策略有所不同:
- 缓存:对于业务数据的正确性可能造成损失还小一点,因为缓存中的数据可以从数据源重新进行构建,但是缓存雪崩和缓存穿透的相关知识,当前场景也有类似的地方,如果业务方并发量很大,可能会对后端数据源造成一定的负载压力,这个问题也是不容忽视。
- 存储:对业务方可能会造成巨大的影响,也许flush操作后的数据是重要配置,也可能是一些基础数据,也可能是业务上的重要一环,如果没有提前做业务降级操作,那么最终反馈到用户的应用可能就是报错或者空白页面等,其后果不堪设想。即使做了相应的降级或者容错处理,对于用户体验也有一定的影响。
所以Redis无论作为缓存还是作为存储,如何能在flush操作后快速恢复数据才是至关重要的持久化文件肯定是恢复数据的媒介。
2.借助AOF机制恢复
Redis执行了flush操作后,AOF持久化文件会受到什么影响呢?如下所示:
- appendonly no:对AOF持久化没有任何影响,因为根本就不存在AOF文件。
- appendonly yes:只不过是在AOF文件中追加了一条记录,例如下面就是AOF文件中的flush操作记录:
*1
$8
flushall
虽然Redis中的数据被清除掉了,但是AOF文件还保存着flush操作之前完整的数据,这对恢复数据是很有帮助的。注意问题如下:
- 如果发生了AOF重写,Redis遍历所有数据库重新生成AOF文件,并会覆盖之前的AOF文件。所以如果AOF重写发生了,也就意味着之前的数据就丢掉了,那么利用AOF文件来恢复的办法就失效了。所以当误操作后,需要考虑如下两件事。
- 调大AOF重写参数auto-aof-rewrite-percentage和auto-aof-rewrite-min-size,让Redis不能产生AOF自动重写。
- 拒绝手动bgrewriteaof。
- 如果要用AOF文件进行数据恢复,那么必须要将AOF文件中的flushall相关操作去掉,为了更加安全,可以在去掉之后使用redis-check-aof这个工具去检验和修复一下AOF文件,确保AOF文件格式正确,保证数据恢复正常。
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恢复基本无望。
(3)和AOF比较来说,如果AOF已经开启了,那么用AOF来恢复是比较合理的方式,但是如果AOF关闭了,那么RDB虽然数据不是很实时,但是也能恢复部分数据,完全取决于RDB是什么时候备份的。当然RDB并不是一无是处,它的恢复速度要比AOF快很多,但是总体来说对于flush操作之后不是最好的恢复数据源。
4.从节点的变化
Redis从节点同步了主节点的flush命令,所以从节点的数据也是被清除了,从节点的RDB和AOF的变化与主节点没有任何区别。
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主节点服务器,恢复数据。
二、Redis的安全性
攻击的特点:
- Redis所在的机器有外网IP。
- Redis以默认端口6379为启动端口,并且是对外网开放的。
- Redis是以root用户启动的。
- Redis没有设置密码。
- Redis的bind设置为0.0.0.0或者""。
(2)Redis密码机制
启动一个有密码的Redis
root@bigjun:/home/redis/clusters/conf# redis-server --requirepass I_need_passwd
通过redis-cli执行命令没有权限
root@bigjun:/home/redis/clusters/conf# redis-cli
127.0.0.1:6379> ping
(error) NOAUTH Authentication required.
通过redis-ci加上-a参数访问配置了密码的Redis
root@bigjun:/home/redis/clusters/conf# redis-cli -h 127.0.0.1 -p 6379 -a I_need_passwd
127.0.0.1:6379> ping
PONG
通过redis-cli连接后,执行auth加密码执行命令
root@bigjun:/home/redis/clusters/conf# redis-cli
127.0.0.1:6379> auth I_need_passwd
OK
127.0.0.1:6379> ping
PONG
这种密码机制能在一定程度上保护Redis的安全,但是在使用requirepass时候要注意一下几点:
- 密码要足够复杂(64个字节以上),因为Redis的性能很高,如果密码比较简单,完全是可以在一段时间内通过暴力破解来破译密码。
- 如果是主从结构的Redis,不要忘记在从节点的配置中加入masterauth(master的密码)配置,否则会造成主从节点同步失效。
- auth是通过明文进行传输的,所以也不是100%可靠,如果被攻击者劫持也相当危险。
(3)伪装危险命令
Redis中包含了很多“危险”的命令,一旦错误使用或者误操作,后果不堪设想,例如如下命令:
- keys:如果键值较多,存在阻塞Redis的可能性。
- flushall/flushdb:数据全部被清除。
- save:如果键值较多,存在阻塞Redis的可能性。
- debug:例如debug reload会重启Redis。
- config:config应该交给管理员使用。
- shutdown:停止Redis。
command配置解决这个问题。
command的作用。例如当前Redis包含10000个键值对,现使用flushall将全部数据清除:
127.0.0.1:6379> flushall
OK
例如Redis添加如下配置:
rename-command flushall jlikfjalijl3i4jl3jql34j
command对flushall命令做了伪装:
127.0.0.1:6379> flushall
(error) ERR unknown command ‘ flushall ’
command的作用,管理员可以对认为比较危险的命令做rename-command处理:
127.0.0.1:6379> jlikfjalijl3i4jl3jql34j
OK
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无法正常工作。
command的相关配置时,需要注意以下几点:
- 对于一些危险的命令(例如flushall),不管是内网还是外网,一律使用rename-command配置
- 建议第一次配置Redis时,就应该配置rename-command,因为rename-command不支持config set。
- 如果涉及主从关系,一定要保持主从节点配置的一致性,否则存在主从数据不一致的可能性。
(4)防火墙
可以使用防火墙限制输入和输出的IP或者IP范围、端口或者端口范围,在比较成熟的公司都会对有外网IP的服务器做一些端口的限制,例如只允许80端口对外开放。因为一般来说,开放外网IP的服务器中Web服务器比较多,但通常存储服务器的端口无需对外开放,防火墙是一个限制外网访问Redis的必杀技。
(5)bind
bind指定的是Redis和哪个网卡进行绑定,和客户端是什么网段没有关系。
- 如果机器有外网IP,但部署的Redis是给内部使用的,建议去掉外网网卡或者使用bind配置限制流量从外网进入。
- 如果客户端和Redis部署在一台服务器上,可以使用回环地址(127.0.0.1)。
- bind配置不支持config set,所以尽可能在第一次启动前配置好。
(6)定期备份数据
攻击了(清理了数据,关闭了进程),那么定期备份的数据能够在一定程度挽回一些损失,定期备份持久化数据是一个比较好的习惯。
(7)不使用默认端口
攻击程序,对目标服务器进行端口扫描,例如MySQL的默认端口3306、Memcache的默认端口11211、Jetty的默认端口8080等都会被设置成攻击目标,Redis作为一款较为知名的NoSQL服务,6379必然也在端口扫描的列表中,虽然不设置默认端口还是有可能被攻击者入侵,但是能够在一定程度上降低被攻击的概率。
(8)使用非root用户启动
root用户作为管理员,权限非常大。如果被入侵者获取root权限后,就可以在这台机器以及相关机器上“为所欲为”了。笔者建议在启动Redis服务的时候使用非root用户启动。事实上许多服务,例如Resin、Jetty、HBase、Hadoop都建议使用非root启动。
三、处理bigkey
bigkey是指key对应的value所占的内存空间比较大,例如一个字符串类型的value可以最大存到512MB,一个列表类型的value最多可以存储232-1个元素。如果按照数据结构来细分的话,一般分为字符串类型bigkey和非字符串类型bigkey。
- 字符串类型:体现在单个value值很大,一般认为超过10KB就是bigkey,但这个值和具体的OPS相关。
- 非字符串类型:哈希、列表、集合、有序集合,体现在元素个数过多。
bigkey无论是空间复杂度和时间复杂度都不太友好。
(1)bigkey的危害
- 内存空间不均匀(平衡):例如在Redis Cluster中,bigkey会造成节点的内存空间使用不均匀。
- 超时阻塞:由于Redis单线程的特性,操作bigkey比较耗时,也就意味着阻塞Redis可能性增大。
- 网络拥塞:每次获取bigkey产生的网络流量较大,假设一个bigkey为1MB,每秒访问量为1000,那么每秒产生1000MB的流量,对于普通的千兆网卡(按照字节算是128MB/s)的服务器来说简直是灭顶之灾,而且一般服务器会采用单机多实例的方式来部署,也就是说一个bigkey可能会对其他实例造成影响,其后果不堪设想。
bigkey的存在并不是完全致命的,如果这个bigkey存在但是几乎不被访问,那么只有内存空间不均匀的问题存在,相对于另外两个问题没有那么重要紧急,但是如果bigkey是一个热点key(频繁访问),那么其带来的危害不可想象,所以在实际开发和运维时一定要密切关注bigkey的存在。
(2)如何发现bigkey
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进行相应的处理和报警,这种方式是比较推荐的方式。
(3)如何删除bigkey
无论是什么数据结构,del命令都将其删除。但是删除bigkey通常来说会阻塞Redis服务。
除了string类型,其他四种数据结构删除的速度有可能很慢,这样增大了阻塞Redis的可能性。既然不能用del命令,就需要用到scan命令的若干类似命令:sscan(set)、hscan(hash)、zscan(sorted set)。
- 对于String类型使用del命令一般不会产生阻塞
- 对于hash、list、set、sorted set,可以使用hscan(以hash为例)命令,每次获取部分(例如100个)field-value,再利用hdel(以hash为例)删除每个field
public void delBigHash(String bigKey) {
Jedis jedis = new Jedis( “ 127.0.0.1 ” , 6379);
// 游标
String cursor = “ 0 ” ;
while (true) {
ScanResult<Map.Entry<String, String>> scanResult = jedis.hscan(bigKey, cursor,
new ScanParams().count(100));
// 每次扫描后获取新的游标
cursor = scanResult.getStringCursor();
// 获取扫描结果
List<Entry<String, String>> list = scanResult.getResult();
if (list == null || list.size() == 0) {
continue;
}
String[] fields = getFieldsFrom(list);
// 删除多个 field
jedis.hdel(bigKey, fields);
// 游标为 0 时停止
if (cursor.equals( “ 0 ” )) {
break;
}
}
// 最终删除 key
jedis.del(bigKey);
}
/**
* 获取 field 数组
* @param list
* @return
*/
private String[] getFieldsFrom(List<Entry<String, String>> list) {
List<String> fields = new ArrayList<String>();
for(Entry<String, String> entry : list) {
fields.add(entry.getKey());
}
return fields.toArray(new String[fields.size()]);
}
(4)最佳思路
由于开发人员对Redis的理解程度不同,在实际开发中出现bigkey在所难免,重要的是,能通过合理的检测机制及时找到它们,进行处理。作为开发人员在业务开发时应注意不能将Redis简单暴力的使用,应该在数据结构的选择和设计上更加合理,例如出现了bigkey,要思考一下可不可以做一些优化(例如拆分数据结构)尽量让这些bigkey消失在业务中,如果bigkey不可避免,也要思考一下要不要每次把所有元素都取出来(例如有时候仅仅需要hmget,而不是hgetall)。最后,可喜的是,Redis将在4.0版本支持lazy delete free的模式,那时删除bigkey不会阻塞Redis。
四、寻找热点key
热门新闻事件或商品通常会给系统带来巨大的流量,对存储这类信息的Redis来说却是一个巨大的挑战。以Redis Cluster为例,它会造成整体流量的不均衡,个别节点出现OPS过大的情况,极端情况下热点key甚至会超过Redis本身能够承受的OPS,因此寻找热点key对于开发和运维人员非常重要。
(1)客户端
距离key“最近”的地方,因为Redis命令就是从客户端发出的,可以在客户端设置全局字典(key和调用次数),每次调用Redis命令时,使用这个字典进行记录。
使用客户端进行热点key的统计非常容易实现,但是问题也很多:
- 无法预知key的个数,存在内存泄露的危险。
- 对于客户端代码有侵入,各个语言的客户端都需要维护此逻辑,维护成本较高。
- 只能了解当前客户端的热点key,无法实现规模化运维统计。
当然除了使用本地字典计数外,还可以使用其他存储来完成异步计数,从而解决本地内存泄露问题。但是另两个问题还是不好解决。
(2)代理端
像Twemproxy、Codis这些基于代理的Redis分布式架构,所有客户端的请求都是通过代理端完成的,如图12-5所示。此架构是最适合做热点key统计的,因为代理是所有Redis客户端和服务端的桥梁。但并不是所有Redis都是采用此种架构。
(3)Redis服务端
使用monitor命令统计热点key是很多开发和运维人员首先想到,monitor命令可以监控到Redis执行的所有命令。
使用Redis服务端的monitor命令也会存在两个问题:
- monitor命令在高并发条件下,会存在内存暴增和影响Redis性能的隐患,所以此种方法适合在短时间内使用。
- 只能统计一个Redis节点的热点key,对于Redis集群需要进行汇总统计。
(4)机器
Redis客户端使用TCP协议与服务端进行交互,通信协议采用的是RESP。如果站在机器的角度,可以通过对机器上所有Redis端口的TCP数据包进行抓取完成热点key的统计。
此种方法对于Redis客户端和服务端来说毫无侵入,是比较完美的方案,但是依然存在两个问题:
- 需要一定的开发成本,但是一些开源方案实现了该功能,例如ELK(ElasticSearch Logstash Kibana)体系下的packetbeat插件,可以实现对Redis、MySQL等众多主流服务的数据包抓取、分析、报表展示。
- 由于是以机器为单位进行统计,要想了解一个集群的热点key,需要进行后期汇总。
(5)四种方案对比
(6)解决热点key的三种思路
- 拆分复杂数据结构:如果当前key的类型是一个二级数据结构,例如哈希类型。如果该哈希元素个数较多,可以考虑将当前hash进行拆分,这样该热点key可以拆分为若干个新的key分布到不同Redis节点上,从而减轻压力。
- 迁移热点key:以Redis Cluster为例,可以将热点key所在的slot单独迁移到一个新的Redis节点上,但此操作会增加运维成本。
- 本地缓存加通知机制:可以将热点key放在业务端的本地缓存中,因为是在业务端的本地内存中,处理能力要高出Redis数十倍,但当数据更新时,此种模式会造成各个业务端和Redis数据不一致,通常会使用发布订阅机制来解决类似问题。