Redis分享 - 理论基础-阿里云开发者社区

开发者社区> 数据库> 正文
登录阅读全文

Redis分享 - 理论基础

简介: 公司团队每周,会讨论一些东西 拿出来大家分享讨论下 请多多指正 思维导图放于钉钉云盘 https://space.dingtalk.com/s/gwHOAv_afALODMHs2APaACAyNTFkZGY5ODRiNjg0MjQ3Yjk3YWYxNzU4NTAzYmVmZg 密码: r75r

Redis

1、访问框架

动态库访问

  • libsimplekv.so

网络访问框架

  • Socket Server

2、操作模块+索引模块

数据结构

  • 简单动态字符串
  • 双向链表
  • 压缩列表
  • 哈希表
  • 跳表
  • 整数数组

组成(key-value)

  • 为了实现从键到值的快速访问,Redis 使用了一个哈希表来保存所有键值对,key用的字符串,value用的是指向具体值的指针,所以不管值是 String,还是集合类型,哈希桶中的元素都是指向它们的指针。
    • 哈希桶中的 entry 元素中保存了key和value指针,分别指向了实际的键和值,这样一来,即使值是一个集合,也可以通过*value指针被查找到
    • 哈希表保存了所有的键值对,所以,我也把它称为全局哈希表。哈希表的最大好处很明显,就是让我们可以用 O(1) 的时间复杂度来快速查找到键值对——我们只需要计算键的哈希值,就可以知道它所对应的哈希桶位置,然后就可以访问相应的 entry 元素
    • 问题:写入大量的数据?
      • 产生
        • 哈希表的冲突问题(碰撞)
        • rehash 可能带来的操作阻塞(扩容)
      • 解决
        • Redis 解决哈希冲突的方式,就是链式哈希。链式哈希也很容易理解,就是指同一个哈希桶中的多个元素用一个链表来保存,它们之间依次用指针连接。
  • value
    • String
    • List
    • Hash
    • Set
    • Zset

3、存储模块

内存分配器

  • redis
    • tcmalloc
    • libc
    • jemalloc(默认)
      • 需要存储大小为130字节的对象,jemalloc会将其放入160字节的内存单元
      • 优点
        • 1、采用多个 arena 来避免线程同步
        • 2、细粒度的锁,比如每一个 bin 以及每一个 extents 都有自己的锁
        • 3、Memory Order 的使用,比如 rtree 的读写访问有不同的原子语义(relaxed, acquire,release)
        • 4、结构体以及内存分配时保证对齐,以获得更好的 cache locality
        • 5、cache_bin 分配内存时会通过栈变量来判断是否成功以避免 cache miss
        • 6、dirty extent 的 delay coalesce 来获得更好的 cache locality;extent 的 lazy purge 来保证更平滑的 gc 机制
        • 7、紧凑的结构体内存布局来减少占用空间,比如 extent.e_bits
        • 8、rtree 引入 rtree_ctx 的两级 cache 机制,提升 extent 信息获取速度的同时减少 cache miss
        • 9、tcache gc 时对缓存容量的动态调整
      • 缺点
        • 1、arena 之间的内存不可见
          • 某个线程在这个 arena 使用了很多内存,之后这个 arena 并没有其他线程使用,导致这个 arena 的内存无法被 gc,占用过多
          • 两个位于不同 arena 的线程频繁进行内存申请,导致两个 arena 的内存出现大量交叉,但是连续的内存由于在不同 arena 而无法进行合并
  • 其他方式
    • glibc
      • malloc
      • free
      • 缺点
        • 键值数据库的键值对通常大小不一,glibc 的分配器在处理随机的大小内存块分配时,表现并不好。一旦保存的键值对数据规模过大,就可能会造成较严重的内存碎片问题
  • 内存分配算法
    • FF(首次适应算法)
      • 从空闲分区表的第一个表目起查找该表,把最先能够满足要求的空闲区分配给作业,这种方法的目的在于减少查找时间。为适应这种算法,空闲分区表(空闲区链)中的空闲分区要按地址由低到高进行排序。该算法优先使用低址部分空闲区,在低址空间造成许多小的空闲区,在高地址空间保留大的空闲区。
    • BF(最佳适应算法)
      • 从全部空闲区中找出能满足作业要求的、且大小最小的空闲分区,这种方法能使碎片尽量小。为适应此算法,空闲分区表(空闲区链)中的空闲分区要按从小到大进行排序,自表头开始查找到第一个满足要求的自由分区分配。该算法保留大的空闲区,但造成许多小的空闲区。
    • 最差适应算法
      • 从全部空闲区中找出能满足作业要求的、且大小最大的空闲分区,从而使链表中的结点大小趋于均匀,适用于请求分配的内存大小范围较窄的系统。为适应此算法,空闲分区表(空闲区链)中的空闲分区按大小从大到小进行排序,自表头开始查找到第一个满足要求的自由分区分配。该算法保留小的空闲区,尽量减少小的碎片产生。

键值对保存在内存还是外存

  • 外存
    • 虽然可以避免数据丢失,但是受限于磁盘的慢速读写(通常在几 ms 级别),键值数据库的整体性能会被拉低
  • redis
    • 内存
      • 是读写很快,毕竟内存的访问速度一般都在百 ns 级别。但是,潜在的风险是一旦掉电,所有的数据都会丢失。

持久化

  • redis
    • 文件
      • AOF
        • 操作过程
        • 底层命令怎么记录的
          • 如set testkey testvalue
          • 解释:
            • “*3”表示当前命令有三个部分,每部分都是由“$+数字”开头,后面紧跟着具体的命令、键或值。这里,“数字”表示这部分中的命令、键或值一共有多少字节。例如,“$3 set”表示这部分有 3 个字节,也就是“set”命令
        • 特点
          • 先执行命令后记录日志
            • 为什么要这样?
              • 先让系统执行命令,只有命令能执行成功,才会被记录到日志中,否则,系统就会直接向客户端报错(避免出现错误命令)
            • 好处
              • 命令执行后才记录日志,所以不会阻塞当前的写操作。
        • 问题
          • 潜在风险
            • 1、如果刚执行完一个命令,还没有来得及记日志就宕机了,那么这个命令和相应的数据就有丢失的风险。
              • Redis 是用作缓存,还可以从后端数据库重新读入数据进行恢复
              • Redis 是直接用作数据库的话,此时,因为命令没有记入日志,所以就无法用日志进行恢复了
            • 2、AOF 虽然避免了对当前命令的阻塞,但可能会给下一个操作带来阻塞风险。
              • AOF 日志也是在主线程中执行的,如果在把日志文件写入磁盘时,磁盘写压力大,就会导致写盘很慢,进而导致后续的操作也无法执行了
            • 解决方式
              • Always(同步写回)
                • 每个写命令执行完,立马同步地将日志写回磁盘
              • Everysec(每秒写回)
                • 每个写命令执行完,只是先把日志写到 AOF 文件的内存缓冲区,每隔一秒把缓冲区中的内容写入磁盘
              • No(操作系统控制的写回)
                • 每个写命令执行完,只是先把日志写到 AOF 文件的内存缓冲区,由操作系统决定何时将缓冲区内容写回磁盘
              • 想要获得高性能,就选择 No 策略;如果想要得到高可靠性保证,就选择Always 策略;如果允许数据有一点丢失,又希望性能别受太大影响的话,那么就选择Everysec 策略
          • 性能问题
            • 1、文件系统本身对文件大小有限制,无法保存过大的文件
            • 2、如果文件太大,之后再往里面追加命令记录的话,效率也会变低
            • 3、如果发生宕机,AOF 中记录的命令要一个个被重新执行,用于故障恢复,如果日志文件太大,整个恢复过程就会非常缓慢,这就会影响到 Redis 的正常使用
            • 解决方式
              • AOF 重写机制
                • 多变一:旧日志文件中的多条命令,在重写后的新日志中变成了一条命令。
                • 简单点来说,就是我们在同一个键中操作多次,日志只会存最终结果,过程数据会删除
                • 产生的潜在问题
                  • 1、fork子进程
                    • 1、fork子进程,fork这个瞬间一定是会阻塞主线程的(注意,fork时并不会一次性拷贝所有内存数据给子进程),fork采用操作系统提供的写实复制(Copy On Write)机制,就是为了避免一次性拷贝大量内存数据给子进程造成的长时间阻塞问题,但fork子进程需要拷贝进程必要的数据结构,其中有一项就是拷贝内存页表(虚拟内存和物理内存的映射索引表),这个拷贝过程会消耗大量CPU资源,拷贝完成之前整个进程是会阻塞的,阻塞时间取决于整个实例的内存大小,实例越大,内存页表越大,fork阻塞时间越久。拷贝内存页表完成后,子进程与父进程指向相同的内存地址空间,也就是说此时虽然产生了子进程,但是并没有申请与父进程相同的内存大小。
                    • 2、fork出的子进程指向与父进程相同的内存地址空间,此时子进程就可以执行AOF重写,把内存中的所有数据写入到AOF文件中。但是此时父进程依旧是会有流量写入的,如果父进程操作的是一个已经存在的key,那么这个时候父进程就会真正拷贝这个key对应的内存数据,申请新的内存空间,这样逐渐地,父子进程内存数据开始分离,父子进程逐渐拥有各自独立的内存空间。因为内存分配是以页为单位进行分配的,默认4k,如果父进程此时操作的是一个bigkey,重新申请大块内存耗时会变长,可能会产阻塞风险。另外,如果操作系统开启了内存大页机制(Huge Page,页面大小2M),那么父进程申请内存时阻塞的概率将会大大提高,所以在Redis机器上需要关闭Huge Page机制。Redis每次fork生成RDB或AOF重写完成后,都可以在Redis log中看到父进程重新申请了多大的内存空间。
                  • 2、AOF重写过程中父进程产生写入(AOF重写不复用AOF本身的日志)
                    • 1、父子进程写同一个文件必然会产生竞争问题,控制竞争就意味着会影响父进程的性能
                    • 2、如果AOF重写过程中失败了,那么原本的AOF文件相当于被污染了,无法做恢复使用。所以Redis AOF重写一个新文件,重写失败的话,直接删除这个文件就好了,不会对原先的AOF文件产生影响。等重写完成之后,直接替换旧文件即可
              • 阻塞
                • 一个拷贝,两处日志
                  • 一个拷贝
                    • 1、每次执行重写时,主线程 fork 出后台的 bgrewriteaof 子进程
                    • 2、fork 会把主线程的内存拷贝一份给bgrewriteaof 子进程,这里面就包含了数据库的最新数据。
                    • 3、bgrewriteaof 子进程就可以在不影响主线程的情况下,逐一把拷贝的数据写成操作,记入重写日志
                  • 两处日志
                    • 如果有写操作,第一处日志就是指正在使用的 AOF 日志,Redis 会把这个操作写到它的缓冲区
                    • 第二处日志,就是指新的 AOF 重写日志(也会被写到重写日志的缓冲区)
                      • 1、等到拷贝数据的所有操作记录重写完成后,重写日志记录的这些最新操作也会写入新的 AOF 文件,以保证数据库最新状态的记录
                      • 2、用新的 AOF 文件替代旧文件
                  • 每次 AOF 重写时,Redis 会先执行一个内存拷贝,用于重写;然后,使用两个日志保证在重写过程中,新写入的数据不会丢失。而且,因为 Redis 采用额外的线程进行数据重写,所以,这个过程并不会阻塞主线程。
      • RDB(类比现实生活的拍照)
        • 意思
          • 内存快照
            • 内存中的数据在某一刻的状态记录
            • 对 Redis 来说,它实现类似照片记录效果的方式,就是把某一时刻的状态以文件的形式写到磁盘上,也就是快照。这样一来,即使宕机,快照文件也不会丢失,数据的可靠性也就得到了保证。这个快照文件就称为 RDB 文件,其中,RDB 就是 Redis DataBase 的缩写。
        • 考虑
          • 对哪些数据做快照?
            • 全量快照(把内存中的所有数据都记录到磁盘中)
              • 产生问题
                • 需要协调数据位置
                • 写数据的时间开销大
              • 生成RDB文件
                • save
                  • 在主线程中执行,会导致阻塞
                • bgsave
                  • 创建一个子进程,专门用于写入 RDB 文件,避免了主线程的阻塞,这也是Redis RDB 文件生成的默认配置
                    • bgsave执行全量快照
                    • 这既提供了数据的可靠性保证,也避免了对 Redis 的性能影响
          • 做快照时,数据还能被增删改吗?
            • 正常来说是不能增删改的,因为一旦有操作,就会可能导致快照不完整,数据不对
            • 但是,redis采用了COW(写时复制技术),在执行快照的同时,正常处理写操作
              • 简单来说,bgsave 子进程是由主线程 fork 生成的,可以共享主线程的所有内存数据。bgsave 子进程运行后,开始读取主线程的内存数据,并把它们写入 RDB 文件。
              • 如果主线程对这些数据也都是读操作(例如图中的键值对 A),那么,主线程和bgsave 子进程相互不影响。但是,如果主线程要修改一块数据(例如图中的键值对 C),那么,这块数据就会被复制一份,生成该数据的副本。然后bgsave 子进程会把这个副本数据写入 RDB 文件,而在这个过程中,主线程仍然可以直接修改原来的数据。
          • 解决
            • Redis 会使用 bgsave 对当前内存中的所有数据做快照,这个操作是子进程在后台完成的,这就允许主线程同时可以修改数据
          • 可以每秒做一次快照吗?
            • 缺点
              • 频繁将全量数据写入磁盘,会给磁盘带来很大压力,多个快照竞争有限的磁盘带宽,前一个快照还没有做完,后一个又开始做了,容易造成恶性循环
              • bgsave 子进程需要通过 fork 操作从主线程创建出来。虽然,子进程在创建后不会再阻塞主线程,但是,fork 这个创建过程本身会阻塞主线程,而且主线程的内存越大,阻塞时间越长。如果频繁 fork 出 bgsave 子进程,这就会频繁阻塞主线程了
            • 解决
              • 增量快照
              • 但是也会有问题,要修改的数据多
        • 缺点
          • 丢失数据比较多,难把控两次快照的时间
      • AOF+RDB
        • 内存快照以一定的频率执行,在两次快照之间,使用 AOF 日志记录这期间的所有命令操作。
        • 在 Redis 重启的时候,可以先加载 RDB 的内容,然后再重放增量 AOF 日志就可以完全替代之前的AOF 全量文件重放,因此重启效率大幅得到提升。
      • 选择
        • 1、数据不能丢失时,内存快照和 AOF 的混合使用是一个很好的选择
        • 2、如果允许分钟级别的数据丢失,可以只使用 RDB
        • 3、如果只用 AOF,优先使用 everysec 的配置选项,因为它在可靠性和性能之间取了一个平衡
    • 数据库恢复
      • 1、需要频繁访问数据库,会给数据库带来巨大的压力
      • 2、,这些数据是从慢速数据库中读取出来的,性能肯定比不上从 Redis 中读取,导致使用这些数据的应用程序响应变慢
  • 内存
  • 磁盘

问题:为了保证数据的可靠性,Redis 需要在磁盘上读写 AOF 和 RDB,但在高并发场景里,这就会直接带来两个新问题:一个是写 AOF 和RDB 会造成 Redis 性能抖动,另一个是 Redis 集群数据同步和实例恢复时,读 RDB 比较慢,限制了同步和恢复速度。

解决

  • NVM(非易失内存)

6、高可用扩展集群支撑模块

数据分片

  • 定义
    • 启动多个 Redis 实例组成一个集群,然后按照一定的规则,把收到的数据划分成多份,每一份用一个实例来保存
  • 如何保存更多数据?
    • 纵向扩展
      • 升级单个 Redis 实例的资源配置,包括增加内存容量、增加磁盘容量、使用更高配置的 CPU。就像下图中,原来的实例内存是 8GB,硬盘是 50GB,纵向扩展后,内存增加到 24GB,磁盘增加到 150GB。
      • 优点
        • 实施起来简单、直接
      • 潜在问题
        • 当使用 RDB 对数据进行持久化时,如果数据量增加,需要的内存也会增加,主线程 fork 子进程时就可能会阻塞(不要求持久化保存redis)
        • 纵向扩展会受到硬件和成本的限制
    • 横向扩展
      • 横向增加当前 Redis 实例的个数,就像下图中,原来使用 1 个 8GB 内存、50GB 磁盘的实例,现在使用三个相同配置的实例
    • 选择
      • 在面向百万、千万级别的用户规模时,横向扩展的 Redis 切片集群会是一个非常好的选择。
    • 数据切片和实例的对应分布关系
      • Redis Cluster1、Redis Cluster 方案采用哈希槽(Hash Slot),来处理数据和实例之间的映射关系2、在 Redis Cluster 方案中,一个切片集群共有 16384个哈希槽,这些哈希槽类似于数据分区,每个键值对都会根据它的key,被映射到一个哈希槽中
        • 过程1、我们在部署 Redis Cluster 方案时,可以使用 cluster create 命令创建集群,此时,Redis 会自动把这些槽平均分布在集群实例上。例如,如果集群中有 N 个实例,那么,每个实例 上的槽个数为 16384/N 个。 2、当然, 我们也可以使用 cluster meet 命令手动建立实例间的连接,形成集群,再使用 cluster addslots 命令,指定每个实例上的哈希槽个数。 3、举个例子,假设集群中不同 Redis 实例的内存大小配置不一,如果把哈希槽均分在各个实 例上,在保存相同数量的键值对时,和内存大的实例相比,内存小的实例就会有更大的容 量压力。遇到这种情况时,你可以根据不同实例的资源配置情况,使用 cluster addslots 命令手动分配哈希槽。
          • 首先根据键值对的 key,按照 CRC16 算法计算一个 16 bit的值;然后,再用这个 16bit 值对 16384 取模,得到 0~16383 范围内的模数,每个模数代表一个相应编号的哈希槽
        • 通过哈希槽,切片集群就实现了数据到哈希槽、哈希槽再到实例的分配
        • 建议
          • 在手动分配哈希槽时,需要把 16384 个槽都分配完,否则Redis 集群无法正常工作。
        • 客户端如何定位数据?
          • 1、Redis 实例会把自己的哈希槽信息发给和它相连接的其它实例,来完成哈希槽分配信息的扩散。当实例之间相互连接后,每个实例就有所有哈希槽的映射关系了
          • 2、客户端收到哈希槽信息后,会把哈希槽信息缓存在本地。当客户端请求键值对时,会先计算键所对应的哈希槽,然后就可以给相应的实例发送请求了。

5、高可用集群支撑模块

高可用

  • 数据尽量少丢失
    • AOF和RDB解决
  • 服务尽量少中断
    • 增加副本冗余量
      • 增加实例
      • 多实例保存同一份数据
      • 产生问题
        • 数据如何保持一致?
        • 数据读写操作可以发给所有实例吗
          • 主从库集群模式(读写分 离)保证数据副本的一致,主从库之间采用的是读写分 离的方式。
            • 读操作
              主库、从库都可以接收
            • 写操作
              首先到主库执行,然后,主库将写操作同步给从库
            • 为什么要采用读写分离?
              • 没有采用读写分离
                • 1、如果所有机器都能写,相当于客户端一个请求,可能会修改多次,导致数据不一致,这个时候读就可能读到老数据
                • 2、如果要保持所有实例数据保存一致,就要加锁,实例间协商是否完成修改等操作
              • 采用读写分离
                • 1、所有的数据的修改都会在主库上进行,不需要协商多个实例,只需要把主库的最新数据同步过去就行了。
                • 如何同步?
                  • 全量复制1、主从库间建立连接、协商同步的过程,主要是为全量复制做准备(从库和主库建立起连接,并告诉主库即将进行同步,主库确认回复后,主从库间就可以开始同步了)从库给主库发送 psync 命令,表示要进行数据同步,主库根据这个命令的参数 来启动复制。psync 命令包含了主库的 runID 和复制进度 offset 两个参数。runID,是每个 Redis 实例启动时都会自动生成的一个随机 ID,用来唯一标记这个实 例。当从库和主库第一次复制时,因为不知道主库的 runID,所以将 runID 设 为“?”。 offset,此时设为 -1,表示第一次复制。主库收到 psync 命令后,会用 FULLRESYNC 响应命令带上两个参数:主库 runID 和主库 目前的复制进度 offset,返回给从库。从库收到响应后,会记录下这两个参数。 这里有个地方需要注意,FULLRESYNC 响应表示第一次复制采用的全量复制,也就是说, 主库会把当前所有的数据都复制给从库。2、主库将所有数据同步给从库。从库收到数据后,在本地完成数据加载。(依赖于RDB)主库执行 bgsave 命令,生成 RDB 文件,接着将文件发给从库。从库接收到 RDB 文件后,会先清空当前数据库,然后加载 RDB 文件。这是因为从库在通过 replicaof 命令开始和主库同步前,可能保存了其他数据。为了避免之前数据的影响,从库需要先把 当前数据库清空。 在主库将数据同步给从库的过程中,主库不会被阻塞,仍然可以正常接收请求。否则, Redis 的服务就被中断了。但是,这些请求中的写操作并没有记录到刚刚生成的 RDB 文件 中。为了保证主从库的数据一致性,主库会在内存中用专门的 replication buffer,记录 RDB 文件生成后收到的所有写操作。 3、主库会把第二阶段执行过程中新收到的写命令,再发送给从 库。具体的操作是,当主库完成 RDB 文件发送后,就会把此时 replication buffer 中的修改操作发给从库,从库再重新执行这些操作。这样一来,主从库就实现同步了。
                    • 简单点来说就是,先把已有的发给从库,如果在发的过程中修改了,那就把修改的放入一个缓存池中,然后等待前面的发完了,再把缓存的发出去。
                    • 问题:(耗时操作)
                      • 生成 RDB 文件
                      • 传输 RDB 文件
                  • 基于长连接的命令传播
                    • 风险点
                      • 网络断连或阻塞
                        主库与从库断了或阻塞,会导致从库数据是旧数据
                  • 增量复制当主从库断连后,主库会把断连期间收到的写操作命令,写入 replication buffer,同时也会把这些操作命令也写入 repl_backlog_buffer 这个缓冲区。 repl_backlog_buffer 是一个环形缓冲区,主库会记录自己写到的位置,从库则会记录自己 已经读到的位置。 刚开始的时候,主库和从库的写读位置在一起,这算是它们的起始位置。随着主库不断接 收新的写操作,它在缓冲区中的写位置会逐步偏离起始位置,我们通常用偏移量来衡量这 个偏移距离的大小,对主库来说,对应的偏移量就是master_repl_offset。主库接收的新 写操作越多,这个值就会越大。 同样,从库在复制完写操作命令后,它在缓冲区中的读位置也开始逐步偏移刚才的起始位 置,此时,从库已复制的偏移量 slave_repl_offset 也在不断增加。正常情况下,这两个偏 移量基本相等。主从库的连接恢复之后,从库首先会给主库发送 psync 命令,并把自己当前的 slave_repl_offset 发给主库,主库会判断自己的 master_repl_offset 和 slave_repl_offset 之间的差距。在网络断连阶段,主库可能会收到新的写操作命令,所以,一般来说,master_repl_offset 会大于 slave_repl_offset。此时,主库只用把 master_repl_offset 和 slave_repl_offset 之间的命令操作同步给从库就行。
                    • Redis repl_backlog_buffer的使用
                    • Redis 增量复制流程
                    • 总结
                      • 1、就是相当于网络等问题之后,此时会有一个repl_backlog_buffer(环形队列)去记录值,有两个偏移量,一个是主库的 master_repl_offse,一个是从库的 slave_repl_offset2、主库的 master_repl_offse会一直往环形队列中加数据,然后从库在同步完rdb之后会读环形队列的主库的 master_repl_offse的 初始值,并用从库的slave_repl_offset从这读起,一直读到两个偏移量相等就可以了
                    • 缺点
                      • 问题
                        • 如果从库的读取速度比较慢,就有可能导致从库还未读取的操作被主库新写的操作覆盖了,这会导致主从库间的数据不一致。
                      • 解决
                        • 调整 repl_backlog_size 这个参数
                        • 缓冲空间的计算公式
                          一般情况:缓冲空间大小 = 主库写入命令速度 * 操作大小 - 主从库间网络传输命令速度 * 操作大小实际工作:repl_backlog_size = 缓冲空间大小 * 2,这也就是 repl_backlog_size 的最终值。
            • 建议
              • 一个 Redis 实例的数据库不要太大,一个实例大小在几 GB 级别比较合适,这样可以减少 RDB 文件生成、传输和重新加载的开销
          • 哨兵集群
            • 哨兵机制
              • 监控监控是指哨兵进程在运行时,周期性地给所有的主从库发送 PING 命令,检测它们是否仍然在线运行。如果从库没有在规定时间内响应哨兵的 PING 命令,哨兵就会把它标记为“下线状态”;同样,如果主库也没有在规定时间内响应哨兵的 PING 命令,哨兵就会判定主库下线,然后开始自动切换主库的流程。
                • 在监控任务中,哨兵需要判断主库是否处于下线状态
                  • 主观下线
                    • 哨兵进程会使用 PING 命令检测它自己和主、从库的网络连接情况,用来判断实例的状态
                      误判:主从切换,导致哨兵认为主库下线,然后进行选举,数据同步,造成一系列的开销解决:通常会采用多实例组成的集群模式进行部署,这也被称为哨兵集群
                  • 客观下线“客观下线”的标准就是,当有 N 个哨兵实例时,最好要有 N/2 + 1 个实例判 断主库为“主观下线”,才能最终判定主库为“客观下线”。这样一来,就可以减少误判 的概率,也能避免误判带来的无谓的主从库切换。(当然,有多少个实例做出“主观下 线”的判断才可以,可以由 Redis 管理员自行设定)。
                    • 针对主库(哨兵集群的少数服从多数)
              • 选主(选择主库)主库挂了以后,哨兵就需要从很多个从库里,按照一定的规则选择一个从库实例,把它作为新的主库。这一步完成后,现在的集群里就有了新主库。
                • 在选主任务中,哨兵也要决定选择哪个从库实例作为主库
                  • 过程我们在多个从库中,先按照一定的筛选条件,把不符合条件的从库去掉。然后,我们再按照一定的规则,给剩下的从库逐个打分,将得分最高的从库选为新主库
                    • 筛选1、如果从库总是和主库断连,而且断连次数超出了一定的阈值,我们就有理由相信,这个从库 的网络状况并不是太好,就可以把这个从库筛掉了。 2、你使用配置项 down-after-milliseconds *10。其中,down-after-milliseconds 是我们认定主从库断连的最大连接超时时间。如果在 down-after-milliseconds 毫秒内,主从节点都没有通过网络联系上,我们就可以认为主从节点断连 了。如果发生断连的次数超过了 10 次,就说明这个从库的网络状况不好,不适合作为新主 库。
                      • 检查从库的当前在线状态
                      • 判断它之前的网络连接状态
                    • 打分
                      • 1、优先级最高的从库得分高
                        用户可以通过 slave-priority 配置项,给不同的从库设置不同优先级。比如,你有两个从库,它们的内存大小不一样,你可以手动给内存大的实例设置一个高优先级。在选主时, 哨兵会给优先级高的从库打高分,如果有一个从库优先级最高,那么它就是新主库了。如 果从库的优先级都一样,那么哨兵开始第二轮打分
                      • 2、和旧主库同步程度最接近的从库得分高
                      • 3、ID 号小的从库得分高
                        • 在优先级和复制进度都相同的情况下,ID 号最小的从库得分最高,会被选为新主库
                      • 主从库同步时有个命令传播的过程。在这个过程中,主库会用 master_repl_offset 记录当前的最新写操作在repl_backlog_buffer 中的位置,而从库会 用slave_repl_offset 这个值记录当前的复制进度。此时,我们想要找的从库,它的 slave_repl_offset 需要最接近 master_repl_offset。如果 在所有从库中,有从库的 slave_repl_offset 最接近 master_repl_offset,那么它的得分就 最高,可以作为新主库。 就像如图所示,旧主库的 master_repl_offset 是 1000,从库 1、2 和 3 的 slave_repl_offset 分别是 950、990 和 900,那么,从库 2 就应该被选为新主库。当然,如果有两个从库的 slave_repl_offset 值大小是一样的(例如,从库 1 和从库 2 的 slave_repl_offset 值都是 990),我们就需要给它们进行第三轮打分了
              • 通知
                在执行通知任务时,哨兵会把新主库的连接信息发给其他从库,让它们执行 replicaof 命令,和新主库建立连接,并进行数据复制。同时,哨兵会把新主库的连接信息通知给客户端,让它们把请求操作发到新主库上(把新主库信息发给从库和客户端)
              • 哨兵机制的三项任务与目标
            • 关键机制
              • 1、基于 pub/sub 机制的哨兵集群组成1、在主从集群中,主库上有一个名为“sentinel:hello”的频道,不同哨兵就是通过 它来相互发现,实现互相通信的。 2、我来举个例子,具体说明一下。在下图中,哨兵 1 把自己的 IP(172.16.19.3)和端口 (26579)发布到“sentinel:hello”频道上,哨兵 2 和 3 订阅了该频道。那么此时,哨兵 2 和 3 就可以从这个频道直接获取哨兵 1 的 IP 地址和端口号。 3、然后,哨兵 2、3 可以和哨兵 1 建立网络连接。通过这个方式,哨兵 2 和 3 也可以建立网 络连接,这样一来,哨兵集群就形成了。它们相互间可以通过网络连接进行通信,比如说对主库有没有下线这件事儿进行判断和协商。4、哨兵除了彼此之间建立起连接形成集群外,还需要和从库建立连接。这是因为,在哨兵的 监控任务中,它需要对主从库都进行心跳判断,而且在主从库切换完成后,它还需要通知 从库,让它们和新主库进行同步。
                • 只有订阅了同一个频道的应用,才能通过发布的消息进行信息交换。
              • 2、基于 INFO 命令的从库列表,这可以帮助哨兵和从库建立连接1、这是由哨兵向主库发送 INFO 命令来完成的。就像下图所示,哨兵 2 给主库发送 INFO 命 令,主库接受到这个命令后,就会把从库列表返回给哨兵。接着,哨兵就可以根据从库列 表中的连接信息,和每个从库建立连接,并在这个连接上持续地对从库进行监控。哨兵 1 和 3 可以通过相同的方法和从库建立连接。2、通过 pub/sub 机制,哨兵之间可以组成集群,同时,哨兵又通过 INFO 命令,获得 了从库连接信息,也能和从库建立连接,并进行监控了。 3、但是,哨兵不能只和主、从库连接。因为,主从库切换后,客户端也需要知道新主库的连 接信息,才能向新主库发送请求操作。所以,哨兵还需要完成把新主库的信息告诉客户端 这个任务。
                • 哨兵是如何知道从库的 IP 地址和端口的呢?
              • 3、基于哨兵自身的 pub/sub 功能,这实现了客户端和哨兵之间的事件通知哨兵就是一个运行在特定模式下的 Redis 实例,只不过它并不服务请求操 作,只是完成监控、选主和通知的任务。所以,每个哨兵实例也提供 pub/sub 机制,客户 端可以从哨兵订阅消息。哨兵提供的消息订阅频道有很多,不同频道包含了主从库切换过 程中的不同关键事件。
                • 括主库下线判断、新主库选定、从库重新配置
                • 客户端从哨兵这里订阅消息
                  • 步骤
                    • 1、客户端读取哨兵的配置文件
                    • 2、可以获得哨兵的地址和端口
                    • 3、哨兵建立网络连接
                    • 4、客户端执行订阅命令,来获取不同的事件消息
            • 由哪个哨兵执行主从切换?
              • 1、一个哨兵获得了仲裁所需的赞成票数后,就可以标记主库为“客观下线”。这个所需的赞 成票数是通过哨兵配置文件中的 quorum 配置项设定的。例如,现在有 5 个哨兵, quorum 配置的是 3,那么,一个哨兵需要 3 张赞成票,就可以标记主库为“客观下 线”了。这 3 张赞成票包括哨兵自己的一张赞成票和另外两个哨兵的赞成票。 2、此时,这个哨兵就可以再给其他哨兵发送命令,表明希望由自己来执行主从切换,并让所 有其他哨兵进行投票。这个投票过程称为“Leader 选举”。因为最终执行主从切换的哨兵 称为 Leader,投票过程就是确定 Leader。 3、在投票过程中,任何一个想成为 Leader 的哨兵,要满足两个条件:第一,拿到半数以上的 赞成票;第二,拿到的票数同时还需要大于等于哨兵配置文件中的 quorum 值。以 3 个哨 兵为例,假设此时的 quorum 设置为 2,那么,任何一个想成为 Leader 的哨兵只要拿到 2 张赞成票,就可以了。
              • 1、在 T1 时刻,S1 判断主库为“客观下线”,它想成为 Leader,就先给自己投一张赞成票, 然后分别向 S2 和 S3 发送命令,表示要成为 Leader。 2、在 T2 时刻,S3 判断主库为“客观下线”,它也想成为 Leader,所以也先给自己投一张赞 成票,再分别向 S1 和 S2 发送命令,表示要成为 Leader。 3、在 T3 时刻,S1 收到了 S3 的 Leader 投票请求。因为 S1 已经给自己投了一票 Y,所以它 不能再给其他哨兵投赞成票了,所以 S1 回复 N 表示不同意。同时,S2 收到了 T2 时 S3 发送的 Leader 投票请求。因为 S2 之前没有投过票,它会给第一个向它发送投票请求的哨 兵回复 Y,给后续再发送投票请求的哨兵回复 N,所以,在 T3 时,S2 回复 S3,同意 S3 成为 Leader。 4、在 T4 时刻,S2 才收到 T1 时 S1 发送的投票命令。因为 S2 已经在 T3 时同意了 S3 的投 票请求,此时,S2 给 S1 回复 N,表示不同意 S1 成为 Leader。发生这种情况,是因为 S3 和 S2 之间的网络传输正常,而 S1 和 S2 之间的网络传输可能正好拥塞了,导致投票请 求传输慢了。5、在 T5 时刻,S1 得到的票数是来自它自己的一票 Y 和来自 S2 的一票 N。而 S3 除 了自己的赞成票 Y 以外,还收到了来自 S2 的一票 Y。此时,S3 不仅获得了半数以上的 Leader 赞成票,也达到预设的 quorum 值(quorum 为 2),所以它最终成为了 Leader。接着,S3 会开始执行选主操作,而且在选定新主库后,会给其他从库和客户端通 知新主库的信息。 6、如果 S3 没有拿到 2 票 Y,那么这轮投票就不会产生 Leader。哨兵集群会等待一段时间 (也就是哨兵故障转移超时时间的 2 倍),再重新选举。这是因为,哨兵集群能够进行成 功投票,很大程度上依赖于选举命令的正常网络传播。如果网络压力较大或有短时堵塞, 就可能导致没有一个哨兵能拿到半数以上的赞成票。所以,等到网络拥塞好转之后,再进 行投票选举,成功的概率就会增加。 7、需要注意的是,如果哨兵集群只有 2 个实例,此时,一个哨兵要想成为 Leader,必须获得2 票,而不是 1 票。所以,如果有个哨兵挂掉了,那么,此时的集群是无法进行主从库切 换的。因此,通常我们至少会配置 3 个哨兵实例。这一点很重要,你在实际应用时可不能 忽略了。
              • 选leader,由leader执行
            • 建议
              • 要保证所有哨兵实例的配置是一致的,尤其是主观下线的判断值 down-after-milliseconds。

4、IO模型

redis

  • 单线程高性能
    • Redis 的网络 IO和键值对读写是由一个线程来完成的,这也是 Redis 对外提供键值存储服务的主要流程
  • 额外线程
    • 持久化、异步删除、集群数据同步
  • 采用高效的数据结构,比如哈希表和跳表
  • 采用IO多路复用机制,使其在网络 IO 操作中能并发处理大量的客户端请求,实现高吞吐率
    • 基本 IO 模型与阻塞点
      • 以 Get 请求为例,为了处理一个 Get 请求,需要监听客户端请求(bind/listen),和客户端建立连接(accept),服务端从 socket 中读取请求(recv),解析客户端发送请求(parse),根据请求类型读取键值数据(get),最后给客户端返回结果,即向 socket 中写回数据(send)。
        • bind/listen、accept、recv、parse 和 send 属于网络 IO 处理,而 get 属于键值数据操作
        • 潜在点: accept() 和 recv()
          • 当Redis监听到一个客户端有请求连接时,但一直未能成功建立起连接时,会阻塞在 accept() 函数这里,导致其他客户端无法和 Redis 建立连接
          • 当 Redis 通过 recv() 从一个客户端读取数据时,如果数据一直没有到达,Redis 也会一直阻塞在 recv()
    • 非阻塞IO模式
      • Redis套接字类型与非阻塞设置
        • 1、socket() 方法会返回主动套接字
        • 2、调用 listen() 方法,将主动套接字转化为监听套接字,此时,可以监听来自客户端的连接请求。
        • 3、调用 accept() 方法接收到达的客户端连接,并返回已连接套接字
      • 连接
        • listen()连接
          • 当 Redis 调用 accept() 但一直未有连接请求到达时,Redis 线程可以返回处理其他操作,而不用一直等待(调用 accept() 时,已经存在监听套接字)
        • accept()连接
          • Redis 调用 recv() 后,如果已连接套接字上一直没有数据到达,Redis 线程同样可以返回处理其他操作。我们也需要有机制继续监听该已连接套接字,并在有数据达到时通知 Redis
        • 一个监听套接字和一个已连接套接字
    • 基于多路复用的高性能 I/O 模型
      • 一个线程处理多个IO流(Linux的select,epoll)
        • 回调机制
          • select/epoll 提供了基于事件的回调机制,即针对不同事件的发生,调用相应的处理函数。
          • 这些事件会被放进一个事件队列,Redis 单线程对该事件队列不断进行处理。这样一来,Redis 无需一直轮询是否有请求实际发生,这就可以避免造成 CPU 资源浪费。同时,Redis 在对事件队列中的事件进行处理时,会调用相应的处理函数,这就实现了基于事件的回调。因为 Redis 一直在对事件队列进行处理,所以能及时响应客户端请求,提升Redis 的响应性能。
          • 理解例子
            • 以连接请求和读数据请求为例:这两个请求分别对应 Accept 事件和 Read 事件,Redis 分别对这两个事件注册 accept 和get 回调函数。当 Linux 内核监听到有连接请求或读数据请求时,就会触发 Accept 事件和 Read 事件,此时,内核就会回调 Redis 相应的 accept 和 get 函数进行处理。
            • 这就像病人去医院瞧病。在医生实际诊断前,每个病人(等同于请求)都需要先分诊、测体温、登记等。如果这些工作都由医生来完成,医生的工作效率就会很低。所以,医院都设置了分诊台,分诊台会一直处理这些诊断前的工作(类似于 Linux 内核监听请求),然后再转交给医生做实际诊断。这样即使一个医生(相当于 Redis 单线程),效率也能提升。
        • 其他方式
          • FreeBSD 的 kqueue
          • Solaris 的 evport
      • 多个监听套接字和多个已连接套接字
      • 内核会一直监听这些套接字上的连接请求或数据请求。一旦有请求到达,就会交给 Redis 线程处理,这就实现了一个 Redis 线程处理多个IO 流的效果。
      • Redis 线程不会阻塞在某一个特定的监听或已连接套接字上,也就是说,不会阻塞在某一个特定的客户端请求处理上
      • Redis 可以同时和多个客户端连接并处理请求,从而提升并发性
      • 多个 FD 就是刚才所说的多个套接字。Redis 网络框架调用 epoll 机制,让内核监听这些套接字。
  • 单线程处理IO请求性能的瓶颈
    • 任意一个请求在server中一旦发生耗时,都会影响整个server的性能,也就是说后面的请求都要等前面这个耗时请求处理完成,自己才能被处理到。
      • 1、操作bigkey:写入一个bigkey在分配内存时需要消耗更多的时间,同样,删除bigkey释放内存同样会产生耗时
      • 2、使用复杂度过高的命令:例如SORT/SUNION/ZUNIONSTORE,或者O(N)命令,但是N很大,例如lrange key 0 -1一次查询全量数据;
      • 3、大量key集中过期:Redis的过期机制也是在主线程中执行的,大量key集中过期会导致处理一个请求时,耗时都在删除过期key,耗时变长
      • 4、AOF刷盘开启always机制:每次写入都需要把这个操作刷到磁盘,写磁盘的速度远比写内存慢,会拖慢Redis的性能
      • 6、主从全量同步生成RDB:虽然采用fork子进程生成数据快照,但fork这一瞬间也是会阻塞整个线程的,实例越大,阻塞时间越久
      • 解决方式
        • lazy-free机制,把bigkey释放内存的耗时操作放在了异步线程中执行,降低对主线程的影响。
    • 并发量非常大时,单线程读写客户端IO数据存在性能瓶颈,虽然采用IO多路复用机制,但是读写客户端数据依旧是同步IO,只能单线程依次读取客户端的数据,无法利用到CPU多核。
      • 解决
        • 多线程,可以在高并发场景下利用CPU多核多线程读写客户端数据,进一步提升server性能,当然,只是针对客户端的读写是并行的,每个命令的真正操作依旧是单线程的
        • 充分利用服务器的多核资源
        • 多线程分摊 Redis 同步 IO 读写负荷

其他操作

  • 多线程
    • 1、线程数增加,系统吞吐量短期上升,长期增长缓慢,有时还有可能下降
      • 当有多个线程要修改这个共享资源时,为了保证共享资源的正确性,就需要有额外的机制进行保证,而这个额外的机制,就会带来额外的开销。
      • 多线程编程模式面临的共享资源的并发访问控制问题。
      • 即使增加了线程,大部分线程也在等待获取访问共享资源的互斥锁,并行变串行,系统吞吐率并没有随着线程的增加而增加。
      • 多线程开发可以引入同步原语(volatile)来保护共享资源的并发访问,但是会降低系统代码的易调试性和可维护性。

XMind: ZEN - Trial Version

版权声明:本文内容由阿里云实名注册用户自发贡献,版权归原作者所有,阿里云开发者社区不拥有其著作权,亦不承担相应法律责任。具体规则请查看《阿里云开发者社区用户服务协议》和《阿里云开发者社区知识产权保护指引》。如果您发现本社区中有涉嫌抄袭的内容,填写侵权投诉表单进行举报,一经查实,本社区将立刻删除涉嫌侵权内容。

分享:
数据库
使用钉钉扫一扫加入圈子
+ 订阅

分享数据库前沿,解构实战干货,推动数据库技术变革

其他文章
最新文章
相关文章