易盾sdk引起项目的整体耗时问题?

  大家好:

    我是烤鸭。今年年初的时候,项目接入易盾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

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
回答: 要安装和管理Flutter SDK工具,你需要以下几个步骤: 1. 首先,你需要下载和安装git客户端。你可以从git官方网站下载适合你操作系统的版本。\[2\] 2. 接下来,你需要从Flutter SDK的官方GitHub仓库中将源代码克隆到本地。你可以使用git命令来完成这个步骤。具体的克隆命令可以在官方文档中找到。\[2\] 3. 一旦你克隆了Flutter SDK的源代码,你就可以使用官方提供的flutter channel命令来管理Flutter SDK的版本。这个命令可以让你升级或回退到不同的Flutter SDK版本。具体的使用方法可以在官方文档中找到。\[1\] 总结起来,为了安装和管理Flutter SDK工具,你需要下载和安装git客户端,并从Flutter SDK的官方GitHub仓库中克隆源代码。然后,你可以使用官方提供的flutter channel命令来管理Flutter SDK的版本。希望这些信息对你有帮助! #### 引用[.reference_title] - *1* *2* [Flutter SDK安装、切换版本及常见问题整理](https://blog.csdn.net/qq_16221009/article/details/125981120)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^control_2,239^v3^insert_chatgpt"}} ] [.reference_item] - *3* [flutter sdk_是什么使Flutter如此强大的流行SDK](https://blog.csdn.net/weixin_26724741/article/details/108971142)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^control_2,239^v3^insert_chatgpt"}} ] [.reference_item] [ .reference_list ]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

烤鸭的世界我们不懂

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值