共计 7014 个字符,预计需要花费 18 分钟才能阅读完成。
之前的文章介绍了 Redis 的简略数据结构的相干应用和底层原理,这篇文章咱们就来聊一下 Redis 应该如何保障高可用。
数据长久化
咱们晓得尽管单机的 Redis 尽管性能非常的杰出,单机可能扛住 10w 的 QPS,这是得益于其基于内存的疾速读写操作,那如果某个工夫 Redis 忽然挂了怎么办?咱们须要一种 长久化 的机制,来保留内存中的数据,否则数据就会间接失落。
Redis 有两种形式来实现数据的长久化,别离是RDB(Redis Database)和AOF(Append Only File),你能够先简略的把 RDB 了解为某个时刻的 Redis 内存中的数据快照,而 AOF 则是所有记录了所有批改内存数据的指令的汇合(也就是 Redis 指令的汇合),而这两种形式都会生成相应的文件落地到磁盘上,实现数据的长久化,不便下次复原应用。
接下来就别离来聊聊这两种长久化计划。
RDB
在 redis 中生成 RDB 快照的形式有两种,一种是应用save
,另一种是bgsave
,然而底层实现上,其调用的是同一个函数,叫rdbsave
,只是其调用的形式不同而已。
生成办法
save
save 命令间接调用 rdbsave
办法,此时会 阻塞 Redis 主过程,直至快照文件生成。
void saveCommand(client *c) {if (server.rdb_child_pid != -1) {addReplyError(c,"Background save already in progress");
return;
}
rdbSaveInfo rsi, *rsiptr;
rsiptr = rdbPopulateSaveInfo(&rsi);
if (rdbSave(server.rdb_filename,rsiptr) == C_OK) {addReply(c,shared.ok);
} else {addReply(c,shared.err);
}
}
bgsave
bgsave 命令会 fork 出一个子过程,由 fork 进去的子过程调用rdbsave
。父过程会持续响应来自客户端的读写申请。子过程实现 RDB 文件生成之后会给父过程发送信号,告诉父过程保留实现。
/* BGSAVE [SCHEDULE] */
void bgsaveCommand(client *c) {
int schedule = 0;
/* The SCHEDULE option changes the behavior of BGSAVE when an AOF rewrite
* is in progress. Instead of returning an error a BGSAVE gets scheduled. */
if (c->argc > 1) {if (c->argc == 2 && !strcasecmp(c->argv[1]->ptr,"schedule")) {schedule = 1;} else {addReply(c,shared.syntaxerr);
return;
}
}
rdbSaveInfo rsi, *rsiptr;
rsiptr = rdbPopulateSaveInfo(&rsi);
if (server.rdb_child_pid != -1) {addReplyError(c,"Background save already in progress");
} else if (hasActiveChildProcess()) {if (schedule) {
server.rdb_bgsave_scheduled = 1;
addReplyStatus(c,"Background saving scheduled");
} else {
addReplyError(c,
"Another child process is active (AOF?): can't BGSAVE right now. ""Use BGSAVE SCHEDULE in order to schedule a BGSAVE whenever"
"possible.");
}
} else if (rdbSaveBackground(server.rdb_filename,rsiptr) == C_OK) {addReplyStatus(c,"Background saving started");
} else {addReply(c,shared.err);
}
}
这也就是为什么 Redis 是单线程的,但却可能在生成 RDB 文件的同时对外提供服务。fork
是 unix 零碎上创立过程的次要办法,会把父过程的所有数据拷贝到子过程中,父子过程共享内存空间。
fork 之后,操作系统内核会把父过程中的所有内存设置为只读,只有当产生写数据时,会产生页异常中断,内核会把对应的内存页拷贝一份,父子过程各持有一份,所以在生成 RDB 过程中,因为应用了 COW,内存脏页会逐步和子过程离开。
那么有没有可能在调用
bgsave
的过程中,我再调用save
命令呢,这个时候岂不是会生成两份 RDB 文件?
实际上在调用 save
命令时,Redis 会判断 bgsave
是否正在执行,如果正在执行服务器就不能再调用底层的 rdbsave
函数了,这样做能够防止两个命令之间呈现资源竞争的状况。
例如,在 save
命令中,有如下的判断:
if (server.rdb_child_pid != -1) {addReplyError(c,"Background save already in progress");
return;
}
而在 bgsave
中又有如下的判断:
if (server.rdb_child_pid != -1) {addReplyError(c,"Background save already in progress");
} else if (hasActiveChildProcess()) {...}
能够看到都是对同一个变量的判断,如下:
pid_t rdb_child_pid; /* PID of RDB saving child */
换句话说,在调用 save、bgsave 命令的时候,会提前去判断 bgsave
是否依然在运行当中,如果在运行当中,则不会继续执行 bgsave 命令。而 save 命令自身就是阻塞的,如果此时有其余的命令过去了都会被阻塞,直到 save 执行结束,才会去解决。
那我把 RDB 文件生成了之后怎么应用呢?
Redis 在启动服务器的时候会调用 rdbLoad
函数,会把生成的 RDB 文件给加载到内存中来,在载入的期间,每载入 1000 个键就会解决一次曾经达到的申请,然而只会解决 publish、subscribe、psubscribe、unsubscribe、punsubscribe 这个五个命令。其余的申请一律返回谬误,直到载入实现。
你吹的这么好,RDB 的优缺点别离是啥?
长处
RDB 策略能够灵便配置周期,取决于你想要什么样的备份策略。例如:
- 每小时生成一次最近 24 小时的数据
- 每天生成最近一周的数据
- 每天生成最近一个月的数据
基于这个策略,能够疾速的复原之前某个时间段的数据。
其次,RDB 十分的适宜做 冷备份 ,你能够把 RDB 文件存储后转移到其余的存储介质上。甚至能够做到 跨云存储,例如放到 OSS 上的同时,又放到 S3 上,跨云存储让数据备份更加的强壮。
而且,基于 RDB 模式的复原速度比 AOF 更快,因为 AOF 是一条一条的 Redis 指令,RDB 则是数据最终的模样。数据量大的话所有 AOF 指令全副 重放 要比 RDB 更慢。
毛病
RDB 作为一个 数据长久化 的计划是可行的,然而如果要通过 RDB 做到 Redis 的 高可用,RDB 就不那么适合了。
因为如果 Redis 此时还没有来得及将内存中的数据生成 RDB 文件,就先挂了,那么间隔上次胜利生成 RDB 文件时新增的这部分数据就会全副失落,而且无奈找回。
而且,如果内存的数据量很大的话,RDB 即便是通过 fork 子过程来做的,然而也须要占用到机器的CPU 资源,也可能会产生很多的也异常中断,也可能造成整个 Redis 进行响应几百毫秒。
AOF
下面提到过 RDB 不能满足 Redis 的高可用。因为在某些状况下,会永久性的失落一段时间内的数据,所以咱们来聊聊另一种解决方案 AOF。首先咱们得有个概念,那就是 RDB 是对以后 Redis Server 中的数据快照,而 AOF 是对变更指令的记录(所有的获取操作不会记录,因为对以后的 Redis 数据没有扭转)。
然而也正因为如此,AOF 文件要比 RDB 文件更大。上面聊一下一个 Redis 命令申请从客户端到 AOF 文件的过程。
AOF 记录过程
首先 Redis 的客户端和服务器之间须要通信,客户端发送的不是咱们写入的字符串,而是专门的 协定文本。如果你能够相熟 Thrift 或者 Protobuf 的话应该就能了解这个协定。
例如执行命令 SET KEY VALUE
,传到服务器就变成了"*3\\r\\n$3\\r\\nSET\\r\\n$3\\r\\nKEY\\r\\n$5\\r\\nVALUE\\r\\n"
。
而后 Redis 服务器就会依据协定文本的内容,抉择适当的 handler 进行解决。当客户端将指令发送到 Redis 服务器之后,只有命令胜利执行,就会将这个命令流传到 AOF 程序中。
留神,流传到 AOF 程序中之后不会马上写入磁盘,因为频繁的 IO 操作会带来微小的开销,会大大降低 Redis 的性能,协定文本会被写到 Redis 服务器中的 aof_buf 中去,也叫 AOF 的 写入缓冲区。
你这全副都写到缓冲区去了,啥时候落地?
每当 serverCron
(先有一个 定时工作 的概念,上面马上就会讲 serverCron 是啥)被执行的时候,flushAppendOnlyFile
这个函数就被调用。
这个命令会调用 write
将写入缓冲区的数据写入到 AOF 文件中,然而这个时候还是 没有 真正的 落到磁盘上 。这是 OS 为了进步写入文件的效率,会将数据临时写入到 OS 的内存的缓冲区内,等到缓冲区被填满了或超过了指定的工夫,才会调用fsync
或者 sdatasync
真正的将缓冲区的内容写入到磁盘中。
然而如果在这期间机器宕了,那么 数据依然会失落。所以如果想要真正的将 AOF 文件保留在磁盘上,必须要调用下面提到的两个函数才行。
ServerCron
作用
当初咱们就来具体聊一下 serverCron 函数,它次要是用于解决 Redis 中的 惯例工作。
什么叫惯例工作?
就比方下面提到的 AOF 写入缓冲区,每次 serverCron 执行的时候就会把缓冲区内的 AOF 写入文件(当然,OS 会写入本人的 buffer 中)。其余的就像 AOF 和 RDB 的长久化操作,主从同步和集群的相干操作,清理生效的客户端、过期键等等。
那这个 cron 距离多久执行一次?
很多博客是间接给出的论断,100ms
执行一次,口说无凭,咱们间接撸源码。上面是 serverCron 的函数定义。
/* This is our timer interrupt, called server.hz times per second.
* .............
*/
int serverCron(struct aeEventLoop *eventLoop, long long id, void *clientData) {
...
server.hz = server.config_hz;
}
为了防止影响大家的思路,我省略了临时对咱们没用的代码和正文。能够看到正文中有called server.hz times per second
。意思就是 serverCron 这个函数将会在每一秒中调用 server.hz 次,那这个 server.hz 又是啥?
server.hz
置信大家都晓得 HZ(赫兹)这个单位,它是频率的国内单位制单位,示意每一条周期性事件产生的次数。所以,咱们晓得这个配置项是用于管制周期性事件产生的频率的。
其赋值的中央在下面的函数中曾经给出,能够看到其初始值是来源于 redis.conf
的配置文件。那让咱们看一下具体的配置。
# Redis calls an internal function to perform many background tasks, like
# closing connections of clients in timeout, purging expired keys that are
# never requested, and so forth.
#
# Not all tasks are performed with the same frequency, but Redis checks for
# tasks to perform according to the specified "hz" value.
#
# By default "hz" is set to 10. Raising the value will use more CPU when
# Redis is idle, but at the same time will make Redis more responsive when
# there are many keys expiring at the same time, and timeouts may be
# handled with more precision.
#
# The range is between 1 and 500, however a value over 100 is usually not
# a good idea. Most users should use the default of 10 and raise this up to
# 100 only in environments where very low latency is required.
hz 10
简略的提取一下有用的信息,Redis 会在外部调用函数来执行很多后盾的工作,而调用这些函数的频率就由这个 hz
来决定的,其默认值为10
。那也就是说,下面提到的 serverCron 函数会在一秒钟执行 10 次,这样均匀下来就是每 100ms(1000ms/10)调用一次。
写入策略
下面说到,如果 Redis 的 AOF 曾经位于 OS 的缓冲中,如果此时宕机,那么 AOF 的数据同样会失落。
你这不行啊,那你这个长久化有什么意义?怎么样数据能力不失落?
这得聊一下 AOF 日志的写入策略,它有三种策略,别离如下:
- always 每个命令都会写入文件并且同步到磁盘
- everysec 每秒钟同步一次数据到磁盘
- no 不强制写,期待 OS 本人去决定什么时候写
很显著 always
这种策略在真正的生产环境上是不可取的,每个命令都去写文件,会造成极大的 IO 开销,会占用 Redis 服务器的很多资源,升高 Redis 的服务效率。
而如果应用 everysec
策略的话,即便产生了断电,机器宕机了,我最多也只会失落一秒钟的数据。
而 no
则齐全交与操作系统去调度,可能会失落较多的数据。
????????,那这 AOF 文件咋用的,怎么复原?
下面提到过,AOF 文件是记录了来自客户端的所有写命令,所以服务器只须要读入并重放一遍即可将 Redis 的状态复原。
然而,Redis 的命令只能在客户端中的上下文才可能执行,所以 Redis 搞了一个没有网络连接的伪客户端来执行命令,直到命令执行结束。
老铁,你这不行啊,万一 AOF 日志数据量很大,你这岂不是要复原很长时间,那服务岂不是不可用了?
确实,随着服务器的运行,AOF 的数据量会越来越大,重放所须要的工夫也会越来越多。所以 Redis 有一个 重写 (AOF Rewrite)机制,来实现对 AOF 文件的 瘦身。
尽管名字叫对 AOF 文件的瘦身,然而实际上要做的操作跟之前曾经生成的 AOF 文件没有一毛钱的关系。
所谓 瘦身 是通过读取 Redis 服务器以后的数据状态来实现的,当然,这里的以后是在服务器失常运行的时候。其实你也能够了解为快照,只不过不是实打实的二进制文件了,而是间接设置快照值的命令。
用人话举个例子,假如你 Redis 中有个键叫 test
,它的值的变动历史是 1 -> 3 -> 5 -> 7 -> 9 这样,那么如果是失常的 AOF 文件就会记录 5 条 Redis 指令。而 AOF Rewrite 此时染指,就只会记录一条test=9
这样的数据。
而之前的 AOF 文件还是照常的写入,当新的 AOF 文件生成后替换即可。
你 tm 在逗我?你在 rewrite 的同时,服务器依然在解决失常的申请,此时如果对服务器的状态做了更改,你这个瘦身之后的 AOF 文件数据不就不统一了?
这种状况确实会呈现,然而 Redis 通过一个 AOF 重写缓冲区 来解决了这个问题。
当 rewrite 开始后,Redis 会 fork 一个子过程,让子过程来实现 AOF 的瘦身操作,父过程则能够失常解决申请。AOF 重写缓冲区会在 rewrite 开始创立了子过程之后开始应用,此时 Redis 服务器会把写的指令同时发送到两个中央:
- aof_buf,也就是下面提到的 AOF 文件的写入缓冲区
- AOF 重写缓冲区
你可能会问,为啥要记录到两个中央?下面提到过,Redis 执行 瘦身 操作时,惯例的 AOF 文件依然是失常生成的,所以新的 Redis 指令肯定会发送到写入缓冲区。
而发送到 AOF 重写缓冲区是为了重放在 瘦身 操作进行当中对 Redis 状态进行的更改,这样 瘦身 之后的 AOF 文件状态能力保障与 Redis 的状态统一。总的来说,就是为了保障 瘦身 的 AOF 文件中的数据状态与 Redis 过后的内存状态保持数据上的一致性。
End
对于 Redis 数据长久化的问题,就先聊这么多,下一期的打算的应该就是聊一聊 Redis 的高可用的相干机制了,感兴趣的能够微信搜寻「SH 的全栈笔记」继续关注,公众号会比其余的平台先推送。
来日文章:
- Redis 根底 - 分析根底数据结构及其用法
- 浅谈 JVM 和垃圾回收
- 简略理解一下 K8S,并搭建本人的集群
- WebAssembly 齐全入门 - 理解 wasm 的前世今生
- 简略理解 InnoDB 底层原理