代码编织梦想

  大家好:

    我是烤鸭。今年年初的时候,项目接入易盾sdk之后,随着接口调用次数增多(用到易盾sdk的接口),项目整体性能变差。写篇文章做个复盘记录,其实同事已经写过了,我借鉴部分再拓展一些。

问题描述

突然收到服务服务报警,整体服务性能下降。
在这里插入图片描述

问题排查

机器cpu有所上升,QPS、GC和内存均正常。人的压力也上来了=.=
在这里插入图片描述

CAT查看time_waiting线程数持续上升。

在这里插入图片描述

执行jstack 查看线程堆栈

jstack -l pid > 1.txt 

发现大量的time_waiting线程,其中90%的线程名字都是这个 idle-connection-evictor

在这里插入图片描述

可以看出这个线程来自 hc.client5 ,再找下易盾sdk和这个类的关系吧。

我们看下 AntispamRequester这个类,是易盾请求的一个实例化基类。

可以看到这个类里用到的 ClientProfile 是初始化的HttpClientConfig,并且创建 AntispamClient 对象的时候做了单例判断,看来是不想创建太多这个对象。

public class AntispamRequester {

    private ClientProfile clientProfile;
    private ConcurrentHashMap<String, Object> clientMap = new ConcurrentHashMap<>();

    public AntispamRequester(String secretId, String secretKey) {
        AssertUtils.notBlank(secretId, "secretId can not be null or empty");
        AssertUtils.notBlank(secretKey, "secretKey can not be null or empty");
        this.clientProfile = createDefaultProfile(secretId, secretKey);
    }

    //...

    public static ClientProfile createDefaultProfile(String secretId, String secretKey) {
        ClientProfile clientProfile = ClientProfile.defaultProfile(new Credentials(secretId, secretKey));

        HttpClientConfig clientConfig = new HttpClientConfig();
        clientConfig.setMaxConnectionCountPerRoute(100);
        clientProfile.setHttpClientConfig(clientConfig);

        return clientProfile;
    }

    //...
    
    private <T extends AntispamClient> T createIfAbsent(Class<T> clazz) {
        String name = clazz.getName();
        Object client = clientMap.get(name);
        if (client != null) {
            return (T) client;
        }
        return (T) clientMap.computeIfAbsent(name, k -> {
            try {
                return clazz.getDeclaredConstructor(ClientProfile.class).newInstance(clientProfile);
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        });
    }
}

再看下 AntispamClient 这个类:

public abstract class AntispamClient {
    protected DefaultClient client;

    public AntispamClient(ClientProfile clientProfile) {
    	// 初始化client
        client = new DefaultClient(clientProfile);
        //...

}

再往下看 HttpClientFactory 的 client的初始化方法,从这得出的结论是易盾封装的hc.client5

public class HttpClientFactory {

    public static CloseableHttpClient create(HttpClientConfig config) {
        // ... 无关的先注释
        return HttpClients.custom()
                .evictIdleConnections(TimeValue.of(config.maxIdleTimeMillis(), TimeUnit.MILLISECONDS))
                .evictExpiredConnections()
                .setConnectionManager(connManager)
                .setDefaultRequestConfig(requestConfig)
                .useSystemProperties()
                .build();
    }
}

SDK使用

猜测是引入了新的易盾sdk导致的,因为其他没那么改动,而且是在随着接口调用次数增多(用到易盾sdk的接口),项目整体性能变差。不过易盾的包和线程池等待有什么关系呢。

易盾给的官方demo的写法:

https://github.com/yidun/yidun-java-sdk/blob/b92c803c8c2c8f8d55db27ce3284bb1b6eb97c1f/yidun-java-sdk-demo/src/main/java/com/netease/yidun/sdk/antispam/AbstractDemo.java

package com.netease.yidun.sdk.antispam;

import com.netease.yidun.sdk.core.client.ClientProfile;
import com.netease.yidun.sdk.core.endpoint.failover.FixedWindowBreakStrategy;
import com.netease.yidun.sdk.core.http.HttpClientConfig;

public class AbstractDemo {

    protected static AntispamRequester createAntispamRequester(String secretId, String secretKey){
        // 实例化一个requester,入参需要传入易盾内容安全分配的secretId,secretKey
        AntispamRequester antispamRequester = new AntispamRequester(secretId, secretKey);

        // 可选自定义请求器的参数,如果不需要自定义设置,可跳过,否则请参考如下注释内容:
//        ClientProfile clientProfile = AntispamRequester.createDefaultProfile("SecretId", "SecretKey");
//        // 设置http请求的相关配置
//        HttpClientConfig httpClientConfig = clientProfile.getHttpClientConfig();
//        httpClientConfig.socketTimeoutMillis(60000);
//
//        // 设置固定窗口的熔断配置
//        FixedWindowBreakStrategy.Config breakerConfig = clientProfile.getBreakerConfig();
//        breakerConfig.statWindowMillis(300000);
//
//        // 设置请求失败时的重试次数
//        clientProfile.setMaxRetryCount(2);
//        AntispamRequester antispamRequester = new AntispamRequester(clientProfile);
        return antispamRequester;
    }
}

项目里也是按照这个写法的,上面看源码 antispamRequester 里可以封装很多个client对象,而每个client对象相当于对http5封装,并且进行了单例判断,理论上不会出问题。

但是按照官方的demo,如果每次都 new AntispamRequester() 呢。

源码分析

回到最开始的地方,idle-connection-evictor 在哪用到的。是构建 HttpClient 的时候根据 evictExpiredConnections 或者 evictIdleConnections,判断是否开启当前线程。

public CloseableHttpClient build() {
    // ... 
    if (!this.connManagerShared) {
        if (closeablesCopy == null) {
            closeablesCopy = new ArrayList<>(1);
        }
        if (evictExpiredConnections || evictIdleConnections) {
            if (connManagerCopy instanceof ConnPoolControl) {
                final IdleConnectionEvictor connectionEvictor = new IdleConnectionEvictor((ConnPoolControl<?>) connManagerCopy,
                        maxIdleTime, maxIdleTime);
                closeablesCopy.add(new Closeable() {

                    @Override
                    public void close() throws IOException {
                        connectionEvictor.shutdown();
                        try {
                            connectionEvictor.awaitTermination(Timeout.ofSeconds(1));
                        } catch (final InterruptedException interrupted) {
                            Thread.currentThread().interrupt();
                        }
                    }

                });
                connectionEvictor.start();
            }
        }
        closeablesCopy.add(connManagerCopy);
    }

    return new InternalHttpClient(...);
}

IdleConnectionEvictor 初始化:

这个线程就是个死循环,用来关闭超过最大超时时间的线程的,可以理解为一个清扫线程。

public IdleConnectionEvictor(final ConnPoolControl<?> connectionManager, final ThreadFactory threadFactory,
                             final TimeValue sleepTime, final TimeValue maxIdleTime) {
    Args.notNull(connectionManager, "Connection manager");
    this.threadFactory = threadFactory != null ? threadFactory : new DefaultThreadFactory("idle-connection-evictor", true);
    final TimeValue localSleepTime = sleepTime != null ? sleepTime : TimeValue.ofSeconds(5);
    this.thread = this.threadFactory.newThread(new Runnable() {
        @Override
        public void run() {
            try {
                while (!Thread.currentThread().isInterrupted()) {
                    localSleepTime.sleep();
                    connectionManager.closeExpired();
                    if (maxIdleTime != null) {
                        connectionManager.closeIdle(maxIdleTime);
                    }
                }
            } catch (final InterruptedException ex) {
                Thread.currentThread().interrupt();
            } catch (final Exception ex) {
            }

        }
    });
}

回到上面的问题,每new一次,就会多x个死循环线程(x取决于client个数)。

解决方案

如果使用易盾的sdk的话,只要保证 AntispamRequester 是单例的就行,如果使用spring,可以注入到ioc。

	/**
     * 易盾AntispamRequester对象
     */
    @Bean("yiDunRequester")
    public AntispamRequester yiDunRequester(){
        //1.默认方式
        AntispamRequester antispamRequester = new AntispamRequester(yiDunUrlConfig.getSecretId(), yiDunUrlConfig.getSecretKey());
        return antispamRequester;
    }

如果使用http的sdk,无论是 http4还是http5 ,都需要考虑资源关闭。

  1. 不要把下面那两个设为true。 evictIdleConnections 和 evictExpiredConnections (这俩默认是false) 和 evictIdleConnections(设置这个值会把evictIdleConnections 变成true),设置的话会启动清扫线程。

    这时候再看易盾的 HttpClientFactory 这个类,如果不设置这俩参数 evictIdleConnections 和 evictExpiredConnections,其实也没事。但是你偷偷设置完了不通知,就有点说不过去了。

    public class HttpClientFactory {
    
        public static CloseableHttpClient create(HttpClientConfig config) {
            // ... 无关的先注释
            return HttpClients.custom()
                    .evictIdleConnections(TimeValue.of(config.maxIdleTimeMillis(), TimeUnit.MILLISECONDS))
                    .evictExpiredConnections()
                    .setConnectionManager(connManager)
                    .setDefaultRequestConfig(requestConfig)
                    .useSystemProperties()
                    .build();
        }
    }
    
  2. 创建共享对象,不再持续创建HttpClient

        /**
         * 类实例对象,避免重复创建
         */
        private static HttpClient httpClient = HttpClient4Utils.createHttpClient(100, 20, 10000, 2000, 2000);
        
        public static HttpClient createHttpClient(int maxTotal, int maxPerRoute, int socketTimeout, int connectTimeout,
                                                  int connectionRequestTimeout) {
            RequestConfig defaultRequestConfig = RequestConfig.custom().setSocketTimeout(socketTimeout)
                    .setConnectTimeout(connectTimeout).setConnectionRequestTimeout(connectionRequestTimeout).build();
            PoolingHttpClientConnectionManager cm = new PoolingHttpClientConnectionManager();
            cm.setMaxTotal(maxTotal);
            cm.setDefaultMaxPerRoute(maxPerRoute);
            CloseableHttpClient httpClient = HttpClients.custom().setConnectionManager(cm)
                    .setDefaultRequestConfig(defaultRequestConfig).build();
            return httpClient;
        }
    
  3. 通过try-with-resources的写法,自动关闭资源。或者自己写try-catch-finally。

public static JSONObject httpGet(String url) throws HttpException {
        String[] strings = url.split("\\?");
        HttpUriRequestBase request = new HttpGet(strings[0] + "?" + UriEncoder.encode(strings[1]));
        try (
                CloseableHttpClient httpClient = getHttpClient();
                CloseableHttpResponse response = httpClient.execute(request)
        ) {
            // ...
            return JSONObject.parseObject(responseContent);
        } catch (IOException e) {
            throw new HttpException(String.format("请求接口失败, url: %s", url), e);
        }
    }

  private static CloseableHttpClient getHttpClient() {
        return HttpClientBuilder.create().build();
 }

总结

官方的SDK最好写清楚使用,如果使用官方demo的情况下,出现服务性能下降的话,属实是无法接受的。

无论使用哪种sdk(服务端的sdk还是客户端的sdk),最好看下代码。尤其是新接入的,有条件的做下性能压测。

算是个老问题,用新形式踩坑了,挺有意思的。

再看看竞品的百度AI的:

https://ai.baidu.com/ai-doc/ANTIPORN/ik3h6xdze

在这里插入图片描述

参考文章

https://blog.csdn.net/qq_41999004/article/details/109141177

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/Angry_Mills/article/details/128731147

什么是sdk? sdk是什么意思?_心如猛虎细嗅蔷薇的博客-爱代码爱编程_sdk是什么意思

其实很简单,SDK 就是 Software Development Kit 的缩写,中文意思就是“软件开发工具包”。这是一个覆盖面相当广泛的名词,可以这么说:辅助开发某一类软件的相关文档、范例和工具的集合都可以叫做“SDK”。具体到我们这个系列教程,我们后面只讨论广义 SDK 的一个子集——即开发 Windows 平台下的应用程序所使用的 SDK。 呵呵

【objectmapper实体转换异常】 com.fasterxml.jackson.databind.exc.mismatchedinputexception_烤鸭的世界我们不懂的博客-爱代码爱编程

  大家好,我是烤鸭:     采坑实录,想把json数据直接转成对象,其中有个属性是list<T>:   异常 1 com.fasterxml.jackson.databind.exc.MismatchedInputException: Cannot deserialize instance of `java.util.ArrayL

项目中使用js-sdk步骤介绍-爱代码爱编程

  项目中使用JS-SDK步骤介绍 需求背景:在微信浏览器中打开网站页面,通过微信浏览器自带的分享按钮分享给朋友或者是朋友圈之后能够自定义分享的图片、描述等相关信息。 实现:使用JS-SDK,利用微信提供的API进行参数配置即可。其实整个过程非常简单,主要是本小白之前没有用过JS-SDK,所以摸索使用方式花费了一些时间。JS-SDK官方文档 步骤介

android sdk文件位置,Android SDK文件夹位于何处?-爱代码爱编程

我通过Air for Android用Adobe Flash创建了一个.apk应用程序。现在,我想通过这款Blackberry在线打包机为黑莓App World做好准备:https://bdsc.webapps.blackberry.com/android/bpaa/apk-compatibility-check 我在哪里可以找到Windows PC

客户端SDK测试是什么?如何测?-爱代码爱编程

01 SDK是什么 客户端SDK是为第三方开发者提供的软件开发工具包,包括SDK接口、开发文档和Demo示例等。SDK和应用之间是什么关系呢?以云信即时消息服务为例,如下图所示,应用客户端通过调用云信SDK接口,进行消息等数据查询存储等操作,或通过协议与云信服务器间进行通信。 02 测什么 1. 客户端SDK测试的对象 客户端SDK测

关于安卓项目targetsdkversion升级到31后报错问题_水很清的博客-爱代码爱编程

Android API已经更新到33了,相应的Android系统目前已经出到12了,作为开发者,项目也需要将targetSdkVersion升级到31,毕竟Google play 已经要求上架的APP targetSdkVersion最低为31。 既然升级到31,那么buildToolsVersion也需要跟着更新到31,当我们将targetSdkVer

android stuido中修改项目sdk版本号_android studio更改sdk版本-爱代码爱编程

通过Android Studio创建项目时,只能选择项目支持的最低SDK版本号,而无法选择当前编译使用的版本号,如图1所示。  图1 选择项目支持的最小版本号 而编译项目的SDK版本号默认是Android Studio安装的最新SDK。如果不想使用最新SDK来编译项目,则可以在创建项目成功后,对编译所需的SDK进行修改。修改的方法有两种,一种是通过