16 异步机制:如何避免单线程模型的阻塞?
16.1 Redis实例有哪些阻塞点?
16.1.1. 客户端交互
因为Redis使用IO多路复用机制,避免主线程一直处在等待网络连接或请求到来的状态,所以网络IO不是导致Redis阻塞的因素。
1.1 第一个阻塞点
集合元素全量查询操作 HGETALL、SMEMBERS,以及集合的聚合统计操作,例如求交、并和差集。
1.2 第二个阻塞点
bigkey 删除操作。
1.3 第三个阻塞点
flushdb flushall。
16.1.2. 磁盘交互
生成RDB快照文件,执行AOF日志重写操作由子进程负责执行,慢速的磁盘IO不会阻碍主线程;但是Redis直接记录AOF日志,会根据不同的写回策略对数据做落盘保存。 如果有大量的写操作需要记录在AOF日志中,并同步写回的话,就会阻塞主线程了。
16.1.3. 主从集群交互
主库在复制的过程中,创建和传输RDB由子线程完成不会阻塞主线程。但是对于从库来说,它在接受了RDB文件后,需要使用flushdb命令清空当前数据库以及把RDB加载到内存也会成为阻塞点。
16.1.4. 切片集群交互
迁移bigkey
16.1.5. 总结阻塞点
- 集合全量查询和聚合操作
- bigkey删除
- 清空数据库
- aof日志同步写
- 从库加载rdb文件
16.2 哪些阻塞点可以异步执行?
除了集合全量查询和聚合操作以及从库加载rdb文件其他三个阻塞点都不在关键路径上可以使用Redis的异步子线程来实现。
- 当集合类型有大量元素建议使用unlink命令
- 清空数据库可以使用 flushdb async | flushall aysnc
17 为什么CPU结构也会影响Redis的性能?
17.1 主流的cpu架构
一个cpu处理器一般有多个运行核心(物理核),每个物理核都可以有私有的一级缓存、二级缓存。每个物理核都会运行两个超线程。
在主流的服务器上,一个cpu处理器会有10到20多个物理核,服务器上通常还会有多个cpu处理器。不同处理器通过总线连接。
一个应用程序访问所在socket的本地内存和访问院端内存的延迟并不一致,我们把这个架构称为非统一内存访问架构(Non-Uniform Memory Access,NUMA架构)
17.2 CPU多核对Redis性能的影响
在cpu多核场景下,Redis实例被频繁调度到不同cpu核上运行的话,那么,对Redis实例的请求处理时间影响就更大了。 每调度一次,一些请求就会收到运行时信息、指令和数据重新加载的影响,导致某些请求的延迟高于其他请求。
使用taskset命令把一个程序绑定在一个核上运行。 taskset -c 0 ./redis-server
17.3 CPU的NUMA架构对Redis性能的影响
Redis实例和网络中断程序的数据交互
网络中断处理程序从网卡硬件中读取数据,并把数据写入操作系统内核维护的一块内存缓冲区。
内核会通过epoll机制触发事件,通知redis实例,redis实例再把数据从内核的内存缓冲区拷贝到自己的内存空间。
如果网络中断处理程序和redis实例各自绑定的cpu核不在同一个cpu socket,那么redis实例读取网络数据时,需要跨cpu socket访问内存,花费很多时间。
在 CPU 多核的场景下,用 taskset 命令把 Redis 实例和一个核绑定,可以减少 Redis 实例在不同核上被来回调度执行的开销,避免较高的尾延迟;在多 CPU 的 NUMA 架构下,如果你对网络中断程序做了绑核操作,建议你同时把 Redis 实例和网络中断程序绑在同一个 CPU Socket 的不同核上,这样可以避免 Redis 跨 Socket 访问内存中的网络数据的时间开销。
18 19 波动的响应延迟:如何应对变慢的Redis ?
18.1 打印120秒内监测到的最大延迟
18.2 影响Redis性能的三大要素
18.2.1 慢查询命令
- 用sccan多次迭代返回代替semembers命令,避免一次返回大量数据,造成数据阻塞。
- 执行排序、交集、并集操作可以在客户端完成,不适用sort、sunion、sinter命令拖累redis实例。
- 因为keys命令需要遍历存储键值对,操作延时高,一般不用于生产环境中
18.2.2 过期key操作
频繁使用带有相同的时间参数的expireate命令设置过期key,导致同一秒内有大量的key同时过期。可以在expireat和expire的过期时间参数上,加上一个一定大小范围的随机数。
18.2.3 文件系统
18.2.3.1 AOF
为了保证数据可靠性,Redis会采用AOF日志或RDB快照。
这三种写回策略依赖文件系统的两个系统调用完成:write和fsync。
- write只要把日志记录写到内核缓冲区就可以返回。
- fsync需要把日志记录写回到磁盘后才能返回。
- 当策略为everysec 和 always,redis需要调用fsync把日志写回磁盘。
使用everysec,运行一秒的丢失,redis使用后台的子线程异步完成fsync的操作;使用always策略来说redis需要确保每个操作记录日志都写回磁盘,不适用后台子线程来执行。
而且使用aof日志时,为了避免日志文件不断增大,redis会执行aof重写,生成体量缩小的新的aof日志文件,重写本身也需要时间,也容易阻塞redis主线程。所以redis使用子进程进行aof重写;当aof重写的压力比较大时,就会导致fsync被阻塞,虽然fsync由后台子线程负责执行的,主线程也会监控fsync的执行进度。
主线程使用后台子线程执行了一次fsync,需要再次把新接手的操作记录写回磁盘时,如果主线程发现上一次的fsync还没执行完,那么它就会阻塞。如果后台子线程频繁阻塞(aof重写占用了大量的IO带宽),主线程也会阻塞,导致redis性能变慢。
18.2.3.2 排查和解决建议
先确认业务方对数据可靠性的要求,明确是否需要每一秒都要记录日志。 如果业务应用对延迟非常敏感,同时允许一定量的数据丢失,可以把配置项no-appendfsync-on-rewrite设置为yes。 这个配置的意思是redis实例吧命令写到内存后,不调用后台线程进行fsync操作。如果实例发生宕机,就会导致数据丢失。 如果配置是no,在aof重写,redis实例仍然会调用后台线程进行fsync操作,就会给实例带来阻塞。
18.2.4 操作系统:swap
18.2.4.1 概念
操作系统的内存swap:内存swap是操作系统里将内存数据在内存和磁盘间来回换入和换出的机制,涉及到磁盘的读写。 swap触发后影响的是redis的主IO线程,极大地增加redis的响应时间。
触发swap主要原因:物理机器内存不足。
18.2.4.2 redis常见情况
- redis实例自身使用了大量的内存,导致物理机器的可用内存不足;
- 和redis实例在同一台机器上运行的其他进程,在进行大量的文件读写操作。文件读写占用系统内存,导致分配给redis的内存量变少进而触发swap。
常见的解决思路是增加机器的内存和使用redis集群。
18.2.4.3 命令
$ redis-cli info | grep process_id
process_id: 5332
cd /proc/5332
18.2.5 操作系统:内存大页
内存大页机制也会影响redis性能
redis为了提供数据可靠性保证,需要将数据做持久化保存由额外的线程执行。 redis主线程仍然可以接受客户端写请求。 客户端的写请求可能会修改正在进行持久化的数据。 过程中,redis采用写时复制机制。
如果采用内存大页,即使客户端请求只修改100B数据,redis也要拷贝2MB的大页。相反如果是常规内存页机制,只用拷贝4KB。 内存大页机制会导致大量的拷贝,影响redis的正常的访存操作,导致性能变慢。
18.2.5.1 解决思路
在 Redis 实例运行的机器上执行如下命令 cat /sys/kernel/mm/transparent_hugepage/enabled
- always : 表明内存大页机制被启动
- never:内存大页被禁止
18.3 总结
- 是否用了慢命令?替换
- 过期key设置了相同时间?
- 是否存在bigkey?使用scan迭代删除;集合查询和聚合使用scan命令在客户端操作
- 业务层如果需要高性能,运行数据丢失将配置项no-appendfsync-on-rewrite设置为yes,避免aof重写和fsync竞争磁盘IO资源导致redis延迟增加,否则可以使用高速固态盘作为aof日志写入盘
- 发生swap,内存是否使用过大?增加机器内存或者使用集群
- 关闭内存大页机制
- 如果使用主从集群,主库实例控制在2-4GB,避免主从复制从库因加载大的rdb文件而阻塞
- 是否使用了多核cpu或numa架构的机器运行redis实例?使用多核时。可以给redis实例绑定物理核;使用numa架构时,注意把redis实例和网络中断处理程序运行在同一个cpu socket上
20 删除数据后,为什么内存占用占用率还是很高?
当数据删除后,Redis释放的内存空间会由内存分配器管理,并不会立即返回给操作系统。Redis释放的内存空间可能并不是连续的,这些不连续的内存空间很可能处于一种闲置的状态。
20.1 内存碎片
应用申请的是一块连续地址空间的N字节,但在剩余的内存空间中,没有大小为N字节的连续空间了,剩余空间就是内存碎片。
20.1.1 内外因
内因:Redis可以使用libc、jemalloc、tcmalloc多种内存分配器来分配内存,默认使用jemalloc。其分配策略之一是按照一系列固定的大小划分内存空间。这样分配方式本身是为了减少分配次数。 外因:大小不一的键值对和键值对修改删除带来的内存空间变化。
20.1.2 使用info命令可以查询内存使用的详细信息。
INFO memory
Memory
- used_memory:1073741736 A redis为了保存数据实际申请使用的空间,包含了碎片
- used_memory_human:1024.00M
- used_memory_rss:1997159792 B操作系统实际分配给Redis的物理内存空间
- used_memory_rss_human:1.86G
- …
- mem_fragmentation_ratio:1.86 B / A
1 < mem_fragmentation < 1.5 是合理的,> 1.5 表明内存碎片率已经超过50%
20.1.3 如何进行碎片清理
通过设置参数,来控制碎片清理的开始和结束时机以及占用cpu比例。
config set activedefrag yes
两个参数设置了触发内存清理的一个条件
两个参数分别用于控制清理操作占用的cpu时间比例
20.1.4 风险 & 总结
碎片清理是有代价的,操作系统需要把多份数据拷贝到新位置,把原有空间释放出来,这会带来时间开销。
21 缓冲区:一个可能引发惨案的地方
21.1 缓冲区功能:
- 用一块内存空间来暂时存放命令数据,以免出现因为数据和命令的处理速度慢于发送速度而导致的数据丢失和性能问题。
- 用来暂存客户端发送的命令数据,或者是服务端返回给客户端的数据结果。
- 主从同步数据时,用来暂存主节点接收的写命令和数据。
21.2 客户端输入和输出缓冲区
21.2.1 输入缓冲区溢出
通过client命令查看qbuf表示输入缓冲区已经使用的大小,如果客户端输入缓冲区溢出,redis的处理办法就是把客户端连接关闭,结果就是业务程序无法进行数据存取。
多个客户端连接占用的内存总量超过了redis的maxmemory配置项就会触发redis进行数据淘汰。
redis服务端允许为每个客户端最多暂存1GB的命令和数据,所以只能从数据命令的发送和处理速度入手,避免客户端写入bigkey,以及避免redis主线程阻塞。
21.2.2 输出缓冲区溢出
redis为每个客户端设置的输出缓冲区
- 大小为16KB的固定缓冲空间,暂存ok响应和出错信息
- 动态增加的缓冲空间,暂存大小可变的响应结果
溢出情况
- 返回bigkey的大量结果,要避免
- 执行了monitor命令 (用来监测redis执行,输出的结果占用输出缓冲区)
- 缓冲区大小设置不合理,设置合理的缓冲区大小上限,或缓冲区连续写入时间和写入量上限。
给普通客户端设置缓冲区大小
client-output-buffer-limit normal 0 0 0 第一个0是这会缓存区大小限制,第二个0和第三个0分别表示缓冲区持续写入量限制和持续写入时间限制,0位不限制,这样的话发送方式不会被阻塞。
订阅客户端设置缓冲区大小
client-output-buffer-limit pubsub 8mb 2mb 60 pubsub 参数表示当前是对订阅客户端进行设置;8mb 表示输出缓冲区的大小上限为 8MB,一旦实际占用的缓冲区大小要超过 8MB,服务器端就会直接关闭客户端的连接;2mb 和 60 表示,如果连续 60 秒内对输出缓冲区的写入量超过 2MB 的话,服务器端也会关闭客户端连接。
21.3 主从集群中的缓冲区
21.3.1 复制缓冲区的溢出问题
主节点在向从节点传输RDB文件同时,会继续接收客户端发送的写命令请求保存在复制缓冲区中。
如果从节点接收和加载RDB比较慢,接收大量写命令最终会导致溢出。主节点也会直接关闭和从节点进行复制操作的连接,导致全量复制失败。
我们可以控制主节点保存的数据量大小,并设置合理的复制缓冲区大小。同时,我们需要控制从节点的数量,来避免主节点中复制缓冲区占用过多内存的问题
21.3.2 复制积压缓冲区的溢出问题
复制积压缓冲区repl_backlog_size是一个大小有限的环形缓冲区。复制积压缓冲区写满后,会覆盖缓冲区的旧命令数据。如果从节点还没有同步这些旧命令数据,就会造成主从节点重新开始执行全量复制。
21.4 总结
客户端通信 & 主从节点复制
客户端的输入和输出缓冲区 & 主从集群主节点上复制缓冲区和复制积压缓冲区