速度快
基于键值对的数据结构服务器
Redis 值可支持多种数据结构,如字符串、哈希、列表、集合、有序集合
丰富的功能
简单稳定
客户端语言多
持久化
主从复制
高可用和分布式
安装(原书中的安装方式):
$ wget http://download.redis.io/releases/redis-3.0.7.tar.gz
$ tar xzf redis-3.0.7.tar.gz
$ ln -s redis-3.0.7 redis
$ cd redis
$ make
$ make install
查看Redis版本(本人安装的是6.x版本Redis):
$ redis-cli -v
redis-cli 6.0.9
$ redis-cli -h {
host} -p {
port}
$ redis-cli -h ip {
host} -p {
port} {
command}
$ redis-cli shutdown
66947:M 29 May 2021 15:25:54.059 # User requested shutdown... #客户端发出的shutdown命令
66947:M 29 May 2021 15:25:54.059 * Saving the final RDB snapshot before exiting. #保存RDB持久化文件
66947:M 29 May 2021 15:25:54.059 * DB saved on disk #将RDB文件保存在磁盘上
66947:M 29 May 2021 15:25:54.059 # Redis is now ready to exit, bye bye... #关闭
停止Redis服务注意点:
kill -9
强制杀死Redis服务,不但不会做持久化操作,还会造成缓冲区等资源不能被优雅关闭,极端情况会造成AOF和复制丢失数据的情况。$ redis-cli shutdown nosave|save
keys *
dbsize
dbsize返回当前数据库中键的总数。dbsize命令在计算键总数时不会遍历所有键,而是直接获取Redis内置的键总数变量,所以时间复杂度为O(1)。而keys命令会遍历所有键,时间复杂度为O(n).
exists key
del key [key ...]
expire key seconds
type key
Redis使用单线程结构和IO多路复用模型来实现高性能的内存数据库服务。
字符串类型是Redis最基础的数据结构,类型可以是字符串(简单的字符串、复杂的字符串(例如JSON、XML))、数字,甚至是二进制(图片、音频、视频),但是值最大不能超过512M。
set key value [ex seconds] [px milliseconds] [nx|xx]
redis还提供setnx和setxx两个命令:
setex key seconds value
setnx key value
get key
mset key value [key value]
mget key [key ...]
incr key
incr命令用于对值做自增操作,返回结果分为三种情况:
append key value
向字符串尾部追加值
strlen key
getset key value
setrange key offset value
getrange key start end
start和end分别是开始和结束的偏移量,偏移量从0开始计算
字符串类型的内部编码有3种:
Redis会根据当前值的类型和长度决定使用哪种内部编码实现。
hset key field value
hget key field
hdel key field [field ...]
hlen key
hmset key field value [field value ...]
hmget key field [field ...]
hexists key field
hkeys key
hvals key
hgetall key
在使用hgetall时,如果哈希元素个数比较多,会存在阻塞Redis的可能。 如果开发人员只需要获取部分field,可以使用hmget,如果一定要获取全部 field-value,可以使用hscan命令,该命令会渐进式遍历哈希类型.
hincrby key field
hincrbyfloat key field
hstrlen key field
哈希类型的内部编码有两种:
哈希类型和关系型数据库不同之处:
缓存用户信息的方法:
set user:1:name tom
set user:1:age 24
set user:1:city shanghai
优点:简单直观,每个属性都支持更新操作
缺点:占用过多的键,内存占用量较大,同时用户信息内聚性较差,一般不用于生产环境
set user:1 serialize(userInfo)
优点:简化编程,如果合理的使用序列化可以提高内存的使用效率。
缺点:序列化和反序列化有一定的开销,同时每次更新属性都需要把全部数据取出进行反序列化,更新后再序列化到Redis中。
hmset user:1 name tomage 23 city beijing
优点:简单直观,如果使用合理可以减少内存空间的使用。
缺点:要控制哈希在ziplist和hashtable两种内部编码的转换,hashtable会 消耗更多内存。
列表类型的内部编码有两种:
集合类型也是用来保存多个元素的,集合中不允许有重复元素,并且集合中的元素是无序的,不能通过索引下标获取元素。
集合类型的内部编码有两种:
有序集合保留了集合不能有重复成员的特性,但是有序集合中的元素可以排序。它给每个元素设置一个分数作为排序的依据。有序集合中的元素不能重复,但是score可以重复。
有序集合类型的内部编码有两种:
rename key newkey # 如果newkey已经存在,则值会发生覆盖
renamenx key newkey # 如果newkey已经存在,则rename失败
注意点:
Redis3.2中会返回OK:
127.0.0.1:6379> rename key key
OK
Redis3.2之前的版本会提示错误:
127.0.0.1:6379> rename key key
(error) ERR source and destination objects are the same
127.0.0.1:6379> randomkey
"hello"
127.0.0.1:6379> randomkey
"jedis"
expire key seconds
expireat key timestamp
keys pattern
scan cursor [match pattern] [count number]
每次scan命令的时间复杂度是O(1)。
如果scan的过程中如果有键的变化(增加、删除、修改),那么可能会遇到以下问题:新增的键可能没有遍历到,遍历出了重复的键等
select dbIndex
Redis客户端执行命令的生命周期:
慢查询只会统计步骤3执行命令的时间,所以没有慢查询并不代表客户端没有超时问题。
如果要Redis将配置持久化到本地配置文件,需要执行config rewrite命 令
(1)获取慢查询日志
slowlog get [n]
127.0.0.1:6379> slowlog get
1) 1) (integer) 666
2) (integer) 1456786500
3) (integer) 11615
4) 1) "BGREWRITEAOF"
2) 1) (integer) 665
2) (integer) 1456718400
3) (integer) 12006
4) 1) "SETEX"
2) "video_info_200"
3) "300"
4) "2"
...
每个慢查询日志有4个属性组成,分别是慢查询日志的标识 id、发生时间戳、命令耗时、执行命令和参数
(2)获取慢查询日志列表当前的长度
slowlog len
(3) 慢查询日志重置
slowlog reset
-r
-r(repeat)选项代表将命令执行多次。
-i
-i(interval)选项代表每隔几秒执行一次命令,-i必须和-r一起使用
-i的单位是秒,不支持毫秒为单位,但是如果想以每隔10毫秒执行 一次,可以用-i0.01
-x
-x选项代表从标准输入(stdin)读取数据作为redis-cli的最后一个参数
-c
-c(cluster)选项是连接Redis Cluster节点时需要使用的,-c选项可以防 止moved和ask异常
-a
如果Redis配置了密码,可以用-a(auth)选项,有了这个选项就不需要 手动输入auth命令。
–scan和–pattern
–scan选项和–pattern选项用于扫描指定模式的键,相当于使用scan命令。
–slave
–slave选项是把当前客户端模拟成当前Redis节点的从节点,可以用来 获取当前Redis节点的更新操作
–rdb
–rdb选项会请求Redis实例生成并发送RDB持久化文件,保存在本地。可使用它做持久化文件的定期备份。
–pipe
–pipe选项用于将命令封装成Redis通信协议定义的数据格式,批量发送 给Redis执行
–bigkeys
–bigkeys选项使用scan命令对Redis的键进行采样,从中找到内存占用比 较大的键值,这些键可能是系统的瓶颈。
–eval
–eval选项用于执行指定Lua脚本
–latency
latency有三个选项,分别是–latency、–latency-history、–latency-dist。 它们都可以检测网络延迟
–stat
–stat选项可以实时获取Redis的重要统计信息
–raw和–no-raw
–no-raw选项是要求命令的返回结果必须是原始的格式,–raw恰恰相反,返回格式化后的结果。
检测当前操作系统能否提供1G的内存给Redis:
redis-server --test-memory 1024
redis-benchmark可以为Redis做基准性能测试
-c(clients)选项代表客户端的并发数量(默认是50)。
-n(num)选项代表客户端请求总量(默认是100000)
-q选项仅仅显示redis-benchmark的requests per second信息
-r选项会在key、counter键上加一个12位的后缀,-r10000代表只对后四 位做随机处理(-r不是随机数的个数)
-P选项代表每个请求pipeline的数据量(默认为1)
-k选项代表客户端是否使用keepalive,1为使用,0为不使用,默认值为 1
-t选项可以对指定命令进行基准测试
–csv选项会将结果按照csv格式输出
Pipeline(流水线)能将一组Redis命令进行组装,通过一次RTT传输给Redis,再将这组Redis命令的执行结果按顺序返回给客户端。
原生批量命令与Pipeline对比:
每次Pipeline组装的命令个数不能太多,否则一方面会增加客户端的等待时间,另一方面会造成一定的网络阻塞,可以将一次包含大量命令的Pipeline拆分成多次较小的Pipeline来完成。Pipeline只能操作一个Redis实例。
Redis提供了简单的事物功能,将一组需要一起执行的命令放到multi和exec两个命令之间。multi命令代表事物开始,exec命令代表事物结束,它们之间的命令是原子顺序执行的。如果要停止事物的执行,可以执行discard命令。
如果事物中的命令出现错误,Redis的处理机制也不一样。
有些应用场景需要在事务之前,确保事务中的key没有被其他客户端修 改过,才执行事务,否则不执行(类似乐观锁)。Redis提供了watch命令来 解决这类问题。
Redis不支持事物中的回滚特性,同时无法实现命令之间的逻辑计算关系
local strings val = "world"
local代表val是一个局部变量,如果没有local代表是全局变量。
(2)数组
在Lua中可使用tables类型实现类似数组的功能,Lua的数组下标从1开始计算:
local tables myArray = {
"redis", "jedis", true, 88.0}
-- 打印结果:true
print(myArray[3])
遍历整个数组可使用for和while
(1)eval
eval 脚本内容 key个数 key列表 参数列表
127.0.0.1:6379> eval 'return "hello "..KEYS[1] .. ARGV[1]' 1 redis world
"hello redisworld"
此时KEYS[1]=“redis”,ARGV[1]=“world”
如果Lua脚本较长,还可以使用redis-cli–eval直接执行文件。eval命令和–eval参数本质是一样的,客户端如果想执行Lua脚本,首先在客户端编写好Lua脚本代码,然后把脚本作为字符串发送给服务端,服务端会将执行结果返回给客户端
(2)evalsha
首先要将Lua脚本加载到Redis服务端,得到该脚本的SHA1校验和, evalsha命令使用SHA1作为参数可以直接执行对应Lua脚本,避免每次发送 Lua脚本的开销。这样客户端就不需要每次执行脚本内容,而脚本也会常驻 在服务端,脚本功能得到了复用。
$ redis-cli script load "$(cat lua_get.lua)"
"7413dc2440db1fea7c0a0bde841fa68eefaf149c"
127.0.0.1:6379> evalsha 7413dc2440db1fea7c0a0bde841fa68eefaf149c 1 redis world
"hello redisworld"
本节将每个独立用户是否访问过网站存放在Bitmaps中,将访问的用户记作1,没有访问的用户记作0,用偏移量作为用户的id。
setbit key offset value
设置键的第offset个位的值(从0算起),假设现在有20个用户,userid=0,5,11,15,19的用户对网站进行了访问,那么当前Bitmaps的值如下所示:
具体操作过程如下,unique:users:2016-04-05代表2016-04-05这天的独立访问用户的Bitmaps:
127.0.0.1:6379> setbit unique:users:2016-04-05 0 1
(integer) 0
127.0.0.1:6379> setbit unique:users:2016-04-05 5 1
(integer) 0
127.0.0.1:6379> setbit unique:users:2016-04-05 11 1
(integer) 0
127.0.0.1:6379> setbit unique:users:2016-04-05 15 1
(integer) 0
127.0.0.1:6379> setbit unique:users:2016-04-05 19 1
(integer) 0
如果此时有一个userid=50的用户访问了网站,那么Bitmaps的结构变成 了下图,第20位~49位都是0。
很多应用的用户id以一个指定数字(例如10000)开头,直接将用户id 和Bitmaps的偏移量对应势必会造成一定的浪费,通常的做法是每次做setbit 操作时将用户id减去这个指定数字。在第一次初始化Bitmaps时,假如偏移 量非常大,那么整个初始化过程执行会比较慢,可能会造成Redis的阻塞。
getbit key offset
获取键的第offset的值(从0开始计算)
bitcount [start] [end]
bitop op destkey key [key ...]
bitop是一个复合操作,它可以做多个Bitmaps的and(交集)、or(并集)、not(非)、xor(异或)操作并将结果保存在destkey中。
bitpos key targetBit [start] [end]
(个人总结:适合密集型的数据存储,稀疏性数据存储会造成较大的空间浪费)
HyperLogLog是一种基数算法,通过HyperLogLog可以利用极小的内存空间完成独立总数的统计,数据集可以是IP、Email、ID等。HyperLogLog提供三个命令:pfadd、pfcount、pfmerge。
pfadd key element [element ...]
pfadd用于向HyperLogLog添加元素,如果添加成功则返回1
pfcount key [key ...]
pfcount用于计算一个或多个HyperLogLog的独立总数
pfmerge destkey sourcekey [sourcekey ...]
pfmerge可以求出多个HyperLogLog的并集并赋值给destkey。
HyperLogLog内存占用量很小,但是存在错误率,开发者在进行数据结构选型时只需要确认下面两条即可:
Redis主要提供了发布消息、订阅频道、取消订阅以及按照模式订阅和取消订阅等命令。
publish channel message
subscribe channel [channel ...]
unsubscribe [channel [channel ...]]
psubscribe pattern [pattern...]
punsubscribe [pattern [pattern ...]]
pubsub channels [pattern]
所谓活跃的频道是指当前频道至少有一个订阅者,其中[pattern]是可以指定具体的模式
(2)查看频道订阅数
pubsub numsub [channel ...]
(3)查看模式订阅数
pubsub numpat
geoadd key longitude latitude member [longitude latitude memeber ....]
longitude、latitude、member分别是该地理位置的经度、纬度、成员
geopos key member [member ...]
geodist key member1 member2 [unit]
其中unit代表返回结果的单位,包含以下四种:
georadius key longitude latitude radiusm|km|ft|mi [withcoord] [withdist] [withhash] [COUNT count] [asc|desc] [store key] [storedist key]
georadiusbymember key member radiusm|km|ft|mi [withcoord] [withdist] [withhash] [COUNT count] [asc|desc] [store key] [storedist key]
geohash key member [member ...]
geohash有如下特点:
zrem key member
GEO没有提供删除成员的命令,但是因为GEO的底层实现是zset,所以可以借用zrem命令实现对地理位置信息的删除。
*<参数数量> CRLF
$<参数1的字节数量> CRLF
<参数1> CRLF
...
$<参数N的字节数量> CRLF
<参数N> CRLF
命令set hello word
对应的格式如下:
*3 # 参数数量有三个
$3 # set的字节数为3
SET
$5 # hello的字节数为5
hello
$5
world
$ nc localhost 6379
set hello world
+OK
sethx
-ERR unknown command `sethx`, with args beginning with:
incr counter
:1
get hello
$5
world
mset java jedis python redis-py
+OK
mget java python
*2
$5
jedis
$8
redis-py
get not_exist_key
$-1
mget hello not_exist_key java
*3
$5
world
$-1
$5
jedis
127.0.0.1:6379> client list
id=795 addr=172.17.0.1:13410 fd=12 name= age=347 idle=261 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=0 qbuf-free=0 obl=0 oll=0 omem=0 events=r cmd=mget
id=796 addr=172.17.0.1:14408 fd=16 name= age=4 idle=0 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=0 qbuf-free=32768 obl=0 oll=0 omem=0 events=r cmd=client
输出结果的每一行代表一个客户端的信息
(1)标识:id、addr、fd、name
(2) 输入缓冲区:qbuf、qbuf-free
Redis为每个客户端分配了输入缓冲区,它的作用是将客户端发送的命令临时保存,同时Redis会从输入缓冲区拉取命令并执行。
client list中qbuf和qbuf-free分别代表这个缓冲区的总容量和剩余容量,Redis没有提供相应的配置来规定每个缓冲区的大小,输入缓冲区会根据输入内容大小的不同动态调整,只是要求每个客户端缓冲区的大小不能超过1G,超过后客户端将被关闭。
输入缓冲使用不当会产生两个问题:
造成输入缓冲区过大的原因:
监控输入缓冲区异常的方法:
(3)输出缓冲区:obl、oll、omem
Redis为每个客户端分配了输出缓冲区,它的作用是保存命令执行的结果返回给客户端,为Redis和客户端交互返回结果提供缓冲。
输出缓冲区的容量可以通过参数client-output-buffer-limit
来进行设置,按照客户端的不同分为三种:普通客户端、发布订阅客户端、slave客户端。
配置命令:
client-output-buffer-limit <class> <hard limit> <soft limit> <soft seconds>
输出缓冲区由两部分组成:固定缓冲区(16KB)和动态缓冲 区,其中固定缓冲区返回比较小的执行结果,而动态缓冲区返回比较大的结 果,例如大的字符串、hgetall、smembers命令的结果等.
固定缓冲区使用的是字节数组,动态缓冲区使用的是列表。当固定缓冲区存满后会将Redis新的返回结果存放在动态缓冲区的队列中,队列中的每个对象就是每个返回结果。
client list中的obl代表固定缓冲区的长度,oll代表动态缓冲区列表的长度,omem代表使用的字节数。
(4)客户端的存活状态
client list中的age和idle分别代表当前客户端已经连接的时间和最近一次的空闲时间
(5)客户端的限制maxclients和timeout
Redis提供了maxclients参数来限制最大客户端连接数,一旦连接数超过 maxclients,新的连接将被拒绝。maxclients默认值是10000,可以通过info clients来查询当前Redis的连接数。Redis提供timeout(单位为秒)参数来限制连接的最大空闲时间,一 旦客户端连接的idle时间超过了timeout,连接将会被关闭。Redis默认的timeout是0,也就是不会检测客户端的空闲。
client setName和client getName
client setName用于给客户端设置名字,这样比较容易标识出客户端的来源
client kill
client kill ip:port
此命令用于杀掉指定IP地址和端口的客户端
client pause timeout(毫秒)
client pause命令用于阻塞客户端timeout毫秒数,在此期间客户端连接将被阻塞。
info clients
127.0.0.1:6379> info clients
# Clients
connected_clients:1414
client_longest_output_list:0
client_biggest_input_buf:2097152
blocked_clients:0
1)connected_clients:代表当前Redis节点的客户端连接数,需要重点监控,一旦超过maxclients,新的客户端连接将被拒绝。
2)client_longest_output_list:当前所有输出缓冲区中队列对象个数的最大值。
3)client_biggest_input_buf:当前所有输入缓冲区中占用的最大容量。
4)blocked_clients:正在执行阻塞命令(例如blpop、brpop、 brpoplpush)的客户端个数。
info stats
127.0.0.1:6379> info stats
# Stats
total_connections_received:80
...
rejected_connections:0
RDB持久化是把当前进程数据生成快照保存到硬盘的过程,触发RDB持久化过程分为手动触发和自动触发。
手动触发对应save和bgsave命令:
自动触发 RDB持久化:
(1)使用save相关配置,如“save m n”。表示m秒内数据集存在n次修改时,自动触发bgsave。
(2)如果从节点执行全量复制操作,主节点自动执行bgsave生成RDB文件并发送给从节点。
(3)执行debug reload命令重新加载Redis时,也会自动触发save操作。
(4)默认情况下执行shutdown命令时,如果没有开启AOF持久化功能则自动执行bgsave。
1)执行bgsave命令,Redis父进程判断当前是否存在正在执行的子进程,如RDB/AOF子进程,如果存在bgsave命令直接返回。
2)父进程执行fork操作创建子进程,fork操作过程中父进程会阻塞,通过info stats
命令查看latest_fork_usec
选项,可以获取最近一个fork操作的耗时,单位为微秒。
3)父进程fork完成后,bgsave命令返回“Background saving started”信息并不再阻塞父进程,可以继续响应其他命令。
4)子进程创建RDB文件,根据父进程内存生成临时快照文件,完成后对原有文件进行原子替换。执行lastsave
命令可以获取最后一次生成RDB的时间,对应info统计的rdb_last_save_time
选项。
5)进程发送信号给父进程表示完成,父进程更新统计信息,具体见info Persistence下的rdb_*相关选项。
保存: RDB文件保存在dir配置指定的目录下,文件名通过dbfilename配置指定。可以通过执行config set dir {newDir}
和config set dbfilename {newFileName}
运行期动态执行,当下次运行时RDB文件会保存到新目录。
优点:
缺点:
AOF持久化:以独立日志的方式记录每次写命令,重启时再重新执行AOF文件中的命令达到恢复数据的目的。
开启AOF功能需要设置配置:appendonly yes
,默认不开启。AOF工作流程:
(1)所有的写入命令会追加到aof_buf
(缓冲区)中。
(2)AOF缓冲区根据对应的策略向硬盘做同步操作。
(3)随着AOF文件越来越大,需要定期对AOF文件进行重写,达到压缩的目的。
(4)当Redis服务器重启时,可以加载AOF文件进行数据恢复。
AOF命令写入的内容直接是文本协议格式。例如set hello world
这条命令,在AOF缓冲区会追加如下文本:
*3\r\n$3\r\nset\r\n$5\r\nhello\r\n$5\r\nworld\r\n
AOF为什么直接采用文本格式?
系统调用write和fsync说明:
AOF文件重写是把Redis进程内的数据转化为写命令同步到新AOF文件的过程。
为什么重写后的AOF文件体积变小?
AOF重写过程可以手动触发和自动触发:
auto-aof-rewrite-min-size
和auto-aof-rewrite-percentage
参数确定自动触发时机。auto-aof-rewrite-min-size
:表示运行AOF重写时文件最小体积,默认为64MB。
auto-aof-rewrite-percentage
:代表当前AOF文件空间 (aof_current_size
)和上一次重写后AOF文件空间(aof_base_size
)的比 值。
自动触发时机=aof_current_size>auto-aof-rewrite-min-size&&(aof_current_size-aof_base_size)/aof_base_size>=auto-aof-rewrite- percentage
AOF重写运作流程:
流程说明:
(1)执行AOF重写请求
如果当前进程正在执行AOF重写,请求不执行并返回如下响应:ERR Background append only file rewriting already in progress
如果当前进程正在执行bgsave操作,重写命令延迟到bgsave完成之后再 执行,返回如下响应:Background append only file rewriting scheduled
(2)父进程执行fork创建子进程,开销等同于bgsave过程。
(3.1)主进程fork操作完成之后,继续响应其他命令。所有修改命令依然写入AOF缓冲区并根据appendfsync策略同步到磁盘,保证原有AOF机制正确性。
(3.2)由于fork操作运用写时复制技术,子进程只能共享fork操作时的内 存数据。由于父进程依然响应命令,Redis使用“AOF重写缓冲区”保存这部 分新数据,防止新AOF文件生成期间丢失这部分数据。
(4)子进程根据内存快照,按照命令合并规则写入到新的AOF文件。每 次批量写入硬盘数据量由配置aof-rewrite-incremental-fsync控制,默认为 32MB,防止单次刷盘数据过多造成硬盘阻塞。
(5.1)新AOF文件写入完成后,子进程发送信号给父进程,父进程更新 统计信息,具体见info persistence下的aof_*相关统计。
(5.2)父进程把AOF重写缓冲区的数据写入到新的AOF文件。
(5.3)使用新AOF文件替换老文件,完成AOF重写。
Redis持久化文件加载流程:
(1)AOF持久化开启且存在AOF文件时,优先加载AOF文件
(2)AOF关闭或者AOF文件不存在时,加载RDB文件
(3)加载AOF/RDB文件成功后,Redis启动成功。
(4)AOF/RDB文件存在错误时,Redis启动失败并打印错误信息。
加载损坏的AOF文件时会拒绝启动,并打印如下日志:
# Bad file format reading the append only file: make a backup of your AOF file, then use ./redis-check-aof --fix
对于错误格式的AOF文件,先进行备份,然后采用redis-check-aof–fix命 令进行修复,修复后使用diff-u对比数据的差异,找出丢失的数据,有些可 以人工修改补全。
Redis做RDB或AOF重写时,都会进行fork操作,对于大多数操作系统来说fork是个重量级错误。fork操作会复制父进程的空间内存页表。
fork耗时问题定位 :可以在info stats统 计中查latest_fork_usec指标获取最近一次fork操作耗时,单位微秒。
改善fork耗时问题 :
(1)优先使用物理机或者高效支持fork操作的虚拟化技术,避免使用Xen虚拟机。
(2)控制Redis实例最大可用内存,fork耗时跟内存量成正比,线上建议每个Redis实例内存控制在10GB以内。
(3)合理配置Linux内存分配策略,避免物理内存不足虑导致fork失败
(4)降低fork操作的频率,如适度放宽AOF自动触发时机,避免不必要的全量复制。
当开启AOF持久化时,常用的同步硬盘的策略是everysec,用于平衡性 能和数据安全性。对于这种方式,Redis使用另一条线程每秒执行fsync同步 硬盘。当系统硬盘资源繁忙时,会造成Redis主线程阻塞。
阻塞流程分析:
(1)主线程负责写入AOF缓冲区。
(2)AOF线程负责每秒执行一次同步磁盘操作,并记录最近一次同步时间。
(3)主线程负责对比上次AOF同步时间:
通过对AOF阻塞流程可以发现两个问题:
AOF阻塞问题定位:
(1)发生AOF阻塞时,Redis输出如下日志,用于记录AOF fsync阻塞导致拖慢Redis服务的行为:
Asynchronous AOF fsync is taking too long (disk is busy). Writing the AOF buffer without waiting for fsync to complete, this may slow down Redis
(2)每当发生AOF追加阻塞事件发生时,在info Persistence统计中, aof_delayed_fsync指标会累加,查看这个指标方便定位AOF阻塞问题。
(3)AOF同步最多允许2秒的延迟,当延迟发生时说明硬盘存在高负载问 题,可以通过监控工具如iotop,定位消耗硬盘IO资源的进程。
参与复制的Redis实例划分为主节点(master)和从节点(slave)。默认情况下,Redis都是主节点。每个从节点只能有一个主节点,而主节点可以同时具有多个从节点。复制的数据流是单向的,只能从主节点复制到从节点。配置复制的方式有以下三种:
(1)在配置文件中加入slaveof {masterHost} {masterPort}
随Redis启动生效。
(2)在redis-server启动命令后加入--slaveof {masterHost} {masterPort}
生效。
(3)直接使用命令:slaveof {masterHost} {masterPort}
生效。
slaveof命令在使用时,可以运行期动态配置,也可以提前写到配置文件中。
slaveof配置都是在从节点发起。
slaveof本身是异步命令,执行slaveof命令时,节点只保存主节点信息后返回,后续复制流程在节点内部异步执行。
执行slaveof no one
来断开与主节点的复制关系。
断开复制主要流程:
从节点断开复制后并不会抛弃原有数据,只是无法再获取主节点上的数据变化。
通过slaveof命令还可以实现切主操作,所谓切主是指把当前从节点对主节点的复制切换到另一个主节点。执行slaveof {newMasterIp} {newMasterPort}
命令即可。
切主操作流程如下:
主节点可以设置requirepass参数进行密码验证,客户端访问时必须使用auth命令进行校验。从节点与主节点 的复制连接是通过一个特殊标识的客户端来完成,因此需要配置从节点的 masterauth参数与主节点密码保持一致,这样从节点才可以正确地连接到主 节点并发起复制流程。
默认情况下,从节点使用slave-read-only=yes
配置为只读模式。由于复制只能从主节点到从节点,对于从节点的任何修改主节点都无法感知,修改从节点会造成主从数据不一致。因此建议线上不要修改从节点的只读模式。
主从节点一般部署在不同机器上,复制时的网络延迟就成为需要考虑的问题,Redis为我们提供了repl-disable-tcp-nodelay
参数用于控制是否关闭 TCP_NODELAY,默认关闭,说明如下:
Redis的复制拓扑结构可以支持单层或多层复制关系,根据拓扑复杂性
可以分为以下三种:一主一从、一主多从、树状主从结构:
一主一从结构
一主一从结构是最简单的复制拓扑结构,用于主节点出现宕机时从节点提供故障转移支持。当应用写命令并发量较高且需要持久化时,可以只在从节点上开启AOF,这样既保证数据安全性同时也避免了持久化对主节点的性能干扰。但需要注意的是,当主节点关闭持久化功能时, 如果主节点脱机要避免自动重启操作。因为主节点之前没有开启持久化功能自动重启后数据集为空,这时从节点如果继续复制主节点会导致从节点数据也被清空的情况,丧失了持久化的意义。安全的做法是在从节点上执行 slaveof no one
断开与主节点的复制关系,再重启主节点从而避免这一问题。
一主多从
一主多从结构(又称为星形拓扑结构)使得应用端可以利用多个从节点 实现读写分离。对于读占比较大的场景,可以把读命令发送到从节点来分担主节点压力。同时在日常开发中如果需要执行一些比较耗时的 读命令,如:keys
、sort
等,可以在其中一台从节点上执行,防止慢查询对主节点造成阻塞从而影响线上服务的稳定性。对于写并发量较高的场景,多个从节点会导致主节点写命令的多次发送从而过度消耗网络带宽,同时也加 重了主节点的负载影响服务稳定性。
树状主从
树状主从结构(又称为树状拓扑结构)使得从节点不但可以复制主节点 数据,同时可以作为其他从节点的主节点继续向下层复制。通过引入复制中间层,可以有效降低主节点负载和需要传送给从节点的数据量。
(1)保存主节点(master)信息。执行slaveof
命令后,从节点只保存主节点的地址信息便直接返回,这时建立复制流程还没有开始。
(2)从节点内部通过每秒运行的定时任务维护复制相关逻辑,当定时任务发现存在新的主节点后,会尝试与该节点建立网络连接。从节点会建立一个socket套接字,专门用于接受主节点发送的复制命令。如果从节点无法建立连接,定时任务会无限重试直到连接成功或者执行slaveof no one
取消复制。
(3)发送ping命令。连接建立成功之后从节点发送ping请求进行首次通信。ping请求主要目的如下:
如果发送ping命令后,从节点没有收到主节点的pong回复或者超时,比如网络超时或者主节点正在阻塞无法响应命令,从节点会断开复制连接,下次定时任务会发起重连。
(4)权限验证。如果主节点设置了requirepass参数,则需要密码验证, 从节点必须配置masterauth参数保证与主节点相同的密码才能通过验证;如果验证失败复制将终止,从节点重新发起复制流程。
(5)同步数据集。主从复制连接正常通信后,对于首次建立复制的场景,主节点会把持有的数据全部发送给从节点,这部分操作是耗时最长的步骤。Redis在2.8版本以后采用新复制命令psync
进行数据同步,原来的sync命令依然支持,保证新旧版本的兼容性。新版同步划分两种情况:全量同步和部分同步
(6)命令持续复制。当主节点把当前的数据同步给从节点后,便完成了复制的建立流程。接下来主节点会持续地把写命令发送给从节点,保证主从数据一致性。
psync命令运行需要以下组件支持:
流程说明:
Partial resynchronization not possible (no cached master)
Full resync from master: 92d1cb14ff7ba97816216f7beb839efe036775b2:216789
client-output-buffer-limit slave256MB 64MB 60
,如果60秒内缓冲区消耗持续大于64MB或者直接超过256MB时,主节点将直接关闭复制客户端连接,造成全量同步失败。流程说明:
repl-timeout
时间,主节点会认为从节点故障并中断复制连接主从节点在建立复制之后,它们之间维护着长连接并彼此发送心跳命令。
client list
命令查看复制相关客户端信息,主节点的连接状态为 flags=M,从节点连接状态为flags=S。repl-ping-slave-period
控制发送频率。replconf ack {offset}
命令,给主节点上报自身当前的复制偏移量。replconf命令主要作用如下:min-slaves-to-write
、min-slaves-max-lag
参数配置定义。主节点不但负责数据读写,还负责把写命令同步给从节点。写命令的发送过程是异步完成,也就是说主节点自身处理完写命令后直接返回给客户端,并不等待从节点复制完成。
由于主从复制过程是异步的,就会造成从节点的数据相对主节点存在延迟。
对于读占比较高的场景,可以通过把一部分读流量分摊到从节点 (slave)来减轻主节点(master)压力,同时需要注意永远只对主节点执行写操作。
当使用从节点响应读请求时,业务端可能会遇到如下问题:
对于有些配置主从之间是可以不一致,比如:主节点关闭AOF但是在从节点开启。但对于内存相关的配置必须要一致,比如maxmemory,hash-max-ziplist-entries等参数。当配置的 maxmemory从节点小于主节点,如果复制的数据量超过从节点maxmemory 时,它会根据maxmemory-policy策略进行内存溢出控制,此时从节点数据已 经丢失,但主从复制流程依然正常进行,复制偏移量也正常。修复这类问题 也只能手动进行全量复制。当压缩列表相关参数不一致时,虽然主从节点存 储的数据一致但实际内存占用情况差异会比较大。
复制风暴是指大量从节点对同一主节点或者对同一台机器的多个主节点 短时间内发起全量复制的过程。复制风暴对发起复制的主节点或者机器造成 大量开销,导致CPU、内存、带宽消耗。
阻塞原因:
执行slowlog get {n}
命令获取最近的n条慢查询命令,默认对于执行超过10毫秒的命令都会记录到一个定长队列中,线上实例建议设置为1毫秒便于及时发现毫秒级以上的命令。慢查询队列长度默认128,可适当调大。
慢查询优化方案:
执行命令rediscli -h {ip} -p {port} bigkeys
可扫描大对象,内部原理采用分段进行scan操作,把历史扫描过的最大对象统计出来便于分析优化。
单线程的Redis处理命令时只能使用一个CPU。而CPU饱和是指Redis把单核CPU使用率跑到接近100%。对于这种情况,首先判断当前Redis的并发量是否达到极限,建议使用redis-cli -h {ip} -p {port} --stat
获取当前Redis使用情况,该命令每秒输出一行统计信息:
(base) ➜ ~ redis-cli --stat
------- data ------ --------------------- load -------------------- - child -
keys mem clients blocked requests connections
0 1.02M 1 0 1 (+0) 2
0 1.02M 1 0 2 (+1) 2
0 1.02M 1 0 3 (+1) 2
0 1.02M 1 0 4 (+1) 2
0 1.02M 1 0 5 (+1) 2
0 1.02M 1 0 6 (+1) 2
0 1.02M 1 0 7 (+1) 2
还有些情况可以通过info commandstats
统计信息分析出命令不合理开销时间。
持久化引起主线程阻塞的操作主要有:fork阻塞、AOF刷盘阻塞、HugePage写操作阻塞。
Redis的主从复制模式可以将主节点的数据改变同步给从节点,这样从节点就可以起到两个作用:第一,作为主节点的一个备份,一旦主节点出了故障不可达的情况,从节点可以作为后备“顶”上来,并且保证数据尽量不丢 失(主从复制是最终一致性)。第二,从节点可以扩展主节点的读能力,一 旦主节点不能支撑住大并发量的读操作,从节点可以在一定程度上帮助主节点分担读压力。
主从复制的问题:
当主节点出现故障时,Redis Sentinel能自动完成故障发现和故障转移并通知应用方,从而实现真正的高可用。
Redis Sentinel与Redis主从复制模式只是多了若干Sentinel节点,所以Redis Sentinel并没有针对Redis节点做特殊处理:
从逻辑架构上看,Sentinel节点集合会定期对所有节点进行监控,特别是对主节点的故障实现自动转移。
下面以1个主节点、2个从节点、3个Sentinel节点组成的Redis Sentinel为例子进行说明:
Redis Sentinel具有以下几个功能:
Redis Sentinel包含若干个Sentinel节点:
Redis Sentinel通过三个定时监控任务完成对各个节点发现和监控:
__sentinel__:hello
频道上发送该Sentinel节点对于主节点的判断以及当前Sentinel节点的信息,同时每个Sentinel节点也会订阅该频道,来了解其他Sentinel节点以及它们对主节点的判断,所以这个定时任务可以完成以下两个工作:__sentinel__:hello
了解其他 的Sentinel节点信息,如果是新加入的Sentinel节点,将该Sentinel节点信息保存起来,并与该Sentinel节点创建连接。主观下线
每个Sentinel节点会每隔1秒对主节点、从节点、其他Sentinel节点发送ping命令做心跳检测,当这些节点超过 down-after-milliseconds
没有进行有效回复,Sentinel节点就会对该节点做失败判定,这个行为叫做主观下线。
客观下线
当Sentinel主观下线的节点是主节点时,该Sentinel节点会通过sentinel is-master-down-by-addr
命令向其他Sentinel节点询问对主节点的判断,当超过
个数,Sentinel节点认为主节点确实有问题,这时该Sentinel节点会做出客观下线的决定。
从节点、Sentinel节点在主观下线后,没有后续的故障转移操作。
主节点被客观下线,这时只需要一个Sentinel节点来完成故障转移的工作,所以Sentinel节点之间会选举一个领导者进行故障转移的工作。Redis使用Raft算法实现领导者选举。
Redis Sentinel领导者选举的大致思路:
sentinel is-master-down-by addr
命令,要求将自己设置为领导者。sentinel is-master-down-by-addr
命令,将同意该请求,否则拒绝。max(quorum, num(sentinels)/2+1)
,那么它将成为领导者。领导者选举出的Sentinel节点负责故障转移,具体步骤如下:
在从节点列表中选出一个节点作为新的主节点,选择方法如下:
Sentinel领导者节点会对第一步选出来的从节点执行slaveof no one
命令让其成为主节点。
Sentinel领导者节点会向剩余的从节点发送命令,让它们成为新主节点的从节点,复制规则和parallel-syncs参数有关。
Sentinel节点集合会将原来的主节点更新为从节点,并保持着对其关注,当其恢复后命令它去复制新的主节点。
分布式数据库首先要解决把整个数据集按照分区规则映射到多个节点的问题,即把数据集划分到多个节点上,每个节点负责整体数据的一个子集。常见的分区规则有哈希分区和顺序分区两种。
哈希分区规则:
hash(key)%N
计算出哈希值,用来决定数据映射到哪一个节点上。Redis Cluster采用虚拟槽分区,所有的键根据哈希函数映射到0~16383整数槽内,计算公式:slot=CRC16(key)&16383
。每一个节点负责维护一部分槽以及槽所映射的键值数据。
(CRC16(key)%16384可以转换为CRC16(key)&16383,详见使用位运算替代模运算)
Redis虚拟槽分区的特点:
Redis集群相对单机在功能上存在一些限制:
在分布式存储中需要提供维护节点元数据信息的机制,所谓元数据是指:节点负责哪些数据,是否出现故障等状态信息。常见的元数据维护方式分为:集中式和P2P方式。Redis集群采用P2P的Gossip(流言)协议, Gossip协议工作原理就是节点彼此不断通信交换信息,一段时间后所有的节点都会知道集群完整的信息,这种方式类似流言传播。
通信过程说明:
集群中每个节点通过一定规则挑选要通信的节点,每个节点可能知道全部节点,也可能仅知道部分节点,只要这些节点彼此可以正常通信,最终它们会达到一致的状态。当节点出故障、新节点加入、主从角色变化、槽信息变更等事件发生时,通过不断的ping/pong消息通信,经过一段时间后所有的节点都会知道整个集群全部节点的最新状态,从而达到集群状态同步的目的。
Gossip协议的主要职责就是信息交换。常用的Gossip消息可分为:ping消息、pong消息、meet消息、fail消息等,它们的通信模式如下:
所有的消息格式划分为:消息头和消息体。消息头包含发送节点自身状态数据,接收节点根据消息头就可以获取到发送节点的相关数据,结构如下:
typedef struct {
char sig[4]; /* 信号标示 */
uint32_t totlen; /* 消息总长度 */
uint16_t ver; /* 协议版本*/
uint16_t type; /* 消息类型,用于区分meet,ping,pong等消息 */
uint16_t count; /* 消息体包含的节点数量,仅用于meet,ping,pong消息类型*/
uint64_t currentEpoch; /* 当前发送节点的配置纪元 */
uint64_t configEpoch; /* 主节点/从节点的主节点配置纪元 */
uint64_t offset; /* 复制偏移量 */
char sender[CLUSTER_NAMELEN]; /* 发送节点的nodeId */
unsigned char myslots[CLUSTER_SLOTS/8]; /* 发送节点负责的槽信息 */
char slaveof[CLUSTER_NAMELEN]; /* 如果发送节点是从节点,记录对应主节点的nodeId */
uint16_t port; /* 端口号 */
uint16_t flags; /* 发送节点标识,区分主从角色,是否下线等 */
unsigned char state; /* 发送节点所处的集群状态 */
unsigned char mflags[3]; /* 消息标识 */
union clusterMsgData data /* 消息正文 */;
} clusterMsg;
集群内所有的消息都采用相同的消息头结构clusterMsg,它包含了发送节点关键信息,如节点id、槽映射、节点标识(主从角色,是否下线)等。 消息体在Redis内部采用clusterMsgData结构声明,结构如下:
union clusterMsgData {
/* ping,meet,pong消息体*/
struct {
/* gossip消息结构数组 */
clusterMsgDataGossip gossip[1];
} ping;
/* FAIL 消息体 */
struct {
clusterMsgDataFail about;
} fail;
// ...
};
接收节点收到ping/meet消息时,执行解析消息头和消息体流程:
消息处理完后回复pong消息,内容同样包含消息头和消息体,发送节点接收到回复的pong消息后,采用类似的流程解析处理消息并更新与接收节点最后通信时间,完成一次消息通信。
通信节点选择过多虽然可以做到信息及时交换但成本过高。节点选择过少会降低集群内所有节点彼此信息交换频率,从而影响故障判定、新节点发现等需求的速度。因此Redis集群的Gossip协议需要兼顾信息交换实时性和成本开销,通信节点选择的规则如下:
根据通信节点选择的流程可以看出消息交换的成本主要体现在单位时间选择发送消息的节点数量和每个消息携带的数据量。
1+10*num(node.pong_received>cluster_node_timeout/2)
,因此 cluster_node_timeout参数对消息发送的节点数量影响非常大。当我们的带宽资源紧张时,可以适当调大这个参数,如从默认15秒改为30秒来降低带宽占用率。过度调大cluster_node_timeout会影响消息交换的频率从而影响故障转移、槽信息更新、新节点发现的速度。因此需要根据业务容忍度和资源消耗进行平衡。同时整个集群消息总交换量也跟节点数成正比。def get_wanted():
int total_size = size(cluster.nodes)
# 默认包含节点总量的1/10
int wanted = floor(total_size/10);
if wanted < 3:
# 至少携带3个其他节点信息
wanted = 3;
if wanted > total_size -2 :
# 最多包含total_size - 2个
wanted = total_size - 2;
return wanted;
根据伪代码可以看出消息体携带数据量跟集群的节点数息息相关,更大的集群每次消息通信的成本也就更高,因此对于Redis集群来说并不是大而全的集群更好。
Redis集群提供了灵活的节点扩容和收缩方案。在不影响集群对外服务的情况下,可以为集群添加节点进行扩容也可以下线部分节点进行缩容。其中原理可以抽象为槽和对应数据在不同节点之间灵活移动。
三个节点分别维护自己负责的槽和对应的数据,如果希望加入一个节点实现集群扩容时,需要通过相关命令把一部分槽和数据迁移给新节点。
Redis集群扩容操作分为如下几步:
(1) 准备新节点
(2) 加入集群
(3) 迁移槽和数据
cluster meet
命令加入到现有集群中。在集群内任意节点执行cluster meet
命令让新节点加入进来。cluster setslot {slot} importing {sourceNodeId}
命令,让目标节点准备导入槽的数据。cluster setslot {slot} migrating {targetNodeId}
命令,让源节点准备迁出槽的数据。cluster getkeysinslot {slot} {count}
命令,获取count个属于{slot}的键。migrate {targetIp} {targetPort} "" 0 {timeout} keys {keys...}
命令,把获取的键通过流水线(pipeline)机制批量迁移到目标节点,批量迁移版本的migrate命令在Redis3.0.6以上版本提供,之前的migrate命令只能单个键迁移。对于大量key的场景,批量键迁移将极大降低节点之间网络IO 次数。cluster setslot {slot} node {targetNodeId}
命令,通知槽分配给目标节点。为了保证槽节点映射变更及时传播,需要遍历发送给所有主节点更新被迁移的槽指向新节点。(3)添加从节点
扩容加入的新节点迁移了部分槽和数据作为主节点,但相比其他主节点目前还没有从节点,因此该节点不具备故障转移的能力。使用cluster replicate{masterNodeId}
命令为主节点添加对应从节点。
流程说明:
a. 首先需要确定下线节点是否有负责的槽,如果是,需要把槽迁移到其他节点,保证节点下线后整个集群槽节点映射的完整性。
b. 当下线节点不再负责槽或者本身是从节点时,就可以通知集群内其他节点忘记下线节点,当所有的节点忘记该节点后可以正常关闭。
cluster forget {downNodeId}
命令实现该功能。当节点接收到cluster forget {down NodeId}
命令后,会把nodeId指定的节点加入到禁用列表中,在禁用列表内的节点不再发送Gossip消息。禁用列表有效期是60秒,超过60秒节点会再次参与消息交换。当下线主节点具有从节点时需要把该从节点指向到其他主节点,因此对于主从节点都下线的情况,建议先下线从节点再下线主节点,防止不必要的全量复制。
Redis集群对客户端通信协议做了比较大的修改,为了追求性能最大化,并没有采用代理的方式而是采用客户端直连节点的方式。
在集群模式下,Redis接收任何键相关命令时首先计算键对应的槽,再根据槽找出所对应的节点,如果节点是自身,则处理键命令;否则回复MOVED重定向错误,通知客户端请求正确的节点。这个过程称为MOVED重定向。
重定向信息包含了键所对应的槽以及负责该槽的节点地址,根据这些信息客户端就可以向正确的节点发起请求。
使用redis-cli命令时,可以加入-c参数支持自动重定向,简化手动发起重定向操作。redis-cli自动帮我们连接到正确的节点执行命令,这个过程是在redis-cli内部维护,实质上是client端接到MOVED信息之后再次发起请求,并不在Redis节点中完成请求转发。节点对于不属于它的键命令只回复重定向响应,并不负责转发。
键命令执行步骤主要分两步:计算槽,查找槽所对应的节点。
def key_hash_slot(key):
int keylen = key.length();
for (s = 0; s < keylen; s++):
if (key[s] == '{'):
break;
if (s == keylen)
return crc16(key,keylen) & 16383;
for (e = s+1; e < keylen; e++):
if (key[e] == '}'):
break;
if (e == keylen || e == s+1)
return crc16(key,keylen) & 16383; /* 使用{
和}之间的有效部分计算槽 */
return crc16(key+s+1,e-s-1) & 16383;
根据伪代码,如果键内容包含{和}大括号字符,则计算槽的有效部分是括号内的内容;否则采用键的全内容计算槽。
键内部使用大括号包含的内容又叫做hash_tag,它提供不同的键可以具备相同slot的功能,常用于Redis IO优化。
例如在集群模式下使用mget等命令优化批量调用时,键列表必须具有相同的slot,否则会报错。这时可以利用hash_tag让不同的键具有相同的slot达到优化的目的。
Pipeline同样可以受益于hash_tag,由于Pipeline只能向一个节点批量发送执行命令,而相同slot必然会对应到唯一的节点,降低了集群使用Pipeline的门槛。
clusterState
结构中,结构所示:typedef struct clusterState {
clusterNode *myself; /* 自身节点,clusterNode代表节点结构体 */
clusterNode *slots[CLUSTER_SLOTS]; /* 16384个槽和节点映射数组,数组下标代表对应的槽 */
...
} clusterState;
slots数组表示槽和节点对应关系,实现请求重定向伪代码如下:
def execute_or_redirect(key):
int slot = key_hash_slot(key);
ClusterNode node = slots[slot];
if(node == clusterState.myself):
return executeCommand(key);
else:
return '(error) MOVED {slot} {node.ip}:{node.port}';
根据MOVED重定向机制,客户端可以随机连接集群内任一Redis获取键所在节点,这种客户端又叫Dummy(傀儡)客户端,它优点是代码实现简单,对客户端协议影响较小,只需要根据重定向信息再次发送请求即可。但是它的弊端很明显,每次执行键命令前都要到Redis上进行重定向才能找到要执行命令的节点,额外增加了IO开销, 这不是Redis集群高效的使用方式。正因为如此通常集群客户端都采用另一 种实现:Smart(智能)客户端。
slot→node
的映射关系,本地就可实现键到节点的查找,从而保证IO效率的最大化,而MOVED重定向负责协助Smart客户端更新slot→node
映射。(error) ASK {slot} {targetIP}: {targetPort}
。
ASK与MOVED虽然都是对客户端的重定向控制,但是有着本质区别。ASK重定向说明集群正在进行slot数据迁移,客户端无法知道什么时候迁移完成,因此只能是临时性的重定向,客户端不会更新slots缓存。但是MOVED重定向说明键对应的槽已经明确指定到新的节点,因此需要更新slots缓存。
typedef struct clusterState {
clusterNode *myself; /*自身节点*/
clusterNode *slots[CLUSTER_SLOTS]; /* 槽和节点映射数组 */
clusterNode *migrating_slots_to[CLUSTER_SLOTS];/* 正在迁出的槽节点数组 */
clusterNode *importing_slots_from[CLUSTER_SLOTS];/* 正在迁入的槽节点数组*/
...
} clusterState;
节点每次接收到键命令时,都会根据clusterState内的迁移属性进行命令处理,如下所示:
migrating_slots_to
数组查看槽是否正在迁出,如果是返回ASK重定向。importing_slots_from
数组获取clusterNode,如果指向自身则执行命令。Redis集群内节点通过ping/pong消息实现节点通信,消息不但可以传播节点槽信息,还可以传播其他状态如:主从状态、节点故障等。因此故障发现也是通过消息传播机制实现的,主要环节包括:主观下线 (pfail)和客观下线(fail)。
主观下线
集群中每个节点都会定期向其他节点发送ping消息,接收节点回复pong 消息作为响应。如果在cluster-node-timeout
时间内通信一直失败,则发送节点会认为接收节点存在故障,把接收节点标记为主观下线(pfail)状态。
1)节点a发送ping消息给节点b,如果通信正常将接收到pong消息,节点a更新最近一次与节点b的通信时间。
2)如果节点a与节点b通信出现问题则断开连接,下次会进行重连。如果一直通信失败,则节点a记录的与节点b最后通信时间将无法更新。
3)节点a内的定时任务检测到与节点b最后通信时间超高cluster-node-timeout
时,更新本地对节点b的状态为主观下线(pfail)。
客观下线
当某个节点判断另一个节点主观下线后,相应的节点状态会跟随消息在集群内传播。ping/pong消息的消息体会携带集群1/10的其他节点状态数据,当接受节点发现消息体中含有主观下线的节点状态时,会在本地找到故障节点的ClusterNode结构,保存到下线报告链表中。
通过Gossip消息传播,集群内节点不断收集到故障节点的下线报告。当半数以上持有槽的主节点都标记某个节点是主观下线时。触发客观下线流 程。这里有两个问题:
1)为什么必须是负责槽的主节点参与故障发现决策?因为集群模式下只有处理槽的主节点才负责读写请求和集群槽等关键信息维护,而从节点只进行主节点数据和状态信息的复制。
2)为什么半数以上处理槽的主节点?必须半数以上是为了应对网络分区等原因造成的集群分割情况,被分割的小集群因为无法完成从主观下线到客观下线这一关键过程,从而防止小集群完成故障转移之后继续对外提供服务。
流程说明:
(1)当消息体内含有其他节点的pfail状态会判断发送节点的状态,如果发送节点是主节点则对报告的pfail状态处理,从节点则忽略。
(2)找到pfail对应的节点结构,更新clusterNode内部下线报告链表。
(3)根据更新后的下线报告链表告尝试进行客观下线。这里针对维护下线报告和尝试客观下线逻辑进行详细说明:
a. 维护下线报告链表
每个节点ClusterNode结构中都会存在一个下线链表结构,保存了其他主节点针对当前节点的下线报告,结构如下:
typedef struct clusterNodeFailReport {
struct clusterNode *node; /* 报告该节点为主观下线的节点 */
mstime_t time; /* 最近收到下线报告的时间 */
} clusterNodeFailReport;
下线报告中保存了报告故障的节点结构和最近收到下线报告的时间,当接收到fail状态时,会维护对应节点的下线上报链表,伪代码如下:
def clusterNodeAddFailureReport(clusterNode failNode, clusterNode senderNode) :
// 获取故障节点的下线报告链表
list report_list = failNode.fail_reports;
// 查找发送节点的下线报告是否存在
for(clusterNodeFailReport report : report_list):
// 存在发送节点的下线报告上报
if(senderNode == report.node):
// 更新下线报告时间
report.time = now();
return 0;
// 如果下线报告不存在,插入新的下线报告
report_list.add(new clusterNodeFailReport(senderNode,now()));
return 1;
每个下线报告都存在有效期,每次在尝试触发客观下线时,都会检测下 线报告是否过期,对于过期的下线报告将被删除。如果在cluster-node-time*2 的时间内该下线报告没有得到更新则过期并删除。
如果在cluster-node-time*2时间内无法收集到一半以上槽节点的下线报 告,那么之前的下线报告将会过期,也就是说主观下线上报的速度追赶不上 下线报告过期的速度,那么故障节点将永远无法被标记为客观下线从而导致 故障转移失败。因此不建议将cluster-node-time设置得过小。
b. 尝试客观下线
集群中的节点每次接收到其他节点的pfail状态,都会尝试触发客观下线:
故障节点变为客观下线后,如果下线节点是持有槽的主节点则需要在它的从节点中选出一个替换它,从而保证集群的高可用。下线主节点的所有从节点承担故障恢复的义务,当从节点通过内部定时任务发现自身复制的主节点进入客观下线时,将会触发故障恢复流程。
cluster-node-time*cluster-slave-validity-factor
,则当前从节点不具备故障转移资格。参数cluster-slave-validity-factor
用于从节点的有效因子,默认为10。struct clusterState {
...
mstime_t failover_auth_time; /* 记录之前或者下次将要执行故障选举时间 */
int failover_auth_rank; /* 记录当前从节点排名 */
}
这里之所以采用延迟触发机制,主要是通过对多个从节点使用不同的延迟选举时间来支持优先级问题。复制偏移量越大说明从节点延迟越低,那么它应该具有更高的优先级来替换故障主节点。所有的从节点中复制偏移量最大的将提前触发故障选举流程。
配置纪元会跟随ping/pong消息在集群内传播,当发送方与接收方都是主节点且配置纪元相等时代表出现了冲突,nodeId更大的一方会递增全局配置纪元并赋值给当前节点来区分冲突。
配置纪元的主要作用:
投票过程其实是一个领导者选举的过程,如集群内有N个持有槽的主节点代表有N张选票。由于在每个配置纪元内持有槽的主节点只能投票给一个 从节点,因此只能有一个从节点获得N/2+1的选票,保证能够找出唯一的从 节点。
Redis集群没有直接使用从节点进行领导者选举,主要因为从节点数必须大于等于3个才能保证凑够 N 2 + 1 \frac{N}{2}+1 2N+1个节点,将导致从节点资源浪费。使用集群内所有持有槽的主节点进行领导者选举,即使只有一个从节点也可以完成选举过程。当从节点收集到N/2+1个持有槽的主节点投票时,从节点可以执行替换主节点操作。
故障主节点也算在投票数内,假设集群内节点规模是3主3从,其中有2 个主节点部署在一台机器上,当这台机器宕机时,由于从节点无法收集到 3/2+1个主节点选票将导致故障转移失败。这个问题也适用于故障发现环节。因此部署集群时所有主节点最少需要部署在3台物理机上才能避免单点问题。
投票作废:每个配置纪元代表了一次选举周期,如果在开始投票之后的cluster-node-timeout*2
时间内从节点没有获取足够数量的投票,则本次选举作废。从节点对配置纪元自增并发起下一轮投票,直到选举成功为止。
(1)主观下线识别时间=cluster-node-timeout
(2)主观下线状态消息传播时间<=cluster-node-timeout/2。消息通信机制对超过cluster-node-timeout/2未通信节点会发起ping消息,消息体在选择包含哪些节点时会优先选取下线状态节点,所以通常这段时间内能够收集到半数以上主节点的pfail报告从而完成故障发现。
(3)从节点转移时间<=1000毫秒。由于存在延迟发起选举机制,偏移量最大的从节点会最多延迟1秒发起选举。通常第一次选举就会成功,所以从节点执行转移时间在1秒以内。
根据以上分析可以预估出故障转移时间,如下:
failover-time(毫秒) ≤ cluster-node-timeout + cluster-node-timeout/2 + 1000
因此,故障转移时间跟cluster-node-timeout参数息息相关,默认15秒。 配置时可以根据业务容忍度做出适当调整,但不是越小越好。
为了保证集群完整性,默认情况下当集群16384个槽任何一个没有指派到节点时整个集群不可用。执行任何键命令返回(error) CLUSTERDOWN Hash slot not served
错误。这是对集群完整性的一种保护措施,保证所有的槽都指派给在线的节点。但是当持有槽的主节点下线时,从故障发现到自动完成转移期间整个集群是不可用状态,对于大多数业务无法容忍这种情况,因此建议将参数cluster-require-full-coverage配置为no,当主节点故障时只影响它负责槽的相关命令执行,不会影响其他主节点的可用性。
集群内Gossip消息通信本身会消耗带宽,官方建议集群最大规模在1000以内,因此单集群不适合部署超大规模的节点。集群内所有节点通过ping/pong消息彼此交换信息,节点间消息通信对宽带的消耗体现在以下几个方面:
在集群模式下内部实现对所有的publish命令都会向所有的节点进行广播,造成每条publish数据都会在集群内所有节点传播一次,加重带宽负担。
集群倾斜指不同节点之间数据量和请求量出现明显差异,这种情况将加大负载均衡和开发运维的难度。
readonly命令是连接级别生效,因此每次新建连接时都需要执行readonly 开启只读状态。执行readwrite命令可以关闭连接只读状态。
Redis集群提供了手动故障转移功能:指定从节点发起转移流程,主从 节点角色进行切换,从节点变为新的主节点对外提供服务,旧的主节点变为 它的从节点。