一次ES检索的性能优化经验记录

news2025/1/11 17:43:31

优化功能: 统一检索能力,为各服务所调用。

该接口并发压力大,压测效果不理想。
初步2k线程两台压测机预发环境压测结果两pod下为400qps左右,单pod 平均qps200,响应时间在五分钟之后达到了峰值,平响达到几十秒开外。

  • 压测环境:内网环境,过网关压测,压测链路:网关→后台服务。

一、优化初期

出现这样的情况,是意想之外的,考虑到,现有的压测环境,在之前已预估es集群资源规划,并提交运维部署,es本身有多种缓存机制, 包括角色划分等,使得集群具有一定的健壮性。已当前的数据,应该被es集群当作热点数据缓存到文件缓存中,除去给到es主节点的4g堆内存与其他data节点的堆内存开外,文件缓存完全够用。
在这里插入图片描述

由于考虑到es集群性能应该还是可以的,碰到这样的性能问题,首先考虑到的是网络io及磁盘io等,因此首先验证io问题,经询问压测同事并经其验证,内网环境下,且压测机性能足够的情况下,在压测几分钟之后的确出现了访问及其缓慢的情况。

此时方向转向磁盘io,经询问运维得知,预发布环境es集群并未按照预想的,进行角色划分,分配资源等,且日志使用相同的es集群

在这里插入图片描述

上图中可以看到es集群的三个节点并未划分角色,同时ram内存占用有两个data数据节点较高,es内存除本身程序的jvm堆内存占用之外,剩余的内存可以被Lucene占用,理论上留给Lucene的内存越多,查询性能越好.

经查看该集群的索引情况,发现日志相关的索引,有的单索引都已经达到了7G之多。
因此可以猜测:在压测过程中,es集群负载较高,在内存未达百分百之前,es查询性能并未明显下降,但当内存占用达到百分百后,性能开始明显下降。
由于猜测无法验证,集群资源都不在自己这,只能与运维沟通,按照我给的es集群配置,来增强正式环境es集群的健壮性。

二、旁敲侧击

在得知无法验证自己的猜想后,转而正好被同事告知,在项目代码中,配置es客户端所连接的集群节点时,只配置了其中一个节点,这让我想起,代码层可能还有优化的余地。

首先来说,只配置一个节点,并不会影响程序的查询效果,这得益于es集群内但节点的多角色性,默认情况下,每个节点都是候选主节点,都有可能成为主节点,同时每个节点又默认都是协调节点,这就导致,只配置了一个节点,但通过协调节点的特性,可以路由到其他节点进行shard查询,并归并结果。

这带来了第一个问题: 由于没有客户端的负载,路由压力虽然很小,但都打到了该节点上,其次是如果真的出现主节点宕机且正好为配置的该节点,则会出现长时间的不可用。

在同事修改完代码配置后添加其余节点后,再次审视之前的代码,发现所有的检索请求,无一例外进行了es聚类分析(聚合),这首先会对es集群带来更高的cpu和内存消耗,因此首先对代码进行一波调优:
拆分es查询条件,细化粒度,只对需要聚合分析的场景进行聚合,避免不必要的性能消耗。

再次进行一波模拟压测。依旧不尽人意,最终考虑到:

  • es查询时效性要求不高
  • 数据一致性要求不高
  • 本身具有多种缓存机制

因此考虑添加caffeine本地缓存,进行有限容量下,无限缓存+主动刷新缓存的策略,来实现性能的第一波跨越

@Configuration
@Slf4j
@EnableCaching
public class CaffeineCacheConfig {

    private static final ExecutorService TASK_EXECUTOR = new ThreadPoolExecutor(30, 50, 5, TimeUnit.SECONDS,
            new ArrayBlockingQueue<>(1000, false),
            new ThreadFactoryBuilder().setNamePrefix("refreshSearchResThread-").build(),
            new ThreadPoolExecutor.AbortPolicy());

    @Bean
    public Caffeine<Object, Object> caffeineCache() {
        return Caffeine.newBuilder()
                .refreshAfterWrite(5, TimeUnit.SECONDS)
                .softValues()
                // 初始的缓存空间大小
                .initialCapacity(1000)
                // 使用自定义线程池
                .executor(TASK_EXECUTOR)
                .removalListener(((key, value, cause) -> log.info("key:{} removed, removalCause:{}.", key, cause.name())))
                // 缓存的最大条数
                .maximumSize(100000);
    }

    @Bean
    public CacheManager cacheManager() {
        CaffeineCacheManager caffeineCacheManager = new CaffeineCacheManager();
        caffeineCacheManager.setCaffeine(caffeineCache());
        caffeineCacheManager.setAllowNullValues(true);
        caffeineCacheManager.setCacheLoader(new CacheLoader<Object, Object>() {
            @Override
            public Object load(Object key) {
                log.info("载入缓存key:{}", key);
                return null;
            }

            @Override
            public Object reload(Object key, Object oldValue) {
                log.info("刷新缓存key:{},oldValue{}", key, oldValue);
                return null;
            }
        });
        return caffeineCacheManager;
    }
}
@Cacheable(cacheNames = {"search:req:hash"}, key = "#searchParamVO.hashCode()", sync = true)

以上采用集成springboot cache注解的方式,采用缓存失效同步刷新,仅一个线程抢锁进入刷新缓存,防止缓存击穿,使用入参的hashCode作为缓存key来实现本地缓存机制。

在修改完代码后,再次进行一波模拟压测,效果显著提升。

在这里插入图片描述

但出于对性能的严苛追求,在观察模拟压测qps情况,以及平响之后发现,平响虽较之前有了明显优化,进入到了个位数的秒级,即六秒多,深感不应该出现这样的问题,仔细观察曲线发现,曲线波动比较频繁,下意识考虑到程序的jvm波动及线程阻塞情况。

此时通过jprofile检测正在运行的jvm,再次进行一波模拟压测,查看线程实时状态(可通过jstack或jconsole查看),发现存在线程hang住的情况,再次观察,发现是logback日志输出时的ASYNC-ALL appender 相关的线程出现了阻塞。

当时出问题的配置:
<appender name="ASYNC-ERROR" class="ch.qos.logback.classic.AsyncAppender">
   <discardingThreshold>0</discardingThreshold>
   <appender-ref ref="FILE-ERROR"/>
</appender>
<appender name="ASYNC-WARN" class="ch.qos.logback.classic.AsyncAppender">
   <discardingThreshold>0</discardingThreshold>
   <appender-ref ref="FILE-WARN"/>
</appender>
<appender name="ASYNC-ALL" class="ch.qos.logback.classic.AsyncAppender">
   <discardingThreshold>0</discardingThreshold>
   <appender-ref ref="FILE-ALL"/>
</appender>
<appender name="ASYNC-DEBUG" class="ch.qos.logback.classic.AsyncAppender">
   <appender-ref ref="FILE-DEBUG"/>
</appender>
<appender name="ASYNC-STDOUT" class="ch.qos.logback.classic.AsyncAppender">
   <appender-ref ref="STDOUT"/>
</appender>

由于当时新加了all日志,即不配置filter过滤日志级别的日志appender,直接copy了上方的warn及error日志的配置,忘了修改其discardingThreshold参数。

discardingThreshold参数的含义为: 队列剩余容量少于discardingThreshold的配置就会丢弃<=INFO级别的日志,warn与error日志的appender为了防止日志丢失,配置了值为0,及阻塞线程直到输出完毕。
但all日志为不分日志级别全都输出,还配置此参数,这就导致了并发压力大的情况下,logback日志线程阻塞队列默认容量256,及queueSize=256,可能会出现的线程阻塞的情况。修改后的参数如下:

<appender name="ASYNC-ERROR" class="ch.qos.logback.classic.AsyncAppender">
   <discardingThreshold>0</discardingThreshold>
   <appender-ref ref="FILE-ERROR"/>
</appender>
<appender name="ASYNC-WARN" class="ch.qos.logback.classic.AsyncAppender">
   <discardingThreshold>0</discardingThreshold>
   <appender-ref ref="FILE-WARN"/>
</appender>
<appender name="ASYNC-ALL" class="ch.qos.logback.classic.AsyncAppender">
   <appender-ref ref="FILE-ALL"/>
   <neverBlock>true</neverBlock>
</appender>
<appender name="ASYNC-DEBUG" class="ch.qos.logback.classic.AsyncAppender">
   <appender-ref ref="FILE-DEBUG"/>
   <neverBlock>true</neverBlock>
</appender>
<appender name="ASYNC-STDOUT" class="ch.qos.logback.classic.AsyncAppender">
   <appender-ref ref="STDOUT"/>
   <neverBlock>true</neverBlock>
</appender>

去除了all日志appender的discardingThreshold参数,添加了neverBlock参数为true。

再次进行模拟压测,平响由6秒提升到了两秒左右,同时波动曲线依旧出现不平稳的情况,此时查看jvm gc情况,在堆内存不足设置过小的情况下,频繁的old gc可能会导致波动曲线不稳的情况,此时对jvm进行参数配置的更改:

-Xms2g -Xmx2g -Xmn512m -XX:MaxMetaspaceSize=256m -XX:SurvivorRatio=8 -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/pid$KaTeX parse error: Expected group after '_' at position 1: _̲(date +%Y-%m-%d_%H:%M:%S)_oom.hprof -Dfile.encoding=UTF-8 -XX:+UseG1GC

再次查看线程情况,发现每隔一段时间,请求的tomcat线程依旧会出现大规模hang住等待的情况,这是由于之前的缓存刷新策略,为了防止缓存击穿,高并发请求下,大量请求在缓存失效的瞬间打到es集群,采用了sync=true的方式。即当缓存失效时,仅有一个抢锁成功的线程,进入业务逻辑,刷新缓存,其他线程阻塞等待缓存刷新完毕。

难道其他线程在此全都等着,这合理吗?

当然不行,我们知道,八股文会告诉你,可以这样搞,但其实生产环境,还真很少有这样做的,如果采用分布式缓存,例如redis,其实可以采取无限缓存+刷新的机制,给到一个逻辑过期时间,当进入的请求获取缓存取到逻辑过期时间判断已过期时,才去抢锁进行刷新缓存,其余的则取旧缓存直接返回。这就避免了线程hang住,高并发下瞬间打爆连接池的情况。

那么我们采用的是本地缓存,有没有什么方法能避免呢?

有,可以通过缓存预热初始化缓存单线程执行,配合定时异步刷新缓存的机制实现,更改后的代码如下:

private static final ExecutorService TASK_EXECUTOR = new ThreadPoolExecutor(30, 50, 5, TimeUnit.SECONDS,
            new ArrayBlockingQueue<>(1000, false),
            new ThreadFactoryBuilder().setNamePrefix("refreshSearchResThread-").build(),
            new ThreadPoolExecutor.AbortPolicy());
    @Resource
    private UnifySearchService unifySearchService;
    private static final Map<String, Object> map = new ConcurrentHashMap<>();

    @Bean
    public @NonNull LoadingCache<Object, Object> caffeineCache(CacheLoader<Object, Object> cacheLoader) {
        return Caffeine.newBuilder()
                .refreshAfterWrite(5, TimeUnit.SECONDS)
                .softValues()
                // 初始的缓存空间大小
                .initialCapacity(1000)
                // 使用自定义线程池
                .executor(TASK_EXECUTOR)
                .removalListener(((key, value, cause) -> log.info("key:{} removed, removalCause:{}.", key, cause.name())))
                // 缓存的最大条数
                .maximumSize(100000)
                .build(cacheLoader);
    }

    @Bean
    public CacheLoader<Object, Object> cacheLoader() {
        return new CacheLoader<Object, Object>() {
            @Override
            public Object load(Object key) {
                log.info("载入缓存数据:{}", key);
                Cache<Object, Object> cache = (Cache<Object, Object>) map.get("search:req");
                return cache.getIfPresent(key);
            }

            @Override
            public Object reload(Object key, Object oldValue) {
                log.info("刷新缓存key:{},oldValue{}", key, oldValue);
                SearchParamVO searchParamVO = (SearchParamVO) key;
                return ResponseResult.success(unifySearchService.search(searchParamVO));
            }
        };
    }

    @Bean
    public CacheManager cacheManager(LoadingCache<Object, Object> caffeineCache) {
        SimpleCacheManager simpleCacheManager = new SimpleCacheManager();
        List<CaffeineCache> caches = new ArrayList<>();
        map.put("search:req", caffeineCache);
        for (String name : map.keySet()) {
            caches.add(new CaffeineCache(name, (Cache<Object, Object>) map.get(name)));
        }
        simpleCacheManager.setCaches(caches);
        return simpleCacheManager;
    }
@Cacheable(cacheNames = {"search:req"}, key = "#searchParamVO", sync = true)

以上通过重写springboot CacheLoader的load和reload方法,在使用spring cache注解时,缓存会通过改loader进行缓存逻辑的执行,配置caffeine软引用,在内存将要满时,gc之后回收缓存对象,来保证系统稳定,同时配置refreshAfterWrite参数为五秒,该参数的意义是,写入后五秒刷新缓存,且有用户请求命中该缓存key的情况下,就会触发reload方法,进行缓存更新,该刷新操作是异步的,并不会造成线程阻塞,刷新期间,其余请求拿到的是旧缓存。对于没有命中缓存的请求,会执行load方法写入缓存,此方法是同步的。

题外话:很多公司具有多级缓存基础架构建设,可以采用本地缓存无限缓存+redis定时缓存的机制实现。我也自行实现过,以后有机会分享
最终,在不懈努力下,效果理想,单节点qps破千:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1030486.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

万里牛与金蝶云星空对接集成查询调拨单连通调拨单新增(万里牛调拨单-金蝶【直接调拨单】)

万里牛与金蝶云星空对接集成查询调拨单连通调拨单新增(万里牛调拨单-金蝶【直接调拨单】) 源系统:万里牛 万里牛是杭州湖畔网络技术有限公司旗下SaaS软件品牌&#xff0c;主要针对电商、外贸、实体门店等业务群体&#xff0c;帮助企业快速布局新零售&#xff0c;提升订单处理效…

Appium - python

一、appium的介绍 Appium是一款开源的自动化测试工具&#xff0c;其支持iOS和安卓平台上的原生的&#xff0c;基于移动浏览器的&#xff0c;混合 的应用。Appium在不同平台中使用了标准的自动化APIs&#xff0c;所以在跨平台时&#xff0c;不需要重新编译或者修改 自己的应用。…

数学建模——统计回归模型

一、基本知识 1、基本统计量 总体&#xff1a;研究对象的某个感兴趣的指标。样本&#xff1a;从总体中随机抽取的独立个体X1,X2,…,Xn&#xff0c;一般称(X1,…,Xn)为一个样本&#xff0c;可以看成一个n维随机向量&#xff0c;它的每一取组值(x1,…,xn)称为样本的观测值。统计…

说说hashCode() 和 equals() 之间的关系?

每天一道面试题&#xff0c;陪你突击金九银十&#xff01; 上一篇关于介绍Object类下的几种方法时面试题时&#xff0c;提到equals()和hashCode()方法可能引出关于“hashCode() 和 equals() 之间的关系&#xff1f;”的面试题&#xff0c;本篇来解析一下这道基础面试题。 先祭一…

四川玖璨电子商务有限公司培训可靠吗?

四川玖璨电子商务有限公司是一家在抖音平台上进行培训的电商公司。如今&#xff0c;随着抖音带货的火热&#xff0c;越来越多的人加入到这个行业中。然而&#xff0c;对于消费者来说&#xff0c;选择一个可靠的抖音培训公司并不容易。 在这个领域中&#xff0c;四川玖璨电子商务…

微软(TTS)文本转语音服务API实现

此博客实现与java实现微软文本转语音&#xff08;TTS&#xff09;经验总结_java tts_${简简单单}的博客-CSDN博客之上&#xff0c;首先感谢博客源码的提供&#xff0c;本人在上面添加了一些详细的注释&#xff0c;方便大家跟好的理解和使用&#xff0c;毕竟我已经用原文调试了一…

openEuler 亮相全球顶级开源盛会 OSSUMMIT 2023,持续推动智能化未来的实现

2023年9月19日&#xff0c;全球顶级开源峰会 OSSUMMIT EU 2023 在西班牙-毕尔巴鄂正式开场。openEuler 作为钻石级别赞助参会。这是 openEuler 继去年正式亮相后的第二次全面参加该峰会。 本次会议&#xff0c;openEuler带来Keynote及多场分论坛演讲&#xff0c;涵盖Linux Kern…

C 初级学习笔记(基础)

目录 1.预处理器指令 预定义宏 预处理器运算符 &#xff08;\&#xff09; 参数化的宏 头文件 .h 引用头文件操作 2.函数&#xff08;标识符&关键字&运算符&#xff09;存储类 函数参数 a. 标识符&关键字 b. 运算符&#xff08;算术、关系、逻辑、位、赋…

【Java 基础篇】Java网络编程基础知识详解

网络编程是现代软件开发中不可或缺的一部分&#xff0c;它使我们能够在不同的计算机之间实现数据传输和通信。Java作为一种强大的编程语言&#xff0c;提供了丰富的网络编程库&#xff0c;使开发者能够轻松地创建网络应用程序。本文将介绍Java网络编程的基础知识&#xff0c;面…

c++opencv RotatedRect 旋转矩形角度转换和顶点顺序转换

这里写自定义目录标题 以下代码记录主要是完成轮廓点求解最小外接矩形之后计算该文本行的角度和旋转矩形的左下&#xff08;bl&#xff09;&#xff0c;左上&#xff08;tl)&#xff0c;右上&#xff08;tr),右下&#xff08;br)的坐标点。 RotatedRect rtminAreaRect(contours…

芯片SoC设计你了解吗?

数字IC设计根据岗位性质一般包含SOC设计&#xff0c;前端设计&#xff0c;ASIC设计&#xff0c;逻辑设计&#xff0c;IP设计&#xff0c;CPU设计等。 有人说&#xff1a;做IP设计就是翻译官&#xff0c;做SOC设计就是连连看。 SoC设计是做什么的&#xff1f;与IP设计有什么不同…

C#里面的三种定时计时器:Timer

在.NET中有三种计时器&#xff1a; 1、System.Windows.Forms命名空间下的Timer控件&#xff0c;它直接继承自Componet。Timer控件只有绑定了Tick事件和设置EnabledTrue后才会自动计时&#xff0c;停止计时可以用Stop()方法控制&#xff0c;通过Stop()停止之后&#xff0c;如果想…

彻底讲透redo日志磁盘顺序写机制

文章目录 引言Redo日志的作用Redo日志的磁盘顺序写机制技术和策略&#xff1a; 刷盘机制详解1. Checkpoint&#xff08;检查点&#xff09;2. Commit&#xff08;提交&#xff09; 优化策略举例说明 参考文档 引言 背景&#xff1a;今天看了一节某培训机构的公开课关于BufferPo…

高速串行总线设计基础

高速串行总线设计&#xff08;1&#xff09; 高速信号设计涉及到方方面面的知识积累&#xff0c;也许你认为即使没有掌握甚至没有听过一些高速设计的专业术语&#xff0c;也没有关系&#xff1f;因为专业集成的IP可以帮你解决这一问题&#xff0c;但殊不知&#xff0c;根基不牢…

ABB COM0011 2RAA005844A0007J编码器模块

ABB COM0011 2RAA005844A0007J 编码器模块是用于测量和反馈旋转或线性位置信息的设备&#xff0c;通常用于自动化、机器控制和运动控制系统。以下是该编码器模块可能具备的产品功能&#xff1a; 位置测量&#xff1a;ABB COM0011 2RAA005844A0007J 编码器模块的主要功能是测量旋…

【初阶数据结构】——堆排序和TopK问题

个人主页 代码仓库 C语言专栏 初阶数据结构专栏 Linux专栏 接上篇二叉树和堆的引入 目录 前言 建堆 插入数据向上调整算法建堆 移动数据向上调整算法建堆 无序数组从H-1层向上移动的向下调整算法建堆 堆排序 TOP-K问题 前言 上篇文章详细讲解了堆&#xff0c;…

腐蚀与膨胀,图像处理中的结构元与腐蚀膨胀问题(数字图像处理大题复习 P9)

文章目录 腐蚀膨胀 腐蚀 我们要用结构元 B 来 腐蚀 图像 A 如果结构元 B 放到图像的某个地方&#xff0c;正好能对上&#xff0c;那么就把中心放到结果图像上 这里显然红色框是对不上的&#xff0c;而黄色是对得上的 对不上的&#xff0c;全部补上0&#xff0c;这就是结果了…

HTTP 响应头Cache-Control

每个资源都可以通过Http头Cache-Control来定义自己的缓存策略&#xff0c;Cache-Control控制谁在什么条件下可以缓存响应以及可以缓存多久。 最快的请求是不必与服务器进行通信的请求&#xff1a;通过响应的本地副本&#xff0c;我们可以避免所有的网络延迟以及数据传输的数据…

新版考勤管理系统正式发布

O2OA(翱途)开发平台 V8.1版本&#xff0c;因老的考勤管理系统已经无法满足用户需求&#xff0c;并且在架构和业务结构上都不再符合现在大多数考勤功能的需求。我们对考勤管理重新进行了开发&#xff0c;全新的版本更好用&#xff0c;更直观。 考勤管理对员工的工作出勤情况进行…

Multisim—用示波器观察电流波形

使用仪表里面的current probe&#xff1a;它显示实时电流&#xff0c;但无法将电流波形在示波器中显示。 仿真Simulate >仪器Instruments > 电流探针 current clamp&#xff1a;也就是电流钳&#xff0c;它能够将流经的电流转成电压并输入到示波器中进行显示。 双击图纸上…