Redis主从同步与故障切换的一些问题
主从数据不一致
主从数据不一致,就是指客户端从从库中读取到的值和主库中的最新值并不一致。
比如主库和从库之前的数据都是20,此时一条修改命令将主库的值由20改为19,接着有一个查询走了从库,此时从库的值还是19。
产生原因:主从库间的命令复制是异步进行的。
具体来说,主库收到写命令,会发给从库,但是在写完主库后就会返回给客户端,并不会等到从库写完才返回给客户端。
从库命令滞后原因:
1、主从库间的网络可能会有传输延迟,所以从库不能及时地收到主库发送的命令,从库上执行同步命令的时间就会被延后。
2、从库收到命令,但此时从库因在执行其他复杂度高的命令而阻塞,无法执行同步命令。
解决办法:
1、采用更好的硬件,保证主从库间的网络连接状况良好。
2、监控主从复制的进度。
Redis 的 INFO replication 命令可以查看主库接收写命令的进度信息(master_repl_offset)和从库复制写命令的进度信息(slave_repl_offset),用 master_repl_offset 减去 slave_repl_offset,这样就能得到从库和主库间的复制进度差值了。
所以我们可以监视这个差值,当达到一定值,我们就不再从这个从库进行读取。
读过期数据
产生原因:Redis 同时使用了两种策略来删除过期的数据,分别是惰性删除策略和定期删除策略。
惰性删除:一个数据过期并不立即删除它,当需要用到这个数据时去检查是否过期,如果过期再删除。
在这种策略下,如果在主库读到过期数据,那么会将其删除。如果在从库读到过期数据,在3.2版本之前,会返回过期数据,3.2版本之后,不会删除,但是会返回一个空值。
定期删除:Redis 每隔一段时间(默认 100ms),就会随机选出一定数量的数据,检查它们是否过期,并把其中过期的数据删除。
定期删除只会删除一小部分数据,如果数据量大,有些数据一直没被访问,就可能一直留存,导致读到过期数据。
尽管使用了3.2版本以上的Redis,还是有可能会读到过期数据,与Redis设置过期时间的命令有关。
Redis设置数据过期时间的命令一共有4个,可以分如下两类:
1、EXPIRE 和 PEXPIRE:它们给数据设置的是从命令执行时开始计算的存活时间;
2、EXPIREAT 和 PEXPIREAT:它们会直接把数据的过期时间设置为具体的一个时间点。
针对于第一类设置过期时间的方法,当主从库进行全量同步时,如果主库收到第一类命令,它会在主库执行,然后等到全量同步结束后发给从库,然后从库执行。这样,此条数据在从库的过期时间就比主库延后了。
解决办法:使用第二类设置办法,避免读到过期数据。
不合理配置项导致的服务挂掉
1.protected-mode 配置项
这个配置项的作用是限定哨兵实例能否被其他服务器访问。当这个配置项设置为 yes 时,哨兵实例只能在部署的服务器本地进行访问。当设置为 no 时,其他服务器也可以访问这个哨兵实例。
如果配置为yes,那么其他哨兵配置在其他服务器上,哨兵前就无法通信,也就无法判断主库是否故障,导致服务不可用。
2.cluster-node-timeout 配置项
这个配置项设置了 Redis Cluster 中实例响应心跳消息的超时时间。
当我们在 Redis Cluster 集群中为每个实例配置了“一主一从”模式时,如果主实例发生故障,从实例会切换为主实例,受网络延迟和切换操作执行的影响,切换时间可能较长,就会导致实例的心跳超时(超出 cluster-node-timeout)。实例超时后,就会被 Redis Cluster 判断为异常。而 Redis Cluster 正常运行的条件就是,有半数以上的实例都能正常运行。
所以,如果执行主从切换的实例超过半数,而主从切换时间又过长的话,就可能有半数以上的实例心跳超时,从而可能导致整个集群挂掉。所以该时间最好调大一点。
另外的一些主从同步问题
1、主从库设置的 maxmemory 不同,如果 slave 比 master 小,那么 slave 内存就会优先达到 maxmemroy,然后开始淘汰数据,此时主从库也会产生不一致。
2、如果主从同步的 client-output-buffer-limit 设置过小,并且 master 数据量很大,主从全量同步时可能会导致 buffer 溢出,溢出后主从全量同步就会失败。如果主从集群配置了哨兵,那么哨兵会让 slave 继续向 master 发起全量同步请求,然后 buffer 又溢出同步失败,如此反复,会形成复制风暴,这会浪费 master 大量的 CPU、内存、带宽资源,也会让 master 产生阻塞的风险。
假如slave可以自动删除过期数据,是否可以保证主从库的一致性?
无法保证。考虑以下场景:
1、主从同步存在网络延迟。例如 master 先执行 SET key 1 10,这个 key 同步到了 slave,此时 key 在主从库都是 10s 后过期,之后这个 key 还剩 1s 过期时,master 又执行了 expire key 60,重设这个 key 的过期时间。但 expire 命令向 slave 同步时,发生了网络延迟并且超过了 1s,如果 slave 可以自动删除过期 key,那么这个 key 正好达到过期时间,就会被 slave 删除了,之后 slave 再收到 expire 命令时,执行会失败。最后的结果是这个 key 在 slave 上丢失了,主从库发生了不一致。
2、主从机器时钟不一致。同样 master 执行 SET key 1 10,然后把这个 key 同步到 slave,但是此时 slave 机器时钟如果发生跳跃,优先把这个 key 过期删除了,也会发生上面说的不一致问题。
所以 Redis 为了保证主从同步的一致性,不会让 slave 自动删除过期 key,而只在 master 删除过期 key,之后 master 会向 slave 发送一个 DEL,slave 再把这个 key 删除掉,这种方式可以解决主从网络延迟和机器时钟不一致带来的影响。
关于 slave-read-only
slave-read-only 主要用来控制 slave 是否可写,但是否主动删除过期 key,根据 Redis 版本不同,执行逻辑也不同。
1、如果版本低于 Redis 4.0,slave-read-only 设置为 no,此时 slave 允许写入数据,但如果 key 设置了过期时间,那么这个 key 过期后,虽然在 slave 上查询不到了,但并不会在内存中删除,这些过期 key 会一直占着 Redis 内存无法释放。
2、Redis 4.0 版本解决了上述问题,在 slave 写入带过期时间的 key,slave 会记下这些 key,并且在后台定时检测这些 key 是否已过期,过期后从内存中删除。
在上述两种情况下,slave都不会主动删除master上的过期key,master 带有过期时间的 key,什么时候删除由 master 自己维护,slave 不会介入。如果 slave 设置了 slave-read-only = no,而且是 4.0+ 版本,slave 也只维护直接向自己写入的带有过期的 key,过期时只删除这些 key。
参考
《Redis核心技术实战》