实战复盘-Redis连接数爆满引发的生产事故与优化策略
1. 事故背景一场由促销活动引发的Redis雪崩那天凌晨三点我被一阵急促的电话铃声惊醒。电话那头是值班同事焦急的声音所有商品页面都打不开了订单系统也瘫痪了我瞬间清醒抓起电脑就开始远程排查。登录服务器后满屏的红色错误日志让我倒吸一口凉气——(error) ERR max number of clients reached。原来我们正在进行的品牌首单价促销活动引爆了流量高峰。这个活动的业务逻辑是用户购买指定品牌商品达到门槛后后续购买同品牌商品可享受首单优惠价。为了快速判断用户资格系统将用户购买记录和活动规则全部缓存在Redis的String类型中数据结构设计如下{ purchasedBrandList: [ { brandId: 9710, brandName: 咿儿润 } ], purchasedEndTime: 1678247960000, purchasedStartTime: 1677643160000, buyStartTime: 1678247960000, buyEndTime: 1678247960000, status: 0 }问题出在流量预估上。我们预计的QPS是5000左右但实际峰值达到了2万。更糟糕的是商品详情页、购物车、订单结算等所有页面都需要实时查询这个Redis缓存。当我在阿里云控制台看到监控图表时连接数曲线就像坐火箭一样直冲上限最终在凌晨2:47分触顶崩溃。2. 问题定位连接泄漏这个老演员最初我以为是新写的工具类有问题毕竟这是我刚改造的支持多DB查询的版本public static String getString(String key, int db) { JedisPool pool getPool(); Jedis jds null; boolean broken false; String t null; try { jds pool.getResource(); jds.select(db); t jds.get(key); } catch (Exception e) { broken true; logger.error(getString:, e); throw new RuntimeException(e); } finally { if (broken) { pool.returnBrokenResource(jds); } else if (jds ! null) { pool.returnResource(jds); } } return t; }但仔细排查后发现真正的罪魁祸首是项目里大量使用的旧工具类。看这段典型的问题代码public T T get(String key, String modulePrefix, ClassT t) { checkJedisPool(); key generateKey(key, modulePrefix); try (Jedis jedis jedisPool.getResource()) { String valueStr jedis.get(key); return parse(valueStr, t); } }表面上看使用了try-with-resources语法似乎会自动关闭连接。但实际上由于历史原因这个工具类被包装在多个代理层中最终连接释放机制失效。更可怕的是这类调用在代码库中出现超过200次通过redis-cli info clients命令查看时发现connected_clients数值始终维持在maxclients上限默认10000。即使业务低峰期也不释放典型的连接泄漏特征。这就像去超市购物每个顾客都拿走购物车却不归还最终超市的购物车被拿光新顾客无法入场。3. 紧急止血三套组合拳恢复服务面对全线崩溃的生产环境我们实施了分级应急方案3.1 临时扩容通过ulimit -n 65535调整系统最大文件描述符修改redis.conf中的maxclients参数到30000执行CONFIG SET timeout 30强制空闲连接超时3.2 流量管控对商品详情页启用本地缓存降低Redis查询频率在Nginx层对价格查询接口做限流5000req/s关闭非核心业务如商品评价、推荐系统3.3 服务重启策略# 分批次重启方案 for service in cart payment product; do kubectl scale deploy $service --replicas0 sleep 60 kubectl scale deploy $service --replicas2 done这个过程中有个关键插曲直接重启Redis会导致所有连接强制断开引发级联故障。我们采用排水重启策略先通过CLIENT PAUSE 5000暂停新请求执行CLIENT KILL TYPE normal清理普通连接最后才安全重启实例4. 根治方案从架构到代码的全方位改造4.1 基础设施升级迁移到阿里云Redis集群版主要配置对比指标原单机版新集群版最大连接数10,00050,000可用性99.9%99.99%数据持久化RDBRDBAOF扩容能力垂直扩容水平扩容4.2 连接池优化采用新一代连接池工具Lettuce对比Jedis的优势支持异步非阻塞IO自动连接回收更好的集群支持配置示例spring: redis: lettuce: pool: max-active: 500 max-idle: 50 min-idle: 10 max-wait: 1000 timeout: 20004.3 代码规范落地制定新的Redis操作规范强制使用try-with-resources语法禁止直接使用Jedis命令统一通过RedisTemplate操作所有操作添加熔断降级改造后的工具类示例Slf4j Component public class RedisOperator { Autowired private StringRedisTemplate redisTemplate; public T OptionalT getWithFallback(String key, ClassT type) { try { String value redisTemplate.execute(connection - connection.stringCommands().get(key.getBytes()), true); return Optional.ofNullable(JSON.parseObject(value, type)); } catch (Exception e) { log.warn(Redis操作失败降级到本地缓存, e); return localCache.get(key); } } }5. 监控体系的建设这次事故暴露了监控的盲区我们新增了以下监控项连接数预警# Prometheus监控规则 - alert: RedisConnectionsHigh expr: redis_connected_clients / redis_maxclients 0.7 for: 5m labels: severity: warning连接持续时间监控通过redis-cli client list命令分析长连接-- Grafana SQL查询 SELECT time, avg(client_duration) as avg_duration FROM redis_clients WHERE time NOW() - 1h GROUP BY time连接池健康度看板活跃连接数等待获取连接的线程数连接获取平均耗时6. 压测验证与熔断配置在预发布环境进行全链路压测使用JMeter模拟10万并发Thread Group └─ HTTP Request (商品详情页) ├─ Redis GET (促销信息) └─ Redis GET (库存信息)根据压测结果配置熔断规则Bean public CircuitBreakerConfig redisCircuitBreaker() { return CircuitBreakerConfig.custom() .failureRateThreshold(50) .waitDurationInOpenState(Duration.ofSeconds(30)) .permittedNumberOfCallsInHalfOpenState(100) .slidingWindowType(SlidingWindowType.COUNT_BASED) .slidingWindowSize(1000) .build(); }这次事故给我们的教训是高并发场景下任何资源泄漏都会被无限放大。就像在高速公路上一辆车抛锚就可能引发大面积拥堵。现在我们的Redis监控看板上连接数指标永远带着醒目的红色警戒线提醒着我们那个惊心动魄的凌晨。
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2455267.html
如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!