在分布式系统中为了解决单点问题(某个服务器程序只有一个节点(只搞一个物理服务器来部署这个服务器程序)。可用性不高:如果这个机器挂了意味着服务就中断了;性能 / 支持的并发量比较有限)。通常会把数据复制多个副本部署到其他服务器,满足故障恢复和负载均衡等需求。在分布式系统中,往往希望有多个服务器来部署 Redis 服务,从而构成一个 Redis 集群,此时就可以让这个集群给整个分布式系统中的其他服务提供更稳定、更高效的数据存储功能。存在以下几种 Redis 的部署方式:
在若干个 Redis 节点中,有的是 “主” 节点,有的是 “从” 节点。从节点上的数据要随着主节点变化,和主节点保持一致。从节点是主节点的副本,在该模式中,从节点上的数据不允许修改,只能读取数据。后续如果有客户端来读取数据,就可以从所有节点中随机挑选一个节点,给这个客户端提供读取数据的服务。
引入更多的计算资源,那么能够支撑的并发量也就大幅提高了。如果是挂掉了某个从节点是没有什么影响的,此时继续从主节点(如果是主节点挂掉了,那还是有一定影响的,从系欸但只能读数据,如果需要写数据就没得写了)或者其它的从节点读取数据,得到的效果是完全相同的。
主从模式主要是针对 “读操作” 进行并发量和可用性的提高,而写操作无论是可用性还是并发都是非常依赖主节点的,但主节点又不能设置多个。在实际业务场景中,读操作往往是比写操作更频繁的。
主从结构是分布式系统中比较经典的一种结构。不仅仅是 Redis 支持,MySQL 等其它的常用组件也是支持的。
主从复制的特点:
- Redis 通过复制功能实现主节点的多个副本。
- 主节点用来写,从节点用来读,这样做可以降低主节点的访问压力。
- 复制支持多种拓扑结构,可以在适当的场景选择合适的拓扑结构。
- 复制分为全量复制,部分复制和实时复制。
- 主从节点之间通过心跳机制保证主从节点通信正常和数据⼀致性。
Redis 为我们提供了复制的功能,实现了相同数据的多个 Redis 副本。复制功能是高可用 Redis 的基础,哨兵和集群都是在复制的基础上构建的。
可以在一个云服务器主机上运行多个 redis-server 进程,此处需要保证多个 redis-server 的端口是不相同的。例如:本来 redis-server 默认的端口是 6379,此时就不能让新启动的 redis-server 再继续使用 6379 了。
- 可以在启动程序时,通过命令行来指定端口号,--port 选项。
- 也可以直接在配置文件中来设定端口。(推荐,更方便)
将 redis.conf 配置文件复制两份:slave1.conf 和 slave2.conf(准备一个主节点和两个从节点)。
修改 slave1.conf:
修改端口号:
按照后台进程的方式来运行:
修改 slave2.conf:
修改端口号:
按照后台进程的方式来运行:
注意:修改配置主要是修改从机的配置,主机配置不变。
接下来,通过命令行启动上述两个 Redis 实例作为从 Redis,并且通过 ps aux | grep redis 确保两个 Redis 均已正确启动:
当前这几个节点并没有构成主从结构,而是各自为政。要想成为主从结构,还需要进一步的进行配置。
通过 redis-cli 可以连接主 Redis 实例,通过 redis-cli -p 6380 连接从 Redis,并且观察复制关系。
参与复制的 Redis 实例划分为主节点(master)和从节点(slave)。每个从节点只能有⼀个主节点,而一个主节点可以同时具有多个从节点。复制的数据流是单向的,只能由主节点到从节点。配置复制的方式有以下三种:
此处以 6379 为主节点,6380 和 6381 为从节点。
在 slave1.conf 末尾添加:
在 slave2.conf 末尾添加:
Redis 服务器的配置文件修改之后需要重新启动才能生效:
上述通过 kill -9 进程的方式来停止 redis-server 是和我们之前通过直接运行 redis-server 命令的方式搭配使用的。而如果是使用 service redis-server start 这种方式启动的,就必须使用 service redis-server stop 来进行停止。(此时如果使用 kill -9 的方式停止,那么在 kill 掉之后,这个 redis-server 进程还能自动启动)。
服务器就是要稳定性和高可用,但是服务器上的某些程序也难以避免出现挂了的情况。如果服务进程挂了,要是能自动重启进程,对于整体的服务不会产生严重影响。
可以看到,相比之前多了一些进程,其中多出来的这些进程用来表示 Redis 从节点和主节点之间的 tcp 连接(从节点启动后就会和主节点建立上 tcp 连接),主节点就相当于服务器,从节点就相当于客户端。
从运行结果中看到复制已经工作了,针对主节点 6379 这边数据产生的任何修改,从节点都能立即感知到并同步,就是前面看到的那些 tcp 连接产生的效果。其次,从节点不能写入数据。
Redis 主从节点复制过程:
可以通过 info replication 命令查看复制相关状态:
主节点上会收到源源不断地 “修改数据” 请求,从节点就需要从主节点这里同步这些修改请求。从节点和主节点之间的数据同步不是瞬间完成的,上面的 offset 就相当于是从节点和主节点之间同步数据的进度,lag 表示延迟情况。
repl_backlog:挤压缓冲区,支持部分同步机制的实现。
slaveof 命令不但可以建立复制,还可以在从节点执行 slaveof no one 来断开与主节点复制关系。例如:在 6380 节点上执行 slaveof no one 来断开复制。
- 断开与主节点复制关系。
- 从节点晋升为主节点。
从节点断开复制后,它就不再从属于其它节点了,也并不会抛弃原有数据,后续主节点如果针对数据做出修改,从节点无法再自动获取主节点上的数据变化。
通过 slaveof 命令还可以实现切主操作,将当前从节点的数据源切换到另⼀个主节点。执行 slaveof {newMasterIp} {newMasterPort} 命令即可。
- 断开与旧主节点复制关系。
- 与新主节点建立复制关系。
- 删除从节点当前所有数据。
- 从新主节点行复制操作。
此时的 6381 只是看起来像是个主节点,但实际上并不是,它仍然是个从节点,只是作为 6380 同步数据的来源,自身仍然是不能修改数据的。
注意:前面是通过 salveof 修改了主从结构,这个修改时临时性的。如果重新启动 Redis 服务器,仍然会按照最初在配置文件中设置的内容来建立主从关系。
对于数据比较重要的节点,主节点会通过设置 requirepass 参数进行密码验证,这时所有的客户端访问必须使用 auth 命令实行校验。从节点与主节点的复制连接是通过⼀个特殊标识的客户端来完成,因此需要配置从节点的 masterauth 参数与主节点密码保持一致,这样从节点才可以正确地连接到主节点并发起复制流程。
默认情况下,从节点使用 slave-read-only=yes 配置为只读模式。由于复制只能从主节点到从节点,对于从节点的任何修改主节点都无法感知,修改从节点会造成主从数据不⼀致,所以建议线上不要修改从节点的只读模式。
主节点和从节点之间通过网络来传输(TCP),TCP 内部支持 nagle 算法(默认开启),开启后就会增加 tcp 的传输延迟,节省网络带宽;关闭就会减少 tcp 的传输延迟,增加网络带宽,从节点能够更快速的和主节点进行同步。其目的和 tcp 的捎带应答是一样的,针对小的 tcp 数据报进行合并,减少包的个数。主从节点一般部署在不同机器上,复制时的网络延迟就成为需要考虑的问题,Redis 提供了 repl-disable-tcp-nodelay 参数用来控制是否关闭 TCP_NODELAY(在主从同步通信过程中是否关闭 tcp 的 nagle 算法),默认为 no,即开启 tcp-nodelay 功能,说明如下:
Redis 的复制拓扑结构可以支持单层或多层复制关系,根据拓扑复杂性可以分为以下三种:一主一从、一主多从、树状主从结构。
一主一从结构是最简单的复制拓扑结构,用于主节点出现宕机时从节点提供故障转移支持,如图所示。
一主一从拓扑:
当应用写命令并发量较高且需要持久化时,此时也是会给主节点造成一些压力,那么可以只在从节点上开启 AOF,这样既可以保证数据安全性同时也避免了持久化对主节点的性能干扰。但需要注意的是,这种设定方式有一个严重的缺陷:当主节点关闭持久化功能时,如果主节点宕机要避免自动重启操作,否则就会丢失数据,进一步的主从同步,也会把从节点的数据也给删了。
改进办法:当主节点挂了之后,需要让主节点从从节点这里获取到 AOF 的文件,再重新启动。
在实际开发中,读请求往往远超过写请求,所以一般会选择一主多从结构。一主多从结构(星形结构)使得应用端可以利⽤多个从节点实现读写分离,如下图所示。
一主多从拓扑:
主节点上的数据发生改变时,就会把改变的数据同时同步给所有的从节点。对于读比重较大的场景,可以把读命令负载均衡到不同的从节点上来分担压力。同时一些耗时的读命令可以指定一台专门的从节点执行,避免破坏整体的稳定性。对于写并发量较高的场景,多个从节点会导致主节点写命令的多次发送从而加重主节点的负载。
树形主从结构(分层结构)使得从节点不但可以复制主节点数据,同时可以作为其他从节点的主节点继续向下层复制。通过引⼊复制中间层,可以有效降低住系欸按负载和需要传送给从节点的数据量,如下图所示。
树形拓扑:
数据写如节点 A 之后会同步给 B 和 C 节点,B 节点进一步把数据同步给 D 和 E 节点。当主节点需要挂载等多个从节点时为了避免对主节点的性能干扰,可以采用这种拓扑结构。此时,主节点就不需要很高的网卡带宽了,但一旦数据进行修改,同步的延时是比之前的结构更长的。
如下图所示,详细介绍建立复制的完整流程。从图中可以看出复制过程⼤致分为 6 个过程:
主从节点建立复制流程图:
开始配置主从同步关系之后,从节点只保存主节点的地址信息,此时建立复制流程还没有开始,在从节点 6380 执行 info replication 可以看到如下信息:
从统计信息可以看出,主节点的 ip 和 port 被保存下来,但是主节点的连接状态(master_link_status)是下线状态。
当定时任务发现存在新的主节点后,会尝试与主节点建立基于 TCP 的网络连接(三次握手,是为了验证(系统层面)通信双方是否能够正确读写数据)。如果从节点无法建立连接,定时任务会无限重试直到连接成功或者用户停止主从复制。
连接建立成功之后,从节点通过 ping 命令确认主节点在应用层上是否能够正常工作。如果 ping 命令的结果 pong 回复超时,从节点会断开 TCP 连接,等待定时任务下次重新建立连接。
如果主节点设置了 requirepass 参数,则需要密码验证,从节点通过配置 masterauth 参数来设置密码。如果验证失败,则从节点的复制将会停止。
对于首次建立复制的场景,主节点会把当前持有的所有数据全部发送给从节点,这步操作基本是耗时最长的,所以又划分称两种情况:全量同步和部分同步。
当从节点复制了主节点的所有数据之后,针对之后的修改命令,主节点会持续的把命令发送给从节点,从节点执行修改命令,保证主从数据的一致性。
PSYNC replicationid offset
主节点的复制 id。主节点重新启动或者从节点晋级成主节点都会生成⼀个 replicationid(同一个节点每次重启生成的 replicationid 都是不同的)。
从节点在和主节点建立复制关系之后,就会获取到主节点的 replicationid。
通过 info replication 即可看到 replicationid:
在一个 Redis 服务器上,replication id 和 run id 都是存在的,两个不同的 id 看起来非常像。
主节点 info replication:
主节点 info server:
二者非常相似,格式也很像。
从节点 info replication:
从节点 info server:
可以看出,每个节点的 run id 都不相同,而具有主从关系的节点的 replid 是相同的。
官方文档明确所里,此处使用的是 replicationid:
replid + offset 共同标识了一个数据集合。
下面这个结构体包含了 Redis 服务器的各自重要数据:
标识一次 Redis 的 “运行”:
runid 主要是用在支撑实现 Redis 哨兵这个功能的,和主从复制没有什么关系。
通过上图,我们可以看到每个节点需要记录两组 master_replid,这个设定解决的问题场景是这样的:
假设有一个主节点 A 和一个从节点 B,从节点 B 获取到 A 的 replid。如果 A 和 B 在通信过程中出现了一些网络抖动,那么 B 可能就会以为 A 挂了,B 自己就会成为主节点,于是 B 给自己生成一个 master_replid。此时 B 也会记得之前旧的 replid,也就是会使用 master_replid2 来保存之前 A 的 master_replid。
参与复制的主从节点都会维护自身复制偏移量(整数)。主节点(master)在处理完写入命令后,会把命令的字节长度做累加记录,统计信息在 info replication 中的 master_repl_offset 指标中。
从节点(slave)每秒钟上报自身的复制偏移量给主节点,因此主节点也会保存从节点的复制偏移量,统计指标如下:
从节点在接受到主节点发送的命令后,也会累加记录自身的偏移量。统计信息在 info replication 中的 slave_repl_offset 指标中:
复制偏移量的维护如图所示。通过对比主从节点的复制偏移量,可以判断主从节点数据是否一致。
replid + offset 共同标识了⼀个 “数据集”。如果两个节点,他们的 replid 和 offset 都相同,则这两个节点上持有的数据,就一定相同。
全量复制是 Redis 最早支持的复制方式,也是主从第一次建立复制时必须经历的阶段。全量复制的运行流程如下图所示。
- 首次和主节点进行数据同步。
- 主节点不方便进行部分复制的时候。
通过分析全量复制的所有流程,会发现全量复制是一件高成本的操作:主节点 bgsave 的时间,RDB 在网络传输的时间,从节点清空旧数据的时间,从节点加载 RDB 的时间等。所以一般应该尽可能避免对已经有大量数据集的 Redis 进行全量复制。
默认情况下,进行全量复制需要主节点生成 RDB 文件到主节点的磁盘中,再把磁盘上的 RDB 文件通过发送给从节点。
Redis 从 2.8.18 版本开始支持无磁盘复制。主节点在执行 RDB 生成流程时,不会生成 RDB 文件到磁盘中了,而是直接把生成的 RDB 数据通过网络发送给从节点,这样就节省了⼀系列的写硬盘和读硬盘的操作开销。
从节点之前也是先把收到的 RDB 数据写入到硬盘中,再进行加载。现在也可以省略这个过程,直接把收到的数据进行加载了。
但是即使引入了无硬盘模式,整个操作仍然是比较重量、耗时的,网络传输(大规模数据全量复制)的过程是没有办法省的。相比于网络传输来说,读写硬盘的开销是很小的。
从节点之前已经从主节点上复制过数据了。因为网络抖动或者从节点重启了,从节点需要重新从主节点这边同步数据,此时看看能不能只同步一小部分(大部分数据都是一致的)。
部分复制主要是 Redis 针对全量复制的过高开销做出的一种优化措施,使用 psync replicationId offset 命令实现。当从节点正在复制主节点时,如果出现网络闪断或者命令丢失等异常情况时,从节点会向主节点要求补发丢失的命令数据,如果主节点的复制积压缓冲区存在数据则直接发送给从节点,这样就可以保持主从节点复制的一致性。补发的这部分数据一般远远小于全量数据,所以开销很小。整体流程如下图所示。
部分复制过程:
复制积压缓冲区是保存在主节点上的一个固定长度的队列,默认大小为 1MB,当主节点有连接的从节点(slave)时被创建,这时主节点(master)响应写命令时,不但会把命令发送给从节点,还会写入复制积压缓冲区,如下图所示。
由于缓冲区本质上是先进先出的定长队列,所以能实现保存最近已复制数据的功能,用于部分复制和复制命令丢失的数据补救。复制缓冲区相关统计信息可以通过主节点的 info replication 中:
根据统计指标,可算出复制积压缓冲区内的可用偏移量范围:[repl_backlog_first_byte_offset, repl_backlog_first_byte_offset + repl_backlog_histlen]。
这个相当于一个基于数组实现的环形队列,上述区间中的值就是 “数组下标”。
如果当前从节点需要的数据已经超出了主节点的积压缓冲区的范围,则无法进行部分复制,只能全量复制了。
主从节点在建立复制连接后,主节点会把自己收到的修改操作,通过 tcp 长连接的方式,源源不断的传输给从节点,从节点就会根据这些请求来同时修改自身的数据,从而保持和主节点数据的一致性。在进行实时复制的时候需要保证连接处于可用状态,所以这样的长连接需要通过心跳包的方式来维护连接状态(这里的心跳是指应用层自己实现的心跳,而不是 TCP 自带的心跳)。
如果主节点发现从节点通信延迟超过 repl-timeout 配置的值(默认 60 秒,这些数据都是可以进行修改的),则判定从节点下线,断开复制客户端连接。从节点恢复连接后,心跳机制继续进行。
从节点和主节点之间断开连接有两种情况:
前面的 6379、6380、6381 这三个 redis server 用的是同一个 aof 文件(最开始创建从节点的配置的文件路径都一样,没有进行修改)。
从节点是通过手动启动的方式运行的,此时 root 用户下启动的 redis 服务器,于是生成的 aof 文件也就是 root 用户的文件。
通过 service redis-server start 启动的 redis 服务器是通过一个 redis 这样的用户来启动的(所属用户是 redis 用户),主要是担心通过 root 启动 redis 权限太高,一旦 redis 被黑客攻破,后果就比较严重了。所以,redis server 需要安装可读可写的方式打开这个 aof 文件,而这个文件对于 root 之外的用户只有读权限。
解决方案:将这三个 Redis 服务器生成的文件给区分开,最靠谱的是:直接把这三个 Redis 的工作目录给区分开(修改配置文件中的 dir 选项)。
停止之前的 Redis 服务器:
删除之前工作目录下已经生成的 aof 文件或者通过 chown 命令修改 aof 文件所属的用户:
给从节点创建出新的目录,作为从节点的工作目录:
并且修改从节点的配置文件,设定新的目录为工作目录:
启动 redis 服务器:
从节点有了自己的 rdb 文件和 aof 文件: