🐼为什么要有分布式系统
我们之前在分享分布式系统的演进架构的过程中,曾经分析了单点问题的弊端。包括但不亚于两个方面:
-
随着用户的增多,每一个用户的请求都会消耗掉一部分资源,包括:网络带宽,内存,CPU...而一台主机的资源是有限的,当主机资源殆尽,可能就宕机了
-
可用性问题,如果这台主机宕机了,那么整个服务就中断了
引入分布式系统,就是为了解决单点问题的。还记得百万服务器的演进之路嘛。通过读写分离,通过引入缓存机制redis,通过将redis部署到多个服务器上,从而构成一个集群。此时就可以让这个集群给整个分布式系统服务,这样更稳定/高效。
🐼Redis分布式系统的部署方式
1.主从复制
2.主从+哨兵
3.集群模式
我们本节先介绍1主从复制。Redis为我们提供了复制的功能,实现了相同数据的多个 Redis 副本。
🐼什么是主从复制
在Redis中,有的节点是主节点,有的节点是从节点。每一个节点的数据按理来说都是相同的,那么对于每一个客户端的请求,原本都是一个主节点来服务,我们就能平摊到多个从节点Redis服务器。
主从复制满足以下特点:
- 从节点得听主节点的(即从节点上的数据要跟随主节点发生变化,从节点得数据要和主节点的数据保持一致)
- 从节点是主节点的副本,后续,如果主节点上数据发生了修改,那么就要同步到从节点上。
- Redis上的主从模式 ,从节点得数据,不允许修改 ,只能读取数据!(当然可以修改配置文件让从节点也能写,但是就没法保证主节点和从节点数据的一致性了)
- 从节点只能有一个主节点,即一个父节点,主节点可以有多个从节点,即多个子节点。
所以,主从复制的本质,就是针对**"读"** 操作的,让读进行更高的并发量 &可用性的提高。
而写操作的话,在实际业务场景中,读操作比写操作往往更频繁,往往写1次,读100次。(从你日常使用软件的角度),写操作,更依赖主节点,而主节点又不能搞多个。如何做呢?后面后说的。
通过主从复制,提高了:
- 可用性问题。主从结构,多个redis基于主从结构的服务器不可能挂了,如果更高的可用性,可以将这些多个redis服务器放到多个机房(异地多活)。如果挂掉 某一个从 节点,那么没啥影响 ~如果挂掉主 节点,那么可能会有些影响,因为没法写了,但是还能读! 所以,总体来说,这样的设计大大提高了高可用性
- 高并发量。这个不必多说。本质引入了更多的计算机资源,即硬件资源,自然并发量也大大提高了。后续如果客户端读取数据,随便挑一个节点读就行了,每一个计算机的压力都不大~
最后说一嘴,主从结构,是分布式系统的一种典型结构,不仅仅是redis中有的,MySQL中也有哦,也支持!
🐼如何配置主从结构
配置主从结构,即需要多台主机(多个Reids服务器),一个做主节点,其余做从节点。按理来说,每一个Redis服务器,都需要一个单独的主机,才能称得上是分布式!
不过,贫穷限制了我的想象,这里就先以一台主机为例,在一台主机上也能配置多个Reids服务器,并且操作和在多个主机上操作其他原理和操作上是一致的。
此处如何配置:
由于是在同一台主机 ,首先,要保证你的每个redis服务器(网络进程) 的端口号不相同!本来你的redis服务器进程默认的端口号是6379,如果你再启动其他redis服务器,就不能是6379了,可以从6380,6381...往后延续了。如果你真的能在多台主机上配置,每个主机都仅有一个redis服务器,那么当我没说,那么你的每台主机都可以用6379端口号,因为ip地址不同。
如何去指定redis-server的端口号呢?
有三种方式:
- 在配置⽂件中加⼊ slaveof {masterHost} {masterPort} 随 Redis 启动生效。
- 在 redis-server 启动命令时加⼊ --slaveof {masterHost} {masterPort} 生效。
- 直接使用redis 命令:slaveof {masterHost} {masterPort} 生效。
不过这里推荐使用方式一,通过修改配置文件,为什么呢?因为其余两种方式,随着,redis服务器重启了,那么就失效了(本质是配置文件没有修改,你只是手动指定了,下次重启,依旧的加载的原始配置文件)。而通过修改配置文件 ,那么就是一劳永逸的。
我们这里以一个主节点两个从节点在同一台主机上为例:
主节点的配置文件不用动,端口号就是6379.
下面我们创建一个目录,从redis的配置文件复制两份从节点的配置文件出来。
bash
mkdir redis.conf
cp /etc/redis/redis.conf slave1.conf
cp /etc/redis/redis.conf slave2.conf

从节点得配置,我们只需要修改两个:
- 更改从节点的端口号,第一个为6380,第二个为6381为例

2.将daemonize的选项改为yes,按照后台进程的方式来进行(即变成守护进程)。

然后更改完配置文件不要忘记重新启动服务器,重启完就是永久生效的。后期就不需要我们手动修改了。
这里我们想手动开启我们指定的这两个配置文件的服务器,使用:
redis-server slave1.conf
redis-server slave2.conf
成功加载了以配置文件为slave1.conf,和slave2.conf配置文件的两个redis服务器。如果想杀掉这两个服务器,需要我们手动指定进程id,然后使用kill -9 停止这两个服务器。
这里要注意,如果我们使用 service redis-server start这样启动的进程,就必须要用service redis-server stop这样的方式停止,本质是为了AOF持久化的保存,以至于不会产生特别大的影响。
此时就已经配置了主从结构了吗?我们验证一下:
当我在127.0.0.1:6379上设置了key = 222, 但是在127.0.0.1:6380和127.0.0.1:6381上并没有读到,这是怎么回事?其实我们可以从建立连接的关系看出端倪,我们好像只启动了三个redis服务器,但是他们之间并没有建立连接
真正
⛺配置主从复制,这里有三种方式:
cpp
1. 在配置⽂件中加⼊ slaveof {masterHost} {masterPort} 随 Redis 启动⽣效。
2. 在 redis-server 启动命令时加⼊ --slaveof {masterHost} {masterPort} ⽣效。
3. 直接使⽤ redis 命令:slaveof {masterHost} {masterPort} ⽣效。
这里我们还是推荐以配置文件的形式进行配置,一劳永逸~
将slaveof 127.0.0.1 6379加入配置文件的末尾。

kill-9 停止刚刚两个进程后,再次加载配置文件重启redis服务器(6380, 6381)的
这次我们观察网络连接情况:

我们的所有的redis服务器都在一个主机上,发现果然有两条双向连接,至于端口号,我们知道,客户端的端口号是os随机分配的,因此我们就看到了上述端口号
这样,主节点就是服务器,从节点就相当于客户端了,当主节点这里数据发生了修改,就通过网络同步给从节点了。
下面,我们分别以redis-server6379服务器,启动客户端1,以redis-server6380服务器,启动客户端2,看看是否会有数据同步的效果。
确实数据同步了

并且我们发现,从节点不能够进行写!
我们可以使用info replication来了解每一个节点的具体属性。

比如,这里有两个字段offset和master_repl_offset,这里的master-repl_offsett - offset就是还没有同步的数据 ,如果**= 0** ,就代表全部已经同步完了,就比如我们这里,由于数据量小,所以同步是一瞬间完成的。其他字段可以参考redis官网
🐼主从复制的其他操作
⛺断开复制
slaveof 命令不但可以建⽴复制,还可以在从节点执行slaveof no one 来断开与主节点复制关系。例如在 6380 节点上执行slaveof no one 来断开复制。往后6379节点的修改,就不会同步给6380了,并且状态一直持续到只有6380用redis-server重启。但是这是暂时的,随着6380服务器的重启,也就恢复了默认了~

断开复制主要流程: 1)断开与主节点复制关系。 2)从节点晋升为主节点。
从节点断开复制后并不会抛弃原有数据,只是⽆法再获取主节点上的数据变化
**⛺**切主操作
通过 slaveof 命令还可以实现切主操作,将当前从节点的数据源切换到另⼀个主节点。执行slaveof {newMasterIp} {newMasterPort} 命令即可。
比如现在6380节点以6379为主节点,6381节点以6380为主节点,这样6379数据同步给6380,6380数据同步给6381.
切主操作主要流程: 1)断开与旧主节点复制关系。 2)与新主节点建⽴复制关系。 3)删除从节点当前所有数据。 4)从新主节点进⾏复制操作。

操作:

6380看起来好像是个主节点,仍然不是,还是个从节点 ,只是作为6381同步数据的来源,自身是仍然不能修改数据的。主从复制只能是主到从,不能是从-从,不然数据就乱了,主节点感知不到数据的变化。

默认情况下,从节点使⽤ slave-read-only=yes 配置为只读模式。由于复制只能从主节点到从节点,对于从节点的任何修改主节点都⽆法感知,修改从节点会造成主从数据不⼀致。所以建议线上不要修改从节点的只读模式。
⛺ 传输延迟
主从节点⼀般部署在不同机器上,复制时的网络延迟就成为需要考虑的问题,Redis 为我们提供了 repl-disable-tcp-nodelay 参数用于控制是否关闭 TCP_NODELAY,默认为 no,即开启 tcpnodelay 功能,说明如下:
• 当关闭时,主节点产⽣的命令数据⽆论⼤小都会及时地发送给从节点,这样主从之间延迟会变小,但增加了⽹络带宽的消耗。适⽤于主从之间的⽹络环境良好的场景,如同机房部署。这样场景一般比较吃低延迟的很重要,比如fps...
• 当开启时,主节点会合并较⼩的 TCP 数据包从⽽节省带宽。默认发送时间间隔取决于 Linux 的内
核,⼀般默认为 40 毫秒。这种配置节省了带宽但增⼤主从之间的延迟。适⽤于主从⽹络环境复杂
的场景,如跨机房部署,对延迟要求每那么高的可以考虑开启这个选项
🐼主从复制的拓扑结构
根据上面的示例。这样,通过info replication,我们就能大致画出来主节点和从节点的拓扑结构了,如果拓扑结构图越深,即树的高度越高,那么是不是同步效率越慢,反之。Redis 的复制拓扑结构可以支持单层或多层复制关系,根据拓扑复杂性可以分为以下三种:⼀主⼀从、⼀主多从、树状主从结构
🍓一主一从/一主多从
🔎思考一个问题,主节点是不是既要读数据请求,还得写数据请求,这样如果主节点的写请求过多,会给主节点造成压力,此时我们可以通过关闭主节点的AOF,只在从节点开启AOF来缓解主节点的压力。不过这里有个问题,就是主节点一旦挂了,那么不能自动重启,一旦自动重启,因为没有AOF文件,就会丢失数据,再一同步,那么从节点的数据就丢失了。
正确做法是,当主节点挂了,就让主节点从从节点这里获取到AOF文件,再启动
这样做法的优点是同步延迟小,同步速率快,因为树的结构只有一层,或者是扁平化结构。
不过如果仅仅采用一主一从/一主多从还有一个缺点,再实际开发中,由于读请求远远多与写请求,这样我们都需要更多的从节点,但是从节点一多,如果仅仅使用这种结构,那么每增加一个从节点,主节点就需要多同步一次,这样大大增加了主节点的负担 ,随着从节点的数量增加,同一条数据,就需要传输多次。如图:

🍓树状主从结构
树形主从结构(分层结构)使得从节点不但可以复制主节点数据,同时可以作为其他从节点的主节点继续向下层复制。通过引⼊复制中间层,可以有效降低住系欸按负载和需要传送给从节点的数据量,如图:
数据写⼊节点 A 之后会同步给 B 和 C 节点,B 节点进⼀步把数据同步给 D 和 E 节点。当主节点需要挂载等多个从节点时为了避免对主节点的性能⼲扰,可以采用这种拓扑结构
优点就是主节点的网卡和带宽不需要那么高的性能了,缺点就是一旦数据修改了,那么同步的延时是更长,因为树的高度增加了
🐼主从复制的原理
主从复制过程⼤致分为 6 个过程,如图:

1.保存主节点(master)的信息。比如主节点的IP和端口号。在从节点 6380 执行info replication 可以看到信息
2.使用套接字,比如connect等方法建立连接,是TCP的连接,目的就是为了验证双方都能够读写。
3.测试主节点是否能够正常工作。连接建立成功之后,从节点通过 ping 命令确认主节点在应⽤层上是⼯作良好的。如果 ping 命令的结果 pong 回复超时,从节点会断开 TCP 连接,等待定时任务下次重新建立连接。
4.redis主节点是否开启了密码 。如果主节点设置了 requirepass 参数,则需要密码验证,从节点通过配置 masterauth 参数来设置密码。如果验证失败,则从节点的复制将会停止
5.对于首次建⽴复制的场景,主节点会把当前持有的所有数据全部发送给从节点,这步操作基本是耗时最长的,所以⼜划分称两种情况:全量同步和部分同步下面会分享
6.当从节点复制了主节点的所有数据之后,针对之后的修改命令,主节点会持续的把命令实时发送给从节点,从节点执行修改命令,保证主从数据的⼀致性。
🐼replicationid和offset
介绍一下info replication查到的两个字段:
✅replicationid/replid (复制id)
主节点的复制 id. 主节点重新启动, 或者从节点晋级成主节点, 都会⽣成⼀个 replicationid. (同⼀个节点, 每次重启, ⽣成的 replicationid 也会变化).从节点在和主节点建⽴连接之后, 就会获取到主节点的 replicationid.所以replid就好像是主节点的**"身份证",来标识是哪个主节点**(后面会有多个主节点的情况)
我们可以看到,有两个replid,master_replid 和 master_replid2。他们有啥区别?第一个就是我们常用的replid,第二个呢?如图:

这个设定解决的问题场景是这样的: ⽐如当前有两个节点 A 和 B, A 为 master, B 为 slave.此时 B 就会记录 A 的 master_replid.
如果网络出现抖动, B 以为 A 挂了, B ⾃⼰就会成为主节点. 于是 B 给自已分配了新master_replid,自已成为了主节点. 此时就会使用master_replid2 来保存之前 A 的 master_replid.后续如果网络恢复了, B 就可以根据 master_replid2 找回之前的主节点.后续如果⽹络没有恢复, B 就按照新的 master_replid ⾃成⼀派, 继续处理后续的数据.所以replid2就好像一个"后悔药",如果需要,还可以重新回到原本主节点的怀抱!
✅offset偏移量
参与复制的主从节点都会维护自身复制偏移量,相当于是"同步的进度"。主节点(master)在处理完写⼊命令后,会把命令的字节⻓度做累加记录,统计信息在 info replication 中的master_repl_offset 指标中。

从节点(slave)每秒钟上报自身的复制偏移量给主节点,因此主节点也会保存从节点的复制偏移量。从节点在接受到主节点发送的命令后,也会累加记录⾃⾝的偏移量,统计信息在 info replication 中的 slave_repl_offset 指标中。

通过对⽐主从节点的复制偏移量,可以判断主从节点数据是否⼀致。即如果从节点和主节点的偏移量一样,那么就是"赶上直播了"。

总结一下,replicationid和offset共同描述了一个**"数据集合"** 。如果发现两个机器的replication和offset都一样,那么可以认为这两个机器的所有数据都是一样的!因为是从同一个master上同步同样大小的数据
🐼数据同步 psync
Redis 使用 psync 命令完成主从数据同步,同步过程分为:全量复制和部分复制。
PSYNC 的语法格式
cpp
PSYNC replicationid offset
psync命令不需要我们手动执行,在服务器首次建立好同步关系后,就会自动执行。
如果 replicationid 设为 ? 并且 offset 设为 -1 此时就是在尝试进行全量复制.
如果 replicationid offset 设为了具体的数值, 则是尝试进行部分复制
• 全量复制:⼀般⽤于初次复制场景,Redis 早期支持的复制功能只有全量复制,它会把主节点全部数据**⼀次性** 发送给从节点,当数据量较⼤时,会对主从节点和网络造成很大的开销 。正是由于全量复制的开销大,才有了部分复制**。首次建立数据同步时,是全量复制**。
• 部分复制:用于处理在主从复制中因网络闪断等原因造成的数据丢失场景,当从节点再次连上主节点后,如果条件允许,主节点会补发数据给从节点。因为补发的数据远小于全量数据,可以有效避免全量复制的过⾼开销。不是说指定部分复制,就一定部分复制,如果主节点判断数据不方便给,那么还是会全量复制。如图:

☑️全量复制
全量复制的实际已经说了:当主节点和从节点首次建立同步或者主节点不方便部分复制
全量复制是 Redis 最早⽀持的复制⽅式,也是主从第⼀次建⽴复制时必须经历的阶段。全量复制的运⾏流程如图所示:

1)从节点发送 psync 命令给主节点进⾏数据同步,由于是第⼀次进行复制,从节点没有主节点的运行 ID 和复制偏移量,所以发送 psync ? -1。
2)主节点根据命令,解析出要进行全量复制,回复 +FULLRESYNC 响应和一些主节点的信息。
3)从节点接收主节点的运行信息进行保存。
4)主节点执行 bgsave 进行 RDB 文件的持久化。为什么要这样?因为主节点的rdb文件可能和当前的版本有较大的差异,所以必须要重新生成一下。
5)从节点发送 RDB ⽂件给从节点,从节点保存 RDB 数据到本地硬盘。
6)主节点将从生成 RDB 到接收完成期间执行的写命令,写⼊缓冲区中,等从节点保存完 RDB ⽂件后,主节点再将缓冲区内的数据补发给从节点,补发的数据仍然按照 rdb 的⼆进制格式追加写⼊到收到的 rdb ⽂件中. 保持主从⼀致性。
7)从节点清空自身原有旧数据。
8)从节点加载 RDB 文件得到与主节点⼀致的数据。
9)如果从节点加载 RDB 完成之后,并且开启了 AOF 持久化功能,它会进行bgrewrite 操作,得到最近的 AOF ⽂件。
上述做法是不是可以简略一下,比如4主节点不生成RDB文件,直接通过网络传输,5从节点不先保存到硬盘,而是直接加载主节点发送来的数据到内存。这种模式我们称为无硬盘模式,diskless
节省了⼀系列的写硬盘和读硬盘的操作开销.不过尽管这样节省了读写硬盘的开销,但是网络传输时不可避免的,网络传输相比于少次的读写硬盘,是**"大头"** ,应该尽可能避免 对已经有大量数据集的 Redis 进行全量复制
☑️部分复制
啥时候进行部分复制?上面也说过,当网络抖动导致从节点需要重新同步数据,从节点会向主节点要求补发丢失的命令数据。当从节点的offset>0时,从节点向主节点进行同步复制时,尝试进行部分复制。部分复制的流程如图:

1)当主从节点之间出现网络中断时,如果超过 repl-timeout 时间,主节点会认为从节点故障并终端复制连接。
2)主从连接中断期间主节点依然响应命令,但这些复制命令都因⽹络中断⽆法及时发送给从节点,所以暂时将这些命令滞留在复制积压缓冲区中。
3)当主从节点⽹络恢复后,从节点再次连上主节点。
4)从节点将之前保存的 replicationId 和 复制偏移量作为psync的参数发送给主节点,请求进行部分复制。
5)主节点接到 psync 请求后,进行必要的验证。随后根据 offset 去复制积压缓冲区查找合适的数据,并响应 +CONTINUE 给从节点。
6)主节点将需要从节点同步的数据发送给从节点,最终完成⼀致性。
什么是复制积压缓冲区?
复制积压缓冲区是保存在主节点上的⼀个固定长度的队列,默认⼤小为 1MB,当主节点有连接的从节点(slave)时被创建,这时主节点(master)响应写命令时,不但会把命令发送给从节点,还会写⼊复制积压缓冲区
由于缓冲区本质上是先进先出的定长队列,所以能实现保存最近已复制数据的功能,⽤于部分复制和复制命令丢失的数据补救。复制缓冲区相关统计信息可以通过主节点的 info replication 中:
比如:
cpp
repl_backlog_active:1 // 开启复制缓冲区
repl_backlog_size:1048576 // 缓冲区最⼤⻓度
repl_backlog_first_byte_offset:7479 // 起始偏移量,计算当前缓冲区可⽤范围
repl_backlog_histlen:1048576 // 已保存数据的有效⻓度
如果当前从节点需要的数据, 已经超出了主节点的积压缓冲区的范围, 则无法进⾏部分复制, 只能全量复制了
✅实时复制
主从节点在建立复制连接后,主节点会把自已收到的 修改操作 , 通过 tcp ⻓连接的⽅式, 源源不断的传输给从节点. 从节点就会根据这些请求来同时修改⾃⾝的数据. 从⽽保持和主节点数据的⼀致性.
另外, 在进行实时复制的时候,需要保证连接处于可用状态,这样的长连接, 需要通过心跳包的方式来维护连接状态. (这⾥的心跳是指应用层自已实现的⼼跳,而不是 TCP自带的心跳).
1)主从节点彼此都有心跳检测机制,各自模拟成对方的客户端进行通信。
2)主节点默认每隔 10 秒对从节点发送 ping 命令,判断从节点的存活性和连接状态。
3)从节点默认每隔 1 秒向主节点发送 replconf ack {offset} 命令,给主节点上报⾃⾝当前的复制偏移量。
如果主节点发现从节点通信延迟超过 repl-timeout 配置的值(默认 60 秒),则判定从节点下线,断开复制客户端连接。从节点恢复连接后,心跳机制继续进行
🐼关于Redis服务器不能重启问题
我们在主从节点的过程中,会常常遇到一个问题,就是redis服务器打不开,比如最常见的一个问题:
当我们开启aof数据持久化时,当我们启动服务器,就要加载这个.aof文件。
我们如果直接使用service redis-server start来启动服务器,可能会启动失败,为什么呢?
我们来看一下.aof的权限

redis-server现需要按照可读可写 的方式来打开这个aof文件,而现在权限的只有root用户有可读可写操作,其他都只能读。
首先,思考一下,为什么.aof文件的创建权限是root的?是因为我们在启动从节点的方式,是通过root用户下启动的服务器:redis-server xxx.conf。于是生成的.aof就是root的。
而使用service redis-server start来启动服务器,是通过redis 这样的用户来启动服务器的,所属的用户是redis用户(为什么多设定了一个redis用户,主要是怕被redis被攻破,权限太高)
所以,方案一,我们将所属的用户改为redis即可,再次重启:
bash
将 /var/lib/redis/ 目录及其所有内容的所有者和组都设置为 redis
sudo chown -R redis:redis /var/lib/redis/
service redis-server start #(该命令以redis身份启动)

可是上述的方案有不合理性!
当前这三个节点(redis-server)共用的同一个.aof文件 。这就不太科学,这三个服务器的数据不一定是一样的!正确的做法是: 把三个redis服务器生成的持久化文件分隔开,最佳实践:把redis服务器所在的工作目录分开!
只需要更改配置文件的dir选项即可分开所在工作目录!
具体做法:
1.停止之前的redis服务器
2.删除之前的工作目录已经存在的.aof文件 / 也可以通过chown来更改.aof的拥有者所属组为redis(chown redis:redis xxx.aof)
3.给从节点创建出新的工作目录,并且修改从节点的配置文件(dir路径),设为新的目录为工作目录。


4.重启redis服务器

这里建议每个从节点的服务器都建立一个自已的目录,我这里的目录结构如图:

🐼主从复制的问题
主从复制,最大的问题是在主节点上,主节点挂了,从节点就迷茫了,虽然能够读 ,但是节点不能进行写 ,不能替换主节点原有的角色。
这就需要我们手动去维护,太麻烦了!并没有体现出高可用。