Posted by Blog on September 23, 2023

背景

近期线上接口频繁超时, 通过日志收集工具发现, 在请求量没有大幅增加时这个接口频繁超时.

定位问题

我首先去确认了这些请求的请求内容是不是特别大.

果然, 好家伙一次请求了1000多个资源, 这不慢才怪. 但是即使是请求了这么多资源也是业务的正常需求, 需要支持! 咋办? 优化呗!

使用Arthas定位耗时节点

其实我很希望能在线上环境运行Arthas, 但是由于权限不足, 申请还挺麻烦, 即是能在线上机器上运行Arthas, 我也会慌得一批, 生怕影响到应用. 所以还是选择了在测试环境操作.

启动Arthas, attach 上测试应用, 执行下面的命令

```shell script trace -n 3 -E com.ofashion.service.goodscenter.service.goods.impl.GoodsServiceImpl getGoodsInfoList ‘#cost>2000’


命令的作用是: 当 getGoodsInfoList 这个方法执行时长超过2000毫秒时则输出耗时记录, 输出3次后进行tear down

另外一边启动JMeter, 并发的请求1000个商品资源.

<img src="https://i.loli.net/2020/11/10/wJ2aHc76gbs1fOQ.png" style="zoom: 40%;" />

在Arthas控制台上能看到方法的链路耗时, 经过几轮定位后发现几个耗时严重的操作.



![](https://i.loli.net/2020/11/10/W1ELb4RFOlrXnIu.png)



发现有以下几个耗时节点:

- ```RedisUtil::hMGet``` 中的反序列化
- 商品的DO与VO之间的属性拷贝



#### 优化耗时节点 - hMGet

```RedisUtil::hMGet```代码如下

```java
public <T> List<T> hMGet(Object key, Collection fields, Class<T> clazz) {
				List<Object> objects = redisTemplate.opsForHash().multiGet(key, fields);
        objects = objects.stream().filter(Objects::nonNull).collect(Collectors.toList());
        // 这一步耗时严重
        return JsonUtil.convertedToArray(JsonUtil.toJson(objects), clazz);
}  

这里在处理时直接把redis客户端返回的数据序列化再反序列化为List, 处理逻辑上没什么问题

通过debug发现redis客户端返回的结构是ArrayList<LinkedTreeMap<String, Object>>

那当前这种先序列化再反序列化的方式就有些浪费资源了, 没有利用上Map的结构, 使用BeanUtil替换反序列化的这一步

得到了以下的代码.

    public <T> List<T> hMGet(Object key, Collection fields, Class<T> clazz) {
        List<Object> objects = redisTemplate.opsForHash().multiGet(key, fields);
        return objects.stream().filter(Objects::nonNull)
                .map(object -> BeanUtil.toBean(object, clazz))
                .collect(Collectors.toList());
    }

优化耗时节点 - 属性拷贝

原代码:

    public void copyFrom(GoodsInfoWithExtensionDTO goodsInfo) {
        BeanUtils.copyProperties(goodsInfo, this);
        this.customTagIdList = CollectionUtils.isEmpty(this.customTagIdList) ? null :
                JsonUtil.fromJson(goodsInfo.getCustomTagIdList(), new TypeToken<List<Integer>>(){});
        this.productCoverImage = StringUtils.isEmpty(goodsInfo.getProductCoverImage()) ? null :
                JsonUtil.fromJson(goodsInfo.getProductCoverImage(), ImageDTO.class);
        this.customImage = StringUtils.isEmpty(goodsInfo.getCustomImage()) ? null :
                JsonUtil.fromJson(goodsInfo.getCustomImage(), new TypeToken<List<ImageDTO>>(){});
        this.goodsImgs = StringUtils.isEmpty(goodsInfo.getGoodsImgs()) ? null :
                JsonUtil.fromJson(goodsInfo.getGoodsImgs(), new TypeToken<List<ImageDTO>>(){});
        this.customTags = StringUtils.isEmpty(goodsInfo.getCustomTags()) ? null :
                JsonUtil.fromJson(goodsInfo.getCustomTags(), new TypeToken<List<String>>(){});
        if (goodsInfo.getGoodsExtension() != null) {
            this.goodsExtension = new GoodsExtensionVO();
            this.goodsExtension.copyFrom(goodsInfo.getGoodsExtension());
        }
    }

GoodsInfoWithExtensionDTOGoodsInfoVO 这两个类有很多都很多属性, 60-70个熟悉左右.

在这个基础上再并发请求1000多个资源这个耗时一下就上来了.

去搜索引擎查了一下, 没有什么理想的解决办法, 于是我回归原始, 把全部的属性拷贝都改成Setter

想想就挺折磨人的, 写这么多setter, 人不疯眼睛也花了.

还好有idea插件的支持, GenerateAllSetter

在配合上正则表达式, 生成出了全部Setter

public GoodsInfoVO(GoodsInfoWithExtensionDTO goodsInfo) {
        this.id = goodsInfo.getId();
        this.gid = goodsInfo.getGid();
        this.weight = goodsInfo.getWeight();
        this.sellerUid = goodsInfo.getSellerUid();
        this.price = goodsInfo.getPrice();
        this.currency = goodsInfo.getCurrency();
        this.productPid = goodsInfo.getProductPid();
        this.productBid = goodsInfo.getProductBid();
        this.productBrandnameE = goodsInfo.getProductBrandnameE();
        this.productBrandnameC = goodsInfo.getProductBrandnameC();
       	// 省略若干个属性的拷贝
}

验证修改的正确性

运行测试用例

OK, 这次修改没有影响软件的可观察行为, 这个方法还是可工作的.

验证修改的性能提升

本次优化的主要目的还是提升性能, 可别忘了为什么开始优化的

引入JMH测试优化前与优化后的效率提升

@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.MILLISECONDS)
@State(Scope.Thread)
public class GoodsInfoServiceJMHTest {

    private ConfigurableApplicationContext context;
    private GoodsInfoService goodsInfoService;
    private GoodsListQuery input10;
    private GoodsListQuery input100;
    private GoodsListQuery input500;
    private GoodsListQuery input1000;
  	// 省略的SetUp 与 TearDown, 主要就是初始化上面几个变量的

    public static void main(String[] args) throws Exception {
        Options opt = new OptionsBuilder().include(GoodsInfoServiceJMHTest.class.getSimpleName())
                .forks(0)
                // 预热5次
                .warmupIterations(5)
                // 执行20次
                .measurementIterations(20)
                .build();
        new Runner(opt).run();
    }

    @Benchmark
    public void before1000() {
        goodsInfoService.getGoodsBaseInfoList(input1000);
    }

    @Benchmark
    public void after1000() {
        goodsInfoService.foo(input1000);
    }

    @Benchmark
    public void before500() {
        goodsInfoService.getGoodsBaseInfoList(input500);
    }

    @Benchmark
    public void after500() {
        goodsInfoService.foo(input500);
    }

    @Benchmark
    public void before500() {
        goodsInfoService.getGoodsBaseInfoList(input500);
    }

    @Benchmark
    public void after500() {
        goodsInfoService.foo(input500);
    }

    @Benchmark
    public void before10() {
        goodsInfoService.getGoodsBaseInfoList(input10);
    }

    @Benchmark
    public void after10() {
        goodsInfoService.foo(input10);
    }
}

JMH测试报告:

Benchmark Mode Cnt Score Error Units
GoodsInfoServiceJMHTest.after500 avgt 20 270.904 ± 43.203   ms/op
GoodsInfoServiceJMHTest.after10 avgt 20 84.956 ± 13.687   ms/op
GoodsInfoServiceJMHTest.after1000 avgt 20 358.784 ± 150.737   ms/op
GoodsInfoServiceJMHTest.after500 avgt 20 141.310 ± 49.689   ms/op
GoodsInfoServiceJMHTest.before500 avgt 20 387.374 ± 93.433   ms/op
GoodsInfoServiceJMHTest.before10 avgt 20 125.956 ± 23.687   ms/op
GoodsInfoServiceJMHTest.before1000 avgt 20 554.784 ± 217.737   ms/op
GoodsInfoServiceJMHTest.before500 avgt 20 241.310 ± 59.689   ms/op

发现效率提升了大概30左右, OK降低到超时阈值以下了, 提测, 上预发布, 上线.

总结

  1. Arthas + JMeter 定位耗时节点
  2. 优化耗时节点
  3. 通过测试用例
  4. 获取JMH测试报告, 达到预期优化时间