什么是脑裂:在集群中有两个主节点,同时都能接收写请求,导致不知道往那个节点写入数据
脑裂可能导致数据丢失
数据丢失一定是脑裂吗,不是。所以
第一步先排查数据丢失原因。
在主从集群中发生数据丢失,最常见的原因就是主库的数据还没有同步到从库,结果主库发生了故障,等从库升级为主库后,未同步的数据就丢失了。如果是这种情况的数据丢失,我们可以通过比对主从库上的复制进度差值来进行判断(有一个环形缓冲区记录了写操作复制的坐标,最初目的是为了同步更快,不需要全量同步,增量同步),也就是计算 master_repl_offset 和 slave_repl_offset 的差值。如果从库上的slave_repl_offset 小于原主库的 master_repl_offset,那么就可以认定数据丢失是由数据同步未完成导致的。并不是脑裂
第二步排查客户端的操作日志,发现脑裂现象
在排查客户端的操作日志时发现,在主从切换后的一段时间内,有一个客户端仍然在和原主库通信,并没有和升级的新主库进行交互。这就相当于主从集群中同时有了两个主库。根据这个迹象,我们就想到了在分布式主从集群发生故障时会出现的一个问题:脑裂。
不同客户端给两个主库发送数据写操作,按道理来说,只会导致新数据会分布在不同的主库上,并不会造成数据丢失。那么,为什么我们的数据仍然丢失了呢?
发现是原主库假故障导致的脑裂
原主库所在的机器有一段时间的 CPU 利用率突然特别高,这是我们在机器上部署的一个数据采集程序导致的。因为这个程序基本把机器的 CPU 都用满了,导致Redis 主库无法响应心跳了,在这个期间内,哨兵就把主库判断为客观下线,开始主从切换了。不过,这个数据采集程序很快恢复正常,CPU 的使用率也降下来了。此时,原主库又开始正常服务请求了。
为什么脑裂会导致数据丢失
主从切换后,从库一旦升级为新主库,哨兵就会让原主库执行 slave of 命令,和新主库重新进行全量同步。而在全量同步执行的最后阶段,原主库需要清空本地的数据,加载新主库发送的 RDB 文件,这样一来,原主库在主从切换期间保存的新写数据就丢失了。
如何应对脑裂问题
可以把 min-slaves-to-write 和 min-slaves-max-lag 这两个配置项搭配起来使用,分别给它们设置一定的阈值,假设为 N 和 T。这两个配置项组合后的要求是,主库连接的从库中至少有 N 个从库,和主库进行数据复制时的 ACK 消息延迟不能超过 T 秒,否则,主库就不会再接收客户端的请求了。
即使原主库是假故障,它在假故障期间也无法响应哨兵心跳,也不能和从库进行同步,自然也就无法和从库进行 ACK 确认了。这样一来,min-slaves-to-write 和 min-slavesmax-lag 的组合要求就无法得到满足,原主库就会被限制接收客户端请求,客户端也就不能在原主库中写入新数据了。
redis6.0新特性
Redis 6.0 版本中添加了很多的新的特性,其中有几个关键特性:面向网络处理的多 IO 线程、客户端缓存、细粒度的权限控制,以及 RESP 3 协议的使用。其中,面向网络处理的多 IO 线程可以提高网络请求处理的速度,而客户端缓存可以让应用直接在客户端本地读取数据,这两个特性可以提升 Redis 的性能。除此之外,细粒度权限控制让 Redis 可以按照命令粒度控制不同用户的访问权限,加强了 Redis 的安全保护。RESP 3 协议则增强客户端的功能,可以让应用更加方便地使用 Redis 的不同数据类型。
Redis 一直被大家熟知的就是它的单线程架构,虽然有些命令操作可以用后台线程或子进程执行(比如数据删除、快照生成、AOF 重写),但是,从网络 IO 处理到实际的读写命令处理,都是由单个线程完成的。随着网络硬件的性能提升,Redis 的性能瓶颈有时会出现在网络 IO 的处理上,也就是说,单个主线程处理网络请求的速度跟不上底层网络硬件的速度。
**Redis 的多 IO 线程只是用来处理网络请求的,对于读写命令,Redis 仍然使用单线程来处理。**这是因为,Redis 处理请求时,网络处理经常是瓶颈,通过多个 IO 线程并行处理网络操作,可以提升实例的整体处理性能。而继续使用单线程执行命令操作,就不用为了保证 Lua 脚本、事务的原子性,额外开发多线程互斥机制了。这样一来,Redis 线程模型实现就简单了。
将 io-thread-do-reads 配置项设置为 yes,表示启动网络处理的多 IO 线程模型,关于多 IO 线程模型中开启的线程的数量一般设置为比机器中 CPU 核数量小得数目,例如当 Redis 运行得机器中含有 8 个 CPU 核,我们就可以设置开启 6 个 IO 线程。
如果你在实际应用中,发现 Redis 实例的 CPU 开销不大,吞吐量却没有提升,可以考虑使用 Redis 6.0 的多线程机制,加速网络处理,进而提升实例的吞吐量。
多线程机制:
1、主线程创建和客户端的连接,并把 Socket 放入全局等待队列中。紧接着,主线程通过轮询方法把 Socket 连接分配给 IO 线程。
2、IO 线程读取并解析请求。因为有多个 IO 线程在并行处理,所以,这个过程很快就可以完成,主线程实际被阻塞的时间会很短。
3、主线程执行请求操作(读写命令依然单线程)
4、主线程执行完请求操作后,会把需要返回的结果写入缓冲区,然后,主线程会阻塞等待IO 线程把这些结果回写到 Socket 中,并返回给客户端。和 IO 线程读取和解析请求一样,IO 线程回写 Socket 时,也是有多个线程在并发执行,所以回写 Socket 的速度也很快。等到 IO 线程回写 Socket 完毕,主线程会清空全局队列,等待客户端的后续请求。
实现服务端协助的客户端缓存
。不过,当把数据缓存在客户端本地时,我们会面临一个问题:如果数据被修改了或是失效了,如何通知客户端对缓存的数据做失效处理?
第一种模式是普通模式。在这个模式下,实例会在服务端记录客户端读取过的 key,并监测 key 是否有修改。一旦 key 的值发生变化,服务端会给客户端发送 invalidate(失效)消息,通知客户端缓存失效了。
第二种模式是广播模式。在这个模式下,服务端会给客户端广播所有 key 的失效情况,不过,这样做了之后,如果 key 被频繁修改,服务端会发送大量的失效广播消息,这就会消耗大量的网络带宽资源。
在实际应用时,我们会让客户端注册希望跟踪的 key 的前缀,当带有注册前缀的 key 被修改时,服务端会把失效消息广播给所有注册的客户端
从简单的基于密码访问到细粒度的权限控制
在 Redis 6.0 版本之前,要想实现实例的安全访问,只能通过设置密码来控制,例如,客户端连接实例前需要输入密码。此外,对于一些高风险的命令(例如 KEYS、FLUSHDB、FLUSHALL 等),在 Redis 6.0之前,我们也只能通过 rename-command 来重新命名这些命令,避免客户端直接调用。
在 6.0 中,我们可以使用 ACL SETUSER(acl setuser)命令创建用户。在有多用户的 Redis 应用场景下,就可以非常方便和灵活地为不同用户设置不同级别的命令操作权限
启用 RESP 3 协议
之前都是使用的 RESP 2。在 RESP 2 中,客户端和服务器端的通信内容都是以字节数组形式进行编码的,客户端需要根据操作的命令或是数据类型自行对传输的数据进行解码,增加了客户端开发复杂度。而 RESP 3 直接支持多种数据类型的区分编码,包括空值、浮点数、布尔值、有序的字典集合、无序的集合等。所谓区分编码,就是指直接通过不同的开头字符,区分不同的数据类型,这样一来,客户端就可以直接通过判断传递消息的开头字符,来实现数据转换操作了,提升了客户端的效率。
基于NVM内存的实践
非易失存储(Non-Volatile Memory,NVM) 器件具有容量大、性能快、能持久化保存数据的特性,这些刚好就是 Redis 追求的目标。同时,NVM 器件像 DRAM 一样,可以让软件以字节粒度进行寻址访问,所以,在实际应用中,NVM 可以作为内存来使用,称为 NVM 内存。
Redis 发展的下一步,就可以基于 NVM 内存来实现大容量实例,或者是实现快速持久化数据和恢复。
Redis 是基于 DRAM 内存的键值数据库,而跟传统的 DRAM 内存相比,NVM 有三个显著的特点:
NVM 内存最大的优势是可以直接持久化保存数据。
数据保存在 NVM 内存上后,即使发生宕机或是掉电,数据仍然存在 NVM 内存上。但如果数据是保存在 DRAM 上,那么,掉电后数据就会丢失。
NVM 内存的访问速度接近 DRAM 的速度。
NVM 内存的容量很大。
NVM 器件的密度大,单个 NVM 的存储单元可以保存更多数据。例如,单根 NVM 内存条就能达到 128GB 的容量,最大可以达到 512GB,而单根 DRAM 内存条通常是 16GB 或 32GB。所以,可以很轻松地用 NVM 内存构建 TB 级别的内存。
Intel 在 2019 年 4 月份时推出的 Optane AEP 内存条(简称 AEP 内存)。AEP 内存给软件提供了两种使用模式,分别对应着使用了 NVM 的容量大和持久化保存数据两个特性。
第一种是 Memory 模式。
这种模式是把 NVM 内存作为大容量内存来使用,也就是说,只使用 NVM 容量大和性能高的特性,没有启用数据持久化的功能。在 Memory 模式下,服务器上仍然需要配置 DRAM 内存,但是,DRAM 内存是被 CPU 用作 AEP 内存的缓存,DRAM 的空间对应用软件不可见。 换句话说,软件系统能使用到的内存空间,就是 AEP 内存条的空间容量。
第二种是 App Direct 模式。
这种模式启用了 NVM 持久化数据的功能。 在这种模式下,应用软件把数据写到 AEP 内存上时,数据就直接持久化保存下来了。所以,使用了 App Direct 模式的 AEP 内存,也叫做持久化内存
当 AEP 内存使用 Memory 模式时,应用软件就可以利用它的大容量特性来保存大量数据,Redis 也就可以给上层业务应用提供大容量的实例;
如果使用持久化内存,就可以充分利用 PM 快速持久化的特点,来避免 RDB 和 AOF 的操作。(回顾一下持久化时redis阻塞点)
AOF重写和RDB快照,Redis都用了子进程的方式操作,所以不会阻塞主线程。但Redis直接记录AOF日志,若有大量的写操作,并且配置的是同步写回的话,就会阻塞主线程了。
从库在接收了RDB文件后,需要使用 FLUSHDB 命令清空当前数据库,这又是一个阻塞点。而且,在从库清空数据库后,需要将RDB文件加载到内存,快慢和rdb文件大小相关。加载RDB文件又是一个阻塞点。