大家好:
我是烤鸭。今年年初的时候,项目接入易盾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 ,都需要考虑资源关闭。
-
不要把下面那两个设为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(); } }
-
创建共享对象,不再持续创建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; }
-
通过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