Skip to content
Xiaoyu's Blog
Go back

记录一次压测经验总结

😀
记录在压测环境高并发情况下的问题解决,纸上读来终觉浅,功能性测试通过了并不代表一切OK了,还需要通过压力测试,否则到线上还是会有问题。我把遇到的问题分为两部分,不断补充完善。

📝 一、并发安全问题

串行化的执行代码是理想情况,在会产生并发问题导致数据不一致的地方合理使用锁是必要的。

压测中遇到的问题是:

  1. 同一个用户同一时刻并发请求更新主表失败
  2. 从库里随机获取一条数据重复

解决问题一:

  1. 对同一个用户加分布式锁。之前该代码是对请求参数中唯一身份信息进行了分布式加锁的,但是针对同一个账号的维度没有加锁。由此对底层代码进行修改,支持分布式锁切面中的”,”表达式。也可以在代码中直接获取锁。Redission支持了这种多个锁的获取,叫做RedissonMultiLock。使用起来也十分方便。Redission分布式锁
  2. 为什么会报错?公司的底层框架默认继承了乐观锁,当一个线程更新完成以后,另一个线程进行更新data_version不一致则报错。加入用户id维度的分布式锁即可解决此问题。
  3. 在解决这个问题的过程中,我还遇到了一个问题,就是当A、B线程同时进入方法时,A线程获取到了锁并执行完方法后,B线程进入代码并未获取到A线程更新完成的数据,导致又一次更新主表。

    当时很快想到了事务的问题,mysql默认事务隔离级别是可重复读,肯定就是A线程的事务未提交。一看代码:

     @Transactional
     @DistributeRedisLock(lockKey = "#id,#idNo")
     public void doSomething(String id,String idNo){
        
        //must need do something...
       
     }

    毫无疑问,@Tracnsactional的优先级大于自定义的获取锁的redis切面,当redis分布式锁切面完成以后,事务才提交。那解决方法就很多了,从声明式事务改为手动式事务或者不用分布式锁切面,又或者调整@Order都是可以的。

解决问题二:

加上for_update,为什么加上for_update就可以了?

select * from t_code where is_used = 0 for update;

🤐 二、超时问题

IO密集型应用,并发一高大家都会知道先加点缓存,但是请求http接口也是需要加缓存的。

1、ssl握手问题

代码中,使用了restTemplate请求外部接口,其中基本大多都为https接口。偶发的出现这个错误

- I/O error on POST request for "https://xxxx.com/xxx": peer not authenticated; nested exception is javax.net.ssl.SSLPeerUnverifiedException: peer not authenticated

在网上搜才知道是ssl握手失败,restTemplate中需要指定ssl握手策略即可解决。

解决方法:

    TrustStrategy acceptingTrustStrategy = (x509Certificates, authType) -> {
                    return true;
                };
    SSLContext sslContext = SSLContexts.custom()
                        .loadTrustMaterial(null, acceptingTrustStrategy)
                        .build();
    HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory(httpClient);
    factory.setConnectTimeout(httpClientProperties.getConnectTimeout());
    factory.setReadTimeout(httpClientProperties.getReadTimeout());
    factory.setConnectionRequestTimeout(httpClientProperties.getConnectTimeout());
    return factory;

2、ssl连接耗时过长

这个问题解决了很久,请求”https://www.xxx.com/getData”接口耗时在200ms,200ms也是比较正常的数字,但是这个接口在并发10到20中有耗时的增加,导致tps一直上不去。

排查问题:

1、首先考虑的是对方服务耗时是不是很高,看了对方服务内耗时和nginx耗时,都是个位数ms耗时

2、考虑是不是restTemplate本身的问题,使用curl -v -w 打印耗时,结果为:

其中:

发现,ssl耗时最高,解决方法:在redisTemplate中加入ssl连接池,完整代码:

 	 	@Bean
    @ConditionalOnMissingBean(name = {"httpsClientHttpRequestFactory"})
    ClientHttpRequestFactory httpsClientHttpRequestFactory(HttpClientProperties httpClientProperties) {
        if (!httpClientProperties.isUsePool()) {
            HttpsClientRequestFactory factory = new HttpsClientRequestFactory();
            factory.setConnectTimeout(httpClientProperties.getConnectTimeout());
            factory.setReadTimeout(httpClientProperties.getReadTimeout());
            return factory;
        } else {
            CloseableHttpClient httpClient;
            try {
                TrustStrategy acceptingTrustStrategy = (x509Certificates, authType) -> {
                    return true;
                };
                SSLContext sslContext = SSLContexts.custom()
                        .loadTrustMaterial(null, acceptingTrustStrategy)
                        .build();
                SSLConnectionSocketFactory connectionSocketFactory = new SSLConnectionSocketFactory(sslContext, new NoopHostnameVerifier());
                Registry<ConnectionSocketFactory> socketFactoryRegistry = RegistryBuilder.<ConnectionSocketFactory>create()
                        .register("https", connectionSocketFactory)
                        .build();
                PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager(socketFactoryRegistry);
                connectionManager.setMaxTotal(100);
                connectionManager.setDefaultMaxPerRoute(20);
                HttpClientBuilder httpClientBuilder = HttpClients.custom();
                httpClientBuilder.setSSLSocketFactory(connectionSocketFactory);
                httpClientBuilder.setConnectionManager(connectionManager);
                httpClientBuilder.setConnectionManagerShared(true);
                httpClient = httpClientBuilder.build();
            } catch (Exception var7) {
                throw new RuntimeException("create httpsClientHttpRequestFactory failed", var7);
            }
            HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory(httpClient);
            factory.setConnectTimeout(httpClientProperties.getConnectTimeout());
            factory.setReadTimeout(httpClientProperties.getReadTimeout());
            factory.setConnectionRequestTimeout(httpClientProperties.getConnectTimeout());
            return factory;
        }
    }

关于更详细的curl使用方法,参考:使用 curl 命令分析请求的耗时情况

📎 其他


Share this post:

Previous Post
HTTP请求方式和编码
Next Post
React学习记录 Day01