1.问题现象
最近业务有做应用架构整合,将原先一部分业务从服务A移到服务B。其中接口功能中有一段http调用风控能力的逻辑。版本上线后,有部分此接口流量进入服务B,此时发现一个非常奇怪的问题,就是服务B这个http接口的超时比例比服务A高很多。
服务A 接口超时率在0.5%左右
服务B 接口超时率在10%左右
2.问题分析
2.1 分析源码
首先找到问题代码,当前写法类似如下,相关业务代码泛化调了
java
public class OkhttpTest {
private static final ExecutorService OAS_CALL_THREAD_POOL =
new ThreadPoolExecutor(2, 20, 60, TimeUnit.SECONDS, new LinkedBlockingQueue<>());
public static void main(String[] args) {
String ret = new OkhttpTest().getRisk();
System.out.println("ret=" + ret);
}
public String getRisk() {
try {
Future<String> result = OAS_CALL_THREAD_POOL.submit(() -> {
return getRemoteRequest("https://openapi.vmall.com/rms/v1/comment/getCommentListByTag?pid=10086764961298&systemTagIds=10050001&pageSize=10&pageNum=2&gbomCode=&sortType=1&showStatistics=false&1711285500237");
});
return result.get(500, TimeUnit.MILLISECONDS);
} catch (TimeoutException e) {
System.out.println("time out");
return "failed";
} catch (Exception e) {
System.out.println(e.getMessage());
return "failed";
}
}
private String getRemoteRequest(String url) {
ConnectionPool connectionPool = new ConnectionPool(512,30, TimeUnit.MINUTES)
OkHttpClient client = new OkHttpClient.Builder()
.readTimeout(500, TimeUnit.MILLISECONDS)
.connectTimeout(500, TimeUnit.MILLISECONDS)
.connectionPool(connectionPool)
.build();
// 创建Request对象,指定URL和请求方法
Request request = new Request.Builder()
.url(url)
.build();
String result;
try {
// 发送请求并获取Response对象
Response response = client.newCall(request).execute();
// 检查请求是否成功
if (response.isSuccessful()) {
// 读取响应体内容
result = response.body().string();
System.out.println("Response: " + result);
} else {
// 处理请求失败的情况
System.out.println("Request failed with code: " + response.code());
result = String.valueOf(response.code());
}
} catch (IOException e) {
// 处理网络异常
e.printStackTrace();
result = "IOException";
} finally {
// 关闭连接(如果需要)
}
return result;
}
}
这种写法通过future来控制超时时间,实际执行结果如下
shell
time out
ret=failed
Response: {"data":xxx}
此时发现虽然接口返回失败,但是请求结果还是执行完成。
shell
原因是因为http调用的超时时间readTimeOut 500,connecttimeout = 1000ms > 500
future 超时时间500
readTimeOut + connecttimeout = 1000ms > future 500 ms
尽管上述代码写的有问题,不应该采用线程池异步等待,可直接使用http的超时机制
,但是因为服务A和服务B代码一致,感觉超时率突然增长很多的原因很难解释。
2.2 分析网络部署差异性
既然代码一致,后面就想到是否是两个应用服务在现网部署环境的差异性。
因为调用接口都是一样的,怀疑网络出口带宽或者其他配置是否存在差异。
首先询问运维查看两个微服务部署网络差异性,反馈无差异,且服务B出口网络、带宽啥的监控没啥问题
接着我们在两台容器上使用tracerout
和curl
命令单独执行http命令看网络耗时 结果也是一致,并无太大不一样地方
至此,网络部署业务差异
2.3 继续分析日志
继续分析ELK上日志,发现服务A现网调用量很大
服务B现网调用量根本不是在一个量级上
在想会不会okhttp有啥连接池之类的东西,因为http连接如果首次连接需要dns解析和建立连接之类的操作,建立完之后,第二次会省去创建连接实践
http长连接机制
翻看创建httpClient代码,默认创建512个连接,30分钟失效,但是实际上从上述调用量来看半个小时估计才调用30次作用,可能每次调用连接都没法复用。
java
ConnectionPool connectionPool = new ConnectionPool(512,30, TimeUnit.MINUTES)
2.4 验证
修改ConnectionPool
参数,继续验证
arduino
public static void main(String[] args) {
long start = System.currentTimeMillis();
String ret = new OkhttpTest().getRisk();
long end1 = System.currentTimeMillis();
System.out.println("ret=" + ret);
ret = new OkhttpTest().getRisk();
long end2 = System.currentTimeMillis();
System.out.println("ret1 time=" + (end1 - start) + " ret2 time=" + (end2 - end1));
}
public String getRisk() {
return getRemoteRequest("https://openapi.vmall.com/rms/v1/comment/getCommentListByTag?pid=10086764961298&systemTagIds=10050001&pageSize=10&pageNum=2&gbomCode=&sortType=1&showStatistics=false&1711285500237");
}
private String getRemoteRequest(String url) {
ConnectionPool connectionPool = new ConnectionPool(1, 30, TimeUnit.MINUTES);
...
}
运行结果,可以看出第二次执行时间大幅降低
lua
Response: {"data":xxx
Response: {"data":xxx
ret1 time=1133 ret2 time=250
3.问题解决
解决措施如下:
- 将
ConnectionPool
中maxIdleConnections
调小一些512->8(后续随着调用量上升在调大一些),keepAliveDuration
时间从30min->60min,尽量保证连接复用 - 将使用future和线程池调用方式改成直接http调用,减少不必要的逻辑
- 适当增加http超时时间配置