百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术教程 > 正文

十亿级流量下,我与Redis时延小突刺的战斗史

mhr18 2024-11-28 08:37 11 浏览 0 评论

一、背景

某一日收到上游调用方的反馈,提供的某一个Dubbo接口,每天在固定的时间点被短时间熔断,抛出的异常信息为提供方dubbo线程池被耗尽。 当前dubbo接口日请求量18亿次,报错请求94W/天,至此开始了优化之旅。

二、快速应急

2.1 快速定位

首先进行常规的系统信息监控(机器、JVM内存、GC、线程),发现虽稍有突刺,但都在合理范围内,且跟报错时间点对不上,先暂时忽略。

其次进行流量分析,发现每天固定时间点会有流量突增的情况,流量突增的点跟报错的时间点也吻合,初步判断为短时大流量导致。

流量趋势

被降级量

接口99线

三、寻找性能瓶颈点

3.1 接口流程分析

3.1.1 流程图

3.1.2 流程分析

收到请求后调用下游接口,使用hystrix熔断器,熔断时间为500MS

根据下游接口返回的数据,进行详情数据的封装,第一步先到本地缓存中获取,如果本地缓存没有,则从Redis进行回源,Redis中无则直接返回,异步线程从数据库进行回源。

如果第一步调用下游接口异常,则进行数据兜底,兜底流程为先到本地缓存中获取,如果本地缓存没有,则从Redis进行回源,Redis中无则直接返回,异步线程从数据库进行回源。

3.2 性能瓶颈点排查

3.2.1 下游接口服务耗时比较长

调用链显示,虽然下游接口的P99线在峰值流量时存在突刺,超出1S,但因为熔断超时的设置(熔断时间500MS,coreSize&masSize=50,下游接口平均耗时10MS以下),判断下游接口不是问题的关键点,为进一步排除干扰,在下游服务存在突刺时能快速失败,调整熔断时间为100MS,dubbo超时时间100MS。

3.2.2 获取详情本地缓存无数据,Redis回源

借助调用链平台,第一步分析Redis请求流量,以此来判断本地缓存的命中率,发现Redis的流量是接口流量的2倍,从设计上来说不应该出现这个现象。开始代码Review,发现在有一处逻辑出现了问题。

没有从本地缓存读取,而是直接从Redis中获取了数据,Redis最大响应时间也确实发现了不合理的突刺,继续分析发现Redis响应时间和Dubbo99线突刺情况基本一致,感觉此时已经找到了问题的原因,心中暗喜。

Redis请求流量

服务接口请求流量

Dubbo99线

Redis最大响应时间

3.2.3 获取兜底数据本地缓存无数据,Redis回源

正常

3.2.4 记录请求结果入Redis

因为当前Redis做了资源隔离,且未在DB后台查询到慢日志,此时分析导致Redis变慢的原因有很多,不过其他的都被主观忽略了,注意力都在请求Redis流量翻倍的问题上了,故优先解决3.2.2中的问题。

四、解决方案

4.1 3.3.2中定位的问题上线

上线前Redis请求量

上线后Redis请求量

上线后Redis流量翻倍问题得到解决,Redis最大响应时间突刺有所缓解,但依旧没能彻底解决,说明大流量查询不是最根本的原因。

redis最大响应时间(上线前)

redis最大响应时间(上线后)

4.2 Redis扩容

在Redis异常流量问题解决后,问题并未得到彻底解决,此时能做的就是静下心来,仔细去梳理导致Redis慢的原因,思路主要从以下三个方面:

  • 出现了慢查询
  • Redis服务出现性能瓶颈
  • 客户端配置不合理

基于以上思路,一个个地进行排查;查询Redis慢查询日志,未发现慢查询。

借用调用链平台详细分析慢的Redis命令,没有了大流量导致的慢查询的干扰,问题定位流程很快,大量的耗时请求在setex方法上,偶尔出现查询的慢请求也都是在setex方法之后,根据Redis单线程的特性判断setex是Redis99线突刺的元凶。找到具体语句,定位到具体业务后,首先申请扩容Redis,由6个master扩到8个master。

Redis扩容前

Redis扩容后

从结果上看,扩容基本上没有效果,说明redis服务本身不是性能瓶颈点,此时剩下的一个就是客户端相关配置了。

4.3 客户端参数优化

4.3.1 连接池优化

Redis扩容没有效果,针对客户端可能出现的问题,此时怀疑地点有两个方向。

第一个是客户端在处理Redis集群模式时,对连接的管理上存在BUG,第二个是连接池参数设置不合理,此时源码分析和连接池参数调整同步进行。

4.3.1.1 判断客户端连接管理上是否有BUG

在分析完,客户端处理连接池的源码后,没有问题,跟预想一致,按照槽位缓存连接池,第一个假设被排除,源码如下。

1、setEx

  public String setex(final byte[] key, final int seconds, final byte[] value) {

    return new JedisClusterCommand<String>(connectionHandler, maxAttempts) {

      @Override

      public String execute(Jedis connection) {

        return connection.setex(key, seconds, value);

      }

    }.runBinary(key);

  }

 

2、runBinary

  public T runBinary(byte[] key) {

    if (key == null) {

      throw new JedisClusterException("No way to dispatch this command to Redis Cluster.");

    }

 

    return runWithRetries(key, this.maxAttempts, false, false);

  }

3、runWithRetries

  private T runWithRetries(byte[] key, int attempts, boolean tryRandomNode, boolean asking) {

    if (attempts <= 0) {

      throw new JedisClusterMaxRedirectionsException("Too many Cluster redirections?");

    }

 

    Jedis connection = null;

    try {

 

      if (asking) {

        // TODO: Pipeline asking with the original command to make it

        // faster....

        connection = askConnection.get();

        connection.asking();

 

        // if asking success, reset asking flag

        asking = false;

      } else {

        if (tryRandomNode) {

          connection = connectionHandler.getConnection();

        } else {

          connection = connectionHandler.getConnectionFromSlot(JedisClusterCRC16.getSlot(key));

        }

      }

 

      return execute(connection);

 

    }

 

4、getConnectionFromSlot

  public Jedis getConnectionFromSlot(int slot) {

    JedisPool connectionPool = cache.getSlotPool(slot);

    if (connectionPool != null) {

      // It can't guaranteed to get valid connection because of node

      // assignment

      return connectionPool.getResource();

    } else {

      renewSlotCache(); //It's abnormal situation for cluster mode, that we have just nothing for slot, try to rediscover state

      connectionPool = cache.getSlotPool(slot);

      if (connectionPool != null) {

        return connectionPool.getResource();

      } else {

        //no choice, fallback to new connection to random node

        return getConnection();

      }

    }

  }

4.3.1.2 分析连接池参数

通过跟中间件团队沟通,以及参考commons-pool2官方文档修改如下;

参数调整后,1S以上的请求量得到减少,但还是存在,上游反馈降级量由每天90万左右降到每天6W个(关于maxWaitMillis设置为200MS后为什么还会有超过200MS的请求,下文有解释)。

参数优化后Reds最大响应时间

参数优化后接口报错量

4.3.2 持续优化

优化不能停止,如何把Redis的所有写入请求降低到200MS以内,此时的优化思路还是调整客户端配置参数,分析Jedis获取连接相关源码;

Jedis获取连接源码

public T borrowObject(final long borrowMaxWaitMillis) throws Exception {

    assertOpen();

 

    final AbandonedConfig ac = this.abandonedConfig;

    if (ac != null && ac.getRemoveAbandonedOnBorrow() &&

            (getNumIdle() < 2) &&

            (getNumActive() > getMaxTotal() - 3) ) {

        removeAbandoned(ac);

    }

 

    PooledObject<T> p = null;

 

    // Get local copy of current config so it is consistent for entire

    // method execution

    final boolean blockWhenExhausted = getBlockWhenExhausted();

 

    boolean create;

    final long waitTime = System.currentTimeMillis();

 

    while (p == null) {

        create = false;

        p = idleObjects.pollFirst();

        if (p == null) {

            p = create();

            if (p != null) {

                create = true;

            }

        }

        if (blockWhenExhausted) {

            if (p == null) {

                if (borrowMaxWaitMillis < 0) {

                    p = idleObjects.takeFirst();

                } else {

                    p = idleObjects.pollFirst(borrowMaxWaitMillis,

                            TimeUnit.MILLISECONDS);

                }

            }

            if (p == null) {

                throw new NoSuchElementException(

                        "Timeout waiting for idle object");

            }

        } else {

            if (p == null) {

                throw new NoSuchElementException("Pool exhausted");

            }

        }

        if (!p.allocate()) {

            p = null;

        }

 

        if (p != null) {

            try {

                factory.activateObject(p);

            } catch (final Exception e) {

                try {

                    destroy(p);

                } catch (final Exception e1) {

                    // Ignore - activation failure is more important

                }

                p = null;

                if (create) {

                    final NoSuchElementException nsee = new NoSuchElementException(

                            "Unable to activate object");

                    nsee.initCause(e);

                    throw nsee;

                }

            }

            if (p != null && (getTestOnBorrow() || create && getTestOnCreate())) {

                boolean validate = false;

                Throwable validationThrowable = null;

                try {

                    validate = factory.validateObject(p);

                } catch (final Throwable t) {

                    PoolUtils.checkRethrow(t);

                    validationThrowable = t;

                }

                if (!validate) {

                    try {

                        destroy(p);

                        destroyedByBorrowValidationCount.incrementAndGet();

                    } catch (final Exception e) {

                        // Ignore - validation failure is more important

                    }

                    p = null;

                    if (create) {

                        final NoSuchElementException nsee = new NoSuchElementException(

                                "Unable to validate object");

                        nsee.initCause(validationThrowable);

                        throw nsee;

                    }

                }

            }

        }

    }

 

    updateStatsBorrow(p, System.currentTimeMillis() - waitTime);

 

    return p.getObject();

}

获取连接的大致流程如下:

是否有空闲连接,有空闲连接就直接返回,没有就创建;

创建时如果超出最大连接数,则判断是否有其他线程在创建连接,如果没则直接返回,如果有则等待maxWaitMis时间(其他线程可能创建失败),如果未超出最大连接,则执行创建连接操作(此时获取连接等待时间可能会大于maxWaitMs)。

如果创建不成功,则判断是否是阻塞获取连接,如果不是则直接抛出异常,连接池不够用,如果是则判断maxWaitMillis是否小于0,如果小于0则阻塞等待,如果大于0则阻塞等待maxWaitMillis。

后续就是根据参数来判断是否需要做连接check等。

根据以上流程分析,maxWaitMills目前设置的为200,以上流程加起来最大阻塞时间为400MS,大部分情况为200MS,不应该出现超出400MS的突刺。

此时问题可能出现在创建连接上,因为创建连接比较耗时,且创建时间不定,重点分析是否有这个场景,通过DB后台监控Redis连接情况。

DB后台监控Redis服务连接

分析上图发现,确实在几个时间点(9:00,12:00,19:00...),redis连接数存在上涨情况,跟Redis突刺时间基本吻合。 感觉(之前的各种尝试后,已经不敢用确定了)问题到此定位清晰(在突增流量过来时,连接池可用连接满足不了需求,会创建连接,造成请求等待)。

此时的想法是在服务启动时就进行连接池的创建,尽量减少新连接的创建,修改连接池参数 v ivo.cache.depend.common.poolConfig.minIdle,结果竟然无效???

啥都不说了,开始撸源码,jedis底层使用的是commons-poll2来管理连接的,查看项目中使用的commons-pool2-2.6.2.jar部分源码;

CommonPool2源码

public GenericObjectPool(final PooledObjectFactory<T> factory,

        final GenericObjectPoolConfig<T> config) {

 

    super(config, ONAME_BASE, config.getJmxNamePrefix());

 

    if (factory == null) {

        jmxUnregister(); // tidy up

        throw new IllegalArgumentException("factory may not be null");

    }

    this.factory = factory;

 

    idleObjects = new LinkedBlockingDeque<>(config.getFairness());

 

    setConfig(config);

}

竟然发现没有初始化连接的地方,开始咨询中间件团队,中间件团队给出的源码(commons-pool2-2.4.2.jar)如下,方法执行后多了一次startEvictor方法的调用?

CommonPool2源码

1、初始化连接池
public GenericObjectPool(PooledObjectFactory<T> factory,
            GenericObjectPoolConfig config) {
super(config, ONAME_BASE, config.getJmxNamePrefix());
if (factory == null) {
            jmxUnregister(); // tidy up
throw new IllegalArgumentException("factory may not be null");
        }
this.factory = factory;
        idleObjects = new LinkedBlockingDeque<PooledObject<T>>(config.getFairness());
        setConfig(config);
        startEvictor(getTimeBetweenEvictionRunsMillis());
    }

为啥不一样???开始检查Jar包,版本不一样,中间件给出的版本是在V2.4.2,项目实际使用的是V2.6.2,分析startEvictor有一步逻辑正是处理连接池预热逻辑。

Jedis连接池预热

1、final void startEvictor(long delay) {

        synchronized (evictionLock) {

            if (null != evictor) {

                EvictionTimer.cancel(evictor);

                evictor = null;

                evictionIterator = null;

            }

            if (delay > 0) {

                evictor = new Evictor();

                EvictionTimer.schedule(evictor, delay, delay);

            }

        }

    }

2、class Evictor extends TimerTask {

       /**

         * Run pool maintenance.  Evict objects qualifying for eviction and then

         * ensure that the minimum number of idle instances are available.

         * Since the Timer that invokes Evictors is shared for all Pools but

         * pools may exist in different class loaders, the Evictor ensures that

         * any actions taken are under the class loader of the factory

         * associated with the pool.

         */

        @Override

        public void run() {

            ClassLoader savedClassLoader =

                    Thread.currentThread().getContextClassLoader();

            try {

                if (factoryClassLoader != null) {

                    // Set the class loader for the factory

                    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(Exception e) {

                    swallowException(e);

                } catch(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 (Exception e) {

                    swallowException(e);

                }

            } finally {

                // Restore the previous CCL

                Thread.currentThread().setContextClassLoader(savedClassLoader);

            }

        }

    }

3、 void ensureMinIdle() throws Exception {

        ensureIdle(getMinIdle(), true);

    }

4、 private void ensureIdle(int idleCount, boolean always) throws Exception {

        if (idleCount < 1 || isClosed() || (!always && !idleObjects.hasTakeWaiters())) {

            return;

        }

 

        while (idleObjects.size() < idleCount) {

            PooledObject<T> p = create();

            if (p == null) {

                // Can't create objects, no reason to think another call to

                // create will work. Give up.

                break;

            }

            if (getLifo()) {

                idleObjects.addFirst(p);

            } else {

                idleObjects.addLast(p);

            }

        }

        if (isClosed()) {

            // Pool closed while object was being added to idle objects.

            // Make sure the returned object is destroyed rather than left

            // in the idle object pool (which would effectively be a leak)

            clear();

        }

    }

修改Jar版本,配置中心增加

vivo.cache.depend.common.poolConfig.timeBetweenEvictionRunsMillis (检查一次连接池中空闲的连接,把空闲时间超过minEvictableIdleTimeMillis毫秒的连接断开,直到连接池中的连接数到minIdle为止)。

vivo.cache.depend.common.poolConfig.minEvictableIdleTimeMillis (连接池中连接可空闲的时间,毫秒)两个参数,重启服务后,连接池正常预热,最终从Redis层面上解决问题。

优化结果如下,性能问题基本得到解决;

Redis响应时间(优化前)

Redis响应时间(优化后)

接口99线(优化前)

接口99线(优化后)

五、总结

出现线上问题时,首先要考虑的还是快速恢复线上业务,将业务的影响度降到最低,所以针对线上的业务,要提前做好限流、熔断、降级等策略,在线上出现问题时能快速找到恢复方案。 对公司各监控平台的熟练使用程度,决定了定位问题的速度,每个开发都要把熟练使用监控平台(机器、服务、接口、DB等)作为一个基本能力。

Redis出现响应慢时,可以优先从Redis集群服务端(机器负载、服务是否有慢查询)、业务代码(是否有BUG)、客户端(连接池配置是否合理)三个方面去排查,基本上能排查出大部分Redis慢响应问题。

Redis连接池在系统冷启动时,对连接池的预热,不同commons-pool2的版本,冷启动的策略也不同,但都需要配置minEvictableIdleTimeMillis参数才会生效,可以看下common-pool2官方文档,对常用参数都做到心中有数,在问题出现时能快速定位。

连接池默认参数在解决大流量的业务上稍显乏力,需要针对大流量场景进行调优处理,如果业务上流量不是很大直接使用默认参数即可。

具体问题要具体分析,不能解决问题的时候要变通思路,通过各种方法去尝试解决问题。

相关推荐

B站收藏视频失效?mybili 收藏夹备份神器完整部署指南

本内容来源于@什么值得买APP,观点仅代表作者本人|作者:羊刀仙很多B站用户都有过类似经历:自己精心收藏的视频突然“消失”,点开一看不是“已被删除”,就是“因UP主设置不可见”。而B站并不会主动通知...

中间件推荐初始化配置

Redis推荐初始化配置bind0.0.0.0protected-modeyesport6379tcp-backlog511timeout300tcp-keepalive300...

Redis中缓存穿透问题与解决方法

缓存穿透问题概述在Redis作为缓存使用时,缓存穿透是常见问题。正常查询流程是先从Redis缓存获取数据,若有则直接使用;若没有则去数据库查询,查到后存入缓存。但当请求的数据在缓存和数据库中都...

后端开发必看!Redis 哨兵机制如何保障系统高可用?

你是否曾在项目中遇到过Redis主服务器突然宕机,导致整个业务系统出现数据读取异常、响应延迟甚至服务中断的情况?面对这样的突发状况,作为互联网大厂的后端开发人员,如何快速恢复服务、保障系统的高可用...

Redis合集-大Key处理建议

以下是Redis大Key问题的全流程解决方案,涵盖检测、处理、优化及预防策略,结合代码示例和最佳实践:一、大Key的定义与风险1.大Key判定标准数据类型大Key阈值风险场景S...

深入解析跳跃表:Redis里的&quot;老六&quot;数据结构,专治各种不服

大家好,我是你们的码农段子手,今天要给大家讲一个Redis世界里最会"跳科目三"的数据结构——跳跃表(SkipList)。这货表面上是个青铜,实际上是个王者,连红黑树见了都要喊声大哥。...

Redis 中 AOF 持久化技术原理全解析,看完你就懂了!

你在使用Redis的过程中,有没有担心过数据丢失的问题?尤其是在服务器突然宕机、意外断电等情况发生时,那些还没来得及持久化的数据,是不是让你夜不能寐?别担心,Redis的AOF持久化技术就是...

Redis合集-必备的几款运维工具

Redis在应用Redis时,经常会面临的运维工作,包括Redis的运行状态监控,数据迁移,主从集群、切片集群的部署和运维。接下来,从这三个方面,介绍一些工具。先来学习下监控Redis实时...

别再纠结线程池大小 + 线程数量了,没有固定公式的!

我们在百度上能很轻易地搜索到以下线程池设置大小的理论:在一台服务器上我们按照以下设置CPU密集型的程序-核心数+1I/O密集型的程序-核心数*2你不会真的按照这个理论来设置线程池的...

网络编程—IO多路复用详解

假如你想了解IO多路复用,那本文或许可以帮助你本文的最大目的就是想要把select、epoll在执行过程中干了什么叙述出来,所以具体的代码不会涉及,毕竟不同语言的接口有所区别。基础知识IO多路复用涉及...

5分钟学会C/C++多线程编程进程和线程

前言对线程有基本的理解简单的C++面向过程编程能力创造单个简单的线程。创造单个带参数的线程。如何等待线程结束。创造多个线程,并使用互斥量来防止资源抢占。会使用之后,直接跳到“汇总”,复制模板来用就行...

尽情阅读,技术进阶,详解mmap的原理

1.一句话概括mmapmmap的作用,在应用这一层,是让你把文件的某一段,当作内存一样来访问。将文件映射到物理内存,将进程虚拟空间映射到那块内存。这样,进程不仅能像访问内存一样读写文件,多个进程...

C++11多线程知识点总结

一、多线程的基本概念1、进程与线程的区别和联系进程:进程是一个动态的过程,是一个活动的实体。简单来说,一个应用程序的运行就可以被看做是一个进程;线程:是运行中的实际的任务执行者。可以说,进程中包含了多...

微服务高可用的2个关键技巧,你一定用得上

概述上一篇文章讲了一个朋友公司使用SpringCloud架构遇到问题的一个真实案例,虽然不是什么大的技术问题,但如果对一些东西理解的不深刻,还真会犯一些错误。这篇文章我们来聊聊在微服务架构中,到底如...

Java线程间如何共享与传递数据

1、背景在日常SpringBoot应用或者Java应用开发中,使用多线程编程有很多好处,比如可以同时处理多个任务,提高程序的并发性;可以充分利用计算机的多核处理器,使得程序能够更好地利用计算机的资源,...

取消回复欢迎 发表评论: