(四)redis高可用保障
A. 持久化
- RDB模式持久化
- 简述:该模式即为快照的概念,直接将redis内存的数据拷贝到本地磁盘,一般设置拷贝的执行间隔会较久,如一天一次或者一小时一次。
- 实现机制:具体实现基于save & bgsave两种方式。
- save:较不常用,易导致redis长时间处于阻塞状态。
- bgsave:RDB模式下默认的持久化方式,会fork子进程进行持久化以保障redis的阻塞仅在fork动作的瞬间发生。
- 触发事件
- 周期性触发:redis.conf配置文件中会设置
save m n
,该配置默认使用bgsave命令,表示在m秒内发生n次修改时触发持久化。master & slave都会基于此配置周期性的进行bgsave,生成相应的rdb文件。 - 主从建立:建立主从关系时slave会执行全量复制(sync),此时master自动触发bgsave生成rdb文件并发送给slave。
- 重启:shutdown后重启,若没有开启aof时会自动执行bgsave。
- debug:debug reload时,会导致save的操作。
- 周期性触发:redis.conf配置文件中会设置
- 优点
- 具体某个时间点的快照,且便于复制到其他redis server上进行数据恢复。
- 恢复数据的速度远快于aof。
- 缺点
- 实时性较差,若对于灾后数据丢失的容忍性较低,则不建议使用。
- rdb格式的版本较多,存在版本互相不兼容的潜在风险。
- AOF模式持久化
- 简述:该模式会独立创建日志以记录redis的所有历史命令,重启后会根据记录重新执行所有命令来恢复所有数据。
- 触发事件:仅在redis重启时且redis.conf配置了
appendonly yes
&appendfilename xxx.aof
会触发该模式。 - 运行顺序
- redis命令会append到aof buffer然后再写入日志,在buffer写入日志文件时,建议配置
everysec
兼顾性能和数据安全; - aof日志文件会越变越大,因此定期会对其重写实现压缩,亦可使用
bgrewriteaof
手动触发压缩; - redis重启后使用aof日志文件开始恢复数据。
- redis命令会append到aof buffer然后再写入日志,在buffer写入日志文件时,建议配置
- 优点:高时效性,保障数据的完整性。
- 缺点:redis时cpu密集型服务,而aof会fork子进程来进行持久化将占用较大内存,当前以copy on write来进行父子进程间内存共享来缓解该问题。
B. 主从机制 & 同步问题
- 简述
- 为保障redis的高可用性生产中几乎不会使用单点redis,至少是最简单主从模式,多会采用哨兵模式或者集群模式。
- master & slave间的数据同步,默认使用
repl-disable-tcp-nodelay no
,即开启tcp-nodelay来降低同步延迟,不会等待合并包以确保高时效性。若设置成repl-disable-tcp-nodelay yes
则默认情况下会增加时延40ms,节约带宽消耗。
1. 数据同步
概述:redis2.8及以上版本,使用
psync
完成主从数据同步,同步过程分为全量复制 & 部分复制两类。-
psync:具体命令为于slave上使用
psync runID offset
。- 若master响应fullresync,则触发全量复制。
- 若master响应continue,则进行部分复制。
- 注意,低版本下使用该命令,基于仅支持sync命令所以会响应error。
全量复制:一般仅在初次建立主从关系时会使用该模式,master通过bgsave将全量rdb数据同步至slave,网络开销较大。
-
部分复制:多用于主从复制时网络闪断等造成的数据丢失场景。若slave再次成功连接master,情况允许下master会补发丢失数据给slave,相对全部重发而言降低了网络开销。为支撑该功能,psync的部分复制过程需要下面三个组件
复制偏移量:slave每秒都会上报自己的offset给master,该数值可参考
info replication
中显示的offset内容。复制积压缓冲区:master有了对应slave时创建的对象,是保存在master上的固定长度的队列(queue, FIFO),默认大小为1mb,其核心功能保存最近已复制数据的功能,用于psync时丢失的数据补救。因为master响应set命令时,把该命令发送给slave的同时也会将之写入复制积压缓冲区。
-
主节点运行ID(runID):redis启动后会动态分配一个40bits的十六进制string作为runID用于识别redis节点。
- slave保存master的runID,便于slave识别自己从谁那儿复制。
- runID可用
info server
进行查看确认。master重启后runID会变化,slave无法部分增量复制,只可全量复制。 - 不可仅靠复制偏移量offset来进行部分复制,因为重启后master可能会加载其他的rdb文件导致数据变化。
退化问题:若复制积压缓冲区失效(slave请求的offset不在队列存储的offset内),runID即便未变化,也无法完成部分复制,只能进行全量复制。
-
心跳:master & slave建立长连接后会发送心跳以维持,双方彼此都有心跳检测机制。
master:默认master每10s对slave进行ping命令,具体周期可通过
repl-ping-slave-period
的参数进行控制。slave:在主线程中每隔1s发送replconf ack上报自己offset,告知master自己的复制偏移量。
判定:master根据replconf命令判定slave的超时时间,可在
info replication
的lag字段确认,正常应该在1-2之间。默认配置repl-timeout为60s,若slave发送给master的时间超过60s,则判定slave下线。
异步复制:master不仅负责数据读写,还需把写命令set 同步给slave。master发送写命令是异步的,自身处理完写命令并回复client,无需等待slave回复复制是否完成。
-
过期数据同步:master存储了大量设置超时的数据时需注意同步问题,数据的删除设计两种策略。
惰性删除:该策略下slave不会主动删除,master处理读取命令时检查key过期与否,若过期执行del删除并把该命令同步给slave让其也删除对应key。
定时删除:master会定时采样一定量的key,若发现采样中有key过期则执行del,再同步给salve同步删除。若此时大量数据超时,master采样速度跟不上过期速度且master没读到过期的key,那么slave就收不到del命令,这会导致从slave上读取到过期数据。升级redis3.2后,读取slave时会再检查key过期与否再决定是否返回给client数据。
2. 简单主从模式
实现方式:slave节点的redis.conf加入
slaveof master-ip master-port
,或者连接客户端使用slaveof xxx
命令。-
复制过程:在slave上配置相关信息后启动时,或者使用
slaveof xxx
命令时已经激活复制的流程。- slave会保存master的信息,可通过
info replication
查询保存的具体信息内容; - slave定时(每秒)维护复制逻辑,依靠这个定时任务发现master的存在并与之建立连接;
- 建立主从连接时具体建立一个socket专门接收master发出的复制命令,打印日志
connecting to master xxxx:6379
; - 若建立连接失败,定时任务会持续尝试,可手动
slaveof no one
结束这个尝试的循环; - 建立主从成功后,slave发送ping命令请求通信,以此检测主从间的socket是否可用,并确认master是否可接受处理命令;
- 若master有设置
requirepass
参数,则需要进行权限验证,确保slave的masterauth
参数与之相同; - 上方步骤全部完成后,开始同步数据。对于首次建立复制的场景master本身完成一次basave,将所有数据都传给slave。这一步操作是最耗时的;
- 后续master会持续将写命令发送给slave,保证主从一致性。
- slave会保存master的信息,可通过
-
配置文件redis.conf参考
bind 0.0.0.0 protected-mode yes port 6379 tcp-backlog 511 timeout 60 tcp-keepalive 300 daemonize yes supervised no pidfile /var/run/redis_6379.pid loglevel notice logfile "/apprun/redis/logs/redis.log" databases 16 always-show-logo yes save 86400 1 stop-writes-on-bgsave-error yes rdbcompression yes rdbchecksum yes dbfilename dump.rdb dir /apprun/data/redis masterauth Admin!123 replica-serve-stale-data yes replica-read-only yes repl-diskless-sync no repl-diskless-sync-delay 5 repl-disable-tcp-nodelay no repl-backlog-size 100mb replica-priority 100 requirepass xx123 maxmemory 5gb # 在redis达到maxmemory阈值时,采用lru算法将设置过期时间的key删除。使用该策略后,内存共享池机制失效。 maxmemory-policy volatile-lru lazyfree-lazy-eviction no lazyfree-lazy-expire no lazyfree-lazy-server-del no replica-lazy-flush no appendonly no appendfilename "appendonly.aof" appendfsync everysec no-appendfsync-on-rewrite no auto-aof-rewrite-percentage 100 auto-aof-rewrite-min-size 64mb aof-load-truncated yes aof-use-rdb-preamble yes lua-time-limit 5000 slowlog-log-slower-than 10000 slowlog-max-len 128 latency-monitor-threshold 0 notify-keyspace-events "" hash-max-ziplist-entries 512 hash-max-ziplist-value 64 list-max-ziplist-size -2 list-compress-depth 0 set-max-intset-entries 512 zset-max-ziplist-entries 128 zset-max-ziplist-value 64 hll-sparse-max-bytes 3000 stream-node-max-bytes 4096 stream-node-max-entries 100 activerehashing yes client-output-buffer-limit normal 0 0 0 client-output-buffer-limit replica 0 0 0 client-output-buffer-limit pubsub 32mb 8mb 60 # 内存回收策略:定时任务删除expired key,hz 10即每秒运行10次检测一次。 hz 10 dynamic-hz yes aof-rewrite-incremental-fsync yes rdb-save-incremental-fsync yes slave-serve-stale-data yes slave-read-only yes slaveof 10.xx.10.xx 6379
3. 哨兵模式sentinel
-
概述:简单的主从模式,若master节点down后,slave切换master需要通过
slaveof no one
手动实现,且调用redis的模块需要修改ip地址并进行重启。基于此种场景,redis sentinel分布式架构应运而生。- 补充说明:简单的主从模式基于vip+keepalived的模式亦可实现自动切换主从且不用修改应用模块的配置文件,但不如哨兵模式简单直接。
-
架构 & 工作流程
- 包含若干sentinel节点和redis数据节点。
- 若sentinel节点发现节点不可达时,会对该节点做下线标识。
- 若被标识的是master节点,sentinel会和其他sentinel节点投票协商。
- 若大多数sentinel认为master不可达,则它们会选举一个sentinel节点完成自动故障转移工作,同时通知redis应用方。
-
故障转移步骤细节
- master故障,两个slave节点和master失去连接(默认结构为1主2从的redis数据节点,3个sentinel节点),主从复制中断。
- sentinel节点监控发现master出现故障。
- 多个sentinel节点投票一致确认故障情况,选举出sentinel-3为leader负责故障转移。
- sentinel-3自动选择slave-1通过
slaveof no one
升级为master,并让slave-2更新自己的master对象,再通知客户端当前的master情况,最后让旧master恢复后也对应更新主从关系。
-
sentinel节点核心功能
- 监控:定期检测redis数据节点、其余sentinel节点是否可达。
- 通知:将故障转移结果通知给应用方。
- 主节点故障转移:实现slave升级master,并维护主从关系。
- 配置提供方:客户端初始化连接的是sentinel节点集合,从中获取master信息。
-
实现方式
sentinel需要投票完成选主,所以server数量为奇数(一般3即可,一主两从,三个sentinel监控);
完成/apprun/redis/etc/下redis.conf的配置内容(默认6379端口),注意配置slaveof xx;
-
进行/apprun/redis/conf/下sentinel.conf配置;
# 3份sentinel配置文件,监控的都是当时作为master节点的redis ip+port,即所有sentinel的配置文件内容其实是一样的 & 投票需要的决定数目 # 使用端口 port 26379 # 工作目录 dir "/apprun/redis" daemonize yes # 日志所在路径 logfile "/apprun/redis/logs/sentinel.log" sentinel deny-scripts-reconfig yes # sentinel monitor <master-name-custemized> <ip> <port> <quorum>,quorum用于判定主节点不可达时需要的票数。 sentinel monitor mymaster 172.xx.xx.3x 6379 2 # sentinel down-after-milliseconds <master-name-custemized> <times>,每个sentinel定期ping master,若5000ms未回复,即判定离线。 sentinel down-after-milliseconds mymaster 5000 sentinel failover-timeout mymaster 18000 sentinel auth-pass mymaster xxpassxxxword
最后使用
./redis-sentinel sentinel.conf
启动,并检查logs文件确认启动成功与否。
-
原理解释
- 三个定时任务
- 每10s,sentinel节点会向master和slave发送
info reolication
命令获取整体拓扑结构。 - 每2s,sentinel节点会向redis数据节点_sentinel_:hello频道发送该sentinel对master的判定信息 & 自己节点信息。所有sentinel节点都订阅该频道,也可了解到其他sentinel对master的判断情况,作为客观下线 & leader选举的依据。
- 每1s,sentinel节点向redis数据节点和其余sentinel发送ping作为心跳检测。
- 每10s,sentinel节点会向master和slave发送
- 主观下线
- 上方定时任务,每秒一次ping,若被ping对象未能及时在
sentinel down-after-milliseconds
参数内响应,该sentinel即判定被ping对象下线,此为主观下线。 - 主观下线时一家之言,存在误判可能性。
- 上方定时任务,每秒一次ping,若被ping对象未能及时在
- 客观下线:sentinel发送
sentinel is-master-down-by-addr
命令,让其他sentinel节点也判定master情况,超过quorum个数,此时的下线即为客观下线,可信度很高。 - sentinel节点leader选举
- 选举出leader后,才能对认定的客观下线master节点执行故障转移操作。
- 选举leader基于Raft算法,大致逻辑为:
- 所有sentinel都可以成为leader,当它确认master主观下线时,向其他sentinel发送
sentinel is-master-down-by-addr
; - 若其他sentinel未同意过别人的该请求,就会同意本次请求(先到先得),否则就拒绝;
- 如果该sentinel发现自己的票数大于半数,那么它将成为leader。
- 所有sentinel都可以成为leader,当它确认master主观下线时,向其他sentinel发送
- 故障转移
- 从slave中选择新的master,基于
slave-priority
参数; - 若
slave-priority
无法选出,根据复制偏移量最大的优先原则; - 若依然无法选举出结果,选择runid最小的slave节点,即谁先启动谁做master。
- 从slave中选择新的master,基于
- 三个定时任务
总结:相对于简单的redis主从,哨兵模式可以只能地自动完成主从切换,基于jedis时不需要配置vip,使用sentinelPool即可。
4. 集群模式
a. 基础部分
- 概述:redis cluster是分布式解决方案,于3.0版本推出,可解决单机内存、并发、流量的瓶颈问题。
- 分区:基于hash分区(非一致性hash),具体为虚拟槽分区(0 - 16383个slots)。
- 每个redis节点接收一部分的slots,若cluster共5个节点,每个节点分到3276个slots;
- 计算公式:slot = CRC16(key) & 16383(取模)。
- 潜在问题
- 批量操作:对于mget mset之类的key批量操作仅在针对于同一个slot时可进行,事务操作同样受限,必须是指向同一个节点的key才可进行事务操作,pipeline相对不影响。
- 负载不均:key是最小颗粒度,若key是hash或者list等big key,均指向同一个slot,会一定程度负载不均衡;
- 结构问题:cluster仅有一个db 0,不支持16个db;复制结构只有一层,不存在树形拓扑结构。
- masterauth & requirepass的区别
- masterauth:slave节点数据同步时,需要用到这个密码来访问master节点;
- requirepass:每个node自己登录时的密码,可以各不相同;
- 注意:slave节点配置的masterauth,需要和master节点的requirepass一致,为防止混乱,建议全部都设成一样。理论上master上不用设置masterauth,但是主从身份可能会切换,所以一般都会配置上。
b. 集群新建
-
节点握手:启动集群后的节点后它们彼此并不互相知晓。节点通过gossip协议彼此通信完成节点握手,具体步骤如下方。
- 客户端发起命令
cluster meet <ip> <port>
,让其与对应的ip port对象握手通信。 - 该对象收到meet消息,保存发起方的节点信息,并回复pong。
- 两者通过定期ping pong命令保持心跳通信。
- 注意,仅需在集群内任一节点执行
cluster meet <ip> <port>
即可,握手状态胡通过消息在集群中传播,其他节点会自动发现后续加入集群的新节点,并发起握手流程。最终,执行cluster nodes确认全部节点都彼此感知并组成集群。
- 客户端发起命令
-
分配槽
- 概述:redis集群将所有数据映射值16384个slots中,所以每个key都会掉落到某个slot中。
- 初始分配命令:登陆不同节点各自分配工作,node1上进行
cluster addslots {0..5461}
,node2上cluster addslots {5462..10922}
,node3上cluster addslots {10923.。16383}
。 - 主从关系确立:剩余3个节点需要完成和前面3个节点的主从关系建立,使用命令
cluster replicate <nodeId>
。
总结:redis官方提供redis-trib.rb工具可帮助更快速搭建集群,使用命令
redis-trib.rb create --replicas <node1ip:port> <node...>
。
c. 节点通信
- 概述:分布式结构需要维护节点的metadata,一般采用方式有集中式和P2P方式。redis cluster采用的是Gossip协议,是P2P方式,让节点彼此不断通信交换信息。
- 过程说明
- 集群中的每个节点会单独开辟一个tcp通道用于节点间的互相通信,一般是16379(服务端口+1w)。
- 每个节点在固定周期内会通过特定规则选择几个节点发送ping消息。
- 接收到ping消息的节点会返回pong消息。
- Gossip协议
- 主要职责:作为集群中信息交换的载体。
- 类别:ping,pong,meet以及fail,ping&pong是最频繁的消息用于保持心跳,meet是通知新节点加入,fail则是下线时。
d. 集群伸缩
- 概述:在不影响提供服务前提下,完成redis集群的节点增加或下线,其主要是内容是slots在不同节点间的灵活移动。
- 扩容集群步骤
- 准备新节点:细节可参考初次启动集群节点的内容。
- 加入集群:依然使用
cluster meet <ip> <port>
实现。加入集群后,新加入的节点都是master的角色,且没有分配槽。 - 注意点:建议避免使用
cluster meet
完成新节点加入集群,使用redis-trib.rb工具更稳定、适合生产环境,具体命令为redis-trib.rb add-node newhost:port existingHost:port
。 - 槽 & 数据迁移:在上方操作完成后,开始进行slots的数据迁移,按slot逐个完成迁移工作,可通过pipeline提升网络利用率。实现槽迁移可用命令
redis-trib.rb reshard host:port --from <源节点id,若有多个节点使用逗号分隔> --to<目标节点id,只可写一个> --slots <迁移槽总数量> --yes --timeout <migrate操作超时时间ms,默认6w> --pipeline <单次迁移key的数量,默认10>
。
- 收缩集群:步骤类似扩容集群,但需先完成迁移工作后再下线,使用
redis-trib.rb del-node ip:port nodeId
完成忘记节点的操作,不建议于生产环境使用cluster forget
进行忘记节点的操作。
e. 请求路由
- 概述:为追求性能,客户端连接集群未采用代理方式,是直连节点的。
- 请求重定向:redis集群收到任何key相关命令,首先计算key对应的slot,根据slot找到具体的节点。若不是本节点,回复MOVE重定向错误,通知客户端找到正确的节点。
- smart客户端:该客户端维护slot --> node映射关系,本地即可实现key到节点的查找,MOVE协助smart更新映射关系。
- ASK重定向:redis集群在线迁移slots和数据完成水平扩容,迁移过程中会存在一部分数据在源节点,一部分在目标节点。
- 数据仍在源节点:基于slots缓存的映射关系,用户请求的key对象能被正常返回。
- 数据已迁移至目标节点:用户请求的key根据计算会找到源节点,但实际key已经迁移到目标节点,返回ASK重定向异常。
f. 故障发现 & 恢复
- 故障发现 & 恢复
- 主观下线:类似哨兵模式sentinel下的概念,主观是仅单节点认定某节点下线,认为其超过timeout阈值未响应。
- 客观下线
- 概述:多节点共同判定某节点下线,一般是半数以上有槽节点投票结果。
- 流程:通知故障节点下线并生效,让故障节点的从节点进行故障转移操作——升级成master。
- 故障恢复
- 资格确认:查看slave和故障master的最后断开时间,判断其是否有资格升级成master。
- 选举:具备资格后,更新触发故障选举时间,待时间达到后开始选举,触发节点的配置纪元更新(该数值只增不减)。
g. 集群运维事项
- 完整性:若slots未全部分配完毕,则此时集群不可用。即便仅一个slot未分配,也会导致集群不可用的状态。
- 集群规模:基于带宽消耗考虑,带宽消耗主要位于gossip协议维系集群时和读写命令。官方建议集群规模在1000以内,避免耽搁集群体量过大。
- 集群倾斜
- 数据倾斜:节点和slots间分配不均、不同slots包含的key数量差异过大、集合对象中包含元素过多(bigkey) & 节点间内存配置不一致。
- 请求倾斜:常出现于hotkey场景下,尽量避免hmget、hgetall这类高复杂度操作导致该类问题影响放大。一般可使用本地缓存降低hotkey调用。
- 集群读写分离:集群一般不做读写分离,若压力过大可直接扩容主节点数量。默认情况下,集群中的slave不接受任何外来读写请求。
- 故障转移
- master变更:主节点需要更换时,可先手动将slave升级,然后再将原来master下线升级。
- 强制故障转移:若master & slave同时故障、master & slave复制出现问题(slave不具备资格升级)、集群半数master故障时,需要强制故障转移。使用的命令有两个,
cluster failover force
&cluster failover takeover
。后者尽可能避免使用,一般仅在大多数主节点故障时采用。
h. 分布式批量操作优化
- 优化方法
- 客户端n次get:n次网络+n次get命令。
- 客户端1次pipeline get:1次网络+n次get命令。
- 客户端1次mget:1次网络+1次get命令。
- 对应场景
- 串行命令:n个key均匀分布在多个节点,只可使用客户端n次get的方法。
- 串行IO:基于集群中的Smart客户端,获得key对应的slot所在的节点,去往一个节点的命令一起发送,但是串行一个个发往不同节点上,实现了客户端1次pipeline get。
- 并行IO:思路同上,最后发送命令时优化成并行的模式。
- hash_tag:利用hash_tag把多个key强制分配到一个redis节点,这样就可以使用mget实现客户端1次mget,这样最高效但是数据分发易不均匀。