近千节点的Redis集群运维经验总结

分享一篇好文章 redis愈来愈实用了前端

服务器宕机并恢复后,须要重启Redis实例,由于集群采用主从结构而且宕机时间比较长,此时宕机上的节点对应的节点都是主节点,宕掉的节点重启后都应该是从节点。启动Redis实例,咱们经过日志发现节点一直从不断的进行主从同步。咱们称这种现象为主从重同步。node

主从同步机制redis

为了分析以上问题,咱们首先应该搞清楚Redis的主从同步机制。如下是从节点正常的主从同步流程日志:数据库

17:22:49.763 * MASTER <-> SLAVE sync started
17:22:49.764 * Non blocking connect for SYNC fired the event.
17:22:49.764 * Master replied to PING, replication can continue...
17:22:49.764 * Partial resynchronization not possible (no cached master)
17:22:49.765 * Full resync from master: c9fabd3812295cc1436af69c73256011673406b9:1745224753247
17:23:42.223 * MASTER <-> SLAVE sync: receiving 1811656499 bytes from master
17:24:04.484 * MASTER <-> SLAVE sync: Flushing old data
17:24:25.646 * MASTER <-> SLAVE sync: Loading DB in memory
17:27:21.541 * MASTER <-> SLAVE sync: Finished with success
17:28:22.818 # MASTER timeout: no data nor PING received...
17:28:22.818 # Connection with master lost.
17:28:22.818 * Caching the disconnected master state.
17:28:22.818 * Connecting to MASTER xxx.xxx.xxx.xxx:xxxx
17:28:22.818 * MASTER <-> SLAVE sync started
17:28:22.819 * Non blocking connect for SYNC fired the event.
17:28:22.824 * Master replied to PING, replication can continue...
17:28:22.824 * Trying a partial resynchronization (request c9fabd3812295cc1436af69c73256011673406b9:1745240101942).
17:28:22.825 * Successful partial resynchronization with master.
以上日志是以从节点的视角呈现的,由于以从节点的角度更能反映主从同步流程,因此如下的分析也以从节点的视角为主。日志很清楚的说明了Redis主从同步的流程,主要步骤为:缓存

从节点接收RDB文件
从节点清空旧数据
从节点加载RDB文件
到此一次全量主从同步完成。等等日志中“Connection with master lost”是什么鬼,为何接下来又进行了一次主从同步。服务器

“Connection with master lost”的字面意思是从节点与主节点的链接超时。在Redis中主从节点须要互相感知彼此的状态,这种感知是经过从节点定时PING主节点而且主节点返回PONG消息来实现的。那么当主节点或者从节点由于其余缘由不能及时收到PING或者PONG消息时,则认为主从链接已经断开。运维

问题又来了何为及时,Redis经过参数repl-timeout来设定,它的默认值是60s。Redis配置文件(redis.conf)中详细解释了repl-timeout的含义:异步

# The following option sets the replication timeout for:
#
# 1) Bulk transfer I/O during SYNC, from the point of view of slave.
# 2) Master timeout from the point of view of slaves (data, pings).
# 3) Slave timeout from the point of view of masters (REPLCONF ACK pings).
#
# It is important to make sure that this value is greater than the value
# specified for repl-ping-slave-period otherwise a timeout will be detected
# every time there is low traffic between the master and the slave.
#
# repl-timeout 60
咱们回过头再来看上边的同步日志,从节点加载RDB文件花费将近三分钟的时间,超过了repl-timeout,因此从节点认为与主节点的链接断开,因此它尝试从新链接并进行主从同步。函数

部分同步工具

这里补充一点当进行主从同步的时候Redis都会先尝试进行部分同步,部分同步失败才会尝试进行全量同步。

Redis中主节点接收到的每一个写请求,都会写入到一个被称为repl_backlog的缓存空间中,这样当进行主从同步的时候,首先检查repl_backlog中的缓存是否能知足同步需求,这个过程就是部分同步。

考虑到全量同步是一个很重量级别而且耗时很长的操做,部分同步机制能在不少状况下极大的减少同步的时间与开销。

重同步问题

经过上面的介绍大概了解了主从同步原理,咱们在将注意力放在加载RDB文件所花费的三分钟时间上。在这段时间内,主节点不断接收前端的请求,这些请求不断的被加入到repl_backlog中,可是由于Redis的单线程特性,从节点是不能接收主节点的同步写请求的。因此不断有数据写入到repl_backlog的同时却没有消费。

当repl_backlog满的时候就不能知足部分同步的要求了,因此部分同步失败,须要又一次进行全量同步,如此造成无限循环,致使了主从重同步现象的出现。不只侵占了带宽,并且影响主节点的服务。

解决方案

至此解决方案就很明显了,调大repl_backlog。

Redis中默认的repl_backlog大小为1M,这是一个比较小的值,咱们的集群中曾经设置为100M,有时候仍是会出现主从重同步现象,后来改成200M,一切太平。能够经过如下命令修改repl_backlog的大小:

//200Mredis-cli -h xxx -p xxx config set repl-backlog-size 209715200
内存碎片

首先对于绝大部分系统内存碎片是必定存在的。试想内存是一整块连续的区域,而数据的长度能够是任意的,而且会随时发生变化,随着时间的推移,在各个数据块中间必定会夹杂着小块的难以利用的内存,因此在Redis中内存碎片是存在的。

在Redis中经过info memory命令能查看内存及碎片状况:

# Memory
used_memory:4221671264 /* 内存分配器为数据分配出去的内存大小,能够认为是数据的大小 */
used_memory_human:3.93G /* used_memoryd的阅读友好形式 */
used_memory_rss:4508459008 /* 操做系统角度上Redis占用的物理内存空间大小,注意不包含swap */
used_memory_peak:4251487304 /* used_memory的峰值大小 */
used_memory_peak_human:3.96G /* used_memory_peak的阅读友好形式 */
used_memory_lua:34816mem_fragmentation_ratio:1.07 /* 碎片率 */
mem_allocator:jemalloc-3.6.0 /* 使用的内存分配器 */
对于每一项的意义请注意查看注释部分,也能够参考官网上info命令memory部分。Redis中内存碎片计算公式为:

mem_fragmentation_ratio = used_memory_rss / used_memory
能够看出上边的Redis实例的内存碎片率为1.07,是一个较小的值,这也是正常的状况,有正常状况就有不正常的状况。发生数据迁移以后的Redis碎片率会很高,如下是迁移数据后的Redis的碎片状况:

used_memory:4854837632
used_memory_human:4.52G
used_memory_rss:7362924544
used_memory_peak:7061034784
used_memory_peak_human:6.58G
used_memory_lua:39936
mem_fragmentation_ratio:1.52
mem_allocator:jemalloc-3.6.0
能够看到碎片率是1.52,也就是说有三分之一的内存被浪费掉了。针对以上两种状况,对于碎片简单的分为两种:

常规碎片

迁移碎片

常规碎片数量较小,并且必定会存在,能够不用理会。那么如何去掉迁移碎片呢?其实方案很简单,只须要先BGSAVE再从新启动节点,从新加载RDB文件会去除绝大部分碎片。

可是这种方案有较长的服务不可用窗口期,因此须要另外一种较好的方案。这种方案须要Redis采用主从结构为前提,主要思路是先经过重启的方式处理掉从节点的碎片,以后进行主从切换,最后处理老的主节点的碎。这样经过极小的服务不可用时间窗口为代价消除了绝大大部分碎片。

Redis Cluster剔除节点失败

Redis Cluster采用无中心的集群模式,集群中全部节点经过互相交换消息来维持一致性。当有新节点须要加入集群时,只须要将它与集群中的一个节点创建联系便可,经过集群间节点互相交换消息全部节点都会互相认识。因此当须要剔除节点的时候,须要向全部节点发送cluster forget命令。

而向集群全部节点发送命令须要一段时间,在这段时间内已经接收到cluster forget命令的节点与没有接收的节点会发生信息交换,从而致使cluster forget命令失效。

为了应对这个问题Redis设计了一个黑名单机制。当节点接收到cluster forget命令后,不只会将被踢节点从自身的节点列表中移除,还会将被剔除的节点添加入到自身的黑名单中。当与其它节点进行消息交换的时候,节点会忽略掉黑名单内的节点。因此经过向全部节点发送cluster forget命令就能顺利地剔除节点。

可是黑名单内的节点不该该永远存在于黑名单中,那样会致使被踢掉的节点不能再次加入到集群中,同时也可能致使不可预期的内存膨胀问题。因此黑名单是须要有时效性的,Redis设置的时间为一分钟。

因此当剔除节点的时候,在一分钟内没能向全部节点发出cluster forget命令,会致使剔除失败,尤为在集群规模较大的时候会常常发生。

解决方案是多个进程发送cluster forget命令,是否是很简单。

迁移数据时的JedisAskDataException异常

问题描述

Redis Cluster集群扩容,须要将一部分数据从老节点迁移到新节点。在迁移数据过程当中会出现较多的JedisAskDataException异常。

迁移流程

因为官方提供迁移工具redis-trib在大规模数据迁移上的一些限制,咱们本身开发了迁移工具,Redis Cluster中数据迁移是以Slot为单位的,迁移一个Slot主要流程以下:

目标节点 cluster setslot <slot> importing <source_id>
源节点 cluster setslot <slot> migrating <target_id>
源节点 cluster getkeysinslot <slot> <count> ==> keys
源节点 migrate <target_ip> <target_port> <key> 0 <timeout>
重复3&4直到迁移完成
任一节点 cluster setslot <slot> node <target_id>
咱们使用Redis中的MIGRATE命令来把数据从一个节点迁移到另一个节点。MIGRATE命令实现机制是先在源节点上DUMP数据,再在目标节点上RESTORE它。

可是DUMP命令并不会包含过时信息,又由于集群中全部的数据都有过时时间,因此咱们须要额外的设置过时时间。因此迁移一个SLOT有点相似以下:

while (from.clusterCountKeysInSlot(slot) != 0) {
keys = from.clusterGetKeysInSlot(slot, 100); for (String key : keys) { //获取key的ttl Long ttl = from.pttl(key); if (ttl > 0) {
from.migrate(host, port, key, 0, 2000); to.asking(); to.pexpire(key, ttl); } }
}
可是上边的迁移工具在运行过程当中报了较多的JedisAskDataException异常,经过堆栈发现是“Long ttl = from.pttl(key)”这一行致使的。为了解释上述异常,咱们须要先了解Redis的一些内部机制。

Redis数据过时机制

Redis数据过时混合使用两种策略

主动过时策略:定时扫描过时表,并删除过时数据,注意这里并不会扫描整个过时表,为了减少任务引发的主线程停顿,每次只扫描一部分数据,这样的机制致使数据集中可能存在较多已通过期可是并无删除的数据。

被动过时策略:当客户端访问数据的时候,首先检查它是否已通过期,若是过时则删掉它,并返回数据不存在标识。

这样的过时机制兼顾了每次任务的停顿时间与已通过期数据不被访问的功能性,充分体现了做者优秀的设计能力,详细参考官网数据过时机制。

Open状态Slot访问机制

在迁移Slot的过程当中,须要先在目标节点将Slot设置为importing状态,而后在源节点中将Slot设置为migrating 状态,咱们称这种Slot为Open状态的Slot。

由于处于Open状态的Slot中的数据分散在源与目标两个节点上,因此若是须要访问Slot中的数据或者添加数据到Slot中,须要特殊的访问规则。Redis推荐规则是首先访问源节点再去访问目标节点。若是源节点不存在,Redis会返回ASK标记给客户端,详细参考官网。

问题分析

让咱们回到问题自己,通过阅读Redis代码发现clusterCountKeysInSlot函数不会触发被动过时策略,因此它返回的数据包含已通过期可是没有被删除的数据。当程序执行到“Long ttl = from.pttl(key);”这一行时,首先Redis会触发触发被动过时策略删掉已通过期的数据,此时该数据已经不存在,又由于该节点处于migrating状态,因此ASK标记会被返回。而ASK标记被Jedis转化为JedisAskDataException异常。

这种异常只须要捕获并跳过便可。

Redis Cluster flush失败

flush是一个极少用到的操做,不过既然碰到过诡异的现象,也记录在此。

问题场景是在Reids Cluster中使用主从模式,向主节点发送flush命令,预期主从节点都会清空数据库。可是诡异的现象出现了,咱们获得的结果是主从节点发生了切换,而且数据并无被清空。

分析以上case,Redis采用单线程模型,flush操做执行的时候会阻塞全部其它操做,包括集群间心跳包。当Redis中有大量数据的时候,flush操做会消耗较长时间。因此该节点较长时间不能跟集群通讯,当达到必定阈值的时候,集群会断定该节点为fail,而且会切换主从状态。

Redis采用异步的方式进行主从同步,flush操做在主节点执行完成以后,才会将命令同步到从节点。此时老的从节点变为了主节点,它不会再接受来自老的主节点的删除数据的操做。

当老的主节点flush完成的时候,它恢复与集群中其它节点的通信,得知本身被变成了从节点,所又会把数据同步过来。最终形成了主从节点发生了切换,而且数据没有被清空的现象。

解决方式是临时调大集群中全部节点的cluster-node-timeout参数。

Redis启动异常问题

这也是个极少碰到的问题,同上也记录在此。

咱们集群中每一个物理主机上启动多个Redis以利用多核主机的计算资源。问题发生在一次主机宕机。恢复服务的过程当中,当启动某一个Redis实例的时候,Redis实例正常启动,可是集群将它标记为了fail状态。

众所周知Redis Cluster中的实例,须要监听两个端口,一个服务端口(默认6379),另外一个是集群间通信端口(16379),它是服务端口加上10000。

通过一番调查发现该节点的服务通信端口,已经被集群中其它节点占用了,致使它不能与集群中其它节点通信,被标记为fail状态。

解决方式是找到占用该端口的Redis进程并重启。

写在最后

运维是一个理论落地的过程,对于运维集群而言任何微小的异常背后都是有缘由的,了解系统内部运行机制,而且着手去探究,才能更好的解释问题,消除集群的隐患

相关文章
相关标签/搜索