在DeepSource,我们努力以高可用性模式运行所有内部基础设施和服务。这确保了部署中的容错性、可靠性和复原力。我们的Redis服务作为一个三节点的HA集群运行,有一个主站、两个从站和一个Redis Sentinel进程,它作为一个辅助进程运行以启动故障切换。最近,我们在Redis集群部署中推出了无盘复制的功能,消除了Redis主节点对复制的持久性需求。在进一步深入研究之前,让我们先了解一些情况:

无盘复制是Redis在2.8.18版本中引入的一项功能。很少有人实施它,这可以归因于对生产部署中出现故障的固有恐惧。
通常情况下,当一个从属设备出现故障或主从设备之间出现网络故障时,主设备会尝试对从属设备的数据进行部分重新同步。从本质上讲,从属设备与主设备重新连接,复制工作逐步进行,拉动迄今为止积累的差异。
然而,当从属机构长时间断开连接,或重新启动,或成为一个全新的从属机构时,主站需要执行完全的重新同步。这是一个相当琐碎的概念,也就是把整个主站数据集转移到从站。从机冲刷旧的数据集,并从头同步新的数据。在成功同步后,连续的变化会像正常的Redis命令一样,以增量的方式流转,因为主数据集本身会因为客户端发送的写命令而被修改。
在完全重新同步期间需要进行批量传输时,问题就出现了。主站创建一个子进程来生成**Redis数据库备份(RDB)**文件(类似于SQL转储文件)。在子进程完成RDB文件的生成后,使用来自父进程的非阻塞I/O将该文件传输到从属进程。最后,当传输完成后,从属进程可以重新加载RDB文件并上线,接收新写入的增量流。
然而,为了执行完全的重新同步,主进程需要:1)将数据写入磁盘上的RDB 2)从磁盘上加载回RDB,将其发送给从属进程
如果设置不当,特别是使用非本地磁盘,或者因为内核参数调整不完善,磁盘压力会导致难以处理的延迟峰值,因此从机需要经常重启,所以不可能避免完全重新同步。因此就进入了无盘复制。
那么什么是无盘复制呢?它是将复制的数据流直接传输到套接字描述符的过程,而不是将其存储在磁盘中,并从磁盘中提供给从属实例。
同时服务于多个从属实例
最初,服务于多个从属实例是很棘手的,因为一旦RDB传输启动,传入的从属实例就必须等待当前的子进程完成对当前从属实例的写入,并转移到新传入的从属实例。
为了解决这个问题,redis.conf 文件包含一个名为repl-diskless-sync-delay 的参数。这个参数的接受值是秒。它设置了一个延迟,以允许传入的从站与主站的子进程同步,进行大规模的重新同步。这很重要,因为一旦传输开始,就不可能为到达的新副本提供服务,这些副本将被排在下一次RDB传输的队列中,所以服务器等待延迟,让更多的副本到达。延迟的单位是秒,默认是5秒。
为了便于操作,重新设计了I/O代码,以便同时为众多的文件描述符提供服务。Antirez设计了算法来解决这个问题。此外,为了使数据传输并行化,即使是在使用阻塞式I/O的情况下,代码将尝试在一个循环中向每个套接字描述符写入少量数据,这样内核就会向多个从机并发地发送数据包:
while(len) {
size_t count = len < 1024 ? len : 1024;
int broken = 0;
for (j = 0; j < r->io.fdset.numfds; j++) {
… error checking removed …
/* Make sure to write 'count' bytes to the socket regardless
* of short writes. */
size_t nwritten = 0;
while(nwritten != count) {
retval = write(r->io.fdset.fds[j],p+nwritten,count-nwritten);
if (retval <= 0) {
… error checkign removed …
}
nwritten += retval;
}
}
p += count;
len -= count;
r->io.fdset.pos += count;
… more error checking removed …
}
处理部分故障
写入文件描述符并不仅仅是这个问题的唯一层面。它的一大块在于实际处理一堆从机,而实际上不需要为其他进入的从机阻塞进程。
然而,当RDB被终止时,子进程需要对已经收到RDB并能继续进行复制流过程的从机进行反馈。子进程返回一个从机ID及其相关错误状态的数组,从而使父进程能够记录从机的错误状态。
注意事项
无盘复制的明显问题是,对磁盘的写入与对套接字的写入不同:
- API是不同的,因为Redis数据库备份代码传统上是写到C文件指针,而我们的情况要求写到套接字,这基本上是写到套接字描述符。
- 磁盘写入主要不倾向于失败,如果不是超硬的I/O错误(如果磁盘已满等等)。但对于套接字来说,这完全是一个不同的游戏,因为写入可能会被延迟,因为接收器可能会变慢,本地内核缓冲区可能会满。
- 超时的问题在套接字的领域里不断扩大。如果接收端由于故障而无法接收数据包,或者只是TCP连接已死,那该怎么办。
根据Redis的作者Salvatore Sanfilippo(又名antirez)的说法,在他面前有两个选择来缓解这个问题:
- 在内存中生成RDB文件,然后进行传输。
- 在生成RDB的过程中,直接并递增地写入套接字。
方式1的风险较大,因为它有过多的内存消耗的开销。该功能必须针对有慢速磁盘的环境,但有更快的网络和更高的带宽,而不消耗太多的内存。因此,方法2被选中。
塑造Redis的复制环境
没有持久性的复制的想法肯定是压倒性的,令人望而生畏,但Redis就是做到了。在非磁盘复制中支持复制,消除了不受欢迎的存储移动部件,而且我们都知道磁盘I/O是缓慢和迟缓的事实。在我们的Kubernetes生态系统中实施这一点,在I/O和缓存指标方面有了明显的改善,使我们的Redis部署变得更加精简。