一、rehash会导致操作阻塞吗?
如下图,Redis的字典结构中包含了两个哈希表:
默认是往ht[0]写数据的,随着数据主键增多,Redis就会触发执行rehash操作了,主要步骤如下:
- 给ht[1]分配更大的空间;
- 将ht[0]的数据拷贝到ht[1];
- 释放ht[0]的空间。
如果直接拷贝数据,肯定是会花很长时间的,进一步会导致阻塞Redis。
为了避免这个问题,Redis中使用的是渐进式的rehash。
二、如何高效存储数据
为了节省存储空间,针对不同的数据类型,优先采用更加紧凑型的编码格式。通过特定条件,可以让Redis选择紧凑型的编码:
比如我们有一个很大集合的数据需要存储,如果数据有一定规律,并且键值大小都小于 hash-max-ziplist-value,并且可以划分成若干份,每份都可以保持在 hash-max-ziplist-entries以内,那么我们就可以分开来存储成多个Hash,这样就可以保证每个Hash都是用上ZIPLIST编码,极大的节省了空间。
但这是用时间换空间的做法:使用了压缩列表,也就意味着查询需要遍历ziplist,降低了查询的性能。在时间和空间之间,我们需要根据具体场景做出合理的抉择。
三、Redis中的性能杀手
Part III 部分,我们已经了解到,接收客户端网络执行命令的网络请求,网络开销对Redis的开销影响很小,那么在Redis中,究竟有哪些影响性能的点呢?这节,我们详细来探讨下。
不像线上性能分析,可以根据各种参数指标去挖掘定位性能点,为了摸底Redis的性能,那么就得从Redis的方法面面入手,我们将从以下各个维度去解析性能影响点。
3.1、内存数据操作
读操作
每当客户端请求数据的时候,都需要等待命令执行完,获取到执行的结果,所以执行命令的复杂度决定了性能的好坏,我们在实际操作中,要尽量使用复杂度低的命令,少用复杂度低的命令,控制一次读取的数据量。
以下是复杂度比较高的命令:
-
集合统计排序:
- SINTER 交集,复杂度O(N*M),其中 N 是最小集合的基数,M 是集合的数量;
- SUNION 并集,复杂度O(N),其中 N 是所有给定集合中元素的总数;
- SDIFF 差集,复杂度O(N) ,其中 N 是所有给定集合中元素的总数;
- SORT 排序,复杂度O(N+M*log(M)),其中 N 是列表或集合中要排序的元素数,M 是返回元素的数量。
-
大数据量查询:
- HGETALL 返回存储在字典中所有的键值对,时间复杂度O(N),其中 N 是字典的大小;
- SMEMBERS 返回集合中的所有成员key,时间复杂度O(N),其中N是设置的基数。
如果实在要执行此类操作,而数据量又比较大,建议将此类操作放到单独的从库中执行。
删除操作
如果我们删除了bigkey,那么就可能导致阻塞主线程。为什么呢?
因为删除操作除了会释放内存空间,还会把空闲空间插入操作系统的空闲内存块链表中,删除的key越大,那么就越耗时。
为了避免删除bigkey对主线程的阻塞,Redis 4.0开始新增了UNLINK命令实现惰性删除。删除bigkey,建议都使用UNLINK命令。
UNLINK命令是先释放掉字典和过期字典中的键值对引用,然后在符合以下任一条件的情况,决定是否需要放到lazyfree队列中进行异步删除:
- Hash、Set底层采用哈希,并且元素个数超过64;
- ZSET底层采用跳跃表,并且元素个数超过64;
- List节点数量超过64。
否则,还是会直接删除。
可见惰性删除也不一定会起效,所以为了杜绝此类性能问题,最好避免在Redis中存储bigkey。
另外,如果我们执行FLUSHALL或者FLUSHDB,也会阻塞线程。为了避免此种情况,可以通过向FLUSHALL/FLUSHDB添加async异步清理选项,redis在清理整个实例或db时会以异步运行。
3.2、磁盘写
AOF日志落盘
如果我们的AOF写回策略是Always同步写,那么每次写数据的过程中,都会因写磁盘而阻塞住了。
如果可以容忍一秒钟的数据丢失,那么,我们可以把AOF写回策略设置为Everysec,这样就会通过异步线程去落盘了,从而避免阻塞主线程。
如果我们使用了Always策略,那么就需要注意了,如果刚好Redis在执行AOF重新,会导致大量的磁盘IO,最终导致操作系统fsync被阻塞,最终导致主线程也被fsync调用阻塞住了。
为了进一步减小重写AOF被阻塞的风险,可以设置为AOF重写是,不进行fsync:
no-appendfsync-on-rewrite yes
3.3、主从同步
当我们要进行主从同步的时候,首先,主库会生成一份完整的RDB,传给从库,从库首先执行FLUSHDB清空原来数据库,然后从库在载入RDB文件,这个过程会导致从库被阻塞。
3.4、切片集群
在切片集群场景,如果刚好有big key需要迁移到其他节点,那么就会导致主线程阻塞,因为Redis Cluster是用的同步迁移。迁移过程中会同时阻塞源节点和目标节点。
而如果使用Codis进行集群,则可以利用其异步迁移的特性减少big key迁移对集群性能的影响。
四、NUMA陷阱
Redis的性能跟CPU也有关?没错。接下来看看NUMA陷阱对Redis性能的影响。
4.1、NUMA
NUMA(Non-Uniform Memory Access),非统一内存访问架构,为多处理器的电脑设计的内存架构,内存访问时间取决于内存相对于处理器的位置。
在NUMA下,处理器访问它自己的本地内存的速度比非本地内存(内存位于另一个处理器,或者是处理器之间共享的内存)快一些。
诞生背景
现代 CPU 的运行速度比它们使用的主内存快得多。在计算和数据处理的早期,CPU 通常比自己的内存运行得慢。
随着第一台超级计算机的出现,处理器和内存的性能线在 1960 年代出现了转折点。从那时起,CPU 越来越多地发现自己“数据匮乏”,不得不在等待数据从内存中到达时停止运行。1980 年代和 1990 年代的许多超级计算机设计专注于提供高速内存访问,而不是更快的处理器,使计算机能够以其他系统无法接近的速度处理大型数据集。
NUMA 尝试通过为每个处理器提供单独的内存来解决这个问题,避免在多个处理器尝试寻址同一内存。
NUMA架构解析
以下是NUMA架构图示:(图片来源:NUMA Deep Dive Part 2: System Architecture)
每个CPU有自己的物理核、L3缓存,以及连接的内存,每个物理核包括L1、L2缓存。不同处理器之间通过QPI总线连接。
每个CPU可以访问本地内存和系统中其他CPU控制的内存,由其他CPU管理的内存被视为远程内存,远程内存通过QPI访问。
在上图中,包含两个CPU,每个CPU包含4个核,每个CPU包含4个通道,每个通道最多3个DIMM,每个通道都填充了一个16 GB的内存,每个CPU用64GB内存,系统总共有128GB内存。
4.2、NUMA对REDIS性能有何影响?
读取远程内存导致的问题
在NUMA架构上,程序可以在不同的CPU上运行,假设一个程序现在CPU 0上面运行,并把数据保存到了CPU 0的内存中,然后继续在CPU 1上面运行,这个时候需要通过QPI访问远程内存,导致增加数据读取的时间,从而导致性能变差。
另外,每次切换CPU,就需要从L3缓存重新加载相关的指令和数据到L1、L2缓存中,如果L3缓存也找不到,则会从内存中加载,导致增加CPU的处理时间。
如何解决?
linux 操作系统提供了一个名为 NUMACTL 的函数,NUMACTL 提供了控制的能力:
- NUMA 调度策略,例如,我想在哪些内核上运行这些任务
- 内存放置策略,在哪里分配数据
为了解决这个问题,我们可以把我们的程序绑定到某一个CPU上面来调度执行,相关命令:
# 查看CPU信息
numactl --hardware
# 指定进程在某个CPU上运行
taskset -c 1 ./redis-server
numactl --cpubind=0 --membind=0 ./redis-server
当然,也可以通过Taskset,进行设置,更详细的相关操作说明,参考:NUMACTL, taskset and other tools for controlling NUMA accesses notes
如果我们的网络中断处理程序也做了绑核操作,建议把Redis和网络中断成本绑定到同一个CPU上。
切记:Redis是需要用到多线程能力的(RDB、AOF文件生成、惰性删除...),我们不能把Redis只绑定到某一个内核中,否则就失去了多线程的能力。
五、内存问题
5.1、内存不足,性能急剧下降
在内存不足的情况下,操作系统会启用swap机制,这个时候,Redis主线程会导致大量磁盘IO,极大的增加Redis响应时间,导致Redis性能急剧下降。
为此,我们除了要给Redis服务设置最大内存之外,还需要监控Redis服务器的内存使用情况,避免与其他内存需求大的应用一起运行。
如何监控SWAP情况
# 找到Redis的进程号,进入/proc目录,进行查看
cd /proc/进程号
cat smaps | egrep '^(Swap|Size)'
如果发现很多几百MB或者上GB的swap,就说明实例的内存压力很大,需要排查是否内存不足导致Redis性能变慢。
5.2、内存大页机制
在发起BGSAVE命令之后,Redis会fork出一个子进程用于执行生成RDB文件。fork的时候采用的是写时复制(Copy-on-write)技术。不会立刻复制所有的内存,只是复制了页表,保证了fork执行足够快。
如果此时正好开启了内存大页机制,即使客户端请求的数据只有几k,也会拷贝2MB的大页,最终导致大量的内存拷贝,从而影响Redis的访问速度。
为了避免这种情况,可以关闭内存大页机制:
[root@VM_0_14_centos ~]# echo never > /sys/kernel/mm/transparent_hugepage/enabled
[root@VM_0_14_centos ~]# echo never > /sys/kernel/mm/transparent_hugepage/defrag
[root@VM_0_14_centos ~]# cat /sys/kernel/mm/transparent_hugepage/defrag
[always] madvise never
[root@VM_0_14_centos ~]# cat /sys/kernel/mm/transparent_hugepage/enabled
[always] madvise never
# 验证是否生效
[root@VM_0_14_centos ~]# grep Huge /proc/meminfo
AnonHugePages: 18432 kB
HugePages_Total: 0
HugePages_Free: 0
HugePages_Rsvd: 0
HugePages_Surp: 0
Hugepagesize: 2048 kB
[root@VM_0_14_centos ~]# cat /proc/sys/vm/nr_hugepages
0
5.3、碎片
Redis并没有实现自己的内存池,也没有基于标准的系统内存分配器进行扩展。因此,系统内存分配器的性能和碎片率会对Redis产生一定的性能影响。向jemalloc这类的分配策略,是以一系列固定大小来进行内存空间的划分的,如8bit、16bit、32bit...这无形之中会导致一些分配的内存用不上,导致内存碎片的出现。
而内存碎片的出现,会导致内存空间利用率不高,导致无法分配新的空间。
如,在Redis中,SDS的空间预分配策略就很容易造成内存碎片。另外,频繁的删改操作、键值对大小不一致,也是造成内存碎片的原因之一。
为了监控Redis是否存在内存碎片,可以执行以下命令:
127.0.0.1:6379> INFO memory
...
mem_fragmentation_ratio:2.64
...
2.64,这个值大不大呢?
这个值的计算公式:
mem_fragmentation_ratio = used_memory_rss/ used_memory
used_memory_rss
:操作系统分配给Redis实例的内存大小,表示Redis进程占用的物理内存大小;used_memory
:Redis使用其分配器分配的内存大小;mem_fragmentation_ratio < 1
:表示Redis内存分配超出了物理内存,操作系统正在进行内存交换;mem_fragmentation_ratio > 1
:合理的指标值;mem_fragmentation_ratio > 1.5
:表示Redis消耗了实际需要物理内存的150%以上,其中50%是内存碎片率。
如果这个值超过1.5(50%)了,说明碎片化还是比较严重的,需要进行优化了。
内存碎片清理方法
在Redis 4.0之前,解决内存碎片问题的唯一方法就是重启Redis了,从Redis 4.0开始,提供了主动内存碎片整理的方式。
通过以下配置,来让Redis触发自动内存清理:
# 启用主动碎片整理
activedefrag yes
# 启动活动碎片整理的最小碎片浪费量
active-defrag-ignore-bytes 100mb
# 启动活动碎片整理的最小碎片百分比
active-defrag-threshold-lower 10
# 主动碎片整理所用CPU时间最小占比,保证能够正常展开清理
active-defrag-cycle-min 25
# 主动碎片整理所用CPU时间最大占比,超过这个值,就停止清理,避免因为清理导致阻塞Redis
active-defrag-cycle-max 75
您可以在不用重启Redis的情况下,通过以下方式启用碎片整理:
redis-cli config set activedefrag "yes"
标签:性能,Redis,碎片,详解,内存,CPU,NUMA
From: https://blog.51cto.com/u_14014612/6607608