Redis归属
随着计算硬件的价格越来越便宜,人们对性能的要求越来越高,很多用户会根据自己的业务场景选择读写速度很快的内存数据库,呀比如大名鼎鼎的redis数据库,其属于kv型的NOSQL数据库。
这里小编可以简单介绍下数据库。数据库可以简单分为关系型数据库和非关系型数据库。
关系型数据库最典型的数据结构是表,由二维表及其之间的联系所组成的一个数据组织,常见的Oracle、Mysql、PostgreSQL、MariaDB、SQLite、SQL Server等,读写速度依赖于磁盘I/O。
非关系型数据库严格上不是一种数据库,应该是一种数据结构化存储方法的集合,可以是文档或者键值对等。一般可以再细分为:
- 文档型
- 举例:mongoDB、CouchDB
- 数据:key-value的键值对,value一般为结构化的数据(类似json),存储的内容是文档型的,有机会对某些字段进行索引
- key-vaue型
- 举例:redis、MemcacheDB
- 数据:通过hashtable实现的key-value的键值对,value的格式多样
- 补充:一般用于内容缓存,读写速度快
- 列式数据库
- 举例:HBase、Cassandra、Hypertable
- 数据:分布式的文件系统,以列簇式存储,将同一列的数据存在一起,方便做数据压缩
- 图型数据库
- 举例:Neo4J、FlockDB
- 数据:图结构,多用于构建知识图谱及最短路径寻址等,比较难做分布式集群方案
Redis分类
redis,可以简单分为主从版本和cluster集群版本。
单机版本:
-
一主多从:可以通过sentinel实现故障转移,或者其他的监控程序当发现主不可用,将从提升为主 -
可以支持读写分离:主负责写,将数据复制到从节点,从节点负责读
cluster集群版本:
- 采用的去中心化的设计思想,节点之间是fullmesh的连接,每个节点上保存了各自的数据和整个集群的状态。
Redis容量
生产上redis数据库可以容纳多少数据,小编去亚马逊的官网瞅了一眼。最大可以扩展到一个内存数据高达 310 TiB 的集群,或使用具有数据分层的集群时为 982 TiB。(参考以下链接)
https://aws.amazon.com/cn/elasticache/redis/
Redis核心问题
云上的redis数据库一般运行在容器中,会有适当的资源配额。在某些场景下,会出现内存满了,容器发生了OOM(out of memory)导致重启,如果该场景redis没有开启持久化配置,就会导致数据丢失。(那可太心痛了)
因此,了解redis容器的内存消耗非常必要~对于一个redis节点,上面运行的redis进程有哪些用到内存的地方呢
-
redis进程本身消耗的内存
- 当一个没有数据的redis节点,它的used_memory_rss(操作系统角度显示redis进程占用的物理内存总量)即表示进程本身消耗的内存,一般也就几MB左右,可以忽略不计
-
redis数据存储消耗的内存
- 相关的数据结构
- 实际存储的数据
- rehash的占用:当key的数据比较多的场景下,渐进实施rehash的过程中,会存储很多中间数据在内存中
-
redis缓存区的内存
-
客户端缓冲区
- 输入缓冲区
- client-query-buffer-limit用来控制客户端传递给redis的数据大小,默认为1G【注意:在使用Redis时应尽量避免超大Key】
- 如果有很多的multi/exec命令请求,也可以根据自己的需求提高这个设置项
- 输出缓冲区(client-output-buffer-limit用来控制输出缓冲区大小)
- 普通客户端(包含monitor clients)
- 用monitor监听redis的请求命令时,如果redis的qps比较高,对应的monitor无法快速接收这些数据,就会在客户端缓冲区形成积压,消耗redis内存
- redis7.0 rc1只支持限制单个普通客户端输入输出缓冲区的上限;听说redis7.0 rc1版本支持限制全部客户端的总内存,用这个config:maxmemory-client(后续详解)
- 从客户端
- 主节点会为每一个从节点单独建立一条连接用于命令复制
- 订阅客户端
- 可以配置输出缓冲区的大小
- 当订阅服务的消息生产大于消费速度,输出缓冲区就会积压,最后可能导致溢出
client-output-buffer-limit <class> <hard limit> <soft limit> <soft seconds>
当客户端的输出缓冲区的大小=hard limit则断开连接;当输出缓冲区的大小=soft limit持续soft seconds也断开连接
普通客户端 client-output-buffer-limit normal 0 0 0 无限制
slave客户端 client-output-buffer-limit slave 内存的1/20mb 内存的1/40mb 60
pub/sub客户端 client-output-buffer-limit pubsub 128mb 64mb 60
-
复制积压缓冲区
-
主从节点之间会进行数据同步,通过psync可以实现全量和部分复制。psync [runId] [offset]
- runid: 每个redis节点重启之后都会生成唯一的runid。节点重启之后,runid会发生变化
- offset:主节点和从节点都各自维护主从复制偏移量。主节点有写入命令的时候,offset=offset+命令的字节长度。从节点收到主节点发送的命令后,也增加offset,并把自己的offset反馈发给主节点。主节点会保存主从的offset,从来判断主从的数据是否一致
-
主节点执行写操作,会把命令发送给从节点,同时还会写入复制积压缓冲区。所有的从节点共享同一份复制积压缓冲区 -
该缓冲区的大小可以根据实际情况设置。计算缓冲空间=主写入命令的速度x命令大小-网络传输命令速度x命令大小。用每秒的缓冲空间x2(扩大一倍)x允许主从断连的时间 -
线上环境一般设置为内存的1/20 -
官方解释
- 单位大小支持b、k、kb、m、mb、g、gb,单位不区分大小写,其中k、m、g间的计算倍数是1000,而kb、mb和gb的计算倍数是1024
# Set the replication backlog size. The backlog is a buffer that accumulates
# replica data when replicas are disconnected for some time, so that when a replica
# wants to reconnect again, often a full resync is not needed, but a partial
# resync is enough, just passing the portion of data the replica missed while
# disconnected.
#
# The bigger the replication backlog, the longer the time the replica can be
# disconnected and later be able to perform a partial resynchronization.
#
# The backlog is only allocated once there is at least a replica connected.
#
# repl-backlog-size 1mb
-
AOF缓冲区
- 增加:服务器执行完一个命令之后,会将命令以协议的格式追加到AOF缓冲区的末尾
- 清零:服务器每次结束一个事件循环之前,都会调用flushAppendOnlyFile函数判断是否要将缓冲区的数据持久化到AOF文件,如果持久化改AOF缓冲区则清空
- 频率:
- appendfsync:always(每个事件循环都要将缓冲区的内容写入主AOF文件【更新内存页缓存】,并同步AOF文件【持久化到磁盘】,出现故障,丢失一个事件循环的数据)
- appendfsync:everysec(每个事件循环都要将缓冲区的内容写入AOF文件,每隔1s在子线程对AOF进行一次同步操作)
- appendfsync:no(每个时间循环都将缓冲区的内容写入AOF文件,什么时间持久化到硬盘看操作系统的频率)
# server.c/main
int main(int argc, char **argv) {
// ... ... 下面列出几个关键调用,其他代码省略了
//全局变量、配置的初始化
initServerConfig();
//运行时的初始化,包括EventLoop、db、初始化TimeEvent、FileEvent和Bio线程等
initServer();
//设置了两个回调函数,在相应的阶段来调用
aeSetBeforeSleepProc(server.el,beforeSleep);
aeSetAfterSleepProc(server.el,afterSleep);
//进入处理事件的无限循环,直到收到退出信号才会返回
aeMain(server.el);
aeDeleteEventLoop(server.el);
return 0;
}
# ae.c/aeMain
void aeMain(aeEventLoop *eventLoop) {
eventLoop->stop = 0;
//收到退出信号时会将stop设置为true
while (!eventLoop->stop) {
//main函数中设置的,意思就是每次调用aeProcessEvents之前调用
if (eventLoop->beforesleep != NULL)
eventLoop->beforesleep(eventLoop);
/*处理所有事件,并且需要调用 eventLoop->afterSleep
执行顺序:
- 处理网络IO事件
- 处理时间事件
*/
aeProcessEvents(eventLoop, AE_ALL_EVENTS|AE_CALL_AFTER_SLEEP);
}
}
-
redis内存碎片
-
概念:申请一块连续地址空间N字节时,剩余内存空间中没有大小为N字节的连续空间,这些剩余空间就是内存碎片 -
产生原因
- 内因:与内存分配器息息相关,内存分配器必须分配一块固定大小的连续内存空间。以jemalloc为例,是按照一系列固定的大小划分内存空间,例如8字节、16字节、32字节、…、2KB、4KB等。当程序申请的内存最接近某个固定值时,jemalloc就会给它分配相应大小的空间。
- 外因:
- 分配的空间比申请的空间大,会造成一部分的碎片
- 键值可能会被修改或者删除,会释放一部分空间
-
info 查看内存碎片值
- mem_fragmentation_ratio,表示Redis当前的内存碎片率;
- used_memory_rss,表示操作系统实际分配给Redis的物理内存空间,里面包含了碎片;
- used_memory,表示Redis为了保存数据实际申请使用的空间
-
什么时候应该清理
- mem_fragmentation_ratio 大于 1.5 。这表明内存碎片率已经超过了50%
-
如何清理
- 暴力重启
- 自动清理(搬迁数据,将不连续的内存空间变成连续的),可能会阻塞redis(可以通过参数设置来控制影响)
# The configuration parameters are able to fine tune the behavior of the
# defragmentation process. If you are not sure about what they mean it is
# a good idea to leave the defaults untouched.
# Enabled active defragmentation
# activedefrag yes
# Minimum amount of fragmentation waste to start active defrag
# 内存碎片超过100MB,开始清理
# active-defrag-ignore-bytes 100mb
# Minimum percentage of fragmentation to start active defrag
# 内存碎片空间占操作系统分配给redis的物理空间的比例达到10%,开始清理
# active-defrag-threshold-lower 10
# Maximum percentage of fragmentation at which we use maximum effort
# 内存碎片超过 100%,则尽最大努力整理
# active-defrag-threshold-upper 100
# Minimal effort for defrag in CPU percentage
# 清理使用的cpu最小为5%
# active-defrag-cycle-min 5
# Maximal effort for defrag in CPU percentage
# 清理使用的cpu超过75%,停止清理
# active-defrag-cycle-max 75
# Maximum number of set/hash/zset/list fields that will be processed from
# the main dictionary scan
# 将从主字典扫描中处理的最大 set/hash/zset/list 字段数
# active-defrag-max-scan-fields 1000
-
fork子进程
- 主从同步的过程中,主节点做了哪些操作
- 主服务器执行 BGSAVE 命令, 生成一个 RDB 文件, 并使用缓冲区储存起在 BGSAVE 命令之后执行的所有写命令。
- 在 RDB 文件创建完毕之后,主服务器会通过socket,将 RDB 文件传送给从服务器。
- 从服务器在接收完主服务器传送过来的 RDB 文件之后,就会载入这个 RDB 文件,从而获得主服务器在执行 BGSAVE 命令时的所有数据。
- 当从服务器完成 RDB 文件载入操作, 并开始上线接受命令请求时,主服务器就会把之前储存在缓存区里面的所有写命令发送给从服务器执行。
- BGSAVE 跟 SAVE 的不同之处就在于,BGSAVE 会先 fork 一个子进程,由子进程来生成 RDB文件(而不是由父进程生成 RDB 文件,因为生成 RDB 文件时进程是没有办法响应新进入的请求的)
- 子进程为什么可以访问父进程的内存呢?
- 早期 UNIX 是直接完整复制一份父进程的内存给子进程,但是明显效率太低而且造成浪费,所以 Linux 采用了 Copy-on-write 技术来处理这个问题,这个技术核心思想就是刚 fork 出来,父子进程各自持有自己的虚拟空间,但是对应的物理空间是同一个,此时把内存页的权限设置为 read-only,当两个进程中的某一个尝试写操作时,触发 page-fault,触发 kernel 中断,kernel 把触发异常的页复制一份,父子进程各自持有独立的一份页,各自读写
- 什么时候会占多大的内存呢?
- 时机:执行BGSAVE(生成RDB、主从全量复制)、AOF REWRITE
- 占用量:
- 取决于子进程在执行写时复制操作过程中,当时写操作的量
- linux的大页机制(Transport Huge Pages),linux内存管理默认为4k,开启THP会变成2M(加速Copy-On-Write消耗)
一点小彩蛋:
redis进程的内存分配有三种内存分配器可供选择,具体的内存分配代码见zmalloc.c。redis并没有自己实现内存池,使用不同的内存分配器,性能不同,存储数据导致碎片率也会有所差异。Redis5.0源码中的README表示:Linux系统默认采用jemalloc内存分配器,因为相比libc大部分场景内存碎片率会更低(但实际上也跟数据息息相关,有些数据libc的碎片率反而更低)。
- tcmalloc
- Google推出的,https://github.com/gperftools/gperftools
- libc
- 标准的内存分配库malloc和free
- jemalloc
- facebook推出的,https://github.com/jemalloc/jemalloc
近期编译redis源码的时候,发现不同的架构,jemalloc的配置是不一样的。你看这里:
Amd64:-with-lg-page=12 --with-lg-hugepage=21
Arm64: --with-lg-page=16 --with-lg-hugepage=21
https://github.com/docker-library/redis/blob/master/5/Dockerfile
gnuArch="$(dpkg-architecture --query DEB_BUILD_GNU_TYPE)"; \
extraJemallocConfigureFlags="--build=$gnuArch"; \
# https://salsa.debian.org/debian/jemalloc/-/blob/c0a88c37a551be7d12e4863435365c9a6a51525f/debian/rules#L8-23
dpkgArch="$(dpkg --print-architecture)"; \
case "${dpkgArch##*-}" in \
amd64 | i386 | x32) extraJemallocConfigureFlags="$extraJemallocConfigureFlags --with-lg-page=12" ;; \
*) extraJemallocConfigureFlags="$extraJemallocConfigureFlags --with-lg-page=16" ;; \
esac; \
extraJemallocConfigureFlags="$extraJemallocConfigureFlags --with-lg-hugepage=21"; \
grep -F 'cd jemalloc && ./configure ' /usr/src/redis/deps/Makefile; \
sed -ri 's!cd jemalloc && ./configure !&'"$extraJemallocConfigureFlags"' !' /usr/src/redis/deps/Makefile; \
grep -F "cd jemalloc && ./configure $extraJemallocConfigureFlags " /usr/src/redis/deps/Makefile; \
|