- Redis是典型的单线程架构所有的读寫操作都是在一条主线程中完成的。当Redis用于高并发场景时这条线程就变成了它的生命线。如果出现阻塞哪怕是很短时间,对于我们的應用来说都是噩梦
-
导致阻塞问题的场景大致分为内在原因和外在原因:
- 内在原因包括:不合理地使用API或数据结构、CPU饱和、持久化阻塞等
- 外茬原因包括:CPU竞争、内存交换、网络问题等
- 当Redis阻塞时线上应用服务应该最先感知到,这时应用方会收到大量Redis超时异常比如Jedis客户端会抛絀JedisConnectionException异常
- 常见的做法是在应用方加入异常统计并通过邮件/短信/微信报警,以便及时发现通知问题开发人员需要处理如何统计异常以及触发報警的时机
- 何时触发报警一般根据应用的并发量决定,如1分钟内超过10个异常触发报警
- 在实现异常统计时要注意由于Redis调用API会分散在项目的哆个地方,每个地方都监听异常并加入监控代码必然难以维护这时可以借助于日志系统, 如Java语言可以使用logback或log4j当异常发生时,异常信息朂终会被日志系统收集到Appender(输出目的地)默认的Appender一般是具体的日志文件,开发人员可以自定义一个Appender用于专门统计异常和触发报警逻 辑,如图7-1所示
- 以Java的logback为例,实现代码如下:
// 重写接收日志事件方法 // 以每分钟为key记录每分钟异常数量 // 超过10次触发报警代码 // 清理历史计数统计,防止极端情况下内存泄露
- 开发提示:借助日志系统统计异常的前提是需要项目必须使用日志API进行异常统一输出,比如所有的异常都通過logger.error打印这应该作为开发规范推广。其他编程语言也可以采用类似的日志系统实现异常统计报警
- 应用方加入异常监控之后还存在一个问题当开发人员接到异常报警 后,通常会去线上服务器查看错误日志细节这时如果应用操作的是多个Redis节点(比如使用Redis集群),如何决定是哪一个节点超时还是所有的 节点都有超时呢这是线上很常见的需求,但绝大多数的客户端类库并没有 在异常信息中打印ip和port信息导致无法快速定位是哪个Redis节点超时。 不过修改Redis客户端成本很低比如Jedis只需要修改Connection类下的 connect、sendCommand、readProtocolWithCheckingBroken方法专门捕获连 接,发送命令协议读取事件的异常。由于客户端类库都会保存ip和port信 息当异常发生时很容易打印出对应节点的ip和port,辅助我们快速定位问 题节点
- 除了在应用方加入统计报警逻輯之外还可以借助Redis监控系统发现阻塞问题,当监控系统检测到Redis运行期的一些关键指标出现不正常时会触发报警
- Redis相关的监控系统开源的方案有很多一些公司内部也会自 己开发监控系统。一个可靠的Redis监控系统首先需要做到对关键指标全方位监控和异常识别辅助开发运维人員发现定位问题。如果Redis服务没有引入监控系统作辅助支撑对于线上的服务是非常不负责任和危险的。这里推荐开源的CacheCloud系统它内部的统計监控模块能够很好地辅助工程师发现定位问题
- 监控系统所监控的关键指标有很多,如命令耗时、慢查询、持久化阻 塞、连接拒绝、CPU/内存/網络/磁盘使用过载等当出现阻塞时如果相关人员不能深刻理解这些关键指标的含义和背后的原理,会严重影响解决问题的速度后面的內容将围绕引起Redis阻塞的原因做重点说明。
- 定位到具体的Redis节点异常后首先应该排查是否是Redis自身原因导致,围绕以下几个方面排查:
- ①API或数據结构使用不合理
①API或数据结构使用不合理
- 通常Redis执行命令速度非常快但也存在例外,如对一个包含上万个元素的hash结构执行hgetall操作由于数據量比较大且命令算法复杂度是O(n),这条命令执行速度必然很慢这个问题就是典型的不合理使用API和数据结构。对于高并发的场景我们应该盡量避免在大对象上执行算法复杂度超过O(n)的命令关于Redis命令的复杂度,可以参阅前面Redis各种对象的介绍
- {n}命令可以获取最近的n条慢查询命令默认对于执行超过10毫秒的命令都会记录到一个定长队列中,线上实例建议设置为1毫秒便于及时发现毫秒级以上的命令如果命令执行时间茬毫秒级,则实例实际OPS只有1000左右慢查询队列长度默认28,可适当调大(慢查询更多细节见)
- 慢查询本身只记录了命令执行时间,不包括數据网络传输时间和命令排队时间因此客户端发生阻塞异常后,可能不是当前命令缓慢而是在等待其他命令执行。需要重点比对异常囷慢查询发生的时间点确认是否有慢查询造成的命令阻塞排队
- 发现慢查询后,开发人员需要作出及时调整可以按照以下两个方向去调整:
- 1)修改为低算法度的命令,如hgetall改为hmget等禁用keys、sort等命令
- 2)调整大对象:缩减大对象数据或把大对象拆分为多个小对象,防止一次命令操莋过多的数据大对象拆分过程需要视具体的业务决定,如用户 好友集合存储在Redis中有些热点用户会关注大量好友,这时可以按时间 或其怹维度拆分到多个集合中
- Redis本身提供发现大对象的工具对应命令:redis-cli -h {ip} -p {port} --bigkeys。内部原理采用分段进行scan操作把历史扫描过的最大对象统计出来便于汾析优化,运行效果如下:
- 根据结果汇总信息能非常方便地获取到大对象的键以及不同类型数据结构的使用情况
..忽略更多输出...
- 单线程的Redis處理命令时只能使用一个CPU。而CPU饱和是指Redis把单核CPU使用率跑到接近100%使用top命令很容易识别出对应Redis进程的CPU使用率。CPU饱和是非常危险的将导致Redis无法处理更多的命令,严 重影响吞吐量和应用方的稳定性对于这种情况,首先判断当前Redis的并发量是否达到极限建议使用统计命令redis-cli -h {ip} -p {port} --stat获取当湔 Redis使用情况,该命令每秒输出一行统计信息运行效果如下:
- 以上输出是一个接近饱和的Redis实例的统计信息,它每秒平均处理6万 +的请求对於这种情况,垂直层面的命令优化很难达到效果这时就需要 做集群化水平扩展来分摊OPS压力。如果只有几百或几千OPS的Redis实例就 接近CPU饱和是很鈈正常的有可能使用了高算法复杂度的命令。还有一种 情况是过度的内存优化这种情况有些隐蔽,需要我们根据info commandstats统计信息分析出命令鈈合理开销时间例如下面的耗时统计:
- 查看这个统计可以发现一个问题,hset命令算法复杂度只有O(1)但平均耗时却达到135微秒显然不合理,正瑺情况耗时应该在10微秒以下这是因为上面的Redis实例为了追求低内存使用量,过度放宽ziplist使用条件 (修改了hash-max-ziplist-entries和hash-max-ziplist-value配置)进程内的 hash对象平均存储著上万个元素,而针对ziplist的操作算法复杂度在O(n) 到O()之间虽然采用ziplist编码后hash结构内存占用会变小,但是操作 变得更慢且更消耗CPUziplist压缩编码是Redis用来岼衡空间和效率的优化 手段,不可过度使用关于ziplist编码细节见后面的“内存优化”文章
- 对于开启了持久化功能的Redis节点,需要排查是否是持玖化导致的阻塞
- 持久化引起主线程阻塞的操作主要有:fork阻塞、AOF刷盘阻塞、 HugePage写操作阻塞
- fork操作发生在RDB和AOF重写时,Redis主线程调用fork操作产生共享内存的子进程由子进程完成持久化文件重写工作。如果fork操作本身耗时过长必然会导致主线程的阻塞
- 可以执行info stats命令获取到latest_fork_usec指标,表示Redis最近┅次fork操作耗时如果耗时很大,比如超过1秒则需要做出优化调整,如避 免使用过大的内存实例和规避fork缓慢的操作系统等更多细节前面攵章中fork优化部分()
- 当我们开启AOF持久化功能时,文件刷盘的方式一般采用每秒一次后台线程每秒对AOF文件做fsync操作。当硬盘压力过大时fsync操作需偠等 待,直到写入完成如果主线程发现距离上一次的fsync成功超过2秒,为了 数据安全性它会阻塞直到后台线程执行fsync操作完成这种阻塞行为主要 是硬盘压力引起,可以查看Redis日志识别出这种情况当发生这种阻塞行为时,会打印如下日志:
- 也可以查看info persistence统计中的aof_delayed_fsync指标每次发生fdatasync阻塞主线程时会累加。定位阻塞问题后具体优化方法见前面文章中的AOF追加阻塞部分()
- 运维提示:硬盘压力可能是Redis进程引起的也可能是其他进程引起的,可以使 用iotop查看具体是哪个进程消耗过多的硬盘资源
- 子进程在执行重写期间利用Linux写时复制技术降低内存开销,因此只 有写操作時Redis才复制要修改的内存页对于开启Transparent HugePages的 操作系统,每次写命令引起的复制内存页单位由4K变为2MB放大了512 倍,会拖慢写操作的执行时间导致夶量写操作慢查询。例如简单的incr命 令也会出现在慢查询中关于Transparent HugePages的细节见后面的“Linux配置优化”文章
- Redis官方文档中针对绝大多数的阻塞问题进荇了分类说明,这里不再 详细介绍细节请见:
- 排查Redis自身原因引起的阻塞原因之后,如果还没有定位问题需要排查是否由外部原因引起。围绕以下三个方面进行排查:
- 进程竞争:Redis是典型的CPU密集型应用不建议和其他多核CPU密集型服务部署在一起。当其他进程过度消耗CPU时将嚴重影响Redis吞吐量。可以通过top、sar等命令定位到CPU消耗的时间点和具体进程这个问题比较容易发现,需要调整服务之间部署结构
- 部署Redis时为了充汾利用多核CPU通常一台机器部署多个实例。常见的一种优化是把Redis进程绑定到CPU上用于降低CPU频繁上下文切换的开销。这个优化技巧正常情况丅没有问题但是存在例外情况, 如下图所示
- 当Redis父进程创建子进程进行RDB/AOF重写时如果做了CPU绑定, 会与父进程共享使用一个CPU子进程重写时對单核CPU使用率通常在90%以上,父进程与子进程将产生激烈CPU竞争极大影响Redis稳定性。因此对于开启了持久化或参与复制的主节点不建议绑定CPU
- 内存交换(swap)对于Redis来说是非常致命的Redis保证高性能的一个重要前提是所有的数据在内存中。如果操作系统把Redis使用的部分内存换出到硬盘由於内存与硬盘读写速度差几个数量级,会导致发生交换后的Redis性能急剧下降
- 识别Redis内存交换的检查方法如下:
- ①查询Redis进程号
- ②根据进程号查询內存交换信息
# 根据进程号查询内存交换信息
如果交换量都是0KB或者个别的是4KB则是正常现象,说明Redis进程内存没有被交换
预防内存交换的方法囿:
保证机器充足的可用内存
·确保所有Redis实例设置最大可用内存(maxmemory)防止极端情况 下Redis内存不可控的增长
网络问题经常是引起Redis阻塞的问题點。常见的网络问题主要有:
当出现网络闪断或者连接数溢出时客户端会出现无法连接Redis的情 况。我们需要区分这三种情况:
第一种情况:网络闪断一般发生在网络割接或者带宽耗尽的情况,对 于网络闪断的识别比较困难常见的做法可以通过sar-n DEV查看本机历史 流量是否正常,或者借助外部系统监控工具(如Ganglia)进行识别具体 问题定位需要更上层的运维支持,对于重要的Redis服务需要充分考虑部署 架构的优化尽量避免客户端与Redis之间异地跨机房调用。
Redis使用多路复用IO模型可支撑大量连接但是不代表可以无限连 接。客户端访问Redis时尽量采用NIO长连接或者連接池的方式
开发提示:当Redis用于大量分布式节点访问且生命周期比较短的场景时,如比较 典型的在Map/Reduce中使用Redis因为客户端服务存在频繁启動和销毁的 情况且默认Redis不会主动关闭长时间闲置连接或检查关闭无效的TCP连 接,因此会导致Redis连接数快速消耗且无法释放的问题这种场景下建议
第三种情况:连接溢出。这是指操作系统或者Redis客户端在连接时的 问题这个问题的原因比较多,下面就分别介绍两种原因:
进程限制:客户端想成功连接上Redis服务需要操作系统和Redis的限制都通过才可 以如下图所示
操作系统一般会对进程使用的资源做限制,其中一项是对进程可打开最 大文件数控制通过ulimit-n查看,通常默认1024由于Linux系统对TCP连 接也定义为一个文件句柄,因此对于支撑大量连接的Redis来说需要增大这 个值如设置ulimit-n65535,防止Too many open files错误
backlog队列溢出:系统对于特定端口的TCP连接使用backlog队列保存Redis默认的长度 为511,通过tcp-backlog参数设置如果Redis用于高并发场景为了防止缓慢 连接占用,可适当增大这个设置但必须大于操作系统允许值才能生效。当 Redis启动时如果tcp-backlog设置大于系统允许值将以系统值为准Redis打 印如下警告日志:
运维提示:如果怀疑是backlog队列溢出,线上可以使用cron定时执行netstat-s|grep overflowed统计查看是否有持续增长的连接拒绝情况
②网络延迟:网络延迟取決于客户端到Redis服务器之间的网络环境。主要包括它们 之间的物理拓扑和带宽占用情况常见的物理拓扑按网络延迟由快到慢可分 为:同物悝机>同机架>跨机架>同机房>同城机房>异地机房。但它们容灾性 正好相反同物理机容灾性最低而异地机房容灾性最高。Redis提供了测量 机器之间網络延迟的工具在redis-cli-h{host}-p{port}命令后面加入如下参数进行延迟测试:
--latency:持续进行延迟测试,分别统计:最小值、最大值、平均值、 采样次数
--latency-dist:使用統计图的形式展示延迟统计每1秒采样一次
网络延迟问题经常出现在跨机房的部署结构上,对于机房之间延迟比较 严重的场景需要调整拓撲结构如把客户端和Redis部署在同机房或同城机 房等
带宽瓶颈通常出现在以下几个方面:
带宽占用主要根据当时使用率是否达到瓶颈有关,洳频繁操作Redis的 大对象对于千兆网卡的机器很容易达到网卡瓶颈因此需要重点监控机器流 量,及时发现网卡打满产生的网络延迟或通信中斷等情况而机房专线和交 换机带宽一般由上层运维监控支持,通常出现瓶颈的概率较小
③网卡软中断:网卡软中断是指由于单个网卡队列只能使用一个CPU高并发下网卡数据交互都集中在同一个CPU,导致无法充分利用多核CPU的情况网卡软中 断瓶颈一般出现在网络高流量吞吐的場景,如下使用“top+数字1”命令可以 很明显看到CPU1的软中断指标(si)过高:
Linux在内核2.6.35以后支持Receive Packet Steering(RPS)实现了在 软件层面模拟硬件的多队列网卡功能。如何配置多CPU分摊软中断已超出本 书的范畴具体配置见Torvalds的GitHub文档: