ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Redis超时排查实战:从连接池到网络链路的全根因定位

Redis超时排查实战:从连接池到网络链路的全根因定位 上周三下午两点多我正开着会监控大屏突然一片飘红。HoRain云上一套自建的Redis哨兵集群超时率从0.1%一路拉高到35%紧接着业务方反馈游戏更新器响应超时、下载中断后台日志里刷满了io.lettuce.core.RedisCommandTimeoutException。那一下午的排查经历让我意识到Redis超时的问题光会看报错根本不够得有一条清晰的排查主线才能从一堆现象里把真正根因拎出来。这篇东西不是教科书是我在HoRain云环境里处理Redis超时问题的实战记录。里面覆盖了客户端连接池、超时参数、服务端慢命令、网络链路、虚拟化CPU抢占、主从切换等常见根因还搭配了几个典型故障案例和排查命令希望能帮后端开发和运维的同学省点弯路。不论你用的是Jedis还是Lettuce不管Redis是物理机部署、虚拟机还是容器只要超时问题出现过这篇文章的排查思路大概率对得上。1. Redis超时问题全景报错形态与根因分类1.1 三种最常见的报错形态排查Redis超时第一件事是看报错的“主语”是谁。同一个“超时”两个字背后可能是完全不同的故障点。连接超时典型报错是connect timed out、Could not get a resource from the pool或者Linux经典的127秒SYN超时。这类报错说明客户端压根没和Redis建立好TCP连接链路在三次握手阶段就断了。常见原因包括安全组没放通、Redis绑定地址不对、服务端tcp-backlog设太小导致accept队列溢出以及网络层面路由不通。判断的关键是报错发生在连接建立之前耗时通常等于连接超时阈值。读写超时典型报错是RedisCommandTimeoutException、socket read timed out。连接已经建好了但命令发出去之后迟迟等不到响应。这种就复杂了可能是Redis实例确实慢慢查询、大key、AOF刷盘阻塞也可能是网络丢包导致数据在传输过程中重传还可能是客户端线程池/事件循环被阻塞命令根本没发出去。响应异常断开报错经常连着Go客户端或者curl时出现比如curl (56) Recv failure: Operation timed out或者下载速度卡在00 kib/s然后报“预期数据长度未收到”。这类问题的特点是连接状态不稳定数据传一半断了常见于服务端超时主动断开、Redis输出缓冲区超限或者网络路径上有设备做了RST。还有个容易被忽略的空闲连接被服务端/防火墙提前断开。客户端从连接池里拿到一个很久没用的连接发命令才发现连接已经死了表现为偶发的超时。这本质上是连接保活策略问题不是Redis性能问题。1.2 超时根因的分层框架把报错形态记牢之后往里套根因就有框架了。我从接触Redis超时排查开始一直用的是四层定位法客户端侧连接池耗尽、超时参数不合理、线程池阻塞、GC停顿实例侧CPU跑满、内存碎片化、AOF/RDB持久化阻塞、慢命令、大key网络侧丢包、重传、带宽打满、MTU问题、防火墙丢包架构侧主从切换、拓扑感知失效、跨机房延迟、集群分片不均排查顺序不固定但大原则是先看服务端到底有没有慢再判断是不是客户端或网络的问题。你如果一上来就疯狂优化连接池参数结果发现是Redis在跑全量RDB fork导致CPU毛刺那纯粹是浪费感情。1.3 被反复误判的“伪超时”另外必须提醒一句不是所有Redis超时都是Redis问题。有一类场景我处理过好多次——业务方报“Redis超时”结果一查是服务实例所在的宿主机CPU steal飙高、磁盘io卡顿Java进程根本没被调度到线程卡在请求Redis之前就已经被阻塞了。也就是说Redis很无辜问题出在应用服务器的资源竞争上。所以排查超时我习惯先看一眼应用服务本身的负载和GC情况再开始查Redis。这一小步能过滤掉大量“伪超时”误报。2. 客户端排查与参数调优先看连接池和超时配置2.1 连接池耗尽最常见的“假性超时”先说一个我踩过好几次的坑。线上业务突然大面积超时看Redis实例CPU不高、慢日志几乎没有但客户端抛的却是Could not get a resource from the pool。这种十有八九是连接池被打满了。以Jedis为例核心参数是这几个参数默认值推荐值说明maxTotal8业务峰值QPS的合理倍数连接池最大连接数太高浪费fd太低容易排队maxIdle8与maxTotal一致最大空闲连接数建议别让空闲连接频繁销毁重建minIdle02~4保持的最小空闲连接防止流量尖峰时临时建连maxWaitMillis-1(无限等待)200~500ms获取连接的最大等待时间设为-1会导致线程无限阻塞blockWhenExhaustedtruetrue连接耗尽时是否阻塞等待testOnBorrowfalsefalse借用时是否检测连接可用性开启有额外开销连接池逻辑说白了就是一个信号量业务线程来借连接池子里有就借给你没有就排队等池子归还。如果等的时间超过maxWaitMillis直接抛超时异常。这种情况下Redis本身是健康的是连接不够分。排查方法很简单抓一份线程栈如果大量线程卡在jedis相关堆栈的borrowObject上那基本实锤。再配合客户端监控里的连接池活跃数曲线看是不是涨到了maxTotal上限。2.2 超时参数设置别把“超时”当盾牌客户端超时参数有讲究Jedis和Lettuce还不一样。Jedis的经典写法是new JedisPool(poolConfig, host, port, timeout)老版本这个timeout同时作用于连接建立和读写是个粗粒度的总超时。新版Jedis已经拆分出connectionTimeout和socketTimeout这更科学。建议连接超时设100~300ms读写超时设500ms~1s具体取决于业务容忍度。Lettuce这边默认的timeout属性是命令超时command timeout另外还有connectTimeout控制建连空闲超时由ioTimeout控制。我在HoRain云上遇到过的RedisCommandTimeoutException大部分都是默认命令超时太短被触发默认值一般是10s但如果你用Spring Data Redis的spring.redis.timeout不设好很容易掉进“默认值兜底”的坑里。一个最关键的实践经验超时阈值必须和业务时延分布对齐。如果P99命令耗时是10ms超时设100ms就很健康如果P99已经到200ms了超时设100ms那是必然雪崩。所以调整超时前先跑一天的慢日志和耗时分布统计。2.3 慢命令与bigkey超时背后的隐形杀手客户端参数再合理也架不住Redis本身执行命令要10秒。慢命令和大key是超时问题最常见的“内鬼”。有一次业务反馈偶发超时我查Redis的slowlog发现HGETALL一条命令执行了800ms再一查key是一个包含几十万字段的hash。每条命令都这么慢Redis是单线程模型所有客户端都在这个命令后面排队结果就是集体超时。用SLOWLOG GET和SLOWLOG LEN就能定位redis-cli -h redis-host -p 6379 slowlog get 50 redis-cli -h redis-host -p 6379 slowlog len再看INFO COMMANDSTATS里各命令的调用次数和耗时累计redis-cli -h redis-host -p 6379 info commandstats对于大key用--bigkeys参数扫描一下几秒钟就能给出大key分布提示redis-cli -h redis-host -p 6379 --bigkeys处理思路通常是拆分大hash、限制HGETALL这类全量操作、或者把大value挪到单独的缓存服务里。别指望调大超时能解决慢命令在生产上是定时炸弹。3. 服务端与网络侧深度排查从实例内部到链路全局3.1 慢日志与延迟监控先给Redis“定责”排查超时不能只停留在客户端“感觉慢”要用数据给Redis定责。三个命令依次跑一遍redis-cli -h redis-host -p 6379 info clients redis-cli -h redis-host -p 6379 slowlog get 30 redis-cli -h redis-host -p 6379 latency latestinfo clients看connected_clients、blocked_clients如果blocked_clients不为0说明有客户端在阻塞等待数据可能是BRPOP、BLPOP这类阻塞命令也可能是有大量执行时间很长的Lua脚本。latency latest是Redis内置的延迟事件监控如果事件名是command并且延迟很高那就说明Redis执行命令本身有卡顿。还有个容易被忽视的点RDB持久化和AOF刷盘会阻塞主线程。INFO persistence里能看到rdb_bgsave_in_progress、aof_last_write_status。特别是开启appendfsync always的场景每次写命令都要刷盘磁盘一慢整个Redis就慢。我曾见过一台机器因为磁盘损坏导致Redis写延迟从毫秒级飙到秒级客户端超时一片。3.2 网络层排查丢包、重传与带宽打满如果Redis实例本身一切健康慢日志没有、延迟正常、CPU空闲那就该怀疑网络链路了。先看有没有TCP重传这是丢包最直接的证据。Linux上可以用ss -ti批量看socket重传ss -ti | grep -E retrans|retrns | head -30或者用tcpdump抓Redis端口流量看TCP层有没有Retransmission标记tcpdump -i eth0 host redis-host and port 6379 -nn -w redis.pcap抓回来后用Wireshark打开在Analyze Expert Info里直接能看到重传率。重传率高说明链路上丢包这时候需要往下游排查交换机的端口错误包计数、云厂商安全组、宿主机网卡软中断。带宽打满也是常见原因。nload、iftop、bwm-ng都行重点看是否有大流量任务比如日志采集、数据导出、全量备份和Redis请求在抢带宽。之前有个案例有人凌晨跑了一个跨机房的Redis数据导出任务出口带宽被占满第二天早上业务高峰期Redis超时飙升。查到最后Redis心跳包都发不出去更别提业务请求了。3.3 虚拟化与容器环境CPU steal和邻居噪声这个问题在虚拟化和容器环境里会反复出现值得单独说。在VMware虚拟机或容器宿主机上跑Redis有时候你从实例内部看CPU使用率并不高但延迟就是不稳定。这时候要看%st——Steal Time即虚拟机等待宿主机调度的时间占比。top # 看 %Cpu(s) 里的 st 字段 vmstat 1 10 # 看 wa / st 列如果%st经常超过20%说明宿主机资源超卖严重同一个物理机上别的虚拟机在抢CPU。Redis这种对延迟极度敏感的服务非常怕CPU调度延迟。遇到这种场景除了迁移实例或者给宿主机减负没有太多更好的办法。另外像容器环境的网络模型比如端口映射NAT、overlay网络都会引入额外转发延迟排查时要把这部分时延也纳入考虑。3.4 主从切换与故障转移窗口还有一个在云上很常见的“隐藏超时源”Redis主从自动切换。哨兵或者Cluster在检测到主节点故障后会触发failover切换过程中会重新选举这段时间客户端会经历一段不可用。如果客户端没有开启拓扑刷新故障转移后还会一直往老节点IP发请求造成持续超时。Lettuce要开启拓扑刷新。Spring Boot下配置YAML文件里的spring.redis.lettuce.cluster.refresh.adaptive和period让客户端定期更新节点拓扑信息。Jedis的JedisCluster本身内置了连接刷新但也要确认没有把重试次数设成0。另外切换过程中如果主从复制延迟很大从节点升主后数据缺失客户端读到的就是过期数据配合超时误报排查起来极其酸爽。4. 典型故障案例复盘从现象到根因的完整路径4.1 案例一游戏更新器响应超时与下载中断现象业务方反馈游戏更新器大面积提示响应超时日志里有类似curl (56) recv failure: Operation timed out的报错同时一个下载任务卡在00 kib/s error: 预期数据长度未收到同时间段Redis监控也出现超时告警。排查思路这类“应用层下载”的报错和“Redis命令超时”同时出现第一反应不能是各自排查。我先把时间线拉出来发现两者高峰完全重合。再去检查带宽曲线果不其然下载节点把出口带宽打满了。Redis的连接建立在同一物理链路上数据包排队太严重命令自然超时。根因是共享带宽资源耗尽Redis本身并没有问题。处理办法给下载服务做限速、加独立带宽或换CDN、把Redis实例迁到不受下载流量影响的网段。这类案例给我的教训是多个服务同时超时先怀疑公共基础设施别一上来就在Redis客户端参数上反复横跳。4.2 案例二Lettuce报RedisCommandTimeoutException但实例一切正常现象Java服务偶发命令超时RedisCommandTimeoutException出现在凌晨和业务高峰两个时段。查Redis的CPU、内存、慢日志全部正常网络也没有重传。到了服务端侧我起了perf top和pidstat看宿主机活动发现高峰期宿主机的磁盘I/O非常高有一批数据同步任务在跑。Redis是单线程事件循环虽然我在实例内部看它自己是“CPU空闲”但底层的磁盘I/O等待、CPU调度延迟把命令执行时间拉长了。根因宿主机I/O资源竞争导致的调度抖动。处理办法把数据同步任务错峰、给Redis实例所在虚拟机打上资源预留标签、或迁移到独占主机。这个案例告诉我Redis超时排查不能只看Redis本身虚拟化层、宿主机邻居、I/O调度都会影响。4.3 案例三Linux 127秒SYN重传与连接超时现象某个新上线的环境里客户端连接Redis偶尔要卡100多秒才报连接超时日志里是connect timed out。第一次遇到时我印象很深因为这个超时时长相比较离谱用time curl -v telnet://redis-host:6379复现稳定卡在127秒。这里说一下127秒的原理Linux TCP建立连接时如果SYN包发出去没有响应核心协议栈按1s、2s、4s、8s、16s...指数退避重传SYN累计重传6次后放弃总时长约127秒。这个超时时间由tcp_syn_retries控制默认等于6。根因排查tcpdump抓包发现SYN包在发出后没有任何SYN-ACK回应说明包在中途被丢弃——最后定位到是防火墙规则误拦了Redis端口。把安全组和防火墙规则修正后连接恢复到毫秒级。这个案例给出一个经验任何Redis连接超时都不要在客户端层面傻等先用telnet或nc直接探测端口连通性确认是哪一层在丢SYN。4.4 案例四分布式锁超时引发的“假故障”现象某服务报“获取Redis分布式锁超时”业务方怀疑Redis挂了。但排查下来Redis完全正常只是锁竞争太激烈部分线程等待锁超过预设阈值。这个问题的核心在于锁等待超时和Redis命令超时是两码事。使用Redisson时waitTime获取锁等待时间和leaseTime锁持有时间要分开设置。如果waitTime设的比Redis命令超时还大一旦Redis抖动所有等待线程都会在锁上积压产生大范围阻塞。我还遇到过更隐蔽的Redisson的看门狗watchdog会在锁持有期间自动续期但如果客户端JVM发生长GC停顿看门狗线程也停了锁到了过期时间被释放此时业务方还没处理完后续线程拿到锁后操作了同一份资源出现数据错乱。处理方案是合理设置leaseTime和业务执行时间上限不能完全依赖看门狗。5. 超时治理与预防把问题消灭在报警之前5.1 全链路超时参数规范超时治理不能只盯着Redis一层。一次用户请求要经过前端AJAX、网关、应用层、Redis超时时间必须是递减的前端AJAX超时建议3~5秒避免用户无谓等待Nginx网关proxy_read_timeout建议2~3秒要给下游留出处理时间应用层Redis命令超时建议500ms~1s具体看业务连接超时建议200~300ms像热词里提到的nginx mirror超时时间和原生jsajax超时处理其实都属于这个链路设计的一部分。如果各层超时都乱设下层超时比上层还长就会出现上游已经断开了下游还在傻傻等待的情况最后前端报错后端日志却是正常的——排查起来特别费劲。5.2 缓存治理从根上降低超时概率大量超时源于缓存设计不合理。我总结了几条可以提前做的治理动作大key拆分value超过10KB就要警惕超过1MB基本是事故隐患。hash可以拆成多个小hash或者改用list配合分页读取热点key单个key读放大成热点用本地缓存或读写分离扛住别都打到Redis单线程上缓存穿透防护不存在的数据也缓存一个空值或者用布隆过滤器避免恶意请求把慢查询源头打满缓存雪崩防护过期时间加随机值避免同一时刻大面积失效合理使用数据类型动不动GET/SET整个JSON字符串不如用HSET拆字段减少序列化开销这些和redis缓存治理、redis数据类型等热词高度相关本质上都是让Redis在低负载下运行超时概率自然大幅下降。5.3 监控与告警最小集光有治理不行得能提前发现。我建议至少把下面这些指标纳入Redis监控指标来源告警建议命令耗时P99INFO COMMANDSTATSP99持续超过50ms告警慢查询数量SLOWLOG LEN5分钟内有超过阈值慢查询即告警连接数趋势INFO CLIENTS接近maxclients的80%告警内存淘汰数INFO STATS的evicted_keys出现淘汰即告警主从复制延迟INFO REPLICATION延迟超过10秒告警大key扫描redis-cli --bigkeys定期任务扫描发现超大key通知业务方云上环境如果有云监控直接用云监控自建环境用redis_exporter配Prometheus也是比较通用的方案。6. 常见问题速查表与避坑清单6.1 超时排查速查表整理一个速查表排查时对照着来能省不少时间症状可能根因验证手段解决方向连接超时telnet不通防火墙/安全组拦截、Redis未监听ss -lntp | grep 6379、云控制台检查规则修正防火墙、修改bind配置连接卡127秒后超时SYN被丢弃网络路径丢包tcpdump看SYN无响应检查防火墙、路由、云网络策略连接池报Could not get a resource连接池耗尽线程栈卡在borrowObject调大maxTotal或优化业务连接使用方式命令超时但慢日志为空网络抖动、带宽打满、CPU stealss -ti看重传、top看st高不高网络链路检查、限速、资源预留慢日志有大量命令bigkey、慢命令、持久化阻塞--bigkeys、INFO persistence拆分大key、优化命令、调整AOF策略故障转移期间全部超时主从切换、客户端拓扑未刷新INFO REPLICATION看角色变化开启客户端拓扑刷新、合理配置哨兵参数所有请求超时且Redis CPU高热点命令、复杂Lua脚本INFO CPU、MONITOR命令观察优化命令、限流、拆分热点偶发超时且GC频繁应用JVM GC停顿抓GC日志观察FGC时间调优JVM参数、减少大对象分配6.2 避坑清单最后再列几条用真金白银换来的经验生产环境严禁执行KEYS *遇到这种命令直接让Redis瞬间进入不可用状态全量扫描会把单线程主循环打死不要盲目调大超时参数掩盖问题。超时调大了只是把慢请求的释放时间延后堆积的线程会越来越多最后演化成整个应用不可用排查超时一定要看时间段分布。凌晨超时和晚高峰超时是两个世界前者往往和定时任务、备份任务有关后者才是真正的容量和热点问题改Redis配置前先备份CONFIG SET也要慎用有些参数改了立刻生效但不持久重启后又变回去容易让人误判客户端重试要有次数限制并且要有退避策略。无限重试在故障时会放大流量把Redis彻底打垮一定给Redis单独留主机资源不要和日志采集、大数据任务混部这类邻居噪声在容器环境里尤其明显主从哨兵切换后要确认客户端重连到新主节点否则会一直连一个“假主”浪费时间我排查Redis超时问题最大的体会是先画时间线再定责最后动手调。时间线上重合的两类现象背后往往是一个共同的根因。比如多个服务同时超时别急着改各自的超时配置先看看是不是带宽、宿主机、公共组件出了问题。Redis本身相对是个“老实人”命令执行机制简单直接大多数超时反而是它外围的“环境问题”。把客户端、网络、容器、持久化这些邻居都伺候好Redis超时基本能消灭在萌芽状态。
返回列表