redis灵魂拷问:为什么响应变慢了

kevinaoc
发布于 2022-4-9 23:08
浏览
0收藏

redis是一个内存的数据库,最大的特点就是访问性能快,但是也有很多时候,我们会遇到响应变慢的情况,今天我们就来聊一聊。

redis是一个单线程的模型,如果主线程阻塞了,肯定会造成响应变慢。下面我们先来看一看阻塞主线程的操作。

1.AOF重写和RDB快照

前面已经讲过了,redis在AOF重写时,主线程会fork出一个bgrewriteaof子进程,而主线程会fork出一个bgsave子进程。这2个操作表面上看不阻塞主线程,但fork子进程的这个过程是在主线程完成的。fork子进程时redis需要拷贝内存页表,如果redis实例很大,这个拷贝会耗费大量的CPU资源,阻塞主线程的时间也会变长。

我们再回顾一下fork bgrewriteaof子进程这张图:redis灵魂拷问:为什么响应变慢了-鸿蒙开发者社区

2.内存大页

redis默认支持内存大页是2MB,使用内存大页,一定程度上可以减少redis的内存分配次数,但是对数据持久化会有一定影响。

还是看上面那张图,在AOF重写和RDB快照过程中,不会阻塞主线程,这时候,主线程依然在接收新的写请求。这时就用到了CopyOnWrite。使用了内存大页,即使redis只修改其中一个大小是1kb的key,也需要拷贝1整页的数据,即2MB。在写入量较多时,大量拷贝就会导致redis性能下降。

在操作系统上执行下面命令,可以看到是否开启内存大页:

[root@master ~]# cat /sys/kernel/mm/transparent_hugepage/enabled
[always] madvise never

下面命令可以关闭内存大页,但是重启后会失效,怎么样永久禁用,可以参考网上方法。

echo never > /sys/kernel/mm/transparent_hugepage/enabled

注意:关闭内存大页是一个折中,对子进程的COW有优势,但是会增加redis的内存分配次数。

3.命令复杂度高

这个是非常常见的redis阻塞操作,比如一次查询的数据量太大,再比如对一个set或者list数据类型执行SORT操作,复杂度是O(N+M*log(M)),可以看下面官网描述:

官网:https://redis.io/commands/sort
Time complexity: O(N+M*log(M)) where N is the number of elements in the list or set to sort, and M the number of returned elements. When the elements are not sorted, complexity is currently O(N) as there is a copy step that will be avoided in next releases.

再比如我们对set数据类型执行SMEMBERS命令,复杂度是o(N),可以看下面官网描述:

官网:https://redis.io/commands/smembers
Time complexity: O(N) where N is the set cardinality.

对于复杂度高的命令,我们要慎重使用,一方面用复杂度低的命令替代,比如用SSCAN替代SMEMBERS。另一方面,对于排序/交集/并集等操作我们可以在客户端完成。

官网对每个命令都给出了复杂度说明,可以参考。地址如下:

https://redis.io/commands/sort

4.bigkey删除操作

对bigkey的删除需要释放大量的空间,空闲内存使用一个链表进行管理,释放的过程就是把需要释放的内存块加入这个链表中,如果释放的太多,加入链表的时间会增加,从而影响redis响应性能。

redis4.0以后引入了layfree机制,可以使用子进程异步删除,从而不影响主线程执行。不过lazyfree默认是关闭的,在redis.conf文件,如下:

# DEL, UNLINK and ASYNC option of FLUSHALL and FLUSHDB are user-controlled.
# It's up to the design of the application to understand when it is a good
# idea to use one or the other. However the Redis server sometimes has to
# delete keys or flush the whole database as a side effect of other operations.
# Specifically Redis deletes objects independently of a user call in the
# following scenarios:
#
# 1) On eviction, because of the maxmemory and maxmemory policy configurations,
#    in order to make room for new data, without going over the specified
#    memory limit. (内存达到maxmemory,需要使用淘汰策略释放内存)
# 2) Because of expire: when a key with an associated time to live (see the
#    EXPIRE command) must be deleted from memory. (key过期了需要删除,这个设置为yes可以防止大量key同时过期)
# 3) Because of a side effect of a command that stores data on a key that may
#    already exist. For example the RENAME command may delete the old key
#    content when it is replaced with another one. Similarly SUNIONSTORE
#    or SORT with STORE option may delete existing keys. The SET command
#    itself removes any old content of the specified key in order to replace
#    it with the specified string.(删除旧的key)
# 4) During replication, when a replica performs a full resynchronization with
#    its master, the content of the whole database is removed in order to
#    load the RDB file just transferred.(主从全量同步,从库需要清空数据加载RDB文件)
#
#(默认是使用阻塞方式删除的,但是把下面的配置改为yes,这样就能像使用UNLINK命令一样使用费阻塞方式来释放内存了)
# In all the above cases the default is to delete objects in a blocking way,
# like if DEL was called. However you can configure each case specifically
# in order to instead release memory in a non-blocking way like if UNLINK
# was called, using the following configuration directives.  

lazyfree-lazy-eviction no
lazyfree-lazy-expire no
lazyfree-lazy-server-del no
replica-lazy-flush no

# It is also possible, for the case when to replace the user code DEL calls
# with UNLINK calls is not easy, to modify the default behavior of the DEL
# command to act exactly like UNLINK, using the following configuration 
# directive:
#把用户所有的DEL操作都使用UNLINK替代,即使用layfree,这里不建议打开,因为不是bigkey的情况删除效率是非常高的
lazyfree-lazy-user-del no

我把注解一块贴了出来,加了一下自己的翻译。如果lazyfree-lazy-user-del不设置为yes,redis是否采用异步删除,还是要看删除的时间的,对于底层采用整数数组和压缩列表的情况,redis是不会采用异步删除的,还就就是String类型,所以尽量不要存String类型的bigkey。

5.从节点全量同步

从节点全量同步过程中,需要先清除内存中的数据,然后再加载RDB文件,这个过程中是阻塞的,如果有读请求到来,只能等到加载RDB文件完成后才能处理请求,所以响应会很慢。

另外,如果redis实例很大,也会造成RDB文件太大,从库加载时间长。所以尽量保持redis实例不要太大,如果超过4G,建议采用切片集群。

6.AOF同步写盘

之前在讲《redis灵魂拷问:聊一聊AOF日志重写》中,讲到appendfsync策略有3种。如果采用always,每个命令都会同步写盘,这个过程是阻塞的,等写盘成功后才能处理下一条命令。

所以是严格不能丢数据的场景,我们尽量还是采用everysec这种策略,当然如果对丢失数据不敏感,那也可以采用no。

7.内存达到maxmemory

上面也提到过,内存达到maxmemory,需要使用淘汰策略来淘汰key,可以采用lazyfree异步删除。但是选择key的过程是阻塞的,这时候就需要选择较快的淘汰策略,比如用随机淘汰来替换LRU和LFU算法淘汰。

这时候也可以扩大切片数量来减轻淘汰key的时间消耗。

阻塞主线程的操作会让redis响应变慢,我们尽量避免使用,当然还有好多可能阻塞主线程的操作,欢迎大家交流。下面我们看一下硬件资源对redis的影响。

(1)使用了swap

使用swap的原因是操作系统不能给redis分配足够大的内存,一旦开启了swap,内存数据就需要不停地跟swap换入和换出,对性能影响非常大。

开启swap的原因无非2个,一个是redis需要的内存太大,操作系统没有能力分配,另一个是机器上其他进程使用了大量的内存。

(2)网络问题

如果网卡负载很大,对redis性能影响会很大。这一方面有可能redis的访问量确实很高,另一方面也可能是有其他流量大的程序占用了带宽。

这个最好从运维层面进行监控。

(3)线程上下文切换

redis虽然是单线程的,但是在多核cpu的情况下,也会发生上下文切换。上下文切换的带来的影响是不能使用一级缓存和二级缓存。我们可以使用下面的命令把redis绑定到一个CPU核上面:

taskset -c 0,6 ./redis-server

这里0,6是同一个物理核的2个逻辑核。这里一定要注意,不能把redis实例绑定到一个逻辑核上面,redis虽然是单线程的,但是如果只用一个逻辑核,fork出的子进程操作会跟主线程争抢CPU资源。

(4)磁盘性能低

对于AOF同步写盘的使用场景,如果磁盘性能低,也会影响redis的响应。我们可以优先采用性能更好的SSD硬盘。

redis响应慢的原因有很多,有的是因为阻塞主线程操作引起的,有的是因为硬件资源问题,这跟内存、CPU、网络、磁盘都有一定关系。

本文主要从阻塞主线程的操作和硬件资源方面介绍了redis影响变慢的一些场景。对于redis响应变慢这个问题,原因还要很多。

本文转载自微信公众号「君哥聊技术」

原文链接:https://mp.weixin.qq.com/s/PYw0Nb5taeOj1Yd79J2izg。

分类
收藏
回复
举报
回复
    相关推荐