范文健康探索娱乐情感热点
热点动态
科技财经
情感日志
励志美文
娱乐时尚
游戏搞笑
探索旅游
历史星座
健康养生
美丽育儿
范文作文
教案论文

springredis上百万的QPS压力太大连接失败,我TM人傻了

  大家好,我们最近业务量暴涨,导致我最近一直 TM 人傻了。前几天晚上,发现由于业务压力激增,某个核心微服务新扩容起来的几个实例,在不同程度上,出现了  Redis 连接失败 的异常: org.springframework.data.redis.RedisConnectionFailureException: Unable to connect to Redis; nested exception is io.lettuce.core.RedisConnectionException: Unable to connect to redis.production.com 	at org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory$ExceptionTranslatingConnectionProvider.translateException(LettuceConnectionFactory.java:1553) ~[spring-data-redis-2.4.9.jar!/:2.4.9] 	at org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory$ExceptionTranslatingConnectionProvider.getConnection(LettuceConnectionFactory.java:1461) ~[spring-data-redis-2.4.9.jar!/:2.4.9] 	at org.springframework.data.redis.connection.lettuce.LettuceConnection.doGetAsyncDedicatedConnection(LettuceConnection.java:1027) ~[spring-data-redis-2.4.9.jar!/:2.4.9] 	at org.springframework.data.redis.connection.lettuce.LettuceConnection.getOrCreateDedicatedConnection(LettuceConnection.java:1013) ~[spring-data-redis-2.4.9.jar!/:2.4.9] 	at org.springframework.data.redis.connection.lettuce.LettuceConnection.openPipeline(LettuceConnection.java:527) ~[spring-data-redis-2.4.9.jar!/:2.4.9] 	at org.springframework.data.redis.connection.DefaultStringRedisConnection.openPipeline(DefaultStringRedisConnection.java:3245) ~[spring-data-redis-2.4.9.jar!/:2.4.9] 	at jdk.internal.reflect.GeneratedMethodAccessor319.invoke(Unknown Source) ~[?:?] 	at jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[?:?] 	at java.lang.reflect.Method.invoke(Method.java:566) ~[?:?] 	at org.springframework.data.redis.core.CloseSuppressingInvocationHandler.invoke(CloseSuppressingInvocationHandler.java:61) ~[spring-data-redis-2.4.9.jar!/:2.4.9] 	at com.sun.proxy.$Proxy355.openPipeline(Unknown Source) ~[?:?] 	at org.springframework.data.redis.core.RedisTemplate.lambda$executePipelined$1(RedisTemplate.java:318) ~[spring-data-redis-2.4.9.jar!/:2.4.9] 	at org.springframework.data.redis.core.RedisTemplate.execute(RedisTemplate.java:222) ~[spring-data-redis-2.4.9.jar!/:2.4.9] 	at org.springframework.data.redis.core.RedisTemplate.execute(RedisTemplate.java:189) ~[spring-data-redis-2.4.9.jar!/:2.4.9] 	at org.springframework.data.redis.core.RedisTemplate.execute(RedisTemplate.java:176) ~[spring-data-redis-2.4.9.jar!/:2.4.9] 	at org.springframework.data.redis.core.RedisTemplate.executePipelined(RedisTemplate.java:317) ~[spring-data-redis-2.4.9.jar!/:2.4.9] 	at org.springframework.data.redis.core.RedisTemplate.executePipelined(RedisTemplate.java:307) ~[spring-data-redis-2.4.9.jar!/:2.4.9] 	at org.springframework.data.redis.core.RedisTemplate$FastClassBySpringCGLIB$81812bd6.invoke() ~[spring-data-redis-2.4.9.jar!/:2.4.9] 	//省略一些堆栈 Caused by: org.springframework.dao.QueryTimeoutException: Redis command timed out 	at org.springframework.data.redis.connection.lettuce.LettuceConnection.closePipeline(LettuceConnection.java:592) ~[spring-data-redis-2.4.9.jar!/:2.4.9] 	... 142 more
  同时,也有业务调用  Redis 命令超时 的异常: org.springframework.data.redis.connection.RedisPipelineException: Pipeline contained one or more invalid commands; nested exception is org.springframework.data.redis.connection.RedisPipelineException: Pipeline contained one or more invalid commands; nested exception is org.springframework.dao.QueryTimeoutException: Redis command timed out 	at org.springframework.data.redis.connection.lettuce.LettuceConnection.closePipeline(LettuceConnection.java:594) ~[spring-data-redis-2.4.9.jar!/:2.4.9] 	at org.springframework.data.redis.connection.DefaultStringRedisConnection.closePipeline(DefaultStringRedisConnection.java:3224) ~[spring-data-redis-2.4.9.jar!/:2.4.9] 	at jdk.internal.reflect.GeneratedMethodAccessor198.invoke(Unknown Source) ~[?:?] 	at jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[?:?] 	at java.lang.reflect.Method.invoke(Method.java:566) ~[?:?] 	at org.springframework.data.redis.core.CloseSuppressingInvocationHandler.invoke(CloseSuppressingInvocationHandler.java:61) ~[spring-data-redis-2.4.9.jar!/:2.4.9] 	at com.sun.proxy.$Proxy355.closePipeline(Unknown Source) ~[?:?] 	at org.springframework.data.redis.core.RedisTemplate.lambda$executePipelined$1(RedisTemplate.java:326) ~[spring-data-redis-2.4.9.jar!/:2.4.9] 	at org.springframework.data.redis.core.RedisTemplate.execute(RedisTemplate.java:222) ~[spring-data-redis-2.4.9.jar!/:2.4.9] 	at org.springframework.data.redis.core.RedisTemplate.execute(RedisTemplate.java:189) ~[spring-data-redis-2.4.9.jar!/:2.4.9] 	at org.springframework.data.redis.core.RedisTemplate.execute(RedisTemplate.java:176) ~[spring-data-redis-2.4.9.jar!/:2.4.9] 	at org.springframework.data.redis.core.RedisTemplate.executePipelined(RedisTemplate.java:317) ~[spring-data-redis-2.4.9.jar!/:2.4.9] 	at org.springframework.data.redis.core.RedisTemplate.executePipelined(RedisTemplate.java:307) ~[spring-data-redis-2.4.9.jar!/:2.4.9] 	at org.springframework.data.redis.core.RedisTemplate$FastClassBySpringCGLIB$81812bd6.invoke() ~[spring-data-redis-2.4.9.jar!/:2.4.9] 	at org.springframework.cglib.proxy.MethodProxy.invoke(MethodProxy.java:218) ~[spring-core-5.3.7.jar!/:5.3.7] 	at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.invokeJoinpoint(CglibAopProxy.java:779) ~[spring-aop-5.3.7.jar!/:5.3.7] 	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:163) ~[spring-aop-5.3.7.jar!/:5.3.7] 	at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:750) ~[spring-aop-5.3.7.jar!/:5.3.7] 	at org.springframework.aop.interceptor.ExposeInvocationInterceptor.invoke(ExposeInvocationInterceptor.java:97) ~[spring-aop-5.3.7.jar!/:5.3.7] 	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186) ~[spring-aop-5.3.7.jar!/:5.3.7] 	at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:750) ~[spring-aop-5.3.7.jar!/:5.3.7] 	at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:692) ~[spring-aop-5.3.7.jar!/:5.3.7] 	at org.springframework.data.redis.core.StringRedisTemplate$EnhancerBySpringCGLIB$c9b8cc15.executePipelined() ~[spring-data-redis-2.4.9.jar!/:2.4.9] //省略一部分堆栈 Caused by: org.springframework.data.redis.connection.RedisPipelineException: Pipeline contained one or more invalid commands; nested exception is org.springframework.dao.QueryTimeoutException: Redis command timed out 	at org.springframework.data.redis.connection.lettuce.LettuceConnection.closePipeline(LettuceConnection.java:592) ~[spring-data-redis-2.4.9.jar!/:2.4.9] 	... 142 more Caused by: org.springframework.dao.QueryTimeoutException: Redis command timed out 	at org.springframework.data.redis.connection.lettuce.LettuceConnection.closePipeline(LettuceConnection.java:592) ~[spring-data-redis-2.4.9.jar!/:2.4.9] 	... 142 more
  我们的 spring-data-redis 的配置是: spring:   redis:     host: redis.production.com     port: 6379     # 命令超时     timeout: 3000     lettuce:       pool:         max-active: 128         max-idle: 128         max-wait: 3000
  这些请求虽然在第一次请求发送到的实例失败了,但是我们有重试的机制,请求最后还是成功了。但是比正常的请求多了 3s,这部分请求占了所有请求的 3% 左右。
  从异常堆栈上面可以看出, 异常的根源都是 redis 命令超时 ,但是为何建立 Redis 连接的时候,也会有 Redis 命令执行呢? lettuce 建立连接的流程
  我们的 Redis 访问,使用的是 spring-data-redis + Lettuce 连接池。默认情况下,Lettuce 中的 Redis 连接建立的流程是: 建立 TCP 连接 进行必要的握手: 针对 Redis 2.x ~ 5.x 的版本:
  1. 如果需要用户名密码,则发送用户名密码信息
  2. 如果开启了连接使用前心跳,则发送 PING 针对 Redis 6.x 的版本:6.x 之后引入了新命令 HELLO,使用这个命令来统一初始化 Redis 连接:REDIS HELLO,这个命令参数中可以带用户名与密码,完成验证。
  针对 Redis 2.x ~ 5.x 的版本,我们可以配置是否在启用连接前发送 PING 心跳,默认为 是 :
  ClientOptions public static final boolean DEFAULT_PING_BEFORE_ACTIVATE_CONNECTION = true;
  我们使用的 Redis 版本是最新的 6.x,所以在建立连接,握手的阶段,一定需要发送一个 HELLO 命令,并等待响应成功才算连接创建成功。
  那么为何这个简单的命令也会超时呢? 通过 JFR 查看 Redis 命令压力
  我们的项目中 redis 操作是通过 spring-data-redis + Lettuce 连接池,启用并且增加了关于 Lettuce 命令的 JFR 监控,可以参考我的这篇文章:这个 Redis 连接池的新监控方式针不戳~我再加一点佐料,截至目前我的 pull request 已经合并,这个特性会在 6.2.x 版本发布。我们看下出问题时间附近的 Redis 命令采集,如下图所示:
  可以看出,这时候 Redis 压力还是比较大的(图中的 firstResponsePercentiles 的单位是微秒)。我们这个时候,有 7 个实例,这个实例时刚启动的,压力相对于其他实例还比较小,就已经出现了连接命令超时。而且我们这里只截取了 HGET 命令,还有 GET 命令执行的次数和 HGET 是同一量级的,然后剩下其他的命令加起来相当于 HGET 的一半。这时候从客户端看,发往 Redis 的命令的 QPS 已经超过了百万。
  从 Redis 的监控来看,压力确实有一些,可能会造成某些命令等待过长时间导致超时异常。 优化思路思考
  我们先明确一点,针对 spring-data-redis + lettuce,如果我们没有使用需要独占连接的命令(包括 Redis 事务以及 Redis Pipeline),那么 我们不需要连接池 ,因为 lettuce 是异步响应式的,对于可以使用共享连接的请求,都会使用同一个实际的 redis 连接进行请求,不需要连接池。但是这个微服务中,使用了大量的 pipeline 命令来提高查询效率。如果我们不使用连接池,那么会导致频繁的连接关闭与创建(每秒几十万个),这样会严重降低效率。 虽然官网说,lettuce 不需要连接池,但是这是在你没有使用事务以及 Pipeline 的情况下 。
  首先, Redis 扩容 :我们的 Redis 部署在公有云上,如果扩容也就是提高机器配置,下一个更高的配置指标相对于当前多了一倍,成本也是高了差不多一倍。目前只有在瞬时压力的时候,会出现少于 3% 的请求失败并重试下一实例,最后还是成功,针对这个对 Redis 进行扩容, 从成本考虑并不值得 。
  然后,对于压力过大的应用,我们是有动态扩容机制存在的。对于失败的请求,我们也是有重试的。但是这个问题给我们带来的影响是: 由于瞬时压力到来,新启动的实例可能一开始就会有大量请求到来,导致接口请求和建立连接之后的心跳请求混合。并且由于这些请求并没有公平队列排序,某些心跳请求响应过慢从而导致失败,重新建立连接依然可能失败。 有些实例可能建立的连接比较少,不能满足并发度需求。导致很多请求其实阻塞在等待连接的过程,从而使 CPU 压力没有一下子变很大,所以没有继续触发扩容。这样对于扩容带来了更大的滞后性。
  其实,如果我们有办法尽量减少或者避免连接创建失败,那么就能很大程度优化这个问题。即在微服务实例开始提供服务前,就将连接池中所有的连接创建好。 如何实现 Redis 连接池连接预创建
  我们首先看看,是否可以借助于官方配置,实现这个连接池。
  我们查看官方文档,发现了这样两个配置:
  min-idle 即连接池中最少的连接数。 time-between-eviction-runs 是定时任务,检查连接池中的连接是否满足至少有 min-idle 的个数,同时,不超过 max-idle 那么多个数。官方文档中说,min-idle 只有配合 time-between-eviction-runs 都配置,才会生效。究其原因是:lettuce 的链接池是基于 commons-pool 实现的。连接池可以配置 min-idle,但是需要手动调用 preparePool,才会创建至少 min-idle 个数的对象:
  GenericObjectPool public void preparePool() throws Exception {     //如果配置了有效的 min-idle,则调用 ensureMinIdle 保证创建至少 min-idle 个数的对象     if (this.getMinIdle() >= 1) {         this.ensureMinIdle();     } }
  那么这个是在什么时候调用呢? commons-pool 有定时任务,初始延迟和定时间隔都是 time-between-eviction-runs,配置的,其内容是: public void run() {     final ClassLoader savedClassLoader =             Thread.currentThread().getContextClassLoader();     try {         if (factoryClassLoader != null) {             // Set the class loader for the factory             final ClassLoader cl = factoryClassLoader.get();             if (cl == null) {                 // The pool has been dereferenced and the class loader                 // GC"d. Cancel this timer so the pool can be GC"d as                 // well.                 cancel();                 return;             }             Thread.currentThread().setContextClassLoader(cl);         }          // Evict from the pool         try {             evict();         } catch(final Exception e) {             swallowException(e);         } catch(final OutOfMemoryError oome) {             // Log problem but give evictor thread a chance to continue             // in case error is recoverable             oome.printStackTrace(System.err);         }         // Re-create idle instances.         try {             ensureMinIdle();         } catch (final Exception e) {             swallowException(e);         }     } finally {         // Restore the previous CCL         Thread.currentThread().setContextClassLoader(savedClassLoader);     } }
  可以看出,这个定时任务执行主要保证当前池内空闲对象个数不超过 max-idle,同时至少有 min-idle 个链接。这些都是 common-pools 自己带的机制。但是没有我们需要的,在连接池一创建就去初始化所有链接。
  这就需要我们自己实现了,我们首先配置 min-idle = max-idle = max-active,这样无论何时连接池中都有同样最大个数的链接。之后,我们在连接池创建出来的地方,修改源码,强制调用 preparePool 去初始化所有链接,即:
  ConnectionPoolSupport // lettuce 初始化创建连接池的时候,会调用这个方法 public static > GenericObjectPool createGenericObjectPool(             Supplier connectionSupplier, GenericObjectPoolConfig config, boolean wrapConnections) {     //省略其他代码      GenericObjectPool pool = new GenericObjectPool(new RedisPooledObjectFactory(connectionSupplier), config) {          @Override         public T borrowObject() throws Exception {             return wrapConnections ? ConnectionWrapping.wrapConnection(super.borrowObject(), poolRef.get())                     : super.borrowObject();         }          @Override         public void returnObject(T obj) {              if (wrapConnections && obj instanceof HasTargetConnection) {                 super.returnObject((T) ((HasTargetConnection) obj).getTargetConnection());                 return;             }             super.returnObject(obj);         }      };     //创建好后,调用 preparePool     try {         pool.preparePool();     } catch (Exception e) {         throw new RedisConnectionException("prepare connection pool failed",e);     }     //省略其他代码 }
  这样,我们就可以实现初始化 Redis 的时候,在微服务真正提供服务之前,初始化所有 Redis 链接。由于这里涉及源码修改,大家目前可以通过在项目中添加同名同路径的类,进行依赖库源码替换。针对这个优化,我也向 lettuce 提了 issue 以及对应的 pull request:

写代码有这16个好习惯,可以减少80非业务的bug1。修改完代码,记得自测一下改完代码,自测一下是每位程序员必备的基本素养。尤其不要抱有这种侥幸心理我只是改了一个变量或者我只改了一行配置代码,不用自测了。改完代码,尽量要求自己都去C语言经典100例10题目6题目用号输出字母C的图案。程序分析可先用号在纸上写出字母C,再分行输出。(感觉输出有点抽象啊哈哈哈哈哈)includecstdiointmain()printf()printC语言经典100例5题目1题目有1234个数字,能组成多少个互不相同且无重复数字的三位数?都是多少?程序分析可填在百位十位个位的数字都是1234。组成所有的排列后再去掉不满足条件的排列。include华为Mate50系列渲染图曝光,设计尽显高端,还有可能首发鸿蒙3。0尽管华为Mate50系列迟迟没有发布,但不少用户对该机的期待值仍是居高不下。既然等不到官方消息,那么不妨来看一组设计师Hoiindi基于最新爆料制作的一组华为Mate50Pro渲染卖完就下架,四款香饽饽小屏机,最小仅3。5英寸手机圈现在大屏是主流,这我们一点也不否认,但总有部分用户比较特别,他们依旧喜欢以往小屏手机的便携性,可以随便揣兜或者是单手操作,服务于生活没有负担。可惜小屏手机目前市场中并不多见,违规与违法,网规与宪法3月份以来,诗坛上好几个人的微信公众号以前发表的有关揭露诗坛乱象和反对诗坛腐败的文章都被删除,甚至给予了封号一个月两个月三个月不许发文章的处罚。腾讯平台方给予这些被删被封的通知中,某大厂面试题一,你能做对几题?单选题1。若用斜杠记法标识子网掩码,则255。255。240。0对应于A。19B。20C。21D。222。通常情况,JVM中使用类加载器的优先级是A。BootstrapClassL盘点那些麒麟970二手手机,6128G,99新最低只要488元有很多朋友想买个手机,预算只有几百,不想要那些性能拉胯的,应该怎么选呢接下来,我给大家推荐一些高性价比的二手机,成色是99新的,外观和功能方面全部正常麒麟970屏幕规格6。3英寸n闭眼买没毛病,2022年这4款手机最值得买,覆盖中高低三档闭眼买没毛病,2022年这4款手机最值得买,覆盖中高低三档本文原创,禁止搬运和抄袭,违者必究!导语了解行情的网友都知道,一款性价比高的手机要比高价低配的手机要来得更有市场得多。要说4月16日区块链资讯汇总(四)1304Coachella音乐节NFTREGEN将于4月19日结束拍卖,当前最高出价125,000美元4月16日消息,由北美顶级音乐节Coachella与FTX合作推出的11NFTRedis内存淘汰机制maxmemorypolicyvolatilelrunoeviction当内存不足以容纳新写入数据时,新写入操作会报错。应该没人用吧。allkeyslru当内存不足以容纳新写入数据
vivoNEX3好评率100,到底贵不贵?最近发布的或者即将发布的手机都有点看头哈,毕竟作为全新的年度旗舰机看头还是非常足。三星GalaxyNote10,三星的旗舰机惊喜还是非常多的,iPhone11系列,这款手机相比去年雷军发狠!小米旗舰终于降至1999,这次该无敌喽我们知道今年手机厂商都非常能打,除了小米打出性价比之外,OV等手机厂商也纷纷推出了性价比能打系列,笔者印象最深的就是vivo的Z系列和iQOO,尤其是最近发布的iQOOneo,骁龙vivo首款5G手机一部2G电影只需十几秒5G一直都炒的非常热,到底5G有多快呢,很多人的概念或许只停留在网络上或者脑补中。而vivo今天在厦门SM广场举办了一场vivo5G比邻计划活动,切实的让大家亲临现场,深刻感受一下网传小米MIX4下个月发布,UFS3。0瀑布屏回顾!前几天小米发布了小米9Pro和小米MIXAlpha概念机,怎么说呢,这两款手机都有些网友吐槽的地方。小米9Pro被吐槽UFS2。1,其实看看3699的价格,你还要什么自行车呢吴宗宪当众亲吻31岁女儿,还说唯一可以乱摸的女艺人就是你了说起台湾的艺人吴宗宪,很多网友会觉得他很有才,确实,吴宗宪不仅唱歌好,而且主持节目也是一把好手,非常幽默诙谐,妙语连珠,和湖南卫视何炅有一拼,不过吴宗宪比何炅更大胆,更放得开。吴宗男子地铁未让座被大爷怒骂,网友不能惯着倚老卖老的毛病现在,媒体上经常报道个别老年人的一些负面的消息,比如说有些老年人碰瓷,有些老年人则倒地后不敢扶起来,比如前一阵子河南一个大妈颜金玲自己摔倒了,被两名路过的中学生扶起来,并且打电话让太嚣张安徽男子当着民警的面,将手机店店主8岁的儿子砍死见过嚣张的,没有见过这么嚣张的!安徽一名男子张某,竟然当着两名民警的面,将一个小手机店的店主的儿子用刀活活砍死!而这件事就发生在前两天。这件事就发生在安徽的淮北经济开发区的一条商业最近手机老卡了!捡到一台小米CC9Pro是你的菜吗?图赏前先解释一下标题哈,手机最近确实很卡,可能小米CC9Pro要发布啦。然后搞到一台小米CC9Pro绿色版本给大家开开眼。小米CC9Pro正面采用的上一代的小米CC9一样,水滴屏设5G套餐出炉,vivo两款手机上榜,网友用5G手机吃了定心丸对于5G手机,很多人的大致想法就是手机贵,流量是个未知数,但是流量肯定用得很快。其实却是这样哈,市面上的5G手机都不咋便宜,vivoiQOOPro5G是性价比最高的5G手机啦,对于大兴机场新国门,带着手机前来打卡昨日被一则消息刷屏,也是一个振奋人心的消息,北京大兴机场正式投运。它是全球规模最大的单体机场行航站楼。总长度约500公里,可以绕北京五环五圈。它还拥有世界最大的屋顶面积高达1。8万任鲁豫回老家被乡亲围观,脚上一双鞋才2500元,网友太朴素了说起鲁豫,很多网友可能会想到鲁豫有约说出你的故事的女主持人,她长得瘦瘦的,瘦得令人感到有些担心,但是她的节目受到了不少观众的喜欢。不过,今天我们说的这个鲁豫,却不是女主持人鲁豫,而