diff --git a/README.md b/README.md index 591e3cf422d48732b420be02e48012c3f0b14a5f..0ccba7dcad2a82b093e35b6e8aa7f1747d5093c5 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,13 @@ -# Java 进阶知识学习 +# 互联网 Java 工程师进阶知识完全扫盲 [![license](https://img.shields.io/badge/license-Attribution--NonCommercial%204.0%20-brightgreen.svg)](https://github.com/doocs/advanced-java/blob/master/LICENSE) [![original](https://img.shields.io/badge/original-%E4%B8%AD%E5%8D%8E%E7%9F%B3%E6%9D%89-blue.svg)](https://github.com/doocs/advanced-java) [![PRs Welcome](https://img.shields.io/badge/PRs-Welcome-brightgreen.svg)](http://makeapullrequest.com) +本系列知识出自中华石杉,石杉老师拥有 10 余年 BAT 一线大厂架构经验,简直神级人物。我对这部分知识做了一个系统的整理,方便学习查阅。 + +> 微信公众号:石杉的架构笔记(shishan100) + ## 分布式系统 ### 系统拆分 @@ -62,7 +66,7 @@ - [Redis 和 Memcached 有什么区别?Redis 的线程模型是什么?为什么单线程的 Redis 比多线程的 Memcached 效率要高得多?](/docs/high-concurrency/redis-single-thread-model.md) - [Redis 都有哪些数据类型?分别在哪些场景下使用比较合适?](/docs/high-concurrency/redis-data-types.md) - [Redis 的过期策略都有哪些?手写一下 LRU 代码实现?](/docs/high-concurrency/redis-expiration-policies-and-lru.md) -- [如何保证 Redis 高并发、高可用、持久化?Redis的主从复制原理能介绍一下么?Redis 的哨兵原理能介绍一下么?](/docs/high-concurrency/how-to-ensure-high-concurrency-and-high-availability-of-redis.md) +- [如何保证 Redis 高并发、高可用、持久化?Redis 的主从复制原理能介绍一下么?Redis 的哨兵原理能介绍一下么?](/docs/high-concurrency/how-to-ensure-high-concurrency-and-high-availability-of-redis.md) - Redis 的持久化有哪几种方式?不同的持久化机制都有什么优缺点?持久化机制具体底层是如何实现的? - Redis 集群模式的工作原理能说一下么?在集群模式下,Redis 的 key 是如何寻址的?分布式寻址都有哪些算法?了解一致性 hash 算法吗?如何动态增加和删除一个节点? - 了解什么是 Redis 的雪崩和穿透?Redis 崩溃之后会怎么样?系统该如何应对这种情况?如何处理 Redis 的穿透? diff --git a/docs/high-concurrency/how-to-ensure-high-concurrency-and-high-availability-of-redis.md b/docs/high-concurrency/how-to-ensure-high-concurrency-and-high-availability-of-redis.md index 62792f1b7bad4b67c605bb86bcaa0c505d3aaa61..b331a415d59df4ea42eea47501ed2a74bbd765f1 100644 --- a/docs/high-concurrency/how-to-ensure-high-concurrency-and-high-availability-of-redis.md +++ b/docs/high-concurrency/how-to-ensure-high-concurrency-and-high-availability-of-redis.md @@ -10,4 +10,5 @@ 如果你用 redis 缓存技术的话,肯定要考虑如何用 redis 来加多台机器,保证 redis 是高并发的,还有就是如何让 redis 保证自己不是挂掉以后就直接死掉了,即 redis 高可用。 由于此节内容较多,因此,会分为多个小节进行讲解。 -- [redis 主从架构](/docs/high-concurrency/redis-master-slave.md) \ No newline at end of file +- [redis 主从架构](/docs/high-concurrency/redis-master-slave.md) +- [redis 基于哨兵实现高可用](/docs/high-concurrency/redis-sentinel.md) \ No newline at end of file diff --git a/docs/high-concurrency/redis-data-types.md b/docs/high-concurrency/redis-data-types.md index 1bfbf75afa5299b47f7851d6f7c6de318e5b08d1..977af5cbef75d4eda309d103055a1225bf9357e5 100644 --- a/docs/high-concurrency/redis-data-types.md +++ b/docs/high-concurrency/redis-data-types.md @@ -64,7 +64,7 @@ rpop mylist ``` ### set -list 是无序集合,自动去重。 +set 是无序集合,自动去重。 直接基于 set 将系统里需要去重的数据扔进去,自动就给去重了,如果你需要对一些数据进行快速的全局去重,你当然也可以基于 jvm 内存里的 HashSet 进行去重,但是如果你的某个系统部署在多台机器上呢?得基于 redis 进行全局的 set 去重。 diff --git a/docs/high-concurrency/redis-master-slave.md b/docs/high-concurrency/redis-master-slave.md index 14ceff95e78aead75e90af66a70e749b10bd45a6..f10d184b4cae797eca89683faaee92ea295b4d18 100644 --- a/docs/high-concurrency/redis-master-slave.md +++ b/docs/high-concurrency/redis-master-slave.md @@ -1,4 +1,4 @@ -# redis master-slave 架构 +# Redis 主从架构 单机的 redis,能够承载的 QPS 大概就在上万到几万不等。对于缓存来说,一般都是用来支撑**读高并发**的。因此架构做成主从(master-slave)架构,一主多从,主负责写,并且将数据复制到其它的 slave 节点,从节点负责读。所有的**读请求全部走从节点**。这样也可以很轻松实现水平扩容,**支撑读高并发**。 @@ -7,11 +7,11 @@ redis replication -> 主从架构 -> 读写分离 -> 水平扩容支撑读高并发 ## redis replication 的核心机制 -- redis 采用异步方式复制数据到 slave 节点,不过 redis2.8 开始,slave node 会周期性地确认自己每次复制的数据量; -- 一个 master node 是可以配置多个slave node的; +- redis 采用**异步方式**复制数据到 slave 节点,不过 redis2.8 开始,slave node 会周期性地确认自己每次复制的数据量; +- 一个 master node 是可以配置多个 slave node 的; - slave node 也可以连接其他的 slave node; -- slave node 做复制的时候,是不会 block master node 的正常工作的 -- slave node在做复制的时候,也不会block对自己的查询操作,它会用旧的数据集来提供服务;但是复制完成的时候,需要删除旧数据集,加载新数据集,这个时候就会暂停对外服务了; +- slave node 做复制的时候,不会 block master node 的正常工作; +- slave node 在做复制的时候,也不会 block 对自己的查询操作,它会用旧的数据集来提供服务;但是复制完成的时候,需要删除旧数据集,加载新数据集,这个时候就会暂停对外服务了; - slave node 主要用来进行横向扩容,做读写分离,扩容的 slave node 可以提高读的吞吐量。 注意,如果采用了主从架构,那么建议必须**开启** master node 的**持久化**,不建议用 slave node 作为 master node 的数据热备,因为那样的话,如果你关掉 master 的持久化,可能在 master 宕机重启的时候数据是空的,然后可能一经过复制, slave node 的数据也丢了。 @@ -21,7 +21,7 @@ redis replication -> 主从架构 -> 读写分离 -> 水平扩容支撑读高并 ## redis 主从复制的核心原理 当启动一个 slave node 的时候,它会发送一个 `PSYNC` 命令给 master node。 -如果这是 slave node 初次连接到 master node,那么会触发一次 `full resynchronization` 全量复制。此时 master 会启动一个后台线程,开始生成一份 `RDB` 快照文件,同时还会将从客户端 client 新收到的所有写命令缓存在内存中。`RDB` 文件生成完毕后, master 会将这个 `RDB` 发送给 slave,slave 会先写入本地磁盘,然后再从本地磁盘加载到内存中,接着 master 会将内存中缓存的写命令发送到 slave,slave 也会同步这些数据。slave node 如果跟 master node 有网络故障,断开了连接,会自动重连,连接之后 master node 仅会复制给 slave 部分缺少的数据。 +如果这是 slave node 初次连接到 master node,那么会触发一次 `full resynchronization` 全量复制。此时 master 会启动一个后台线程,开始生成一份 `RDB` 快照文件,同时还会将从客户端 client 新收到的所有写命令缓存在内存中。`RDB` 文件生成完毕后, master 会将这个 `RDB` 发送给 slave,slave 会先**写入本地磁盘,然后再从本地磁盘加载到内存**中,接着 master 会将内存中缓存的写命令发送到 slave,slave 也会同步这些数据。slave node 如果跟 master node 有网络故障,断开了连接,会自动重连,连接之后 master node 仅会复制给 slave 部分缺少的数据。 ![redis-master-slave-replication](/img/redis-master-slave-replication.png) @@ -53,14 +53,14 @@ slave node 内部有个定时任务,每秒检查是否有新的 master node ### 全量复制 - master 执行 bgsave ,在本地生成一份 rdb 快照文件。 -- master node 将 rdb 快照文件发送给 slave node,如果 rdb 复制时间超过 60秒(repl-timeout),那么 slave node 就会认为复制失败,可以适当调节大这个参数(对于千兆网卡的机器,一般每秒传输100MB,6G文件,很可能超过 60s) +- master node 将 rdb 快照文件发送给 slave node,如果 rdb 复制时间超过 60秒(repl-timeout),那么 slave node 就会认为复制失败,可以适当调大这个参数(对于千兆网卡的机器,一般每秒传输 100MB,6G 文件,很可能超过 60s) - master node 在生成 rdb 时,会将所有新的写命令缓存在内存中,在 slave node 保存了 rdb 之后,再将新的写命令复制给 slave node。 -- 如果在复制期间,内存缓冲区持续消耗超过64MB,或者一次性超过256MB,那么停止复制,复制失败。 +- 如果在复制期间,内存缓冲区持续消耗超过 64MB,或者一次性超过 256MB,那么停止复制,复制失败。 ```bash client-output-buffer-limit slave 256MB 64MB 60 ``` -- slave node接收到 rdb 之后,清空自己的旧数据,然后重新加载 rdb 到自己的内存中,同时**基于旧的数据版本**对外提供服务。 -- 如果 slave node 开启了 AOF,那么会立即执行 BGREWRITEAOF,重写 AOF. +- slave node 接收到 rdb 之后,清空自己的旧数据,然后重新加载 rdb 到自己的内存中,同时**基于旧的数据版本**对外提供服务。 +- 如果 slave node 开启了 AOF,那么会立即执行 BGREWRITEAOF,重写 AOF。 ### 增量复制 - 如果全量复制过程中,master-slave 网络连接断掉,那么 slave 重新连接 master 时,会触发增量复制。 @@ -68,8 +68,7 @@ client-output-buffer-limit slave 256MB 64MB 60 - msater就是根据 slave 发送的 psync 中的 offset 来从 backlog 中获取数据的。 ### heartbeat - -主从节点互相都会发送heartbeat信息。 +主从节点互相都会发送 heartbeat 信息。 master 默认每隔 10秒 发送一次 heartbeat,slave node 每隔 1秒 发送一个 heartbeat。 @@ -77,7 +76,7 @@ master 默认每隔 10秒 发送一次 heartbeat,slave node 每隔 1秒 发送 master 每次接收到写命令之后,先在内部写入数据,然后异步发送给 slave node。 ## redis 如何才能做到高可用 -如果系统在 365天内,有 99.99% 的时间,都是可以哗哗对外提供服务的,那么就说系统是高可用的。 +如果系统在 365 天内,有 99.99% 的时间,都是可以哗哗对外提供服务的,那么就说系统是高可用的。 一个 slave 挂掉了,是不会影响可用性的,还有其它的 slave 在提供相同数据下的相同的对外的查询服务。 diff --git a/docs/high-concurrency/redis-sentinel.md b/docs/high-concurrency/redis-sentinel.md new file mode 100644 index 0000000000000000000000000000000000000000..dd3d8ac0aae5a51b7df272515418a055d55a262a --- /dev/null +++ b/docs/high-concurrency/redis-sentinel.md @@ -0,0 +1,135 @@ +# Redis 哨兵集群实现高可用 + +## 哨兵的介绍 +sentinel,中文名是哨兵。哨兵是 redis 集群机构中非常重要的一个组件,主要有以下功能: +- 集群监控:负责监控 redis master 和 slave 进程是否正常工作。 +- 消息通知:如果某个 redis 实例有故障,那么哨兵负责发送消息作为报警通知给管理员。 +- 故障转移:如果 master node 挂掉了,会自动转移到 slave node 上。 +- 配置中心:如果故障转移发生了,通知 client 客户端新的 master 地址。 + +哨兵用于实现 redis 集群的高可用,本身也是分布式的,作为一个哨兵集群去运行,互相协同工作。 +- 故障转移时,判断一个 master node 是否宕机了,需要大部分的哨兵都同意才行,涉及到了分布式选举的问题。 +- 即使部分哨兵节点挂掉了,哨兵集群还是能正常工作的,因为如果一个作为高可用机制重要组成部分的故障转移系统本身是单点的,那就很坑爹了。 + +## 哨兵的核心知识 +- 哨兵至少需要 3 个实例,来保证自己的健壮性。 +- 哨兵 + redis 主从的部署架构,是**不保证数据零丢失**的,只能保证 redis 集群的高可用性。 +- 对于哨兵 + redis 主从这种复杂的部署架构,尽量在测试环境和生产环境,都进行充足的测试和演练。 + +哨兵集群必须部署 2 个以上节点,如果哨兵集群仅仅部署了 2 个哨兵实例,quorum = 1。 +``` ++----+ +----+ +| M1 |---------| R1 | +| S1 | | S2 | ++----+ +----+ +``` +配置 `quorum=1`,如果 master 宕机, s1 和 s2 中只要有 1 个哨兵认为 master 宕机了,就可以进行切换,同时 s1 和 s2 会选举出一个哨兵来执行故障转移。但是同时这个时候,需要 majority,也就是大多数哨兵都是运行的。 +``` +2 个哨兵,majority=2 +3 个哨兵,majority=2 +4 个哨兵,majority=2 +5 个哨兵,majority=3 +... +``` +如果此时仅仅是 M1 进程宕机了,哨兵 s1 正常运行,那么故障转移是 OK 的。但是如果是整个 M1 和 S1 运行的机器宕机了,那么哨兵只有 1 个,此时就没有 majority 来允许执行故障转移,虽然另外一台机器上还有一个 R1,但是故障转移不会执行。 + +经典的 3 节点哨兵集群是这样的: +``` + +----+ + | M1 | + | S1 | + +----+ + | ++----+ | +----+ +| R2 |----+----| R3 | +| S2 | | S3 | ++----+ +----+ +``` + +配置 `quorum=2`,如果 M1 所在机器宕机了,那么三个哨兵还剩下 2 个,S2 和 S3 可以一致认为 master 宕机了,然后选举出一个来执行故障转移,同时 3 个哨兵的 majority 是 2,所以还剩下的 2 个哨兵运行着,就可以允许执行故障转移。 + +## redis 哨兵主备切换的数据丢失问题 +### 两种情况和导致数据丢失 +主备切换的过程,可能会导致数据丢失: +- 异步复制导致的数据丢失 +因为 master->slave 的复制是异步的,所以可能有部分数据还没复制到 slave,master 就宕机了,此时这部分数据就丢失了。 + +![async-replication-data-lose-case](/img/async-replication-data-lose-case.png) + +- 脑裂导致的数据丢失 +脑裂,也就是说,某个 master 所在机器突然**脱离了正常的网络**,跟其他 slave 机器不能连接,但是实际上 master 还运行着。此时哨兵可能就会**认为** master 宕机了,然后开启选举,将其他 slave 切换成了 master。这个时候,集群里就会有两个 master ,也就是所谓的**脑裂**。 + +此时虽然某个 slave 被切换成了master,但是可能 client 还没来得及切换到新的 master,还继续向旧 master 写数据。因此旧 master 再次恢复的时候,会被作为一个 slave 挂到新的 master 上去,自己的数据会清空,重新从新的 master 复制数据。而新的 master 并没有后来 client 写入的数据,因此,这部分数据也就丢失了。 + +![redis-cluster-split-brain](/img/redis-cluster-split-brain.png) + +### 数据丢失问题的解决方案 +进行如下配置: +```bash +min-slaves-to-write 1 +min-slaves-max-lag 10 +``` +表示,要求至少有 1 个 slave,数据复制和同步的延迟不能超过 10 秒。 + +如果说一旦所有的 slave,数据复制和同步的延迟都超过了 10 秒钟,那么这个时候,master 就不会再接收任何请求了。 + +- 减少异步复制数据的丢失 +有了 `min-slaves-max-lag` 这个配置,就可以确保说,一旦 slave 复制数据和 ack 延时太长,就认为可能 master 宕机后损失的数据太多了,那么就拒绝写请求,这样可以把 master 宕机时由于部分数据未同步到 slave 导致的数据丢失降低的可控范围内。 + +- 减少脑裂的数据丢失 +如果一个 master 出现了脑裂,跟其他 slave 丢了连接,那么上面两个配置可以确保说,如果不能继续给指定数量的 slave 发送数据,而且 slave 超过 10 秒没有给自己 ack 消息,那么就直接拒绝客户端的写请求。因此在脑裂场景下,最多就丢失 10 秒的数据。 + +## sdown 和 odown 转换机制 +- sdown 是主观宕机,就一个哨兵如果自己觉得一个 master 宕机了,那么就是主观宕机 +- odown 是客观宕机,如果 quorum 数量的哨兵都觉得一个 master 宕机了,那么就是客观宕机 + +sdown 达成的条件很简单,如果一个哨兵 ping 一个 master,超过了 `is-master-down-after-milliseconds` 指定的毫秒数之后,就主观认为 master 宕机了;如果一个哨兵在指定时间内,收到了 quorum 数量的 其它哨兵也认为那个 master 是 sdown 的,那么就认为是 odown 了。 + +## 哨兵集群的自动发现机制 +哨兵互相之间的发现,是通过 redis 的 pub/sub 系统实现的,每个哨兵都会往`__sentinel__:hello`这个 channel 里发送一个消息,这时候所有其他哨兵都可以消费到这个消息,并感知到其他的哨兵的存在。 + +每隔两秒钟,每个哨兵都会往自己监控的某个 master+slaves 对应的`__sentinel__:hello` channel 里**发送一个消息**,内容是自己的 host、ip 和 runid 还有对这个 master 的监控配置。 + +每个哨兵也会去**监听**自己监控的每个 master+slaves 对应的`__sentinel__:hello` channel,然后去感知到同样在监听这个 master+slaves 的其他哨兵的存在。 + +每个哨兵还会跟其他哨兵交换对 `master` 的监控配置,互相进行监控配置的同步。 + +## slave 配置的自动纠正 +哨兵会负责自动纠正 slave 的一些配置,比如 slave 如果要成为潜在的 master 候选人,哨兵会确保 slave 复制现有 master 的数据; 如果 slave 连接到了一个错误的 master 上,比如故障转移之后,那么哨兵会确保它们连接到正确的 master 上。 + +## slave->master 选举算法 +如果一个 master 被认为 odown 了,而且 majority 数量的哨兵都允许主备切换,那么某个哨兵就会执行主备切换操作,此时首先要选举一个 slave 来,会考虑 slave 的一些信息: + +- 跟 master 断开连接的时长 +- slave 优先级 +- 复制 offset +- run id + +如果一个 slave 跟 master 断开连接的时间已经超过了`down-after-milliseconds`的 10 倍,外加 master 宕机的时长,那么 slave 就被认为不适合选举为 master。 +``` +(down-after-milliseconds * 10) + milliseconds_since_master_is_in_SDOWN_state +``` + +接下来会对 slave 进行排序: +- 按照 slave 优先级进行排序,slave priority 越低,优先级就越高。 +- 如果 slave priority 相同,那么看 replica offset,哪个 slave 复制了越多的数据,offset 越靠后,优先级就越高。 +- 如果上面两个条件都相同,那么选择一个 run id 比较小的那个 slave。 + +## quorum 和 majority +每次一个哨兵要做主备切换,首先需要 quorum 数量的哨兵认为 odown,然后选举出一个哨兵来做切换,这个哨兵还得得到 majority 哨兵的授权,才能正式执行切换。 + +如果 quorum < majority,比如 5 个哨兵,majority 就是 3,quorum 设置为2,那么就 3 个哨兵授权就可以执行切换。 + +但是如果 quorum >= majority,那么必须 quorum 数量的哨兵都授权,比如 5 个哨兵,quorum 是 5,那么必须 5 个哨兵都同意授权,才能执行切换。 + +## configuration epoch +哨兵会对一套 redis master+slaves 进行监控,有相应的监控的配置。 + +执行切换的那个哨兵,会从要切换到的新 master(salve->master)那里得到一个 configuration epoch,这就是一个 version 号,每次切换的 version 号都必须是唯一的。 + +如果第一个选举出的哨兵切换失败了,那么其他哨兵,会等待 failover-timeout 时间,然后接替继续执行切换,此时会重新获取一个新的 configuration epoch,作为新的 version 号。 + +## configuraiton 传播 +哨兵完成切换之后,会在自己本地更新生成最新的 master 配置,然后同步给其他的哨兵,就是通过之前说的 pub/sub 消息机制。 + +这里之前的 version 号就很重要了,因为各种消息都是通过一个 channel 去发布和监听的,所以一个哨兵完成一次新的切换之后,新的 master 配置是跟着新的 version 号的。其他的哨兵都是根据版本号的大小来更新自己的 master 配置的。 \ No newline at end of file diff --git a/img/async-replication-data-lose-case.png b/img/async-replication-data-lose-case.png new file mode 100644 index 0000000000000000000000000000000000000000..0e57d0f98bcb109268b0f003632cad065eacd269 Binary files /dev/null and b/img/async-replication-data-lose-case.png differ diff --git a/img/redis-cluster-split-brain.png b/img/redis-cluster-split-brain.png new file mode 100644 index 0000000000000000000000000000000000000000..b19d0db2e1d7756aea529f2605853a31453e4539 Binary files /dev/null and b/img/redis-cluster-split-brain.png differ