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

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

该接口并发压力大,压测效果不理想。
初步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());@Beanpublic 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);}@Beanpublic CacheManager cacheManager() {CaffeineCacheManager caffeineCacheManager = new CaffeineCacheManager();caffeineCacheManager.setCaffeine(caffeineCache());caffeineCacheManager.setAllowNullValues(true);caffeineCacheManager.setCacheLoader(new CacheLoader<Object, Object>() {@Overridepublic Object load(Object key) {log.info("载入缓存key:{}", key);return null;}@Overridepublic 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());@Resourceprivate UnifySearchService unifySearchService;private static final Map<String, Object> map = new ConcurrentHashMap<>();@Beanpublic @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);}@Beanpublic CacheLoader<Object, Object> cacheLoader() {return new CacheLoader<Object, Object>() {@Overridepublic Object load(Object key) {log.info("载入缓存数据:{}", key);Cache<Object, Object> cache = (Cache<Object, Object>) map.get("search:req");return cache.getIfPresent(key);}@Overridepublic Object reload(Object key, Object oldValue) {log.info("刷新缓存key:{},oldValue{}", key, oldValue);SearchParamVO searchParamVO = (SearchParamVO) key;return ResponseResult.success(unifySearchService.search(searchParamVO));}};}@Beanpublic 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.rhkb.cn/news/143502.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

跨境电商引流之Reddit营销,入门保姆级攻略

在当今竞争激烈的在线市场中&#xff0c;企业不断寻求新的方法来加强其数字营销工作。Reddit 是最受欢迎的社交媒体平台之一&#xff0c;为企业提供了巨大的潜力&#xff0c;可以通过引人入胜且相关的内容来接触目标受众。然而&#xff0c;将 Reddit 用于营销目的需要仔细考虑某…

企业专线成本高?贝锐蒲公英轻松实现财务系统远程访问

在办公及信息系统领域&#xff0c;许多企业纷纷采用金蝶等财务管理软件来提升运营效率。以某食品制造企业为例&#xff0c;该企业总部位于广州&#xff0c;并拥有湖北仙桃工厂、广州从化工厂和湖南平江工厂三大生产基地。为提高管理效率&#xff0c;该企业在广州总部局域网内部…

游戏社区-搭建的目的和意义是什么

在游戏社区中&#xff0c;用户的活跃度指标是至关重要的&#xff0c;因此在必要时&#xff0c;我们会进行指标转化&#xff0c;以丰富的内容形式来促进用户的活跃度&#xff1b;作为一个垂直社区&#xff0c;我们可以通过聚合和培养一批游戏KOL&#xff0c;建立用户之间的紧密联…

成都瀚网科技:抖音上线地方方言自动翻译功能

为了让很多方言的地域历史、文化、习俗能够以短视频的形式生产、传播和保存&#xff0c;解决方言难以被更多用户阅读和理解的问题&#xff0c;平台正式上线推出当地方言自动翻译功能。创作者可以利用该功能&#xff0c;将多个方言视频“一键”转换为普通话字幕供大众观看。 具体…

leetcode 23. 合并 K 个升序链表

2023.9.25 本题要合并k个有序链表&#xff0c;最朴素的方法可以联想到之前做的合并两个有序链表。 用一个for循环遍历lists数组&#xff0c;利用合并两个有序链表的代码&#xff0c;不断合并lists中的链表&#xff0c;最后返回头节点即可。 代码如下&#xff1a; /*** Definit…

vue-cli创建项目、vue项目目录结(运行vue项目)、ES6导入导出语法、vue项目编写规范

vue-cli创建项目、vue项目目录结构、 ES6导入导出语法、vue项目编写规范 1 vue-cli创建项目 1.1 vue-cli 命令行创建项目 1.2 使用vue-cli-ui创建 2 vue项目目录结构 2.1 运行vue项目 2.2 vue项目的目录结构 3 es6导入导出语法 4 vue项目编写规范 4.1 修改项目 4.2 以后…

VC++判断程序是否已经运行;仅运行一次

VC判断程序是否已经运行&#xff1b;仅运行一次 BOOL CClientApp::InitInstance() {...//判断程序是否已经运行&#xff1b;仅运行一次CreateMutex(NULL,true,_T("xxxxx")); //xxxxx&#xff1a;为程序标识码if(GetLastError()ERROR_ALREADY_EXISTS) { AfxMess…

C#求100-999之间的水仙花数,你知道多少个?让我们一起来探索!

目录 背景: 扩展: 水仙花数例子: 效果展示:​ 总结: 背景: 水仙花数&#xff08;Narcissistic number&#xff09;也被称为超完全数字不变数&#xff08;pluperfect digital invariant, PPDI&#xff09;、自恋数、自幂数、阿姆斯壮数或阿姆斯特朗数&#xff08;Armstrong…

博主老程序员长期个人接单

主要技术栈 &#xff1a; 后端: .net winform webapi 前端&#xff1a;vue2 vue3 微信小程序 数据库&#xff1a; sqlserver mysql 小程序案例&#xff1a;快猪小寓微信小程序客户端 后台管理系统 联系微信 或 QQ 35568701

网络编程day04(网络属性函数、广播、组播、TCP并发)

今日任务 对于newfd的话&#xff0c;最好是另存然后传入给分支线程&#xff0c;避免父子线程操作同一个文件描述符 ------------在tcp多线程服务端---------- 如果使用全局变量&#xff0c;或者指针方式间接访问&#xff0c;会导致所有线程共用一份newfd和cin&#xff0c;那么…

冲刺十五届蓝桥杯P0001阶乘求和

文章目录 题目描述思路分析代码解析 题目描述 思路分析 阶乘是蓝桥杯中常考的知识。 首先我们需要知道 int 和long的最大值是多少。 我们可以知道19的阶乘就已经超过了long的最大值&#xff0c;所以让我们直接计算202320232023&#xff01;的阶乘是不现实的。 所以我们需要…

基于Linux socket聊天室-多线程服务器模型(01)

​前言 socket在实际系统程序开发当中&#xff0c;应用非常广泛&#xff0c;也非常重要。实际应用中服务器经常需要支持多个客户端连接&#xff0c;实现高并发服务器模型显得尤为重要。高并发服务器从简单的循环服务器模型处理少量网络并发请求&#xff0c;演进到解决C10K&…

什么是推挽电路?

推挽电路原理&#xff1a; 可以简单理解为推和拉&#xff1b; 此电路总共用到两个元器件&#xff0c;对应图中的Q1----NPN三极管&#xff0c;Q2----PNP三极管&#xff0c;两个电阻R1和R2起到限流的作用&#xff1b;两个三极管的中间对应信号的输出。 下面就举例说明是如何工作的…

基于JAVA,SpringBoot和Vue的前后端分离的求职招聘系统

✌全网粉丝20W,csdn特邀作者、博客专家、CSDN新星计划导师、java领域优质创作者,博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java技术领域和毕业项目实战✌ &#x1f345;文末获取项目下载方式&#x1f345; 一、项目背景介绍&#xff1a; 这个系统的研发背景是…

时序预测 | MATLAB实现POA-CNN-BiLSTM鹈鹕算法优化卷积双向长短期记忆神经网络时间序列预测

时序预测 | MATLAB实现POA-CNN-BiLSTM鹈鹕算法优化卷积双向长短期记忆神经网络时间序列预测 目录 时序预测 | MATLAB实现POA-CNN-BiLSTM鹈鹕算法优化卷积双向长短期记忆神经网络时间序列预测预测效果基本介绍程序设计参考资料 预测效果 基本介绍 MATLAB实现POA-CNN-BiLSTM鹈鹕算…

使用SPY++查看窗口信息去排查客户端UI软件问题

目录 1、使用SPY查看窗口的信息 2、使用SPY查看某些软件UI窗口用什么UI组件实现的 2.1、查看海康视频监控客户端安装包程序 2.2、查看华为协同办公软件WeLink 2.3、查看字节协同办公软件飞书 2.4、查看最新版本的Chrome浏览器 2.5、查看小鱼易连视频会议客户端软件 2.6…

第十四届蓝桥杯大赛软件赛决赛 C/C++ 大学 B 组 试题 A: 子 2023

[蓝桥杯 2023 国 B] 子 2023 试题 A: 子 2023 【问题描述】 小蓝在黑板上连续写下从 1 1 1 到 2023 2023 2023 之间所有的整数&#xff0c;得到了一个数字序列&#xff1a; S 12345678910111213 ⋯ 20222023 S 12345678910111213\cdots 20222023 S12345678910111213⋯2…

讯飞星火认知大模型Java后端接口

文章目录 1.免费申请星火大模型套餐2.Java后端接口说明2.1 项目地址2.2 项目说明2.3 项目结构2.4 项目代码&#x1f340; maven 依赖&#x1f340; application.yml 配置文件&#x1f340; config 包&#x1f4cc; XfXhConfig &#x1f340; dto 包&#x1f4cc; MsgDTO&#x…

element中使用el-steps 进度条效果demo(整理)

<template><div class"margin-top20"><!-- align-center 不要居中就去掉 --><!-- process-status 这几个参数值&#xff1a;改变颜色 wait / process / finish / error / --><!-- active 到第几个是绿色 --><el-steps :space&qu…