You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

162 lines
14 KiB
Markdown

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# 32 | Redis主从同步与故障切换有哪些坑
你好,我是蒋德钧。
Redis的主从同步机制不仅可以让从库服务更多的读请求分担主库的压力而且还能在主库发生故障时进行主从库切换提供高可靠服务。
不过在实际使用主从机制的时候我们很容易踩到一些坑。这节课我就向你介绍3个坑分别是主从数据不一致、读到过期数据以及配置项设置得不合理从而导致服务挂掉。
一旦踩到这些坑业务应用不仅会读到错误数据而且很可能会导致Redis无法正常使用我们必须要全面地掌握这些坑的成因提前准备一套规避方案。不过即使不小心掉进了陷阱里也不要担心我还会给你介绍相应的解决方案。
好了,话不多说,下面我们先来看看第一个坑:主从数据不一致。
## 主从数据不一致
主从数据不一致,就是指客户端从从库中读取到的值和主库中的最新值并不一致。
举个例子假设主从库之前保存的用户年龄值是19但是主库接收到了修改命令已经把这个数据更新为20了但是从库中的值仍然是19。那么如果客户端从从库中读取用户年龄值就会读到旧值。
那为啥会出现这个坑呢?其实这是因为**主从库间的命令复制是异步进行的**。
具体来说,在主从库命令传播阶段,主库收到新的写命令后,会发送给从库。但是,主库并不会等到从库实际执行完命令后,再把结果返回给客户端,而是主库自己在本地执行完命令后,就会向客户端返回结果了。如果从库还没有执行主库同步过来的命令,主从库间的数据就不一致了。
那在什么情况下,从库会滞后执行同步命令呢?其实,这里主要有两个原因。
一方面,主从库间的网络可能会有传输延迟,所以从库不能及时地收到主库发送的命令,从库上执行同步命令的时间就会被延后。
另一方面,即使从库及时收到了主库的命令,但是,也可能会因为正在处理其它复杂度高的命令(例如集合操作命令)而阻塞。此时,从库需要处理完当前的命令,才能执行主库发送的命令操作,这就会造成主从数据不一致。而在主库命令被滞后处理的这段时间内,主库本身可能又执行了新的写操作。这样一来,主从库间的数据不一致程度就会进一步加剧。
那么,我们该怎么应对呢?我给你提供两种方法。
首先,**在硬件环境配置方面,我们要尽量保证主从库间的网络连接状况良好**。例如我们要避免把主从库部署在不同的机房或者是避免把网络通信密集的应用例如数据分析应用和Redis主从库部署在一起。
另外,**我们还可以开发一个外部程序来监控主从库间的复制进度**。
因为Redis的INFO replication命令可以查看主库接收写命令的进度信息master\_repl\_offset和从库复制写命令的进度信息slave\_repl\_offset所以我们就可以开发一个监控程序先用INFO replication命令查到主、从库的进度然后我们用master\_repl\_offset减去slave\_repl\_offset这样就能得到从库和主库间的复制进度差值了。
如果某个从库的进度差值大于我们预设的阈值,我们可以让客户端不再和这个从库连接进行数据读取,这样就可以减少读到不一致数据的情况。不过,为了避免出现客户端和所有从库都不能连接的情况,我们需要把复制进度差值的阈值设置得大一些。
我们在应用Redis时可以周期性地运行这个流程来监测主从库间的不一致情况。为了帮助你更好地理解这个方法我画了一张流程图你可以看下。
![](https://static001.geekbang.org/resource/image/3a/05/3a89935297fb5b76bfc4808128aaf905.jpg)
当然,监控程序可以一直监控着从库的复制进度,当从库的复制进度又赶上主库时,我们就允许客户端再次跟这些从库连接。
除了主从数据不一致以外,我们有时还会在从库中读到过期的数据,这是怎么回事呢?接下来,我们就来详细分析一下。
## 读取过期数据
我们在使用Redis主从集群时有时会读到过期数据。例如数据X的过期时间是202010240900但是客户端在202010240910时仍然可以从从库中读到数据X。一个数据过期后应该是被删除的客户端不能再读取到该数据但是Redis为什么还能在从库中读到过期的数据呢
其实这是由Redis的过期数据删除策略引起的。我来给你具体解释下。
**Redis同时使用了两种策略来删除过期的数据分别是惰性删除策略和定期删除策略**
先说惰性删除策略。当一个数据的过期时间到了以后,并不会立即删除数据,而是等到再有请求来读写这个数据时,对数据进行检查,如果发现数据已经过期了,再删除这个数据。
这个策略的好处是尽量减少删除操作对CPU资源的使用对于用不到的数据就不再浪费时间进行检查和删除了。但是这个策略会导致大量已经过期的数据留存在内存中占用较多的内存资源。所以Redis在使用这个策略的同时还使用了第二种策略定期删除策略。
定期删除策略是指Redis每隔一段时间默认100ms就会随机选出一定数量的数据检查它们是否过期并把其中过期的数据删除这样就可以及时释放一些内存。
清楚了这两个删除策略,我们再来看看它们为什么会导致读取到过期数据。
首先虽然定期删除策略可以释放一些内存但是Redis为了避免过多删除操作对性能产生影响每次随机检查数据的数量并不多。如果过期数据很多并且一直没有再被访问的话这些数据就会留存在Redis实例中。业务应用之所以会读到过期数据这些留存数据就是一个重要因素。
其次,惰性删除策略实现后,数据只有被再次访问时,才会被实际删除。如果客户端从主库上读取留存的过期数据,主库会触发删除操作,此时,客户端并不会读到过期数据。但是,从库本身不会执行删除操作,如果客户端在从库中访问留存的过期数据,从库并不会触发数据删除。那么,从库会给客户端返回过期数据吗?
这就和你使用的Redis版本有关了。如果你使用的是Redis 3.2之前的版本那么从库在服务读请求时并不会判断数据是否过期而是会返回过期数据。在3.2版本后Redis做了改进如果读取的数据已经过期了从库虽然不会删除但是会返回空值这就避免了客户端读到过期数据。所以**在应用主从集群时尽量使用Redis 3.2及以上版本**。
你可能会问只要使用了Redis 3.2后的版本,就不会读到过期数据了吗?其实还是会的。
为啥会这样呢这跟Redis用于设置过期时间的命令有关系有些命令给数据设置的过期时间在从库上可能会被延后导致应该过期的数据又在从库上被读取到了我来给你具体解释下。
我先给你介绍下这些命令。设置数据过期时间的命令一共有4个我们可以把它们分成两类
* EXPIRE和PEXPIRE它们给数据设置的是**从命令执行时开始计算的存活时间**
* EXPIREAT和PEXPIREAT**它们会直接把数据的过期时间设置为具体的一个时间点**。
这4个命令的参数和含义如下表所示
![](https://static001.geekbang.org/resource/image/06/e1/06e8cb2f1af320d450a29326a876f4e1.jpg)
为了方便你理解,我给你举两个例子。
第一个例子是使用EXPIRE命令当执行下面的命令时我们就把testkey的过期时间设置为60s后。
```
EXPIRE testkey 60
```
第二个例子是使用EXPIREAT命令例如我们执行下面的命令就可以让testkey在2020年10月24日上午9点过期命令中的1603501200就是以秒数时间戳表示的10月24日上午9点。
```
EXPIREAT testkey 1603501200
```
好了,知道了这些命令,下面我们来看看这些命令如何导致读到过期数据。
当主从库全量同步时如果主库接收到了一条EXPIRE命令那么主库会直接执行这条命令。这条命令会在全量同步完成后发给从库执行。而从库在执行时就会在当前时间的基础上加上数据的存活时间这样一来从库上数据的过期时间就会比主库上延后了。
这么说可能不太好理解,我再给你举个例子。
假设当前时间是2020年10月24日上午9点主从库正在同步主库收到了一条命令EXPIRE testkey 60这就表示testkey的过期时间就是24日上午9点1分主库直接执行了这条命令。
但是主从库全量同步花费了2分钟才完成。等从库开始执行这条命令时时间已经是9点2分了。而EXPIRE命令是把testkey的过期时间设置为当前时间的60s后也就是9点3分。如果客户端在9点2分30秒时在从库上读取testkey仍然可以读到testkey的值。但是testkey实际上已经过期了。
为了避免这种情况,我给你的建议是,**在业务应用中使用EXPIREAT/PEXPIREAT命令把数据的过期时间设置为具体的时间点避免读到过期数据。**
好了,我们先简单地总结下刚刚学过的这两个典型的坑。
* 主从数据不一致。Redis采用的是异步复制所以无法实现强一致性保证主从数据时时刻刻保持一致数据不一致是难以避免的。我给你提供了应对方法保证良好网络环境以及使用程序监控从库复制进度一旦从库复制进度超过阈值不让客户端连接从库。
* 对于读到过期数据这是可以提前规避的一个方法是使用Redis 3.2及以上版本另外你也可以使用EXPIREAT/PEXPIREAT命令设置过期时间避免从库上的数据过期时间滞后。不过这里有个地方需要注意下**因为EXPIREAT/PEXPIREAT设置的是时间点所以主从节点上的时钟要保持一致具体的做法是让主从节点和相同的NTP服务器时间服务器进行时钟同步**。
除了同步过程中有坑以外,主从故障切换时,也会因为配置不合理而踩坑。接下来,我向你介绍两个服务挂掉的情况,都是由不合理配置项引起的。
## 不合理配置项导致的服务挂掉
这里涉及到的配置项有两个,分别是**protected-mode和cluster-node-timeout。**
**1.protected-mode 配置项**
这个配置项的作用是限定哨兵实例能否被其他服务器访问。当这个配置项设置为yes时哨兵实例只能在部署的服务器本地进行访问。当设置为no时其他服务器也可以访问这个哨兵实例。
正因为这样如果protected-mode被设置为yes而其余哨兵实例部署在其它服务器那么这些哨兵实例间就无法通信。当主库故障时哨兵无法判断主库下线也无法进行主从切换最终Redis服务不可用。
所以我们在应用主从集群时要注意将protected-mode 配置项设置为no并且将bind配置项设置为其它哨兵实例的IP地址。这样一来只有在bind中设置了IP地址的哨兵才可以访问当前实例既保证了实例间能够通信进行主从切换也保证了哨兵的安全性。
我们来看一个简单的小例子。如果设置了下面的配置项那么部署在192.168.10.3/4/5这三台服务器上的哨兵实例就可以相互通信执行主从切换。
```
protected-mode no
bind 192.168.10.3 192.168.10.4 192.168.10.5
```
**2.cluster-node-timeout配置项**
**这个配置项设置了Redis Cluster中实例响应心跳消息的超时时间**。
当我们在Redis Cluster集群中为每个实例配置了“一主一从”模式时如果主实例发生故障从实例会切换为主实例受网络延迟和切换操作执行的影响切换时间可能较长就会导致实例的心跳超时超出cluster-node-timeout。实例超时后就会被Redis Cluster判断为异常。而Redis Cluster正常运行的条件就是有半数以上的实例都能正常运行。
所以,如果执行主从切换的实例超过半数,而主从切换时间又过长的话,就可能有半数以上的实例心跳超时,从而可能导致整个集群挂掉。所以,**我建议你将cluster-node-timeout调大些例如10到20秒**。
## 小结
这节课我们学习了Redis主从库同步时可能出现的3个坑分别是主从数据不一致、读取到过期数据和不合理配置项导致服务挂掉。
为了方便你掌握,我把这些坑的成因和解决方法汇总在下面的这张表中,你可以再回顾下。
![](https://static001.geekbang.org/resource/image/9f/93/9fb7a033987c7b5edc661f4de58ef093.jpg)
最后关于主从库数据不一致的问题我还想再给你提一个小建议Redis中的slave-serve-stale-data配置项设置了从库能否处理数据读写命令你可以把它设置为no。这样一来从库只能服务INFO、SLAVEOF命令这就可以避免在从库中读到不一致的数据了。
不过你要注意下这个配置项和slave-read-only的区别slave-read-only是设置从库能否处理写命令slave-read-only设置为yes时从库只能处理读请求无法处理写请求你可不要搞混了。
## 每课一问
按照惯例我给你提个小问题我们把slave-read-only设置为no让从库也能直接删除数据以此来避免读到过期数据你觉得这是一个好方法吗
欢迎在留言区写下你的思考和答案,我们一起交流讨论。如果你觉得今天的内容对你有所帮助,也欢迎你分享给你的朋友或同事。我们下节课见。