Troubleshooting系列-一次基于okhttp外部接口调用超时问题分析

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出口网络、带宽啥的监控没啥问题

接着我们在两台容器上使用traceroutcurl命令单独执行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.问题解决

解决措施如下:

  1. ConnectionPoolmaxIdleConnections调小一些512->8(后续随着调用量上升在调大一些),keepAliveDuration时间从30min->60min,尽量保证连接复用
  2. 将使用future和线程池调用方式改成直接http调用,减少不必要的逻辑
  3. 适当增加http超时时间配置

4.参考资料

深入浅出 OkHttp 源码解析及应用实践
OkHttp 系列七:连接池与请求服务拦截器

相关推荐
程序媛-徐师姐5 分钟前
Java 基于SpringBoot+vue框架的老年医疗保健网站
java·vue.js·spring boot·老年医疗保健·老年 医疗保健
yngsqq6 分钟前
c#使用高版本8.0步骤
java·前端·c#
尘浮生15 分钟前
Java项目实战II基于微信小程序的校运会管理系统(开发文档+数据库+源码)
java·开发语言·数据库·微信小程序·小程序·maven·intellij-idea
小白不太白95020 分钟前
设计模式之 模板方法模式
java·设计模式·模板方法模式
Tech Synapse22 分钟前
Java根据前端返回的字段名进行查询数据的方法
java·开发语言·后端
.生产的驴22 分钟前
SpringCloud OpenFeign用户转发在请求头中添加用户信息 微服务内部调用
spring boot·后端·spring·spring cloud·微服务·架构
xoxo-Rachel28 分钟前
(超级详细!!!)解决“com.mysql.jdbc.Driver is deprecated”警告:详解与优化
java·数据库·mysql
乌啼霜满天24930 分钟前
JDBC编程---Java
java·开发语言·sql
微信-since8119238 分钟前
[ruby on rails] 安装docker
后端·docker·ruby on rails