导航:首页 > 网络连接 > redis网络cpu连接监控

redis网络cpu连接监控

发布时间:2023-01-29 04:02:59

A. 追踪Redis Sentinel的CPU占有率长期接近100%的问题 二

在 《追踪Redis Sentinel的CPU占有率长期接近100%的问题》 一文中,通过结合Redis Sentinel的源码,发现由于出现了"Too many open files"问题,致使Sentinel的acceptTcpHandler事件处理函数会被频繁并快速调用,最终导致了CPU长期接近100%的现象。但对于为什么会出现“Too many open files”这个问题,本文将在上一篇的基础上,继续探讨和分析。

“Too many open files”这个错误其实很常见,想必大家早已对其有一定的了解,这里打算再简单的介绍一下。

很明显,“Too many open files”即说明打开的文件(包括socket)数量过多,已经超出了系统设定给某个进程最大的文件描述符的个数,超过后即无法继续打开新的文件,并且报这个错误。

首先,我们需要了解有关open files的基本知识。详细的概念大家可以谷歌,网上也有各种各样的解决办法,这里只对open files做简单的介绍和总结。

我们在linux上运行ulimit -a 后,出现:

如图open files的个数为1024,很明显这个值太小了(linux默认即为1024),当进程耗尽1024个文件或socket后,就会出现“Too many open files”错误。现实生产环境中这个值很容易达到,所以一般都会进行相应修改。

最简单的修改方式是ulimit -n 65535,但这样重启系统后又会恢复。永久生效的方法是修改/etc/security/limits.conf 文件,在最后加入:

修改完成后,重启系统或者运行sysctl -p使之生效。

soft 和hard,表示对进程所能打开的文件描述符个数限制,其概念为:

他们的区别就是软限制可以在程序的进程中自行改变(突破限制),而硬限制则不行(除非程序进程有root权限)。
上面的规则大家最好自己尝试一下,以增加印象。

重启后,运行ulimit -a,可以看到open files 的值改变为:

简单介绍完open files,我们再来了解下file-max。这个参数相信有很多人经常与ulimit中的open files混淆,他们的区别我们必须了解。

file-max 从字面意思就可以看出是文件的最大个数,运行cat /proc/sys/fs/file-max,可以看到:

这表示当前系统所有进程一共可以打开的文件数量为387311。请务必注意”系统所有进程"这几个字。

运行 vim /etc/sysctl.conf ,有时候你会看到类似:fs.file-max = 8192。出现这个则表示用户手动设置了这个值,没有则系统会有其默认值。手动设置的话,只需要在sysctl.conf 中加上上述语句即可。

回到ulimit中的open files,它与file-max的区别就是:opem filese表示当前shell以及由它启动的进程的文件描述符的限制,也就是说ulimit中设置的open files,只是当前shell及其子进程的文件描述符的限定。是否清楚?可以简单的理解为:

好了,对于 file-max与open files的简单介绍到此为止。现在的问题就是,“Too many open files”到底是碰到哪个设置的雷区造成的。

结合上一篇,我们知道sentinel主要在执行accept函数时出现了“Too many open files”错误,熟悉accept这个系统调用的朋友很清楚,accept会接收客户端的请求,成功的话会建立连接,并返回新的socket描述符。所以,我们确定这里的“Too many open files”指的即是socket的数目过多。

我们猜测,是否是有大量的Jedis连接同时存在,耗尽服务器的socket资源,导致新的连接请求无法建立。所以,我们查看一下sentinel服务器的TCP连接,运行:netstat -anp | grep 26379,得到:

由上图可以发现,有非常多处于ESTABLISHED状态的TCP连接,运行 netstat -anp | grep 118:26379 | wc -l查看他们的个数:

可以看到,Sentinel同时维持了4071个TCP连接,而且过了很久之后,仍然是这么多,不会有大幅变化。

这时,也许你会想到,是否因为系统文件描述符的限制导致Sentinel无法建立更多的Socket,从而产生“Too many open files”的错误。所以,马上在Sentinel服务器上运行cat /proc/sys/fs/file-max,发现:

这个值很大,看似一切正常。继续运行sudo cat /proc/5515/limits,发现:

看到上图,我们似乎发现了端倪,这里的hard和soft都是4096,与前面的4072比较接近。为什么会这么低?我们继续查看一下ulimit,运行ulimit -a :

可以看到,ulimit中open files 的设置为64000,为什么会不一致?按理说sentinel应该最大有64000+的open files。

对于这个矛盾,一开始我怎么也想不明白。最后我推测:应该是在最早启动sentinel启动的时候,系统的设置为4096,sentinel启动之后,又在某个时间又改为64000,而sentinel进程确保持原有设置,从而导致很快达到限制。

我马上查看了进程的启动时间,运行ps -eo pid,lstart,etime | grep 5515:

发现进程启动于2015年12月2日,接着再次运行 ll /etc/security/limits.conf:

发现确实在2016年4月改动过, 然后我咨询了运维人员,他们告知我,确实改动过,但由多少改动到多少,他们也忘了。。。

为了了解Sentinel启动时的状况,紧接着查看了Sentinel的日志,下面是Sentinel启动时打印的画面:

上图说明了进程是2015年12月2日启动的,特别注意最开头的几行,非常关键:

这几句的意思是:

问题很清楚了,redis sentinel最大可以支持10000个客户端,也就是10032个文件描述符,但由于当前被人为限制到4096 了,所以,自动降低了标准。

因此,我猜测,最早open files的限制为4096时,Sentinel已经启动了,只要进程启动,改多少都没有用。很明显,在生产环境上,4096个连接请求很快就会达到。

接着,我继续查看了Sentinel的配置文件,如下图所示:

上图中, “Generated by CONFIG REWRITE”之前的都是是人工配置,其后为Sentinel自动重写的配置。

熟悉Redis的朋友都知道。Sentinel可能会对其配置文件进行更新重写:

我们很快注意到了maxclients 4064这个配置项,此时我很迷惑。我们知道,在Sentinel中是无法手动运行config set命令的,那这个4096必然不是来自于人工配置,Sentinel为什么要自动重写4064这个值。其实,仔细发现,这里Sentinel限制了最多4064个连接,加上32个预留,刚好为4096。

于是,综上,我猜测,Sentinel在启动的时候发现自己的10032个open files的预期与事实设置的4096不符,所以被迫遵守4096,减去预留的32,最终maxclients 只有4064,并且之后因为某些原因重写了配置,所以输出了这个值。

好吧,我的一贯作风,先猜测,再让源码说话。

我们可以通过异常信息定位异常所处源码,所以我搜索了前面提到的Sentinel在启动时打印的关于maxclients 的日志信息中的文本,如“You requested maxclients of ...”。

这个异常出现在adjustOpenFilesLimit函数,通过函数名可以清楚它的作用,然后发现它的调用链只是:
``main()->initServer()->adjustOpenFilesLimit()```

所以,可以确定,在Sentinel服务器启动并进行初始化的时候,会调用adjustOpenFilesLimit函数对open files个数进行调整。调整策略是什么呢?我们查看源码:

在第1行中,REDIS_MIN_RESERVED_FDS即预留的32,是Sentinel保留的用于额外的的操作,如listening sockets, log files 等。同时,这里读取了server.maxclients的值,看来server.maxclients具有初始化值,通过经过定位源码,发现调用链:

即Sentinel在启动时,调用initServerConfig()初始化配置,执行了server.maxclients = REDIS_MAX_CLIENTS(REDIS_MAX_CLIENTS为10000),所以server.maxclients就有了初始值10000。

回到adjustOpenFilesLimit()函数,adjustOpenFilesLimit最终目的就是得到适合的soft,并存在server.maxclients中,因为该函数比较重要,下面专门作出解释:
1 先得到maxfiles的初始值,即Sentinel的期望10032
2 然后获取进程当前的soft和hard,并存入limit ,即执行getrlimit(RLIMIT_NOFILE,&limit) :

调整过程为:
1 先用oldlimit变量保存进程当前的soft的值(如4096)
2 然后,判断oldlimit<maxfiles ,如果真,表示当前soft达不到你要求,需要调整。调整的时候,策略是从最大值往下尝试,以逐步获得Sentinel能申请到的最大soft。

尝试过程为:
1 首先f保存要尝试的soft值,初始值为maxfiles (10032),即从10032开始调整。
2 然后开始一个循环判断,只要f大于oldlimit,就执行一次setrlimit(RLIMIT_NOFILE,&limit) ,然后f减16:

这样,用这种一步一步尝试的方法,最终可用得到了Sentiel能获得的最大的soft值,最后减去32再保存在server.maxclients中。

另外,当得到Sentinel能获得的最合适的soft值f后,还要判断f与oldlimit(系统最初的soft限制,假设为4096),原因如下:
也许会直到f==4096才设置成功,但也会出现f<4096的情况,这是因为跨度为16,最后一不小心就减多了,但最后的soft值不应该比4096还小。所以,f=oldlimit就是这个意思。

最后,还有一个判断:

上面的过程我们用简单的表示为:

adjustOpenFilesLimit()的分析到此结束。但有一点一定要明确,adjustOpenFilesLimit()只会在Sentinel初始化的时候执行一次,目的就是将最合适的soft保存到了server.maxclients (第xx行),以后不会再调用。这样,一旦设置了server.maxclients ,只要Sentinel不重启,这个值就不会变化,这也就解释了为什么Sentinel启动之后再改变open files没有效果的原因了。

那什么时候发生了重写呢?即“Generated by CONFIG REWRITE”这句话什么时候会输出?接着上面,我又在源码里搜索了“Generated by CONFIG REWRITE”这句话,发现了常量REDIS_CONFIG_REWRITE_SIGNATURE,通过它继而发现如下调用链:

上面的调用链中,*表示有很多地方会调用sentinelFlushConfig()。

什么时候调用sentinelFlushConfig()呢?经过查找,发现有很多条件都可以触发sentinelFlushConfig函数的调用,包括Leader选举、故障转移、使用Sentinel set 设置命令、Sentinel处理info信息等等。

而sentinelFlushConfig()则会利用rewriteConfig(),针对具体的配置项,分别进行重写,最终将Sentinel所有的状态持久化到了配置文件中。如下所示,在rewriteConfig()中,可以看到非常多的重写类型, 这些重写类型都是与redis的各个配置选项一一对应的:

当然,我们只需要找到其中关于max clients的重写即可,所以在该函数中,我们找到了调用:

可以看到,该函数传入了Sentinel当前的server.maxclients(已经在启动时调整过了,前面分析过),以及默认的REDIS_MAX_CLIENTS即10032。该函数作用就是将当前的server.maxclients的值重写到配置文件中去。什么时候重写呢,即当默认值与当前值不同的时候(也就是force==true的时候),具体可以查看其源码,篇幅限制我们不做详细介绍。

通过前面一大堆的分析,我们可以得出结论:

讲到这里,还有一个问题就是,为什么Sentinel服务器会长期持有4000多个Established状态的TCP连接而不释放。按目前生产环境的规模,正常情况下业务客户端使用的Jedis建立的TCP连接不应该有这么多。

经过查看,发现Sentinel上的很多连接在对应的客户端中并没有存在。如红框所示IP10.X.X.74上:

总计有992个连接:

而实际上在10.X.X.74上,只有5个与Sentinel的连接长期存在:

也就是说,在Sentinel中有大量的连接是无效的,客户端并没有持有,Sentinel一直没有释放。这个问题, 就涉及到了TCP保活的相关知识。

我们首先要了解,操作系统通常会自身提供TCP的keepalive机制,如在linux默认配置下,运行sysctl -a |grep keep,会看到如下信息:

上面表示如果连接的空闲时间超过 7200 秒(2 小时),Linux 就发送保持活动的探测包。每隔75秒发一次,总共发9次,如果9次都失败的话,表示连接失效。

TCP提供这种机制帮助我们判断对端是否存活,当TCP检测到对端不可用时,会出错并通知上层进行处理。keepalive机制默认是关闭的,应用程序需要使用SO_KEEPALIVE进行启用。

了解到这个知识之后,我们开始分析。在Redis的源码中,发现有如下调用链:

还记得acceptTcpHandler吗,acceptTcpHandler是TCP连接的事件处理器,当它为客户端成功创建了TCP连接后,会通过调用createClient函数为每个连接(fd)创建一个redisClient 实例,这个redisClient 与客户端是一一对应的。并且,还会设置一些TCP选项,如下所示。

如果用户在Redis中没有手动配置tcpkeepalive的话,server.tcpkeepalive = REDIS_DEFAULT_TCP_KEEPALIVE,默认为0。
由第x-x行我们可以明确,Redis服务器与客户端的连接默认是关闭保活机制的,因为只有当server.tcpkeepalive不为0(修改配置文件或config set)时,才能调用anetKeepAlive方法设置TCP的keepalive选项。

我们知道,Sentinel是特殊模式的Redis,我们无法使用config set命令去修改其配置,包括tcpkeepalive 参数。所以,当Sentinel启动后,Sentinel也使用默认的tcpkeepalive ==0这个设置,不会启用tcpkeepalive ,与客户端的TCP连接都没有保活机制。也就是说,Sentinel不会主动去释放连接,哪怕是失效连接。

但是,TCP连接是双向的,Sentinel无法处理失效连接,那Jedis客户端呢?它是否可以主动断掉连接?我们定位到了Jedis建立连接的函数connect(),如下所示:

由第x行可以看到,Jedis启用了TCP的keepalive机制,并且没有设置其他keepalive相关选项。也就是说,Jedis客户端会采用linux默认的TCP keepalive机制,每隔7200秒去探测连接的情况。这样,即使与Sentinel的连接出问题,Jedis客户端也能主动释放掉,虽然时间有点久。

但是,实际上,如前面所示,Sentinel服务器上有很多失效连接持续保持,为什么会有这种现象?

对于上面的问题,能想到的原因就是,在Jedis去主动释放掉TCP连接前,该连接被强制断掉,没有进行完整的四次挥手的过程。而Sentinel却因为没有保活机制,没有感知到这个动作,导致其一直保持这个连接。

能干掉连接的元兇,马上想到了防火墙,于是我又询问了运维,结果,他们告知了我一个噩耗:
目前,生产环境上防火墙的设置是主动断掉超过10分钟没有数据交换的TCP连接。

好吧,绕了一大圈,至此,问题已经很清楚了。

终于,我们得出了结论:

有了前面的分析,其实解决办法很简单:

关于“追踪Redis Sentinel的CPU占有率长期接近100%的问题”到此就结束了,在写这两篇博文的时候,我收货了很多自己没有掌握的知识和技巧。现在觉得,写博文真的是一件值得坚持和认真对待的事情,早应该开始。不要问我为什么,当你尝试之后,也就和我一样明白了。

这两篇文章的分析过程肯定有疏漏和不足之处,个人能力有限,希望大家能够理解,并多多指教,非常感谢!我会继续进步!

前面提到过,在每个客户端上,都可以发现5个正常的TCP连接,他们是什么呢?让我们重新回到Jedis。

在《追踪Redis Sentinel的CPU占有率长期接近100%的问题 一》中,我们提到Jedis SentinelPool会为每一个Sentinel建立一个MasterListener线程,该线程用来监听主从切换,保证客户端的Jedis句柄始终对应在Master上。在这里,即会有5个MasterListener来对应5个Sentinel。

其实,MasterListener的监听功能根据Redis的pub sub功能实现的。MasterListener线程会去订阅+switch-master消息,该消息会在master节点地址改变时产生,一旦产生,MasterListener就重新初始化连接池,保证客户端使用的jedis句柄始终关联到Master上。

如下所示为MasterListener的线程函数,它会在一个无限循环中不断的创建Jedis句柄,利用该句柄去订阅+switch-master消息,只要发生了主从切换,就会触发onMessage。

如何实现订阅功能呢,我们需要查看subscribe函数的底层实现,它实际使用client.setTimeoutInfinite()->connect建立了一个TCP连接,然后使用JedisPubSub的proceed方法去订阅频道,并且无限循环的读取订阅的信息。

在procee的方法中,实际先通过subscribe订阅频道,然后调用process方法读取订阅信息。

其实,subscribe函数就是简单的向服务器发送了一个SUBSCRIBE命令。

而process函数,篇幅较长,此处省略,其主要功能就是以无限循环的方式不断地读取订阅信息

综上,MasterListener线程会向Sentinel创建+switch-master频道的TCP订阅连接,并且会do while循环读取该频道信息。如果订阅或读取过程中出现Tcp连接异常,则释放Jedis句柄,然后等待5000ms 后重新创建Jedis句柄进行订阅。当然,这个过程会在一个循环之中。

至此,也就解释了为何每个业务客户端服务器和Sentinel服务器上,都有5个长期保持的、状态正常的TCP连接的原因了。

B. redis cpu使用率怎么查看

只需在同一台机器上启动Redis的多个实例,将其当作不同的服务器即可。
单一的实例在某些时候可能是不够用的,所以如果想使用多个CPU,这就需要开始思考早期的一些数据段。
这里需要注意的是,使用Redis Pipelining在Linux系统上运行,每秒可以提供500K的请求,因此,如果应用程序主要使用O(N)或O(log(N))命令,会消耗更多的CPU。
注意
Redis
Pipelining用于解决因客户端和服务器的网络延迟而造成的请求延迟。
这一功能其实很早就有,即使较早版本的Redis,也能使用这个功能。
此功能可以将一系列请求连续发送到Server端,不必等待Server端的返回信息,而Server端会将请求放进一个有序的管道中,执行完成后,再一次性将返回值发送回来。

C. 京东面试官:Redis 这些我必问

缓存好处:高性能 + 高并发


数据库查询耗费了800ms,其他用户对同一个数据再次查询 ,假设该数据在10分钟以内没有变化过,并且 10 分钟之内有 1000 个用户 都查询了同一数据,10 分钟之内,那 1000 每个用户,每个人查询这个数据都感觉很慢 800ms
比如 :某个商品信息,在 一天之内都不会改变,但是这个商品每次查询一次都要耗费2s,一天之内被浏览 100W次
mysql 单机也就 2000qps,缓存单机轻松几万几十万qps,单机 承载并发量是 mysql 单机的几十倍。


在中午高峰期,有 100W 个用户访问系统 A,每秒有 4000 个请求去查询数据库,数据库承载每秒 4000 个请求会宕机,加上缓存后,可以 3000 个请求走缓存 ,1000 个请求走数据库。
缓存是走内存的,内存天然可以支撑4w/s的请求,数据库(基于磁盘)一般建议并发请求不要超过 2000/s

redis 单线程 ,memcached 多线程
redis 是单线程 nio 异步线程模型

一个线程+一个队列

redis 基于 reactor 模式开发了网络事件处理器,这个处理器叫做文件事件处理器,file event handler,这个文件事件处理器是单线程的,所以redis 是单线程的模型,采用 io多路复用机制同时监听多个 socket,根据socket上的事件来选择对应的事件处理器来处理这个事件。
文件事件处理器包含:多个 socket,io多路复用程序,文件事件分派器,事件处理器(命令请求处理器、命令恢复处理器、连接应答处理器)
文件事件处理器是单线程的,通过 io 多路复用机制监听多个 socket,实现高性能和线程模型简单性
被监听的 socket 准备好执行 accept,read,write,close等操作的时候,会产生对应的文件事件,调用之前关联好的时间处理器处理
多个 socket并发操作,产生不同的文件事件,i/o多路复用会监听多个socket,将这些 socket放入一个队列中排队。事件分派器从队列中取出socket给对应事件处理器。
一个socket时间处理完后,事件分派器才能从队列中拿到下一个socket,给对应事件处理器来处理。

文件事件:
AE_READABLE 对应 socket变得可读(客户端对redis执行 write操作)
AE_WRITABLE 对应 socket 变得可写(客户端对 redis执行 read操作)
I/O 多路复用可以同时监听AE_REABLE和 AE_WRITABLE ,如果同时达到则优先处理 AE_REABLE 时间
文件事件处理器:
连接应答处理器 对应 客户端要连接 redis
命令请求处理器 对应 客户端写数据到 redis
命令回复处理器 对应 客户端从 redis 读数据

流程:

一秒钟可以处理几万个请求

普通的 set,get kv缓存

类型 map结构,比如一个对象(没有嵌套对象)缓存到 redis里面,然后读写缓存的时候,可以直接操作hash的字段(比如把 age 改成 21,其他的不变)
key=150
value = {

}

有序列表 ,元素可以重复
可以通过 list 存储一些列表型数据结构,类似粉丝列表,文章评论列表。
例如:微信大 V的粉丝,可以以 list 的格式放在 redis 里去缓存
key=某大 V value=[zhangsan,lisi,wangwu]
比如 lrange 可以从某个元素开始读取多少个元素,可以基于 list 实现分页查询功能,基于 redis实现高性能分页,类似微博下来不断分页东西。
可以搞个简单的消息队列,从 list头怼进去(lpush),list尾巴出来 (brpop)

无序集合,自动去重
需要对一些数据快速全局去重,(当然也可以基于 HashSet,但是单机)
基于 set 玩差集、并集、交集的操作。比如:2 个人的粉丝列表整一个交集,看看 2 个人的共同好友是谁?
把 2 个大 V 的粉丝都放在 2 个 set中,对 2 个 set做交集(sinter)

排序的 set,去重但是可以排序,写进去的时候给一个分数,自动根据分数排序

排行榜:

zadd board score username

例如:
zadd board 85 zhangsan
zadd board 72 wangwu
zadd board 96 lis
zadd board 62 zhaoliu

自动排序为:
96 lisi
85 zhangsan
72 wangwu
62 zhaoliu

获取排名前 3 的用户 : zrevrange board 0 3
96 lisi
85 zhangsan
72 wangwu

查看zhaoliu的排行 :zrank board zhaoliu 返回 4

内存是宝贵的,磁盘是廉价的
给key设置过期时间后,redis对这批key是定期删除+惰性删除
定期删除:
redis 默认每隔 100ms随机抽取一些设置了过期时间的 key,检查其是否过期了,如果过期就删除。
注意:redis是每隔100ms随机抽取一些 key来检查和删除,而不是遍历所有的设置过期时间的key(否则CPU 负载会很高,消耗在检查过期 key 上)
惰性删除:
获取某个key的时候, redis 会检查一下,这个key如果设置了过期时间那么是否过期,如果过期了则删除。
如果定期删除漏掉了许多过期key,然后你也没及时去查,也没走惰性删除,如果大量过期的key堆积在内存里,导致 redis 内存块耗尽,则走内存淘汰机制。

内存淘汰策略:

LRU 算法:

缓存架构(多级缓存架构、热点缓存)
redis 高并发瓶颈在单机,读写分离,一般是支撑读高并发,写请求少,也就 一秒一两千,大量请求读,一秒钟二十万次。


一主多从,主负责写,将数据同步复制到其他 slave节点,从节点负责读,所有读的请求全部走从节点。主要是解决读高并发。、
主从架构->读写分离->支撑10W+读QPS架构


master->slave 复制,是异步的
核心机制:

master持久化对主从架构的意义:
如果开启了主从架构,一定要开启 master node的持久化,不然 master宕机重启数据是空的,一经复制,slave的数据也丢了

主从复制原理:


第一次启动或者断开重连情况:

正常情况下:
master 来一条数据,就异步给 slave

全年 99.99%的时间,都是出于可用的状态,那么就可以称为高可用性
redis 高可用架构叫故障转移,failover,也可以叫做主备切换,切换的时间不可用,但是整体高可用。
sentinal node(哨兵)

作用:


quorum = 1 (代表哨兵最低个数可以尝试故障转移,选举执行的哨兵)
master 宕机,只有 S2 存活,因为 quorum =1 可以尝试故障转移,但是没达到 majority =2 (最低允许执行故障转移的哨兵存活数)的标准,无法执行故障转移


如果 M1 宕机了,S2,S3 认为 master宕机,选举一个执行故障转移,因为 3 个哨兵的 majority = 2,所以可以执行故障转移

丢数据:

解决方案:

sdown 主观宕机,哨兵觉得一个 master 宕机(ping 超过了 is-master-down-after-milliseconds毫秒数)
odown 客观宕机,quorum数量的哨兵都觉得 master宕机
哨兵互相感知通过 redis的 pub/sub系统,每隔 2 秒往同一个 channel里发消息(自己的 host,ip,runid),其他哨兵可以消费这个消息
以及同步交换master的监控信息。
哨兵确保其他slave修改master信息为新选举的master
当一个 master被认为 odown && marjority哨兵都同意,那么某个哨兵会执行主备切换,选举一个slave成为master(考虑 1. 跟master断开连接的时长 2. slave 优先级 3.复制 offset 4. runid)
选举算法:

quorum 数量哨兵认为odown->选举一个哨兵切换->获得 majority哨兵的授权(quorum majority 需要 majority个哨兵授权,quorum >= majority 需要 quorum 哨兵授权)
第一个选举出来的哨兵切换失败了,其他哨兵等待 failover-time之后,重新拿confiuration epoch做为新的version 切换,保证拿到最新配置,用于 configuration传播(通过 pu/sub消息机制,其他哨兵对比 version 新旧更新 master配置)

高并发:主从架构
高容量:Redis集群,支持每秒几十万的读写并发
高可用:主从+哨兵

持久化的意义在于故障恢复数据备份(到其他服务器)+故障恢复(遇到灾难,机房断电,电缆被切)

AOF 只有一个,Redis 中的数据是有一定限量的,内存大小是一定的,AOF 是存放写命令的,当大到一定的时候,AOF 做 rewrite 操作,就会基于当时 redis 内存中的数据,来重新构造一个更小的 AOF 文件,然后将旧的膨胀很大的文件给删掉,AOF 文件一直会被限制在和Redis内存中一样的数据。AOF同步间隔比 RDB 小,数据更完整

优点:

缺点:

AOF 存放的指令日志,数据恢复的时候,需要回放执行所有指令日志,RDB 就是一份数据文件,直接加载到内存中。

优点:

缺点:

AOF 来保证数据不丢失,RDB 做不同时间的冷备


支持 N 个 Redis master node,每个 master node挂载多个 slave node
多master + 读写分离 + 高可用

数据量很少,高并发 -> replication + sentinal 集群
海量数据 + 高并发 + 高可用 -> redis cluster

hash算法->一致性 hash 算法-> redis cluster->hash slot算法

redis cluster :自动对数据进行分片,每个 master 上放一部分数据,提供内置的高可用支持,部分master不可用时,还是可以继续工作
cluster bus 通过 16379进行通信,故障检测,配置更新,故障转移授权,另外一种二进制协议,主要用于节点间进行高效数据交换,占用更少的网络带宽和处理时间

key进行hash,然后对节点数量取模,最大问题只有任意一个 master 宕机,大量数据就要根据新的节点数取模,会导致大量缓存失效。


key进行hash,对应圆环上一个点,顺时针寻找距离最近的一个点。保证任何一个 master 宕机,只受 master 宕机那台影响,其他节点不受影响,此时会瞬间去查数据库。
缓存热点问题:
可能集中在某个 hash区间内的值特别多,那么会导致大量的数据都涌入同一个 master 内,造成 master的热点问题,性能出现瓶颈。
解决方法:
给每个 master 都做了均匀分布的虚拟节点,这样每个区间内大量数据都会均匀的分布到不同节点内,而不是顺时针全部涌入到同一个节点中。

redis cluster 有固定 16384 个 hash slot,对每个key计算 CRC16 值,然后对16384取模,可以获取 key对应的 hash slot
redis cluster 中每个 master 都会持有部分 slot ,当一台 master 宕机时候,会最快速度迁移 hash slot到可用的机器上(只会短暂的访问不到)
走同一个 hash slot 通过 hash tag实现


集群元数据:包括 hashslot->node之间的映射表关系,master->slave之间的关系,故障的信息
集群元数据集中式存储(storm),底层基于zookeeper(分布式协调中间件)集群所有元数据的维护。好处:元数据的更新和读取,时效性好,一旦变更,其他节点立刻可以感知。缺点:所有元数据的更新压力全部集中在一个地方,可能会导致元数据的存储有压力。
goosip: 好处:元数据的更新比较分散,有一定的延时,降低了压力。缺点:更新有延时,集群的一些操作会滞后。(reshared操作时configuration error)

自己提供服务的端口号+ 10000 ,每隔一段时间就会往另外几个节点发送ping消息,同时其他几点接收到ping之后返回pong

故障信息,节点的增加和移除, hash slot 信息

meet:某个节点发送 meet给新加入的节点,让新节点加入集群中,然后新节点就会开始于其他节点进行通信
ping:每个节点都会频繁给其他节点发送ping,其中包含自己的状态还有自己维护的集群元数据,互相通过ping交换元数据
ping:返回ping和meet,包含自己的状态和其他信息
fail:某个节点判断另一个节点fail之后,就发送 fail 给其他节点,通知其他节点,指定的节点宕机了

ping 很频繁,且携带元数据,会加重网络负担
每个节点每秒会执行 10 次 ping,每次选择 5 个最久没有通信的其他节点
当如果发现某个节点通信延迟达到了 cluster_node_timeout /2 ,那么立即发送 ping, 避免数据交换延迟过长,落后时间太长(2 个节点之间 10 分钟没有交换数据,整个集群处于严重的元数据不一致的情况)。
每次ping,一个是带上自己的节点信息,还有就是带上1/10其他节点的信息,发送出去,进行数据交换
至少包含 3 个其他节点信息,最多包含总节点-2 个其他节点的信息

客户端发送到任意一个redis实例发送命令,每个redis实例接受到命令后,都会计算key对应的hash slot,如果在本地就本地处理,否则返回moved给客户端,让客户端进行重定向 (redis-cli -c)

通过tag指定key对应的slot,同一个 tag 下的 key,都会在一个 hash slot中,比如 set key1:{100} 和 set key2:{100}

本地维护一份hashslot->node的映射表。
JedisCluster 初始化的时候,随机选择一个 node,初始化 hashslot->node 映射表,同时为每个节点创建一个JedisPool连接池,每次基于JedisCluster执行操作,首先JedisCluster都会在本地计算key的hashslot,然后再本地映射表中找到对应的节点,如果发现对应的节点返回moved,那么利用该节点的元数据,更新 hashslot->node映射表(重试超过 5 次报错)

hash slot正在迁移,那么会返回ask 重定向给jedis,jedis 接受到ask重定向之后,,会重定向到目标节点去执行

判断节点宕机:
如果一个节点认为另外一个节点宕机了, 就是pfail,主观宕机
如果多个节点都认为另外一个节点宕机了,那么就是fail,客观宕机(跟哨兵原理一样)
在cluster-node-timeout内,某个节点一直没有返回 pong,那么就被认为是 pfail
如果一个节点认为某个节点pfail了,那么会在gossip消息中,ping给其他节点,如果超过半数的节点认为pfail了,那么就会变成fail。
从节点过滤:
对宕机的 mster node ,从其所有的 slave node中,选择一个切换成 master node
检查每个 slave node与master node断开连接的时间,如果超过了cluster-node-timeout * cluster-slave-validity-factor,那么就没资格切换成 master(和哨兵一致)
从节点选举:
每个从节点,根据自己对 master 复制数据的 offset,设置一个选举时间,offset越大(复制数据越多)的从节点,选举时间越靠前,所有的 master node 开始投票,给要进行选举的 slave进行投票,如果大部分 master node(N/2 +1) 都投票给某个从节点,那么选举通过,从节点执行主备切换,从节点切换成主节点
总结:和哨兵很像,直接集成了 replication 和 sentinal

方案:
事前:保证 redis 集群高可用性 (主从+哨兵或 redis cluster),避免全盘崩溃
事中:本地 ehcache 缓存 + hystrix 限流(保护数据库) & 降级,避免 MySQL被打死
事后: redis持久化,快速恢复缓存数据,继续分流高并发请求

限制组件每秒就 2000 个请求通过限流组件进入数据库,剩余的 3000 个请求走降级,返回一些默认 的值,或者友情提示
好处 :


4000 个请求黑客攻击请求数据库里没有的数据
解决方案:把黑客查数据库中不存在的数据的值,写到缓存中,比如: set -999 UNKNOWN


读的时候,先读缓存,缓存没有,就读数据库,然后取出数据后放入缓存,同时返回响应
更新的时候,删除缓存,更新数据库
为什么不更新缓存:
更新缓存代价太高(更新 20 次,只读 1 次),lazy思想,需要的时候再计算,不需要的时候不计算

方案:先删除缓存,再修改数据库


方案:写,读路由到相同的一个内存队列(唯一标识,hash,取模)里,更新和读操作进行串行化(后台线程异步执行队列串行化操作),(队列里只放一个更新查询操作即可,多余的过滤掉,内存队列里没有该数据更新操作,直接返回 )有该数据更新操作则轮询取缓存值,超时取不到缓存值,直接取一次数据库的旧值


TP 99 意思是99%的请求可以在200ms内返回
注意点:多个商品的更新操作都积压在一个队列里面(太多操作积压只能增加机器),导致读请求发生大量的超时,导致大量的读请求走数据库
一秒 500 写操作,每200ms,100 个写操作,20 个内存队列,每个队列积压 5 个写操作,一般在20ms完成


方案:分布式锁 + 时间戳比较

10台机器,5 主 5 从,每个节点QPS 5W ,一共 25W QPS(Redis cluster 32G + 8 核 ,Redis 进程不超过 10G)总内存 50g,每条数据10kb,10W 条数据1g,200W 条数据 20G,占用总内存不到50%,目前高峰期 3500 QPS

作者: mousycoder

D. redis cpu绑定

1.启动时绑定

taskset -c 0 /usr/local/bin/redis-server /etc/redis/redis.conf

2.运行中绑定

3.redis cpu绑定查看

E. redis 线上问题排查思路总结

 日常我们使用redis 缓存时,经常会遇到各种各样的问题,其中redis 偶发性连接超时,是经常遇到的一个问题,下面介绍一下我们之前是如何处理的这个问题。

1、redis 服务监控

      通过监控工具,首先排查一下redis 服务端是否是超时,可以从服务器cpu ,内存使用情况,qps等判断server 端是否超时。如果server 侧没有问题,就需要排查客户端。如果server 侧存在问题,就需要排查服务器哪里出了问题,单机性能使用率太高是否可以升级成哨兵模式或者高可用集群模式。

2、redis 客户端排查

     首先查看业务日志,查看一下redis 使用情况是否是存在连接数占满或者创建失败的异常,如果存在,在客户端服务器,使用top 指令,查看使用率高的线程,然后jstack pid,查看当前线程的使用情况。如果出现大量的线程状态显示time_waiting 或者waiting 。则表示连接数一直没有释放,可以通过调整客户端配置的redis 连接池参数,比如配置max连接数和min连接数,time_out超时时间等等。

3、redis 热key排查

排查redis 热key,腾讯云或者阿里云服务器可以使用监控热key的工具。redis 4.0 以后,提供了—hotkey 指令,可以通过热key 指令来监控热key。如果发现异常热key,比如spring-redis-session的热key,存储的是一段时间戳,并且访问率非常高,qps 几十万/s。这时候需要考虑热key是否对业务产生影响,可以通过配置spring.session.store-type=none,关闭存储redis.这时候热key访问量下降,业务key 可以正常访问。

通过以上方式,排查生产中遇到的redis 连接问题,可以排查线上遇到的问题,基本都可以解决掉。

F. 如何实现高可用的 redis 集群

Redis 因具有丰富的数据结构和超高的性能以及简单的协议,使其能够很好的作为数据库的上游缓存层。但在大规模的 Redis 使用过程中,会受限于多个方面:单机内存有限、带宽压力、单点问题、不能动态扩容等。

基于以上, Redis 集群方案显得尤为重要。通常有 3 个途径:官方 Redis Cluster ;通过 Proxy 分片;客户端分片 (Smart Client) 。以上三种方案各有利弊。

Redis Cluster( 官方 ) :虽然正式版发布已经有一年多的时间,但还缺乏最佳实践;对协议进行了较大修改,导致主流客户端也并非都已支持,部分支持的客户端也没有经过大规模生产环境的验证;无中心化设计使整个系统高度耦合,导致很难对业务进行无痛的升级。

Proxy :现在很多主流的 Redis 集群都会使用 Proxy 方式,例如早已开源的 Codis 。这种方案有很多优点,因为支持原声 redis 协议,所以客户端不需要升级,对业务比较友好。并且升级相对平滑,可以起多个 Proxy 后,逐个进行升级。但是缺点是,因为会多一次跳转,平均会有 30% 左右的性能开销。而且因为原生客户端是无法一次绑定多个 Proxy ,连接的 Proxy 如果挂了还是需要人工参与。除非类似 Smart Client 一样封装原有客户端,支持重连到其他 Proxy ,但这也就带来了客户端分片方式的一些缺点。并且虽然 Proxy 可以使用多个,并且可以动态增加 proxy 增加性能,但是所有客户端都是共用所有 proxy ,那么一些异常的服务有可能影响到其他服务。为每个服务独立搭建 proxy ,也会给部署带来额外的工作。

而我们选择了第三种方案,客户端分片 (Smart Client) 。客户端分片相比 Proxy 拥有更好的性能,及更低的延迟。当然也有缺点,就是升级需要重启客户端,而且我们需要维护多个语言的版本,但我们更爱高性能。

下面我们来介绍一下我们的Redis集群:

概貌:

如图0所示,

我们的 Redis 集群一共由四个角色组成:

Zookeeper :保存所有 redis 集群的实例地址, redis 实例按照约定在特定路径写入自身地址,客户端根据这个约定查找 redis 实例地址,进行读写。

Redis 实例:我们修改了 redis 源码,当 redis 启动或主从切换时,按照约定自动把地址写到 zookeeper 特定路径上。

Sentinel : redis 自带的主从切换工具,我们通过 sentinel 实现集群高可用。

客户端( Smart Client ):客户端通过约定查找 redis 实例在 ZooKeeper 中写入的地址。并且根据集群的 group 数,进行一致性哈希计算,确定 key 唯一落入的 group ,随后对这个 group 的主库进行操作。客户端会在Z ooKeeper 设置监视,当某个 group 的主库发生变化时,Z ooKeeper 会主动通知客户端,客户端会更新对应 group 的最新主库。

我们的Redis 集群是以业务为单位进行划分的,不同业务使用不同集群(即业务和集群是一对一关系)。一个 Redis 集群会由多个 group 组成 ( 一个 group 由一个主从对 redis 实例组成 ) 。即 group 越多,可以部署在更多的机器上,可利用的内存、带宽也会更多。在图0中,这个业务使用的 redis 集群由 2 个 group 组成,每个 group 由一对主从实例组成。

Failover

如图1所示,

当 redis 启动时,会 把自己的 IP:Port 写入到 ZooKeeper 中。其中的 主实例模式启动时会在 /redis/ 业务名 / 组名 永久节点写入自己的 IP:Port (如果节点不存在则创建)。由 主模式 变成 从模式 时,会创建 /redis/ 业务名 / 组名 /slaves/ip:port 临时节 点,并写入自己的 IP:Port (如果相同节点已经存在,则先删除,再创建)。而从实例 模式 启动时会创建 /redis/ 业务名 / 组名 /slaves/ip:port 临时节点,并写入自己的 ip:port (如果相同节点已经存在,则先删除,再创建)。由 从模式 变成 主模式 时,先删除 /redis/ 业务名 / 组名 /slaves/ip:port 临时节点,并在 /redis/ 业务名 / 组名 永久节点写入自己的 IP:Port 。

ZooKeeper 会一直保存当前有效的 主从实例 IP:Port 信息。至于主从自动切换过程,使用 redis 自带的 sentinel 实现,现设置为超过 30s 主 server 无响应,则由 sentinel 进行主从实例的切换,切换后就会触发以主、从实例通过以上提到的一系列动作,从而完成最终的切换。

而客户端侧通过给定业务名下的所有 groupName 进行一致性哈希计算,确定 key 落入哪个组。 客户端启动时,会从 ZooKeeper 获取指定业务名下所有 group 的 主从 IP:Port ,并在 ZooKeeper 中设置监视(监视的作用是当 ZooKeeper 的节点发生变化时,会主动通知客户端)。若客户端从 Zookeeper 收到节点变化通知,会重新获取最新的 主从 I:Port ,并重新设置监视( ZooKeeper 监视是一次性的)。通过此方法,客户端可以实时获知当前可访问最新的 主从 IP:Port 信息。

因为我们的所有 redis 实例信息都按照约定保存在 ZooKeeper 上,所以不需要针对每个实例部署监控,我们编写了一个可以自动通过 ZooKeeper 获取所有 redis 实例信息,并且监控 cpu 、 qps 、内存、主从延迟、主从切换、连接数等的工具。

发展:

现在 redis 集群在某些业务内存需求超过预期很多后,无法通过动态扩容进行扩展。所以我们正在做动态扩容的支持。原先的客户端我们是通过一致性哈希进行 key 的
路由策略,但这种方式在动态扩容时会略显复杂,所以我们决定采用实现起来相对简单的预分片方式。一致性哈希的好处是可以无限扩容,而预分片则不是。预分片
时我们会在初始化阶段指定一个集群的所有分片数量,这个数量一旦指定就不能再做改变,这个预分片数量就是后续可以扩容到最大的 redis 实例数。假设预分片 128 个 slot ,每个实例 10G 也可以达到 TB 级别的集群,对于未来数据增长很大的集群我们可以预分片 1024 ,基本可以满足所有大容量内存需求了。

原先我们的 redis 集群有四种角色, Smart Client, redis , sentinel , ZooKeeper 。为了支持动态扩容,我们增加了一个角色, redis_cluster_manager (以下简称 manager ),用于管理 redis 集群。主要工作是初始化集群(即预分片),增加实例后负责修改Z ooKeeper 状态,待客户端做好准备后迁移数据到新增实例上。为了尽量减少数据迁移期间对现性能带来的影响,我们每次只会迁移一个分片的数据,待迁移完成,再进行下一个分片的迁移。

如图2所示

相比原先的方案,多了 slots 、M anager Lock 、 clients 、M igrating Clients 节点。

Slots: 所有分片会把自身信息写入到 slots 节点下面。 Manager 在初始化集群时,根据设置的分片数,以及集群下的 group 数,进行预分片操作,把所有分片均匀分配给已有 group 。分片的信息由一个 json 串组成,记录有分片的状态 (stats) ,当前拥有此分片的 group(src) ,需要迁移到的 group(dst) 。分片的状态一共有三种: online 、 pre_migrate 、 migrating 。

Online 指这个分片处于正常状态,这时 dst 是空值,客户端根据 src 的 group 进行读写。

Pre_migrate 是指这个分片被 manager 标记为需要迁移,此时 dst 仍然为空, manager 在等所有 client 都已经准备就绪,因为 ZooKeeper 回掉所有客户端有时间差,所以如果某些 client 没有准备就绪的时候 manager 进行了数据迁移,那么就会有数据丢失。

Migrating 是 manager 确认了所有客户端都已经做好迁移准备后,在 dst 写入此分片需要迁移的目标 group 。待迁移完成,会在 src 写入目标 group_name , dst 设为空, stats 设为 online 。

Manager Lock: 因为我们是每次只允许迁移一个 slot ,所以不允许超过一个 manager 操作一个集群。所以 manager 在操作集群前,会在M anager Lock 下注册临时节点,代表这个集群已经有 manager 在操作了,这样其他 manager 想要操作这个集群时就会自动退出。

Clients 和M igrating Clients 是为了让 manager 知道客户端是否已经准备就绪的节点。客户端通过 uid 代表自己,格式是 客户端语言 _ 主机名 _pid 。当集群没有进行迁移,即所有分片都是 online 的时候,客户端会在 clients 下创建 uid 的临时节点。

当某个 slot 从 online 变成 pre_migrate 后,客户端会删除 clients 下的 uid 临时节点,然后在M igrating Clients 创建 uid 临时节点。注意,因为需要保证数据不丢失,从 pre_migrate 到 migrating 期间,这个 slot 是被锁定的,即所有对这个 slot 的读写都会被阻塞。所以 mananger 会最多等待 10s ,确认所有客户端都已经切换到准备就绪状态,如果发现某个客户端一直未准备就绪,那么 mananger 会放弃此次迁移,把 slot 状态由 pre_migrate 改为 online 。如果客户端发现 slot 状态由 pre_migrate 变成 online 了,那么会删除 migrating_clients 下的 uid 节点,在 clients 下重新创建 uid 节点。还需要注意的一点是,有可能一个客户刚启动,并且正在往 clients 下创建 uid 节点,但是因为网络延迟还没创建完成,导致 manager 未确认到这个 client 是否准备就绪,所以 mananger 把 slot 改为 pre_migrate 后会等待 1s 再确认所有客户端是否准备就绪。

如果 Manager 看到 clients 下已经没有客户端的话(都已经准备就绪),会把 slot 状态改为 migrating 。 Slot 变成 migrating 后,锁定也随之解除, manager 会遍历 src group 的数据,把对应 slot 的数据迁移到 dst group 里。客户端在 migrating 期间如果有读写 migrating slot 的 key ,那么客户端会先把这个 key 从 src group 迁移到 dst group ,然后再做读写操作。即这期间客户端性能会有所下降。这也是为什么每次只迁移一个 slot 的原因。这样即使只有 128 个分片的集群,在迁移期间受到性能影响的 key 也只有 1/128 ,是可以接受的。

Manager 发现已经把 slot 已经迁移完毕了,会在 src 写入目标 group_name , dst 设为空, stats 设为 online 。客户端也删除 migrating_clients 下的 uid ,在 clients 下创建 uid 节点。

G. Redis变慢了(八) - 网卡负载过高

如果以上产生性能问题的场景,你都规避掉了,而且Redis也稳定运行了很长时间,但在某个时间点之后开始,访问Redis开始变慢了,而且一直持续到现在,这种情况是什么原因导致的?

之前我们就遇到这种问题, 特点就是从某个时间点之后就开始变慢,并且一直持续 。这时你需要检查一下机器的网卡流量,是否存在网卡流量被跑满的情况。

网卡负载过高,在网络层和TCP层就会出现数据发送延迟、数据丢包等情况。Redis的高性能除了内存之外,就在于网络IO,请求量突增会导致网卡负载变高。

如果出现这种情况,你需要排查这个机器上的哪个Redis实例的流量过大占满了网络带宽,然后确认流量突增是否属于业务正常情况,如果属于那就需要及时扩容或迁移实例,避免这个机器的其他实例受到影响。

运维层面,我们需要对机器的各项指标增加监控,包括网络流量,在达到阈值时提前报警,及时与业务确认并扩容。

以上我们总结了Redis中常见的可能导致延迟增大甚至阻塞的场景,这其中既涉及到了业务的使用问题,也涉及到Redis的运维问题。

可见,要想保证Redis高性能的运行,其中涉及到CPU、内存、网络,甚至磁盘的方方面面,其中还包括操作系统的相关特性的使用。

作为开发人员,我们需要了解Redis的运行机制,例如各个命令的执行时间复杂度、数据过期策略、数据淘汰策略等,使用合理的命令,并结合业务场景进行优化。

作为DBA运维人员,需要了解数据持久化、操作系统fork原理、Swap机制等,并对Redis的容量进行合理规划,预留足够的机器资源,对机器做好完善的监控,才能保证Redis的稳定运行。

H. 四个大点,搞懂 Redis 到底快在哪里

现在我们都用高级语言来编程,比如Java、python等。也许你会觉得C语言很古老,但是它真的很有用,毕竟unix系统就是用C实现的,所以C语言是非常贴近操作系统的语言。Redis就是用C语言开发的,所以执行会比较快。

Redis将所有数据放在内存中,非数据同步正常工作中,是不需要从磁盘读取数据的,0次IO。内存响应时间大约为100纳秒,这是Redis速度快的重要基础。先看看CPU的速度:

拿我的电脑来说,主频是3.1G,也就是说每秒可以执行3.1*10^9个指令。所以说CPU看世界是非常非常慢的,内存比它慢百倍,磁盘比他慢百万倍,你说快不快?

借了一张《深入理解计算机系统》的图,展示了一个典型的存储器层次结构,在L0层,CPU可以在一个时钟周期访问到,基于SRAM的高速缓存春续期,可以在几个CPU时钟周期访问到,然后是基于DRAM的主存,可以在几十到几百个时钟周期访问到他们。

第一,单线程简化算法的实现,并发的数据结构实现不但困难且测试也麻烦。第二,单线程避免了线程切换以及加锁释放锁带来的消耗,对于服务端开发来说,锁和线程切换通常是性能杀手。当然了,单线程也会有它的缺点,也是Redis的噩梦: 阻塞。如果执行一个命令过长,那么会造成其他命令的阻塞,对于Redis是十分致命的 ,所以Redis是面向快速执行场景的数据库。

除了Redis之外,Node.js也是单线程,Nginx也是单线程,但他们都是服务器高性能的典范。

在这之前先要说一下传统的阻塞I/O是如何工作的:当使用read或者write对某一文件描述符(File Descriptor FD)进行读写的时候,如果数据没有收到,那么该线程会被挂起,直到收到数据。阻塞模型虽然易于理解,但是在需要处理多个客户端任务的时候,不会使用阻塞模型。

I/O多路复用实际上是指多个连接的**管理可以在同一进程。**多路是指网络连接,复用只是同一个线程。在网络服务中,I/O多路复用起的作用是一次性把多个连接的事件通知业务代码处理,处理的方式由业务代码来决定。在I/O多路复用模型中,最重要的函数调用就是I/O 多路复用函数,该方法能同时监控多个文件描述符(fd)的读写情况,当其中的某些fd可读/写时,该方法就会返回可读/写的fd个数。

Redis使用epoll作为I/O多路复用技术的实现,再加上Redis自身的事件处理模型将epoll的read、write、close等都转换成事件,不在网络I/O上浪费过多的时间。实现对多个FD读写的监控,提高性能。

举个形象的例子吧。比如一个tcp服务器处理20个客户端socket。A方案:顺序处理,如果第一个socket因为网卡读数据处理慢了,一阻塞后面都玩蛋去。B方案:每个socket请求都创建一个分身子进程来处理,不说每个进程消耗大量系统资源,光是进程切换就够操作系统累的了。C方案**(I/O复用模型,epoll) :将用户socket对应的fd注册进epoll(实际上服务器和操作系统之间传递的不是socket的fd而是fd_set的数据结构),然后epoll只告诉哪些需要读/写的socket,只需要处理那些活跃的、有变化的socket fd的就好了。这样,整个过程只在调用epoll的时候才会阻塞,收发客户消息是不会阻塞的。

:-D 搜索微信号(ID: 芋道源码 ),可以获得各种 Java 源码解析、原理讲解、面试题、学习指南。

:-D 并且,回复【 书籍 】后,可以领取笔者推荐的各种 Java 从入门到架构的 100 本书籍。

:-D 并且,回复【 技术群 】后,可以加入专门讨论 Java、后端、架构的技术群。

I. Redis常见延迟问题排查手册!附33条优化建议

Redis作为内存数据库,拥有非常高的性能,单个实例的QPS能够达到10W左右。但我们在使用Redis时,经常时不时会出现访问延迟很大的情况,如果你不知道Redis的内部实现原理,在排查问题时就会一头雾水。

很多时候,Redis出现访问延迟变大,都与我们的使用不当或运维不合理导致的。

下面我们就来分析一下Redis在使用过程中,经常会遇到的延迟问题以及如何定位和分析。

如果在使用Redis时,发现访问延迟突然增大,如何进行排查?

首先,第一步,建议你去查看一下Redis的慢日志。Redis提供了慢日志命令的统计功能,我们通过以下设置,就可以查看有哪些命令在执行时延迟比较大。

首先设置Redis的慢日志阈值,只有超过阈值的命令才会被记录,这里的单位是微妙,例如设置慢日志的阈值为5毫秒,同时设置只保留最近1000条慢日志记录:

# 命令执行超过5毫秒记录慢日志

CONFIG SET slowlog-log-slower-than 5000

# 只保留最近1000条慢日志

CONFIG SET slowlog-max-len 1000

设置完成之后,所有执行的命令如果延迟大于5毫秒,都会被Redis记录下来,我们执行SLOWLOG get 5查询最近5条慢日志:

127.0.0.1:6379> SLOWLOG get 5

1) 1) (integer) 32693 # 慢日志ID

2) (integer) 1593763337 # 执行时间

3) (integer) 5299 # 执行耗时(微妙)

4) 1) 'LRANGE' # 具体执行的命令和参数

2) 'user_list_2000'

3) Ɔ'

4) '-1'

2) 1) (integer) 32692

2) (integer) 1593763337

3) (integer) 5044

4) 1) 'GET'

2) 'book_price_1000'

...

通过查看慢日志记录,我们就可以知道在什么时间执行哪些命令比较耗时, 如果你的业务经常使用O(n)以上复杂度的命令, 例如sort、sunion、zunionstore,或者在执行O(n)命令时操作的数据量比较大,这些情况下Redis处理数据时就会很耗时。

如果你的服务请求量并不大,但Redis实例的CPU使用率很高,很有可能是使用了复杂度高的命令导致的。

解决方案就是,不使用这些复杂度较高的命令,并且一次不要获取太多的数据,每次尽量操作少量的数据,让Redis可以及时处理返回。

如果查询慢日志发现,并不是复杂度较高的命令导致的,例如都是SET、DELETE操作出现在慢日志记录中,那么你就要怀疑是否存在Redis写入了大key的情况。

Redis在写入数据时,需要为新的数据分配内存,当从Redis中删除数据时,它会释放对应的内存空间。

如果一个key写入的数据非常大,Redis 在分配内存时也会比较耗时。 同样的,当删除这个key的数据时, 释放内存也会耗时比较久。

你需要检查你的业务代码,是否存在写入大key的情况,需要评估写入数据量的大小,业务层应该避免一个key存入过大的数据量。

那么有没有什么办法可以扫描现在Redis中是否存在大key的数据吗?

Redis也提供了扫描大key的方法:

redis-cli -h $host -p $port --bigkeys -i 0.01

使用上面的命令就可以扫描出整个实例key大小的分布情况,它是以类型维度来展示的。

需要注意的是当我们在线上实例进行大key扫描时,Redis的QPS会突增,为了降低扫描过程中对Redis的影响,我们需要控制扫描的频率,使用-i参数控制即可,它表示扫描过程中每次扫描的时间间隔,单位是秒。

使用这个命令的原理,其实就是Redis在内部执行scan命令,遍历所有key,然后针对不同类型的key执行strlen、llen、hlen、scard、zcard来获取字符串的长度以及容器类型(list/dict/set/zset)的元素个数。

而对于容器类型的key,只能扫描出元素最多的key,但元素最多的key不一定占用内存最多,这一点需要我们注意下。不过使用这个命令一般我们是可以对整个实例中key的分布情况有比较清晰的了解。

针对大key的问题,Redis官方在4.0版本推出了lazy-free的机制,用于异步释放大key的内存,降低对Redis性能的影响。即使这样,我们也不建议使用大key,大key在集群的迁移过程中,也会影响到迁移的性能,这个后面在介绍集群相关的文章时,会再详细介绍到。

有时你会发现,平时在使用Redis时没有延时比较大的情况,但在某个时间点突然出现一波延时,而且 报慢的时间点很有规律,例如某个整点,或者间隔多久就会发生一次。

如果出现这种情况,就需要考虑是否存在大量key集中过期的情况。

如果有大量的key在某个固定时间点集中过期,在这个时间点访问Redis时,就有可能导致延迟增加。

Redis的过期策略采用主动过期+懒惰过期两种策略:

注意, Redis的主动过期的定时任务,也是在Redis主线程中执行的 ,也就是说如果在执行主动过期的过程中,出现了需要大量删除过期key的情况,那么在业务访问时,必须等这个过期任务执行结束,才可以处理业务请求。此时就会出现,业务访问延时增大的问题,最大延迟为25毫秒。

而且这个访问延迟的情况, 不会记录在慢日志里。 慢日志中 只记录真正执行某个命令的耗时 ,Redis主动过期策略执行在操作命令之前,如果操作命令耗时达不到慢日志阈值,它是不会计算在慢日志统计中的,但我们的业务却感到了延迟增大。

此时你需要检查你的业务,是否真的存在集中过期的代码,一般集中过期使用的命令是expireat或pexpireat命令,在代码中搜索这个关键字就可以了。

如果你的业务确实需要集中过期掉某些key,又不想导致Redis发生抖动,有什么优化方案?

解决方案是, 在集中过期时增加一个随机时间,把这些需要过期的key的时间打散即可。

伪代码可以这么写:

# 在过期时间点之后的5分钟内随机过期掉

redis.expireat(key, expire_time + random(300))

这样Redis在处理过期时,不会因为集中删除key导致压力过大,阻塞主线程。

另外,除了业务使用需要注意此问题之外,还可以通过运维手段来及时发现这种情况。

我们需要对这个指标监控,当在 很短时间内这个指标出现突增 时,需要及时报警出来,然后与业务报慢的时间点对比分析,确认时间是否一致,如果一致,则可以认为确实是因为这个原因导致的延迟增大。

有时我们把Redis当做纯缓存使用,就会给实例设置一个内存上限maxmemory,然后开启LRU淘汰策略。

当实例的内存达到了maxmemory后,你会发现之后的每次写入新的数据,有可能变慢了。

导致变慢的原因是,当Redis内存达到maxmemory后,每次写入新的数据之前,必须先踢出一部分数据,让内存维持在maxmemory之下。

这个踢出旧数据的逻辑也是需要消耗时间的,而具体耗时的长短,要取决于配置的淘汰策略:

具体使用哪种策略,需要根据业务场景来决定。

我们最常使用的一般是allkeys-lru或volatile-lru策略,它们的处理逻辑是,每次从实例中随机取出一批key(可配置),然后淘汰一个最少访问的key,之后把剩下的key暂存到一个池子中,继续随机取出一批key,并与之前池子中的key比较,再淘汰一个最少访问的key。以此循环,直到内存降到maxmemory之下。

如果使用的是allkeys-random或volatile-random策略,那么就会快很多,因为是随机淘汰,那么就少了比较key访问频率时间的消耗了,随机拿出一批key后直接淘汰即可,因此这个策略要比上面的LRU策略执行快一些。

但以上这些逻辑都是在访问Redis时,真正命令执行之前执行的,也就是它会影响我们访问Redis时执行的命令。

另外,如果此时Redis实例中有存储大key,那么在淘汰大key释放内存时,这个耗时会更加久,延迟更大,这需要我们格外注意。

如果你的业务访问量非常大,并且必须设置maxmemory限制实例的内存上限,同时面临淘汰key导致延迟增大的的情况,要想缓解这种情况,除了上面说的避免存储大key、使用随机淘汰策略之外,也可以考虑拆分实例的方法来缓解,拆分实例可以把一个实例淘汰key的压力分摊到多个实例上,可以在一定程度降低延迟。

如果你的Redis开启了自动生成RDB和AOF重写功能,那么有可能在后台生成RDB和AOF重写时导致Redis的访问延迟增大,而等这些任务执行完毕后,延迟情况消失。

遇到这种情况,一般就是执行生成RDB和AOF重写任务导致的。

生成RDB和AOF都需要父进程fork出一个子进程进行数据的持久化,在fork执行过程中,父进程需要拷贝内存页表给子进程,如果整个实例内存占用很大,那么需要拷贝的内存页表会比较耗时,此过程会消耗大量的CPU资源,在完成fork之前,整个实例会被阻塞住,无法处理任何请求,如果此时CPU资源紧张,那么fork的时间会更长,甚至达到秒级。这会严重影响Redis的性能。

具体原理也可以参考我之前写的文章:Redis持久化是如何做的?RDB和AOF对比分析。

我们可以执行info命令,查看最后一次fork执行的耗时latest_fork_usec,单位微妙。这个时间就是整个实例阻塞无法处理请求的时间。

除了因为备份的原因生成RDB之外,在 主从节点第一次建立数据同步时 ,主节点也会生成RDB文件给从节点进行一次全量同步,这时也会对Redis产生性能影响。

要想避免这种情况,我们需要规划好数据备份的周期,建议 在从节点上执行备份,而且最好放在低峰期执行。 如果对于丢失数据不敏感的业务,那么不建议开启AOF和AOF重写功能。

另外,fork的耗时也与系统有关,如果把Redis部署在虚拟机上,那么这个时间也会增大。所以使用Redis时建议部署在物理机上,降低fork的影响。

很多时候,我们在部署服务时,为了提高性能,降低程序在使用多个CPU时上下文切换的性能损耗,一般会采用进程绑定CPU的操作。

但在使用Redis时,我们不建议这么干,原因如下。

绑定CPU的Redis,在进行数据持久化时,fork出的子进程,子进程会继承父进程的CPU使用偏好,而此时子进程会消耗大量的CPU资源进行数据持久化,子进程会与主进程发生CPU争抢,这也会导致主进程的CPU资源不足访问延迟增大。

所以在部署Redis进程时,如果需要开启RDB和AOF重写机制,一定不能进行CPU绑定操作!

上面提到了,当执行AOF文件重写时会因为fork执行耗时导致Redis延迟增大,除了这个之外,如果开启AOF机制,设置的策略不合理,也会导致性能问题。

开启AOF后,Redis会把写入的命令实时写入到文件中,但写入文件的过程是先写入内存,等内存中的数据超过一定阈值或达到一定时间后,内存中的内容才会被真正写入到磁盘中。

AOF为了保证文件写入磁盘的安全性,提供了3种刷盘机制:

当使用第一种机制appendfsync always时,Redis每处理一次写命令,都会把这个命令写入磁盘,而且 这个操作是在主线程中执行的。

内存中的的数据写入磁盘,这个会加重磁盘的IO负担,操作磁盘成本要比操作内存的代价大得多。如果写入量很大,那么每次更新都会写入磁盘,此时机器的磁盘IO就会非常高,拖慢Redis的性能,因此我们不建议使用这种机制。

与第一种机制对比,appendfsync everysec会每隔1秒刷盘,而appendfsync no取决于操作系统的刷盘时间,安全性不高。因此我们推荐使用appendfsync everysec这种方式,在最坏的情况下,只会丢失1秒的数据,但它能保持较好的访问性能。

当然,对于有些业务场景,对丢失数据并不敏感,也可以不开启AOF。

如果你发现Redis突然变得非常慢, 每次访问的耗时都达到了几百毫秒甚至秒级 ,那此时就检查Redis是否使用到了Swap,这种情况下Redis基本上已经无法提供高性能的服务。

我们知道,操作系统提供了Swap机制,目的是为了当内存不足时,可以把一部分内存中的数据换到磁盘上,以达到对内存使用的缓冲。

但当内存中的数据被换到磁盘上后,访问这些数据就需要从磁盘中读取,这个速度要比内存慢太多!

尤其是针对Redis这种高性能的内存数据库来说,如果Redis中的内存被换到磁盘上,对于Redis这种性能极其敏感的数据库,这个操作时间是无法接受的。

我们需要检查机器的内存使用情况,确认是否确实是因为内存不足导致使用到了Swap。

如果确实使用到了Swap,要及时整理内存空间,释放出足够的内存供Redis使用,然后释放Redis的Swap,让Redis重新使用内存。

释放Redis的Swap过程通常要重启实例,为了避免重启实例对业务的影响,一般先进行主从切换,然后释放旧主节点的Swap,重新启动服务,待数据同步完成后,再切换回主节点即可。

可见,当Redis使用到Swap后,此时的Redis的高性能基本被废掉,所以我们需要提前预防这种情况。

我们需要对Redis机器的内存和Swap使用情况进行监控,在内存不足和使用到Swap时及时报警出来,及时进行相应的处理。

如果以上产生性能问题的场景,你都规避掉了,而且Redis也稳定运行了很长时间,但在某个时间点之后开始,访问Redis开始变慢了,而且一直持续到现在,这种情况是什么原因导致的?

之前我们就遇到这种问题, 特点就是从某个时间点之后就开始变慢,并且一直持续。 这时你需要检查一下机器的网卡流量,是否存在网卡流量被跑满的情况。

网卡负载过高,在网络层和TCP层就会出现数据发送延迟、数据丢包等情况。Redis的高性能除了内存之外,就在于网络IO,请求量突增会导致网卡负载变高。

如果出现这种情况,你需要排查这个机器上的哪个Redis实例的流量过大占满了网络带宽,然后确认流量突增是否属于业务正常情况,如果属于那就需要及时扩容或迁移实例,避免这个机器的其他实例受到影响。

运维层面,我们需要对机器的各项指标增加监控,包括网络流量,在达到阈值时提前报警,及时与业务确认并扩容。

以上我们总结了Redis中常见的可能导致延迟增大甚至阻塞的场景,这其中既涉及到了业务的使用问题,也涉及到Redis的运维问题。

可见,要想保证Redis高性能的运行,其中涉及到CPU、内存、网络,甚至磁盘的方方面面,其中还包括操作系统的相关特性的使用。

作为开发人员,我们需要了解Redis的运行机制,例如各个命令的执行时间复杂度、数据过期策略、数据淘汰策略等,使用合理的命令,并结合业务场景进行优化。

作为DBA运维人员,需要了解数据持久化、操作系统fork原理、Swap机制等,并对Redis的容量进行合理规划,预留足够的机器资源,对机器做好完善的监控,才能保证Redis的稳定运行。

在上文中,主要讲解了 Redis 常见的导致变慢的场景以及问题定位和分析,主要是由业务使用不合理和运维不当导致的。

J. 普罗米修斯监控能否监控redis耗时

普罗米修斯(Prometheus)是一个开源的监控系统,可以用于监控各种应用和服务的性能和状态。

Prometheus 可以通过对应用程序或服务的监控指标进行采集,来监控应用程序或服务的性能和状态。这些监控指标可以是应用程序或服务的 CPU 利用率、内存使用情况、网络流量等。

因此,如果要监控 Redis 的耗时,可以使用 Prometheus 采集 Redis 的监控指标,包括 Redis 命令的执行时间、网络流量等。这样,就可以通过 Prometheus 监控 Redis 的性能和状态,并发现可能的性能瓶颈。

此外,Prometheus 还可以与其他监控工具集成,如 Grafana、Zabbix 等,以提供更为丰富的监控功能。

希望这些信息能帮助您了解 Prometheus 监控 Redis 的情况。

阅读全文

与redis网络cpu连接监控相关的资料

热点内容
家里wifi浏览网站网络慢 浏览:579
我的网络的ld在哪里 浏览:628
手机卡用不了网络是什么原因 浏览:863
苹果54g卡能用3g网络吗 浏览:291
怎么设置网络打印机的地址 浏览:484
windows7无法连接网络 浏览:623
手抄报网络安全五年级图片 浏览:346
网络办公室归属哪个部门 浏览:937
内蒙古广电网络怎么连 浏览:793
铜陵白姜网络营销有哪些 浏览:745
如何引导网络作业 浏览:142
无线网络账户信息会被盗用吗 浏览:499
网络营销被封号 浏览:526
哪个大学设有网络和新媒体专业 浏览:653
索尼z2设置网络接入点 浏览:12
没有路由器如何设置以太网络 浏览:535
徐正溪周洁琼电视重置无线网络适配器 浏览:68
台式电脑wifi网络延迟怎么办 浏览:491
无网络地区能安装wifi吗 浏览:181
宿舍没有电脑怎么装无线网络 浏览:382

友情链接