redis总结

问题总结

  • CPU使用上的“坑”,例如数据结构的复杂度、跨CPU核的访问
  • 内存使用上的“坑”,例如主从同步和AOF的内存竞争
  • 存储持久化上的“坑”,例如在SSD上做快照的性能抖动
  • 网络通信上的“坑”,例如多实例时的异常网络丢包

主线总结

  • 高性能主线,包括线程模型、数据结构、持久化、网络框架
  • 高可靠主线,包括主从复制、哨兵机制
  • 高可扩展主线,包括数据分片、负载均衡

关于单线程

Redis是单线程,主要是指Redis的网络IO和键值对读写是由一个线程来完成的,这也是Redis对外提供键值存储服务的主要流程

采用单线程的一个核心原因是避免多线程开发的并发控制问题。单线程的Redis也能获得高性能,跟多路复用的IO模型密切相关,因为这避免了accept()和send()/recv()潜在的网络IO操作阻塞点

但Redis的其他功能,比如持久化、异步删除、集群数据同步等,其实是由额外的线程执行的

redis持久化

AOF(AppendOnlyFile)

Redis执行完命令后,以文本形式记录Redis收到的每一条命令

这样做的好处

  1. 不需要验证命令是否正确,只要能执行成功,就可以记录日志
  2. 不会阻塞当前的写操作

这样做的潜在风险

  1. 执行完一个命令,还没有来得及记日志就宕机了
  2. 虽然避免了对当前命令的阻塞,但可能会给下一个操作带来阻塞风险

三种写回策略

  1. Always,同步写回:每个写命令执行完,立马同步地将日志写回磁盘
    • 好处 - 可靠性高,数据基本不丢失
    • 坏处 - 每个写命令都要落盘,性能影响较大
  2. Everysec,每秒写回:每个写命令执行完,只是先把日志写到AOF文件的内存缓冲区,每隔一秒把缓冲区中的内容写入磁盘
    • 性能适中,宕机时丢失1秒内的数据
  3. No,操作系统控制的写回:每个写命令执行完,只是先把日志写到AOF文件的内存缓冲区,由操作系统决定何时将缓冲区内容写回磁盘
    • 性能好,宕机时丢失数据较多

AOF日志过大,带来的问题

  1. 文件系统本身对文件大小有限制,无法保存过大的文件
  2. 如果文件太大,之后再往里面追加命令记录的话,效率也会变低
  3. 如果发生宕机,AOF中记录的命令要一个个被重新执行,用于故障恢复,如果日志文件太大,整个恢复过程就会非常缓慢,这就会影响到Redis的正常使用

AOF重写机制

  • 当一个键值对被多条写命令反复修改时,AOF文件会记录相应的多条命令
  • 但是,在重写的时候,是根据这个键值对当前的最新状态,为它生成对应的写入命令

和AOF日志由主线程写回不同,重写过程是由后台子进程bgrewriteaof来完成的,这也是为了避免阻塞主线程,导致数据库性能下降

所以需要2个日志文件,1个日志用于重写(全量日志),另1个日志用于写入重写过程中新写入的日志(增量日志)

等(全量日志)重写完以后,重写(增量日志),当数据为最新状态时,替换掉(全量日志),来实现日志瘦身

RDB快照(RedisDatabase)

虽然AOF有重写机制,但是日志量大的时候,故障恢复还是很慢,需要把日志命令都执行一遍

此时,我们可以使用另一种持久化方式,内存快照

Redis提供了两个命令来生成RDB文件,分别是save和bgsave

  • save:在主线程中执行,会导致阻塞
  • bgsave:创建一个子进程,专门用于写入RDB文件,避免了主线程的阻塞,这也是Redis RDB文件生成的默认配置

Redis借助操作系统提供的写时复制技术(Copy-On-Write,COW),在执行快照的同时,正常处理写操作

在bgsave写入RDB文件的过程中,如果某块数据需要修改,就会复制一份,把副本写入RDB文件,此时修改该数据就不会对写入RDB造成影响

虽然bgsave执行时不阻塞主线程,但也不要频繁地执行全量快照

  1. 频繁将全量数据写入磁盘,会给磁盘带来很大压力,多个快照竞争有限的磁盘带宽,前一个快照还没有做完,后一个又开始做了,容易造成恶性循环
  2. bgsave子进程需要通过fork操作从主线程创建出来。虽然,子进程在创建后不会再阻塞主线程,但是,fork这个创建过程本身会阻塞主线程,而且主线程的内存越大,阻塞时间越长

混合使用AOF日志和内存快照

虽然跟AOF相比,快照的恢复速度快,但是,快照的频率不好把握,如果频率太低,两次快照间一旦宕机,就可能有比较多的数据丢失

如果频率太高,又会产生额外开销,那么,还有什么方法既能利用RDB的快速恢复,又能以较小的开销做到尽量少丢数据呢

  • 数据不能丢失时,内存快照和AOF的混合使用是一个很好的选择
  • 如果允许分钟级别的数据丢失,可以只使用RDB
  • 如果只用AOF,优先使用everysec的配置选项,因为它在可靠性和性能之间取了一个平衡

redis主从

从库设置replicaof就可以完成简单的主从同步

  • 从库给主库发送psync命令,表示要进行数据同步,主库根据这个命令的参数来启动复制(基于RDB文件)
  • psync命令包含了主库的runID和复制进度offset两个参数
  • runID,是每个Redis实例启动时都会自动生成的一个随机ID,用来唯一标记这个实例
  • 当从库和主库第一次复制时,因为不知道主库的runID,所以将runID设为“?”。offset,此时设为-1,表示第一次复制
  • 主库收到psync命令后,会用FULLRESYNC响应命令带上两个参数:主库runID和主库目前的复制进度offset,返回给从库
  • 从库收到响应后,会记录下这两个参数。这里有个地方需要注意,FULLRESYNC响应表示第一次复制采用的全量复制,也就是说,主库会把当前所有的数据都复制给从库
  • 从库收到复制(RDB文件),会先清空当前数据库,然后加载RDB文件
  • 但是,这些请求中的写操作并没有记录到刚刚生成的RDB文件中。为了保证主从库的数据一致性,主库会在内存中用专门的replication buffer,记录RDB文件生成后收到的所有写操作
  • 最后,主库会把第二阶段执行过程中新收到的写命令,再发送给从库。具体的操作是,当主库完成RDB文件发送后,就会把此时replication buffer中的修改操作发给从库,从库再重新执行这些操作。这样一来,主从库就实现同步了

在一主多从的场景,可能会出现主库压力过大的情况,我们可以使用主-从-从模式,即:部分从库replicaof指向另一个从库

主从库间网络断了怎么办

当主从库断连后,主库会把断连期间收到的写操作命令,写入replicationbuffer,同时也会把这些操作命令也写入repl_backlog_buffer这个缓冲区

repl_backlog_buffer是一个环形缓冲区,主库会记录自己写到的位置,从库则会记录自己已经读到的位置

  • 环形缓冲区记录主库偏移量(master_repl_offset)和从库偏移量(slave_repl_offset),正常情况下两者偏移量基本相等
  • 主从库的连接恢复之后,从库首先会给主库发送psync命令,并把自己当前的slave_repl_offset发给主库进行比较,来实现增量同步

不过,有一个地方需要注意,因为repl_backlog_buffer是一个环形缓冲区,所以在缓冲区写满后,主库会继续写入,此时,就会覆盖掉之前写入的操作

如果从库的读取速度比较慢,就有可能导致从库还未读取的操作被主库新写的操作覆盖了,这会导致主从库间的数据不一致

因此,我们要想办法避免这一情况,一般而言,我们可以调整repl_backlog_size这个参数。这个参数和所需的缓冲空间大小有关

缓冲空间的计算公式是:缓冲空间大小=主库写入命令速度操作大小-主从库间网络传输命令速度操作大小

在实际应用中,考虑到可能存在一些突发的请求压力,我们通常需要把这个缓冲空间扩大一倍,即repl_backlog_size=缓冲空间大小*2,这也就是repl_backlog_size的最终值

比如,主库每秒写入2000个操作,每个操作的大小为2KB,网络每秒能传输1000个操作,那么有1000个操作需要缓冲起来,这就至少需要2MB的缓冲空间

否则,新写的命令就会覆盖掉旧操作了。为了应对可能的突发压力,我们最终把repl_backlog_size设为4MB

哨兵机制:主库挂了,如何不间断服务

在Redis主从集群中,哨兵机制是实现主从库自动切换的关键机制,它有效地解决了主从复制模式下故障转移的这三个问题

哨兵其实就是一个运行在特殊模式下的Redis进程,主从库实例运行的同时,它也在运行

哨兵主要负责的就是三个任务:监控、选主(选择主库)和通知

  • 监控 - 哨兵进程在运行时,周期性地给所有的主从库发送PING命令,检测它们是否仍然在线运行
    • 如果从库没有在规定时间内响应哨兵的PING命令,哨兵可以把它标记为主观下线即可
    • 如果检测到的是主库,就不能简单的把它标记为主观下线了,因为误判的代价是很大的
    • 需要哨兵集群中的每个实例向主库发送PING命令,当有N个哨兵实例时,最好要有N/2+1个实例判断主库为主观下线,才能最终判定主库为客观下线
  • 选主 - 主库挂了以后,哨兵就需要从很多个从库里,按照一定的规则选择一个从库实例,把它作为新的主库。这一步完成后,现在的集群里就有了新主库
    • 在选主时,除了要检查从库的当前在线状态,还要判断它之前的网络连接状态
    • 如果从库总是和主库断连,而且断连次数超出了一定的阈值,我们就有理由相信,这个从库的网络状况并不是太好,就可以把这个从库筛掉了
    • 可以通过配置主从库断连的最大连接超时时间,如:down-after-milliseconds * 10
    • 如果在down-after-milliseconds毫秒内,主从节点都没有通过网络联系上,我们就可以认为主从节点断连了
    • 如果发生断连的次数超过了10次,就说明这个从库的网络状况不好,不适合作为新主库
    • 在筛选完主库备选后,进行3轮打分,最终选出主库
    • 第一轮 - 优先级最高的从库得分高,通过slave-priority配置,我们可以把配置较高的从库的优先级提高
    • 第二轮 - 和旧主库同步程度最接近的从库得分高,根据master_repl_offset和slave_repl_offset来判断
    • 第三轮 - ID号小的从库得分高
  • 通知 - 在执行通知任务时,哨兵会把新主库的连接信息发给其他从库,让它们执行replicaof命令,和新主库建立连接,并进行数据复制
    • 同时,哨兵会把新主库的连接信息通知给客户端,让它们把请求操作发到新主库上

哨兵集群的发现

哨兵实例之间可以相互发现,要归功于Redis提供的pub/sub机制,也就是发布/订阅机制

在主从集群中,主库上有一个名为“sentinel:hello”的频道,不同哨兵就是通过它来相互发现,实现互相通信的

那么哨兵是如何发现从库的呢,这是由哨兵向主库发送INFO命令来完成的

哨兵给主库发送INFO命令,主库接受到这个命令后,就会把从库列表返回给哨兵

接着,哨兵就可以根据从库列表中的连接信息,和每个从库建立连接,并在这个连接上持续地对从库进行监控

那么客户端是如何通过哨兵集群,来更新主从集群的在线状态呢

每个哨兵实例也提供pub/sub机制,客户端可以从哨兵订阅消息。哨兵提供的消息订阅频道有很多,不同频道包含了主从库切换过程中的不同关键事件

  • 主库下线事件
    • +sdown(实例进入主观下线状态)
    • -sdown(实例退出主观下线状态)
    • +odown(实例进入客观下线状态)
    • -odown(实例退出客观下线状态)
  • 从库重新配置事件
    • +slave-reconf-sent(哨兵发送SLAVEOF命令重新配置从库)
    • +slave-reconf-inprog(从库配置了新主库,但尚未进行同步)
    • +slave-reconf-done(从库配置了新主库,且和新主库完成同步)
  • 新主库切换
    • +switch-master(主库地址发生变化)

对于主从切换,当然不是哪个哨兵想执行就可以执行的,否则就乱套了

所以,这就需要哨兵集群在判断了主库“客观下线”后,经过投票仲裁,选举一个Leader出来,由它负责实际的主从切换,即由它来完成新主库的选择以及通知从库与客户端

任何一个实例只要自身判断主库“主观下线”后,就会给其他实例发送is-master-down-by-addr命令

接着,其他实例会根据自己和主库的连接情况,做出 Y 或 N 的响应,Y 相当于赞成票,N 相当于反对票

注意:要保证所有哨兵实例的配置是一致的,尤其是主观下线的判断值 down-after-milliseconds

切片集群

在Redis3.0之前,官方并没有针对切片集群提供具体的方案,我们一般会使用一致性哈希等方案

从3.0开始,官方提供了一个名为Redis Cluster的方案,用于实现切片集群

Redis Cluster方案采用哈希槽(Hash Slot)来处理数据和实例之间的映射关系

一个切片集群共有16384个哈希槽,这些哈希槽类似于数据分区,每个键值对都会根据它的key,被映射到一个哈希槽中

我们在部署Redis Cluster方案时,可以使用cluster create命令创建集群,此时,Redis会自动把这些槽平均分布在集群实例上

如果集群中有N个实例,那么每个实例上的槽个数为16384/N个

当然,我们也可以使用cluster meet命令手动建立实例间的连接,形成集群

再使用cluster addslots命令,指定每个实例上的哈希槽个数,因为每个实例的配置可能是不同的

redis-cli -h 172.16.19.3 –p 6379 cluster addslots 0,1

redis-cli -h 172.16.19.4 –p 6379 cluster addslots 2,3

redis-cli -h 172.16.19.5 –p 6379 cluster addslots 4

上面只是一个举例,实际在手动分配哈希槽时,需要把16384个槽都分配完,否则Redis集群无法正常工作

客户端如何定位数据

在定位键值对数据时,它所处的哈希槽是可以通过计算得到的,这个计算可以在客户端发送请求时来执行

但是光知道哈希槽没用,还需要知道哈希槽对应的实例

Redis实例会把自己的哈希槽信息发给和它相连接的其它实例,来完成哈希槽分配信息的扩散。当实例之间相互连接后,每个实例就有所有哈希槽的映射关系了

客户端收到哈希槽信息后,会把哈希槽信息缓存在本地。当客户端请求键值对时,会先计算键所对应的哈希槽,然后就可以给相应的实例发送请求了

在集群中,实例有新增或删除,Redis需要重新分配哈希槽,会导致客户端缓存的哈希槽信息不对

Redis Cluster方案提供了一种重定向机制,所谓的"重定向",就是指,客户端给一个实例发送数据读写操作时,这个实例上并没有相应的数据

这个实例就会给客户端返回下面的MOVED命令响应结果,这个结果中就包含了新实例的访问地址,客户端要再给一个新实例发送操作命令

redis配置文件

redis.conf

#AOF日志配置,每秒执行一次
appendfsync everysec

#bgsave配置
#900s检查一次,增量的数据变更命令超过1,就触发;
save 900 1
#300s更改10次
save 300 10
#60s更改命令1w条,就触发;
sava 60 10000

#从库设置<主库地址><主库端口>
#对于主-从-从模式,也可以设置另一个从库的地址
replicaof 172.16.19.3 6379

# 调整所需的缓冲空间大小
repl_backlog_size

# 主从库断连的最大连接超时时间及次数
down-after-milliseconds * 10

# 从库选举优先级
slave-priority

# 哨兵配置
sentinel monitor <master-name> <ip> <redis-port> <quorum> 

# 哨兵切换主库赞成票,一半是哨兵集群数/2+1
quorum

results matching ""

    No results matching ""