导读:本文介绍,在使用 Redis 过程中我们需要关注的两个主要方面:QPS、内存
在实际使用Redis过程中我们需要关注两点:
- QPS,即Redis每秒处理请求数。Redis作为单线程架构服务,如果发生阻塞将是致命的。
- 内存,Redis作为内存数据库,考虑到内存价格昂贵,如何充分合理的使用内存,是Redis使用者必须考虑的问题。本节,将就这两个问题做重点分析。
阻塞
我们知道,Redis是典型单线程架构。这种架构下,所有的读写命令操作都是由主线程完成。主线程的处理能力将决定着Redis整体的性能。那么,哪些因素会导致Redis阻塞呢?
Redis自身因素
Redis本身的架构设计会是导致阻塞的潜在原因。
1.Redis提供的操作命令很多是O(N)时间复杂度的,如果使用不当会导致单条命令执行时间过长而阻塞后续请求。
2. 持久化,持久化涉及写磁盘操作,如果频繁写AOF文件,或者频繁生成RDB到fork子进程也会导致主进程阻塞。
3. Redis是单线程的,所以只能使用一个CPU,如果CPU使用率过高,必然导致主进程阻塞。在小对象存取时候,内存速度和带宽看上去不是很重要,但是对大对象(> 10KB),它们就变得重要起来。图-1给出了2020年对于硬件执行速度。从图可以看出,当前从内存中读取连续1,000,000B的数据需要0.003ms,主存的一次访问耗时100ns,对于value比较多的复杂数据结构整体读取可能会耗时数百毫秒甚至更高。比如HGETALL
一个上百万feild
的hash
会导致Redis主进程阻塞数秒。
- 图-1 Latency Numbers Every Programmer Should Know
对于持久化,有如下三点会导致阻塞:
1. 生成RDB和AOF重写,需要fork
子进程。Redis主进程调用fork
系统调用产生子进程,由子进程负责完成持久化和重新工作,如果fork
本身过长,将会导致主进程阻塞。
2. AOF写磁盘,aof_buf
数据同步到磁盘磁盘上是由后台线程来完成的,由于涉及磁盘操作,当磁盘压力过大,后台线程在执行fsync
时,可能需要等待,直到写入完成。当主线程发现距离上次fsync
成功时间超过2s将会阻塞起来,直到fsync
完成。
3. 系统开启HugePage写操作导致阻塞,重写期间为了减小内存开销,会利用Linux系统支持的COW机制,只有在内存页有写入操作时才会复制该页,如果开启了HugePage每次复制的内存页将会从4kB变成2MB,放大了512倍,将会拖慢写入速度。
环境因素
1. CPU竞争,这部分需要从两个点来看,第一,Redis是CPU密集型应用,当和其他服务(尤其是多核CPU密集型应用)混布时会发生资源抢占情况,导致Redis吞吐下降;第二,如果是对Redis做了核绑定,正常情况下这种优化能够确保Redis独占一个CPU核,但当Redis进程fork子进程进行RDB生成或者AOF重写时,会和父进程共享该固定CPU核,导致父进程吞吐下降。
2. 内存交换,Redis高性能的一个决定性前提就是数据都在内存中,如果数据swap到磁盘中,将导致读写速度急剧下降。
3. 网络问题,网络问题是Redis阻塞原因的怀疑重点,主要有:连接拒绝、网络延迟、网卡软中断
内存
Redis最大的特性就是数据都在内存中,那么如何合理的规划这些数据就至关重要了。
Redis 内存消耗
Redis内存消耗主要包括:
1. 自身内存,极少,通常在3MB左右;
2. 对象内存,最大的一部分,存储着用户数据,包括key
和value
,可以简单理解为sizeof(key) sizeof(value)
;
3. 缓冲内存,包括,客户端缓冲区(所有接入到Redis服务器TCP连接的输入输出缓冲,输入缓冲区最大1G,输出可以通过client-output-buffer-limit
控制。主要包括:普通客户端、从客户端、订阅客户端)、复制积压缓冲区(Redis 2.8版本之后提供了一个固定大小的、用户复制功能的缓冲区,根据repl-backlog-size
控制,默认1MB)、AOF缓冲区(此缓冲区用于AOF重写期间写入命令);
4. 内存碎片
5. 子进程内存消耗,这部分主要是Redis在进行RDB生成和AOF重写期间fork
子进程消耗内存。其中,1~3之和是used_memory
,4是used_memory_rss-used_memory
Redis 内存回收
有两个场景会触发内存回收:
1. 过期数据删除
2. 惰性删除,当读取到带过期时间的数据,并且数据已经过期,这时会触发删除操作,并向客户端返回-2
;
3. 定时删除,Redis内部维护了一个定时任务,默认每秒运行10次。通过自适应算法来删除过期数据。
4. 内存使用量超过maxmemory
触发数据淘汰,支持如下删除策略:
noeviction
,默认策略,不删除任何数据;volation-lru
,对设置了过期时间的数据,通过模拟LRU算法进行删除,直到获取足够空间(线上使用策略);allkeys-lru
,对所有数据,通过模拟LRU算法进行删除,直到获取足够空间;allkeys-random
,对全体数据进行随机删除,直到获取足够空间;valatile-random
,对设置了过期时间的数据进行随机删除,直到获取足够空间;valatile-ttl
,根据数据TTL
属性,删除最近要过期的数据,如果没有则相当于noeviction
redisObject对象
Redis所有对象在内部都定义为redisObject
结构体,如代码-1。通过代码-1可知,一个redisObject
结构体需要占16B。Redis存储的所有数据类型(如string
、hash
、list
、set
、zset
等)都通过redisObject
来封装。结合前面数据结构小结,我们可知同一种数据结构至少有两种编码方式,不同的编码需要使用的存储空间是不同,如何合理地使用数据结构和编码将影响到存储空间的使用效率。
// redis-5.0.0 src/server.h 602
typedef struct redisObject {
unsigned type:4; // 对象类型 4bits
unsigned encoding:4; // 编码类型 4bits
unsigned lru:LRU_BITS; // LRU计时器 24bits
int refcount; // 引用计数 32bits
void *ptr; // 数据指针 64bits
} robj;
代码-1 redisObject 结构体
reference
Redis官网 Redis开发与运维 How Twitter Uses Redis To Scale - 105TB RAM, 39MM QPS, 10,000 Instances Latency Numbers Every Programmer Should Know