Redis 核心技术
持久化
Redis 是个基于内存的数据库。那服务一旦宕机,内存中的数据将全部丢失。
RDB 持久化
RDB 就是 Redis DataBase 的缩写,中文名为快照/内存快照,RDB 持久化是把当前进程数据生成快照保存到磁盘上的过程,由于是某一时刻的快照,那么快照中的值要早于或者等于内存中的值。
手动触发
手动触发分别对应 save 和 bgsave 命令
save 命令:阻塞当前 Redis 服务器,直到 RDB 过程完成为止,对于内存比较大的实例会造成长时间阻塞,线上环境不建议使用
bgsave 命令:Redis 进程执行 fork 操作创建子进程,RDB 持久化过程由子 进程负责,完成后自动结束。阻塞只发生在 fork 阶段,一般时间很短
自动触发
在以下 4 种情况时会自动触发
redis.conf 中配置
save m n
,即在 m 秒内有 n 次修改时,自动触发 bgsave 生成 rdb 文件;主从复制时,从节点要从主节点进行全量复制时也会触发 bgsave 操作,生成当时的快照发送到从节点;
执行 debug reload 命令重新加载 redis 时也会触发 bgsave 操作;
默认情况下执行 shutdown 命令时,如果没有开启 aof 持久化,那么也会触发 bgsave 操作;
配置项
核心配置
其他配置
如何处理持久化时的写操作
RDB 中的核心思路是 Copy-on-Write,来保证在进行快照操作的这段时间,需要压缩写入磁盘上的数据在内存中不会发生变化。 在正常的快照操作中,一方面 Redis 主进程会 fork 一个新的快照进程专门来做这个事情,这样保证了 Redis 服务不会停止对客户端包括写请求在内的任何响应。 另一方面这段时间发生的数据变化会以副本的方式存放在另一个新的内存区域,待快照操作结束后才会同步到原来的内存区域。
RDB 优缺点
优点
RDB 文件是某个时间节点的快照,默认使用 LZF 算法进行压缩,压缩后的文件体积远远小于内存大小,适用于备份、全量复制等场景;
Redis 加载 RDB 文件恢复数据要远远快于 AOF 方式;
缺点
RDB 方式实时性不够,无法做到秒级的持久化;
每次调用 bgsave 都需要 fork 子进程,fork 子进程属于重量级操作,频繁执行成本较高;
RDB 文件是二进制的,没有可读性,AOF 文件在了解其结构的情况下可以手动修改或者补全;
版本兼容 RDB 文件问题;
针对 RDB 不适合实时持久化的问题,Redis 提供了 AOF 持久化方式来解决
AOF 持久化
Redis 是“写后”日志,Redis 先执行命令,把数据写入内存,然后才记录日志。日志里记录的是 Redis 收到的每一条命令,这些命令是以文本形式保存。PS: 大多数的数据库采用的是写前日志(WAL),例如 MySQL,通过写前日志和两阶段提交,实现数据和逻辑的一致性。
而 AOF 日志采用写后日志,即先写内存,后写日志。
写后日志优点:
避免额外的检查开销:Redis 在向 AOF 里面记录日志的时候,并不会先去对这些命令进行语法检查。所以,如果先记日志再执行命令的话,日志中就有可能记录了错误的命令,Redis 在使用日志恢复数据时,就可能会出错。
不会阻塞当前的写操作。
缺点:
如果命令执行完成,写日志之前宕机了,会丢失数据。 主线程写磁盘压力大,导致写盘慢,阻塞后续操作。
AOF 实现原理
AOF 日志记录 Redis 的每个写命令,步骤分为:命令追加(append)、文件写入(write)和文件同步(sync)。
命令追加 当 AOF 持久化功能打开了,服务器在执行完一个写命令之后,会以协议格式将被执行的写命令追加到服务器的 aof_buf 缓冲区。
文件写入和同步 关于何时将 aof_buf 缓冲区的内容写入 AOF 文件中,Redis 提供了三种写回策略:
主从复制
主从复制概述
主从复制,是指将一台 Redis 服务器的数据,复制到其他的 Redis 服务器。前者称为主节点(master),后者称为从节点(slave);数据的复制是单向的,只能由主节点到从节点。
主要作用:
数据冗余:主从复制实现了数据的热备份,是持久化之外的一种数据冗余方式。
故障恢复:当主节点出现问题时,可以由从节点提供服务,实现快速的故障恢复;实际上是一种服务的冗余。
负载均衡:在主从复制的基础上,配合读写分离,可以由主节点提供写服务,由从节点提供读服务(即写 Redis 数据时应用连接主节点,读 Redis 数据时应用连接从节点),分担服务器负载;尤其是在写少读多的场景下,通过多个从节点分担读负载,可以大大提高 Redis 服务器的并发量。
高可用基石:除了上述作用以外,主从复制还是哨兵和集群能够实施的基础,因此说主从复制是 Redis 高可用的基础。
主从复制原理
主从复制过程大体可以分为 3 个阶段:连接建立阶段(即准备阶段)、数据同步阶段、命令传播阶段
1.连接建立阶段。该阶段的主要作用是在主从节点之间建立连接,为数据同步做好准备。
2.数据同步阶段。该阶段可以理解为从节点数据的初始化,是主从复制最核心的阶段,根据主从节点当前状态的不同,可以分为全量复制和部分复制。
3.命令传播阶段。数据同步阶段完成后,主从节点进入命令传播阶段;在这个阶段主节点将自己执行的写命令发送给从节点,从节点接收命令并执行,从而保证主从节点数据的一致性。
延迟与不一致
需要注意的是,命令传播是异步的过程,即主节点发送写命令后并不会等待从节点的回复;因此实际上主从节点之间很难保持实时的一致性,延迟在所难免。
全量复制和部分复制(数据同步阶段)
注意:在 2.8 版本之前只有全量复制,而 2.8 版本后有全量和增量复制:
全量(同步)复制
:比如第一次同步时
增量(同步)复制
:只会把主从库网络断连期间主库收到的命令,同步给从库
全量复制
当我们启动多个 Redis 实例的时候,它们相互之间就可以通过 replicaof(Redis 5.0 之前使用 slaveof)命令形成主库和从库的关系,之后会按照三个阶段完成数据的第一次同步。
第一阶段是主从库间建立连接、协商同步的过程,主要是为全量复制做准备。
第二阶段,主库将所有数据同步给从库。从库收到数据后,在本地完成数据加载。这个过程依赖于内存快照生成的 RDB 文件。
第三个阶段,主库会把第二阶段执行过程中新收到的写命令,再发送给从库。
增量复制
如果主从库在命令传播时出现了网络闪断,那么,从库就会和主库重新进行一次全量复制,开销非常大。从 Redis 2.8 开始,网络断了之后,主从库会采用增量复制的方式继续同步。
当 slave 连接到 master,会执行 PSYNC <runid> <offset>
发送记录旧的 master 的 runid
(replication ID)和偏移量 offset
,这样 master 能够只发送 slave 所缺的增量部分。
但是如果 master 的复制积压缓存区没有足够的命令记录,或者 slave 传的 runid
(replication ID)不对,就会进行完整重同步,即 slave 会获得一个完整的数据集副本
runid
(replication ID),主服务器运行 id,Redis 实例在启动时,随机生成一个长度 40 的唯一字符串来标识当前节点
offset
,复制偏移量。主服务器和从服务器各自维护一个复制偏移量,记录传输的字节数。当主节点向从节点发送 N 个字节数据时,主节点的 offset 增加 N,从节点收到主节点传来的 N 个字节数据时,从节点的 offset 增加 N
replication backlog buffer
,复制积压缓冲区。是一个固定长度的 FIFO 队列,大小由配置参数repl-backlog-size
指定,默认大小 1MB。需要注意的是该缓冲区由 master 维护并且有且只有一个,所有 slave 共享此缓冲区,其作用在于备份最近主库发送给从库的数据
主从复制的一些问题
延迟与不一致问题
Redis 的主从数据是异步同步的,所以分布式的 Redis 系统并不满足「一致性」要求。当客户端在 Redis 的主节点修改了数据后,立即返回,即使在主从网络断开的情况下,主节点依旧可以正常对外提供修改服务,所以 Redis 满足「可用性」。Redis 采用的策略是「最终一致性」。
由于主从复制的命令传播是异步的,延迟与数据的不一致不可避免。
如果应用对数据不一致的接受程度程度较低,可能的优化措施包括:
优化主从节点之间的网络环境(如在同机房部署);
监控主从节点延迟(通过 offset)判断,如果从节点延迟过大,通知应用不再通过该从节点读取数据;
使用集群同时扩展写负载和读负载等。
在命令传播阶段以外的其他情况下,从节点的数据不一致可能更加严重,例如连接在数据同步阶段,或从节点失去与主节点的连接时等。
从节点的
slave-serve-stale-data
参数便与此有关:它控制这种情况下从节点的表现;yes(默认值)代表从节点仍能够响应客户端的命令;no 代表从节点只能响应 info、slaveof 等少数命令。
如果对数据一致性要求很高,则应设置为 no。
数据过期问题
在主从复制场景下,为了主从节点的数据一致性,从节点不会主动删除数据,而是由主节点控制从节点中过期数据的删除。由于主节点不能保证主节点及时对过期数据执行删除操作,因此,当客户端通过 Redis 从节点读取数据时,很容易读取到已经过期的数据。
Redis 3.2 中,从节点在读取数据时,增加了对数据是否过期的判断:如果该数据已过期,则不返回给客户端;将 Redis 升级到 3.2 可以解决数据过期问题。
故障切换问题
在没有使用哨兵的读写分离场景下,应用针对读和写分别连接不同的 Redis 节点; 当主节点或从节点出现问题而发生更改时,需要及时修改应用程序读写 Redis 数据的连接; 连接的切换可以手动进行,或者自己写监控程序进行切换,但前者响应慢、容易出错,后者实现复杂,成本都不算低。
哨兵机制
Redis Sentinel,即 Redis 哨兵,在 Redis 2.8 版本开始引入。哨兵的核心功能是主节点的自动故障转移。
哨兵作用
监控:Sentinel 节点会定期检测 Redis 数据节点、其余 Sentinel 节点是否可达
通知:Sentinel 节点会将故障转移的结果通知给应用方
主节点故障转移:实现从节点晋升为主节点并维护后续正确的主从关系
配置提供者:在 Redis Sentinel 结构中,客户端在初始化的时候连接的是 Sentinel 节点集合 ,从中获取主节点信息。
哨兵机制
当主节点出现故障时,由 Redis Sentinel 自动完成故障发现和转移,并通知应用方,实现高可用性。
其实整个过程只需要一个哨兵节点来完成,首先使用 Raft 算法(选举算法)实现选举机制,选出一个哨兵节点来完成转移和通知。
哨兵原理
哨兵监控:检测节点问题
主观下线 &客观下线:当有一台 Sentinel 机器发现问题时,它就会主观对它主观下线。 但是当多个 Sentinel 都发现有问题的时候,才会出现客观下线。
领导选举:确定新的 master 节点
故障转移:节点切换
监控任务
任务 1: 每个哨兵节点每 10 秒会向主节点和从节点发送 info 命令获取拓扑结构图,并当有新的从节点加入时可以马上感知到。
任务 2:每 2 秒每个 Sentinel 通过 Master 节点的 channel 交换信息(pub/sub)。哨兵实例之间可以相互发现,组成集群。
任务 3: 每隔 1 秒每个哨兵会向主节点、从节点及其余哨兵节点发送一次 ping 命令做一次心跳检测,这个也是哨兵用来判断节点是否正常的重要依据。
客观下线: 当主观下线的节点是主节点时,此时该哨兵 3 节点会通过指令 sentinel is-masterdown-by-addr 寻求其它哨兵节点对主节点的判断,当超过 quorum(选举)个数,此时哨兵节点则认为该主节点确实有问题,这样就客观下线了,大部分哨兵节点都同意下线操作,也就说是客观下线。
领导选举
每个在线的哨兵节点都可以成为领导者,当它确认(比如哨兵 3)主节点下线时,会向其它哨兵发 is-master-down-by-addr 命令,征求判断并要求将自己设置为领导者,由领导者处理故障转移;
当其它哨兵收到此命令时,可以同意或者拒绝它成为领导者;
如果哨兵 3 发现自己在选举的票数大于等于 num(sentinels)/2+1 时,将成为领导者,如果没有超过,继续选举
进行故障转移操作
故障转移
将 slave-1 脱离原从节点,升级主节点,
将从节点 slave-2 指向新的主节点
通知客户端主节点已更换
将原主节点(oldMaster)变成从节点,指向新的主节点
常见问题
异步复制导致数据丢失
因为 master->slave 的复制是异步,所以可能有部分还没来得及复制到 slave 就宕机了,此时这些部分数据就丢失了。
在异步复制的过程当中,通过 min-slaves-max-lag 这个配置,就可以确保的说,一旦 slave 复制数据和 ack 延迟时间太长,就认为可能 master 宕机 后损失的数据太多了,那么就拒绝写请求,这样就可以把 master 宕机时由于部分数据未同步到 slave 导致的数据丢失降低到可控范围内
集群脑裂导致的数据丢失
某个 master 所在机器突然脱离了正常的网络,跟其它 slave 机器不能连接,但是实际上 master 还运行着。
此时虽然某个 slave 被切换成了 master ,但是可能 client 还没来得及切换成新的 master ,还继续写向旧的 master 的数据可能就丢失了。 因此旧 master 再次恢复的时候,会被作为一个 slave 挂到新的 master 上去,自己的数据会被清空,重新从新的 master 复制数据。
可以要求至少有 1 个 slave,数据复制和同步的延迟不能超过 10 秒。(牺牲一部分可用性提升一致性)
Redis Cluster 集群架构
哨兵模式解决了主从复制不能自动故障转移,达不到高可用的问题,但还是存在难以在线扩容,Redis 容量受限于单机配置的问题。Cluster 模式实现了 Redis 的分布式存储,即每台节点存储不同的内容,来解决在线扩容的问题。
(分片存储)Redis3.0 加入了 Redis 的集群模式,实现了数据的分布式存储,对数据进行分片,将不同的数据存储在不同的 master 节点上面,从而解决了海量数据的存储问题。
(指令转换)Redis 集群采用去中心化的思想,没有中心节点的说法,对于客户端来说,整个集群可以看成一个整体,可以连接任意一个节点进行操作,就像操作单一 Redis 实例一样,不需要任何代理中间件,当客户端操作的 key 没有分配到该 node 上时,Redis 会返回转向指令,指向正确的 Redis 节点。
(主从和哨兵)Redis 也内置了高可用机制,支持 N 个 master 节点,每个 master 节点都可以挂载多个 slave 节点,当 master 节点挂掉时,集群会提升它的某个 slave 节点作为新的 master 节点。
数据分区算法:哈希槽
常见的算法
普通 hash 算法:将 key 使用 hash 算法计算之后,按照节点数量来取余,即 hash(key)%N。优点就是比较简单,但是扩容或者摘除节点时需要重新根据映射关系计算,会导致数据重新迁移。
一致性 hash 算法:为每一个节点分配一个 token,构成一个哈希环;查找时先根据 key 计算 hash 值,然后顺时针找到第一个大于等于该哈希值的 token 节点。优点是在加入和删除节点时只影响相邻的两个节点,缺点是加减节点会造成部分数据无法命中,所以一般用于缓存,而且用于节点量大的情况下,扩容一般增加一倍节点保障数据负载均衡。
哈希槽分区算法
Redis 集群中有 16384 个哈希槽(槽的范围是 0 -16383,哈希槽),将不同的哈希槽分布在不同的 Redis 节点上面进行管理,也就是说每个 Redis 节点只负责一部分的哈希槽。
在对数据进行操作的时候,集群会对使用 CRC16 算法对 key 进行计算并对 16384 取模(slot = CRC16(key)%16383)
得到的结果就是 Key-Value 所放入的槽,通过这个值,去找到对应的槽所对应的 Redis 节点,然后直接到这个对应的节点上进行存取操作。
GET/SET 请求处理过程
客户端向集群中的节点发送数据命令,需要进行计算出命令要处理的键是属于哪个槽的,并检查是否指派给了自己。
Moved 重定向
如果键所在的槽正好指派当前节点,那么节点直接执行这个命令。
如果键所在的槽并没有指派给当前节点,那么节点会向客户端返回一个 MOVED 错误,指引客户端转向(redirect)至正确的节点,并再次发送之前想要执行的命令。
ASK 重定向
Ask 重定向发生于集群伸缩时,集群伸缩会导致槽迁移,当我们去源节点访问时,此时数据已经可能已经迁移到了目标节点,使用 Ask 重定向来解决此种情况。
smart 客户端
上述两种重定向的机制使得客户端的实现更加复杂,提供了 smart 客户端(JedisCluster)来减低复杂性,追求更好的性能。客户端内部负责计算/维护键-> 槽 -> 节点映射,用于快速定位目标节点。
实现原理
从集群中选取一个可运行节点,使用 cluster slots 得到槽和节点的映射关系
上述映射关系存到本地,通过映射关系就可以直接对目标节点进行操作(CRC16(key) -> slot -> node),很好地避免了 Moved 重定向,并为每个节点创建 JedisPool
至此就可以用来进行命令操作
集群的主从复制和故障转移
主从复制
Redis 集群的主从复制,其实和单机的主从复制是一样的。单个节点中有一个 master 和多个 slave 。这些 slave 会自动的同步 master 中的数据。
故障转移
同样的 ,当个节点中可以配置多个哨兵,来监控这个节点中的 master 是否下线了,如果下线了就会将这个节点的 slave 选择一个升级成 master 并继承之前 master 的分片,继续工作。
不同的是,在集群模式中,并没有配置哨兵。之前 master 单个节点,才需要部署 sentinel 集群。现在集群有多个 master ,哨兵的这些工作,完全可以交给 master 来做。
在集群中,每个节点的 master 定期的向其他节点 master 发送 ping
命令,如果没有收到 pong
响应,则会认为主观下线,并将这个消息发送给其他的 master。其他的 master 在接收到这个消息后就保存起来。当某个节点的 master 收到 半数以上的消息认为这个节点主观下线后,就会判定这个节点客观下线。并将这个节点客观下线的消息通知给其他的 master。 这个客观节点下线后,其他的 master 节点 就会选举 下线的 master 中的 slave 一个变成 新的 master 继续工作。从而实现故障自动转移。
这个选举过程和哨兵模式中是一样的,只不过是 master 代替了 sentinel 的工作。
扩缩容
扩容
当集群出现容量限制或者其他一些原因需要扩容时,redis cluster 提供了比较优雅的集群扩容方案。
首先将新节点加入到集群中,可以通过在集群中任何一个客户端执行 cluster meet 新节点 ip:端口,或者通过 redis-trib add node 添加,新添加的节点默认在集群中都是主节点
迁移数据的大致流程是,首先需要确定哪些槽需要被迁移到目标节点,然后获取槽中 key,将槽中的 key 全部迁移到目标节点,然后向集群所有主节点广播槽(数据)全部迁移到了目标节点。
缩容
缩容的大致过程与扩容一致,需要判断下线的节点是否是主节点,以及主节点上是否有槽,若主节点上有槽,需要将槽迁移到集群中其他主节点,槽迁移完成之后,需要向其他节点广播该节点准备下线(cluster forget nodeId)。
最后需要将该下线主节点的从节点指向其他主节点,当然最好是先将从节点下线。
参考
版权声明: 本文为 InfoQ 作者【苏格拉格拉】的原创文章。
原文链接:【http://xie.infoq.cn/article/93de7ba54678d3172a5294c04】。文章转载请联系作者。
评论