记录一次首页优化的经历

         公司最近要进行多品牌合一,原来五个品牌的app要合并为一个。品牌立项、审批、方案确定,历史数据迁移、前期的基础工程搭建,兼容以及涉及三方的交互以及改造,需求梳理等也都基本完成,原来计划9月中旬进行上线,但是上线后服务端的压测一直通不过-首页抗不过太高的并发。

        app首页里面是一个信息流,里面包含运营以及用户发布的各种视频、动态以及问答之类的帖子,广告位、运营后台配置的推荐帖以及会在信息流指定位置放置一些banner位等,还包括智能推荐、三方推荐以及一部分自研的推荐数据(服务端有开关配置走那种推荐系统),服务端这块统一由一个接口组装各种数据后返回给app端。组装的数据大部分是存在redis缓存里面,比如广告位、banner位以及一些运营自己在后台配置的一些配置,这些因为变化不大或者基本不怎么变化,从缓存读取耗时也都很快的,耗时比较慢的点主要集中在 内部的feign调用以及查询es信息流时候要进行组装信息,这些在压测时候基本占用了整个应用耗时的90%以上。使用hutool的StopWatch统计耗时大概如下这种:(单位 ms)

 

000000201 08% 组装es数据

000000599 25% feign调用uc获取黑名单数据

000000495 21% feign调用社区查询置顶数据

000000603 25% 查询帖子列表

000000394 16% 修改意向车型

000000002 00% 获取内容标签数据

000000000 00% 获取热门话题数据

000000002 00% 获取专题组件数据

000000090 04% 获取信息流广告位数据

000000001 00% 获取快速入口数据

000000002 00% 获取轮播广告数据

      

 在单台机器 2核4G,压测100并发的情况下,平均耗时能达到2s左右,p90 能在3s多,整体性能很差。

      在压测时候,使用arthas的监控工具,在耗时比较严重的地方进行监控,首先是feign调用的地方,发现有个奇怪的线程,使用trace命令 监控超过30ms的请求,发现很少 

  trace com.gwm.marketing.service.impl.HomeRecommendServiceImpl addSuperStreams '#cost > 30' -n 50 --skipJDKMethod false

 

   但是在调用方的耗时 能达到惊人的1s以上,如果是并发量比较少的情况,比如50并发左右,这个耗时不超过200ms,但是一旦并发过高立马耗时就上来了。服务端内部是通过feign调用来进行rpc调用的,而且被调用方的耗时也很快,初步怀疑是 不是 调用方 存在gc的问题,或者是调用方 线程池阻塞 以及  feign调用在高并发下的默认的链接存在阻塞导致的。另外还有发现es以及部分日志也陆续存在耗时比较久的情况,总体上这些加起来 ,导致整个耗时比较严重。

       首先看了下服务器的gc,单机2核4g 配置的gc信息如下:

   -Djava.net.preferIPv4Stack=true -Djava.util.Arrays.useLegacyMergeSort=true -Dfile.encoding=UTF-8 -server -Xms512m -Xmx512m -XX:NewSize=128m -XX:MaxNewSize=128m -Xss256k -Drocketmq.client.logUseSlf4j=true -XX:+UseG1GC -XX:+PrintGCTimeStamps -Xloggc:log/gc-%t.log -XX:+UseGCLogFileRotation -XX:GCLogFileSize=10M

     这个配置还使用的是G1,首页的这个场景,首页是服务器的内存过小,使用g1的话,对于我们想要的是吞吐量更大的其实有些差距,我们的目的是为了吞吐量更大,响应时间更快,那么使用cms的方式其实更好。然后让运维改了一下 gc的回收方式为cms:

-Djava.net.preferIPv4Stack=true -Djava.util.Arrays.useLegacyMergeSort=true -Dfile.encoding=UTF-8 -server -Xms512m -Xmx512m -XX:NewSize=128m -XX:MaxNewSize=128m -Xss256k -Drocketmq.client.logUseSlf4j=true -XX:+UseConcMarkSweepGC -XX:+CMSIncrementalMode -XX:CMSInitiatingOccupancyFraction=75 -Xloggc:log/gc-%t.log -XX:+UseGCLogFileRotation -XX:GCLogFileSize=10M

 实测后发现有一定的提升,比如使用g1的话,压测5分钟的耗时 和使用cms的压测耗时

 

 

  这个是能提升一定的性能,然后耗时比较严重的第二个部分 就是 feign调用- 为何单个接口的耗时很快,但是在feign调用时候就很慢?

      首先feign调用 我们的配置是走了内网的,即不走网关-

      

 

      我们的项目是基于nepxion里面的openfeign,openfeign使用的是默认的HttpURLConnection的链接方式,而且看了我们的openfeign的版本是 2.2.3的,这个版本还没有池化的概念-就是每次的rpc通过

feign调用的话,每次都需要走tpc的三次握手和四次挥手, 这个耗时在请求量比较少的情况下体现不出来,但是在并发量比较高的情况下,就体现出来了- 表现就是 明明在被调用方的耗时很小,但是在调用方显示出来的耗时就是很长,这个通过arthas和skywalking 的监控都证明了这一点。skywalking显示的 self耗时很长但是业务耗时很快,arthas在容器内看好是情况也是如此,因此需要对feign调用进行改造。

      首先是对open-feign的版本进行升级,openfeign支持httpclient以及okhttpclient ,在最新的 4.2.0-SNAPSHOT 上 OkHttpFeignLoadBalancedConfiguration 里面已经是使用了连接池了,源码如下:

    

@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(OkHttpClient.class)
@ConditionalOnProperty("spring.cloud.openfeign.okhttp.enabled")
@ConditionalOnBean({ LoadBalancerClient.class, LoadBalancerClientFactory.class })
@EnableConfigurationProperties(LoadBalancerClientsProperties.class)
class OkHttpFeignLoadBalancerConfiguration {@Bean@ConditionalOnMissingBean@Conditional(OnRetryNotEnabledCondition.class)public Client feignClient(okhttp3.OkHttpClient okHttpClient, LoadBalancerClient loadBalancerClient,LoadBalancerClientFactory loadBalancerClientFactory,List<LoadBalancerFeignRequestTransformer> transformers) {OkHttpClient delegate = new OkHttpClient(okHttpClient);return new FeignBlockingLoadBalancerClient(delegate, loadBalancerClient, loadBalancerClientFactory,transformers);}@Bean@ConditionalOnMissingBean@ConditionalOnClass(name = "org.springframework.retry.support.RetryTemplate")@ConditionalOnBean(LoadBalancedRetryFactory.class)@ConditionalOnProperty(value = "spring.cloud.loadbalancer.retry.enabled", havingValue = "true",matchIfMissing = true)public Client feignRetryClient(LoadBalancerClient loadBalancerClient, okhttp3.OkHttpClient okHttpClient,LoadBalancedRetryFactory loadBalancedRetryFactory, LoadBalancerClientFactory loadBalancerClientFactory,List<LoadBalancerFeignRequestTransformer> transformers) {OkHttpClient delegate = new OkHttpClient(okHttpClient);return new RetryableFeignBlockingLoadBalancerClient(delegate, loadBalancerClient, loadBalancedRetryFactory,loadBalancerClientFactory, transformers);}}

     而我们这个2.2.3的还不支持池化,故首先把open-feign的版本升级了,以支持可以池化,另外就是需要配置  池化的参数以及开启okhttpclient

feign.httpclient.enabled = false #httpClient 关闭
feign.okhttp.enabled = true #okHttpClient 启用
feign.httpclient.maxConnections = 1000   #最大连接数
feign.httpclient.maxConnectionsPerRoute = 300   #feign单个路径的最大连接数
feign.httpclient.connectionTimeout = 3000  #超时时间
   
   这样优化后发现feign调用的性能好了不少。
     还有就是 针对feign调用,因为默认走的是10s超过,如果这个超时时间这么长,对系统会影响很大,而且还是首页,决定在首页里面 使用feign调用的如果超过指定的读超时和连接超时,则直接使用默认值来进行代替。代码如下:
  
                    try {Request.Options options = new Request.Options(connectionTimeoutConfig.getFeignConnectionTimeout(),connectionTimeoutConfig.getFeignReadTimeout());data = gwmInterFeignClient.queryLikeAndCollectStatusByThreadsOptionLimit(appUserThreadStatusQuery,options).getData();} catch (Exception e) {log.error("===feign调用超时====请求废弃");data = new AppUserThreadFDto();}

      再就是在arthas下面发现有日志也存在耗时严重的情况,对于非必要的日志,进行不去进行打印 或者修改日志级别为debug,当然还有一种方式 就是把写日志的方式 改成异步来写。

      另外还发现代码里面如果判断一个对象是否在集合里,也会存在性能耗时的问题,这个需要改为并发执行 ,类似代码如下: 

优化前:

                    streams.forEach(stream -> {stream.setIsLike(CollectionUtils.isNotEmpty(data.getLikeThreads()) ? (data.getLikeThreads().contains(stream.getThreadId()) ? ONE : ZERO) : ZERO);stream.setIsCollect(CollectionUtils.isNotEmpty(data.getCollectThreads()) ? (data.getCollectThreads().contains(stream.getThreadId()) ? ONE : ZERO) : ZERO);

优化后:

                    streams.forEach(stream -> {stream.setIsLike(CollectionUtils.isNotEmpty(finalData.getLikeThreads())? (finalData.getLikeThreads().parallelStream().anyMatch(d->d.equals(stream.getThreadId()))? ONE : ZERO) : ZERO);stream.setIsCollect(CollectionUtils.isNotEmpty(finalData.getCollectThreads()) ? (finalData.getCollectThreads().parallelStream().anyMatch(d->d.equals(stream.getThreadId())) ? ONE : ZERO) : ZERO);});

 

除了这个还有es的耗时严重的的问题,我们的es 采用的是5.7的版本,比较老了,es主要是组装数据 以及查询相对比较方便,库表大概有几千万的帖子数据,但是每次耗时 有时候也能达到400-500ms的延迟。

 这个暂时没找到原因,后面有时间了在继续。

        还有就是在首页里面加上sentinel 限流,首页里面有一部分耗时比较长的部分,比如 调用三方的接口,比如推荐部分,这部分三方接口本身就有一些性能压力,我们这边在调用时候 是使用的http来进行调用的,超时时间是3s,实测发现,当并发达到10 到20的时候,这部分基本就3s超时了。一部分是他们的推荐规则确实挺复杂的,推荐的数据要进行达标,要根据用户的个个业务维度去进行筛选,虽然数据已经提前写入缓存了,调用里面业务有些复杂 导致耗时比较严重,特别是并发稍微高的情况下。于是在调用他们这些推荐数据的地方 加了sentinel 的限流。

        加sentinel限流的时候,一般有两种方式,一种是针对指定的资源,比如接口、指定的bean等进行限流和降级,还有一种是sentinel + openfeign 来进行限流和降级。两种方式的效果是类似的,只是配置略微有一些区别,另外第一种方式是有限制的,即限制的资源需要是spring 容器里面的bean对象,否则限流和降级不会生效。我使用的是第一种方式,代码如下

 

    @Override@SentinelResource(value = "queryRecommend", fallback = "getStaticData",blockHandler = "blockExceptionHandler")public List<String> queryRecommend(HomeRecommendStreamQuery query, HomeRecommendStreamDto streamDto, String sourceApp) {//用户身份StopWatch st = StopWatch.create("内容中台推荐数据");String userType = this.getUserType(query.getIdentity(), query.getSource(), query.getUserId());//获取当前有用户是否推荐用户信息boolean isRecommend = true;if (StringUtils.isNotEmpty(query.getUserId())) {Set<String> userClosed = gwmRedisTemplate.opsForValue().get(USER_CLOSED_RECOMMEND);if (CollectionUtils.isNotEmpty(userClosed)) {boolean anyMatch = userClosed.stream().anyMatch(str -> str.equals(query.getUserId()));if (anyMatch) {isRecommend = false;}}}//构建推荐系统对象信息RecommendQuery build = RecommendQuery.builder().userType(userType)//推荐系统需要唯一标识,如果未登录用户传设备id,登录用户传userID.userId(StringUtils.isNotEmpty(query.getUserId()) ? query.getUserId() : query.getDeviceId()).intendedBrandList(query.getIntendedBrandList()).intendedVehicleList(query.getIntendedVehicleList()).recommendationSwitch(isRecommend).strategyType(query.getStrategyType()).deviceId(query.getDeviceId()).page(query.getPage()).pageSize(query.getSize()).build();st.start("推荐调用uc黑名单接口");if (StringUtils.isNotEmpty(query.getUserId())) {CmsAppUserIdQuery qry = new CmsAppUserIdQuery();qry.setUserId(query.getUserId());List<String> list = feignUserCenterClient.getBlackUserIds(qry, sourceApp).getData();if(query.isCheckBlack()){build.setBlacklistUsers(list);}query.setBlacklistUsers(list);}st.stop();// 调用内容中台推荐接口st.start("调用内容中台推荐接口");String recommend = okhttp3Util.postBody(config.getUrl(), build);st.stop();logger.info("首页推荐查询推荐系统入参:{},推荐系统返回:{} , ssoId:{}", JSONObject.toJSONString(build), recommend, query.getUserId());RecommendResponse<RecommendStrategyDto> response = parseBody(recommend, RecommendStrategyDto.class);List<String> idList = null;if (Objects.nonNull(response)) {if (response.isSuccessful()) {RecommendStrategyDto recommendStrategyDto = response.getData();streamDto.setStrategyType(recommendStrategyDto.getStrategyType());idList = recommendStrategyDto.getContentIds();} else if (!response.recommendChannelNormal()) {streamDto.setRecommendChannelAvailable(false);return new ArrayList<>();}}//查询推荐系统数据为空if (CollectionUtils.isEmpty(idList) && query.getPage() == 1) {//nacos兜底,取默认配置数据idList = Optional.ofNullable(config.getPostIds()).orElse(Collections.emptyList());}return idList;}
    public List<String> getStaticData(HomeRecommendStreamQuery query, HomeRecommendStreamDto streamDto, String sourceApp) {logger.info("==========start sentinel 降级策略==========");try {Random random = new Random();int r = random.nextInt(100) + 1;logger.debug("========随机从缓存获取一组帖子id:" + r);Set<String> stringSet = gwmRedisTemplate.opsForValue().get(RedisConstants.THREAD_HOT_SCORE_KEY + r);logger.debug("=========随机从缓存获取帖子result=======" + JSONObject.toJSON(stringSet));if (CollectionUtils.isNotEmpty(stringSet)) {List<String> stringList = new ArrayList<>(stringSet);return stringList;} else {logger.debug("============读取默认配置数据========" + config.getPostIds());return Optional.ofNullable(config.getPostIds()).orElse(Collections.emptyList());}} catch (Exception e) {throw new RuntimeException(e);}}/*** blockHandler需要设置为static** @param ex* @return*/public  List<String> blockExceptionHandler(HomeRecommendStreamQuery query, HomeRecommendStreamDto streamDto, String sourceApp,BlockException ex) {try {Random random = new Random();int r = random.nextInt(100) + 1;logger.debug("========随机从缓存获取一组帖子id:" + r);Set<String> stringSet = gwmRedisTemplate.opsForValue().get(RedisConstants.THREAD_HOT_SCORE_KEY + r);logger.debug("=========随机从缓存获取帖子result=======" + JSONObject.toJSON(stringSet));if (CollectionUtils.isNotEmpty(stringSet)) {List<String> stringList = new ArrayList<>(stringSet);return stringList;} else {logger.debug("============读取默认配置数据========" + config.getPostIds());return Optional.ofNullable(config.getPostIds()).orElse(Collections.emptyList());}} catch (Exception e) {throw new RuntimeException(e);}}

      总结起来就是:针对首页的性能问题,优化了gc的方式,feign调用的改造,部分代码改为并行、日志优化 、es的查询优化以及使用sentinel做限流降级。 当然还有就是,我们这个首页这么多的内容都写到了一个接口里面,对于服务端的性能确实压力很大,后面看能不能进行拆分,让端上进行单个接口的请求,由一个接口变为多个接口,这样也能减少不少服务端的压力。最后优化后的压测结果:

 

 

    目前可优化的部分主要是集中在es这部分上面了。

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

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

相关文章

软工作业3:结对项目——实现一个自动生成小学四则运算题目的命令行程序

这个作业属于哪个课程 https://edu.cnblogs.com/campus/gdgy/CSGrade22-34/这个作业要求在哪里 结对项目 - 作业 - 计科22级34班 - 班级博客 - 博客园 (cnblogs.com)这个作业的目标 结对项目——实现一个自动生成小学四则运算题目的命令行程序成员1 陈奕奕 3222004552成员2 林闰…

基于LangChain手工测试用例转App自动化测试生成工具

在传统编写 App 自动化测试用例的过程中,基本都是需要测试工程师,根据功能测试用例转换为自动化测试的用例。市面上自动生成 Web 或 App 自动化测试用例的产品无非也都是通过录制的方式,获取操作人的行为操作,从而记录测试用例。整个过程类似于但是通常录制出来的用例可用性…

单细胞数据 存储方式汇总

(单细胞下游分析——不同类型的数据读入,与部分数据类型的转化) .h5ad(anndata 数据格式) 10x_mtx(cell ranger输出,三个文件) 就是cell ranger上游比对分析产生的3个文件: ├── xxx_feature_bc_matrix │ ├── barcodes.tsv.gz:细胞标签(barcode) │ ├──…

springcloud组件openfeign超时时间设置

openfeign超时时间设置有两种方式 1、通过配置类;2、通过配置文件 1、使用配置类代码如下:@Configuration public class FeignConfig {@Beanpublic Request.Options options(){//第一个参数是连接超时时间,第二个参数是处理超时时间return new Request.Options(5000,3000);}…

python虚拟环境venv

创建目录 mkdir pyenv 进入 cd pyenv 初始化环境 python3 -m venv .进入bin目录 jihan@jihandeMacBook-Pro pyenv % cd bin jihan@jihandeMacBook-Pro bin % ls Activate.ps1 activate activate.csh activate.fish pip pip3 pip3.12 python python3 python3.12 jihan@jiha…

进行网站监控有必要吗?

在当今数字化高速发展的时代,网站已经成为了企业、组织乃至个人展示自身形象、提供服务、进行交流互动的重要平台。那么,进行网站监控有必要吗?答案无疑是肯定的。 进行网站监控,首先是保障用户体验的关键。对于访问网站的用户来说,他们期望能够快速、稳定地获取所需信息或…

Typora+picgo+jsDelivr实现免费图床

Typora+picgo+github+jsDelivr实现免费图床 需求 typora中写markdown图片是保存在本地的,为了简化写博客时繁琐的插入图片步骤,直接使用typora+picgo将图片上传到云端,这样发博客就只要复制markdown即可 步骤 前期准备:github中创建一个仓库用于保存图片 名字随便填就行1.下…

线上间歇性卡顿问题

事情起因 最近一段时间我们公司有个项目是做视力筛查的,平时都是正常的,但是最近这两天突然会时不时地卡顿一下,一卡就是几分钟。排查过程 1.查看日志 卡顿首先是排查日志,日志报的是feign调用学生服务超时,进到学生服务查看时,看到日志报的是事务超时2.继续排查,既然是…

关于springcloud中openfeign中服务调用日志输出

在使用openfeign进行服务调用的时候,我们可以通过一些配置,获取到服务调用的日志输出,可以从消费端看到日志 有两种方法:一、使用配置类;二、使用配置文件配置 日志输出级别有四种: NONE:不输出(默认) BASIC:只输出请求方式、url、请求成功与否 HEADERS:输出请求头的…

《现代操作系统》第10章——实例研究1:UNIX、Linux和Android

《现代操作系统》第 10 章——实例研究 1:UNIX、Linux 和 Android 10.1 UNIX 与 Linux 的历史 第一次使 UNIX 的两种流派一致的严肃尝试来源于 IEEE(它是一个得到高度尊重的中立组织)标准委员会的赞助。有上百名来自业界、学界以及政府的人员参加了此项工作。他们共同决定将…

linux使用yum命令报错Cannot find a valid baseurl for repo: base/7/x86_64

【问题】 在VMware上安装搭建centOS 7虚拟机,配置好网络后,尝试通过yum命令进行安装docker容器。执行命令报错: 已加载插件:fastestmirror, langpacks Loading mirror speeds from cached hostfile  Could not retrieve mirrorlist http://mirrorlist.centos.org/?relea…

研发工程师的「第一性原理」思维

回顾复盘五年来的研发经历,愈发认同身边同事强调的“第一性原理”思维,仅做浅浅记录和分享一、定义与理论介绍第一性原理(First Principles),又称基本原理,是指从最基本的假设和定义出发,通过逻辑推理和演绎得出结论的一种思维方法。它强调对事物的本质和根源进行深入的…