Redis是典型的单线程架构, 所有的读写操作都是在一条主线程中完成的。 当Redis用于高并发场景时, 这条线程就变成了它的生命线。 如果出现阻塞, 哪怕是很短时间, 对于我们的应用来说都是噩梦。 导致阻塞问题的场景大致分为内在原因和外在原因:
·内在原因包括: 不合理地使用API或数据结构、 CPU饱和、 持久化阻塞等。
·外在原因包括: CPU竞争、 内存交换、 网络问题等。
发现阻塞
当Redis阻塞时, 线上应用服务应该最先感知到, 这时应用方会收到大量Redis超时异常, 比如Jedis客户端会抛出JedisConnectionException异常。 常见的做法是在应用方加入异常统计并通过邮件/短信/微信报警, 以便及时发现通知问题。
可以借助于日志系统,如Java语言可以使用logback或log4j。 当异常发生时, 异常信息最终会被日志系统收集到Appender(输出目的地) , 默认的Appender一般是具体的日志文件, 开发人员可以自定义一个Appender, 用于专门统计异常和触发报警逻辑。
如果应用操作的是多个Redis节点(比如使用Redis集群),那么我们就需要找到对应报错的redis节点。但绝大多数的客户端类库并没有在异常信息中打印ip和port信息, 导致无法快速定位是哪个Redis节点超时。不过修改Redis客户端成本很低, 比如Jedis只需要修改Connection类下的
connect、 sendCommand、 readProtocolWithCheckingBroken方法专门捕获连接, 发送命令, 协议读取事件的异常。
也可以选择redis监控系统来发现阻塞问题,可以机子公司开发或者是借助开源的监控系统,例如CacheCloud。
监控系统所监控的关键指标有很多, 如命令耗时、 慢查询、 持久化阻塞、 连接拒绝、 CPU/内存/网络/磁盘使用过载等。
内在原因
·API或数据结构使用不合理。
·CPU饱和的问题。
·持久化相关的阻塞。
API或数据结构使用不合理
对于高并发的场景我们应该尽量避免在大对象上执行算法复杂度超过O(n) 的命令。例如在一个大的hash对象上执行hgetall命令。
1.发现慢查询
Redis原生提供慢查询统计功能, 执行slowlog get{n}命令可以获取最近的n条慢查询命令, 默认对于执行超过10毫秒的命令都会记录到一个定长队列中, 线上实例建议设置为1毫秒便于及时发现毫秒级以上的命令。
2.如何发现大对象
Redis本身提供发现大对象的工具, 对应命令: redis-cli-h{ip}-p{port}bigkeys。 内部原理采用分段进行scan操作, 把历史扫描过的最大对象统计出来便于分析优化。
CPU饱和
单线程的Redis处理命令时只能使用一个CPU。 而CPU饱和是指Redis把单核CPU使用率跑到接近100%。 使用top命令很容易识别出对应Redis进程的CPU使用率。 CPU饱和是非常危险的, 将导致Redis无法处理更多的命令, 严重影响吞吐量和应用方的稳定性。 对于这种情况, 首先判断当前Redis的并发量是否达到极限, 建议使用统计命令redis-cli-h{ip}-p{port}–stat获取当前Redis使用情况
持久化阻塞
对于开启了持久化功能的Redis节点, 需要排查是否是持久化导致的阻塞。 持久化引起主线程阻塞的操作主要有: fork阻塞、 AOF刷盘阻塞、HugePage写操作阻塞。
1.fork阻塞
fork操作发生在RDB和AOF重写时, Redis主线程调用fork操作产生共享内存的子进程, 由子进程完成持久化文件重写工作。 如果fork操作本身耗时过长, 必然会导致主线程的阻塞。
2.AOF刷盘阻塞
当我们开启AOF持久化功能时, 文件刷盘的方式一般采用每秒一次, 后台线程每秒对AOF文件做fsync操作。 当硬盘压力过大时, fsync操作需要等待, 直到写入完成。 如果主线程发现距离上一次的fsync成功超过2秒, 为了数据安全性它会阻塞直到后台线程执行fsync操作完成。 这种阻塞行为主要是硬盘压力引起。
3.HugePage写操作阻塞
子进程在执行重写期间利用Linux写时复制技术降低内存开销, 因此只有写操作时Redis才复制要修改的内存页。 对于开启Transparent HugePages的操作系统, 每次写命令引起的复制内存页单位由4K变为2MB, 放大了512倍, 会拖慢写操作的执行时间, 导致大量写操作慢查询。
外在原因
·CPU竞争
·内存交换
·网络问题
CPU竞争
·进程竞争: Redis是典型的CPU密集型应用, 不建议和其他多核CPU密集型服务部署在一起。 当其他进程过度消耗CPU时, 将严重影响Redis吞吐量。 可以通过top、 sar等命令定位到CPU消耗的时间点和具体进程, 这个问题比较容易发现, 需要调整服务之间部署结构。
·绑定CPU: 部署Redis时为了充分利用多核CPU, 通常一台机器部署多个实例。 常见的一种优化是把Redis进程绑定到CPU上, 用于降低CPU频繁上下文切换的开销。
内存交换
内存交换(swap) 对于Redis来说是非常致命的, Redis保证高性能的一个重要前提是所有的数据在内存中。 如果操作系统把Redis使用的部分内存换出到硬盘, 由于内存与硬盘读写速度差几个数量级, 会导致发生交换后的Redis性能急剧下降。
1) 查询Redis进程号
2) 根据进程号查询内存交换信息
如果交换量都是0KB或者个别的是4KB, 则是正常现象, 说明Redis进程内存没有被交换。 预防内存交换的方法有:
·保证机器充足的可用内存。
·确保所有Redis实例设置最大可用内存(maxmemory) , 防止极端情况下Redis内存不可控的增长。
·降低系统使用swap优先级, 如echo10>/proc/sys/vm/swappiness。
网络问题
网络问题经常是引起Redis阻塞的问题点。 常见的网络问题主要有: 连接拒绝、 网络延迟、 网卡软中断等。
1.连接拒绝
当出现网络闪断或者连接数溢出时, 客户端会出现无法连接Redis的情况。 我们需要区分这三种情况: 网络闪断、 Redis连接拒绝、 连接溢出。
第一种情况: 网络闪断。 一般发生在网络割接或者带宽耗尽的情况, 对于网络闪断的识别比较困难, 常见的做法可以通过sar-n DEV查看本机历史流量是否正常, 或者借助外部系统监控工具(如Ganglia) 进行识别。 具体问题定位需要更上层的运维支持, 对于重要的Redis服务需要充分考虑部署架构的优化, 尽量避免客户端与Redis之间异地跨机房调用。
第二种情况: Redis连接拒绝。
Redis通过maxclients参数控制客户端最大连接数, 默认10000。 当Redis连接数大于maxclients时会拒绝新的连接进入,info stats的rejected_connections统计指标记录所有被拒绝连接的数量。Redis使用多路复用IO模型可支撑大量连接, 但是不代表可以无限连接。 客户端访问Redis时尽量采用NIO长连接或者连接池的方式。
第三种情况: 连接溢出。 这是指操作系统或者Redis客户端在连接时的问题。 这个问题的原因比较多, 下面就分别介绍两种原因: 进程限制、backlog队列溢出。
(1) 进程限制
客户端想成功连接上Redis服务需要操作系统和Redis的限制都通过才可以。操作系统一般会对进程使用的资源做限制, 其中一项是对进程可打开最大文件数控制, 通过ulimit-n查看, 通常默认1024。 由于Linux系统对TCP连接也定义为一个文件句柄, 因此对于支撑大量连接的Redis来说需要增大这
个值, 如设置ulimit-n65535, 防止Too many open files错误。
(2) backlog队列溢出
系统对于特定端口的TCP连接使用backlog队列保存。 Redis默认的长度为511, 通过tcp-backlog参数设置。 如果Redis用于高并发场景为了防止缓慢连接占用, 可适当增大这个设置, 但必须大于操作系统允许值才能生效。 当Redis启动时如果tcp-backlog设置大于系统允许值将以系统值为准。
2.网络延迟
3.网卡软中断