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 系列七:连接池与请求服务拦截器

相关推荐
Rust研习社34 分钟前
组合真的优于继承吗?为什么 Rust 和 Go 都拥抱组合舍弃继承?
后端·rust·编程语言
IT_陈寒1 小时前
JavaScript的闭包把我坑惨了,说好的内存会自动回收呢?
前端·人工智能·后端
CaffeinePro2 小时前
Pydantic深度使用:数据校验、枚举、ORM映射
后端·fastapi
Chenyiax2 小时前
从 Chat 到 Responses:OpenAI API 抽象为什么变了?
后端
MariaH2 小时前
Koa和Express的区别
后端
MariaH2 小时前
Koa框架的使用
后端
luckdewei3 小时前
那个用 passlib 做认证的新同事,上线第一天就把用户密码写进了日志
后端
ping某5 小时前
为什么 Nginx 明明监听了 80,转发后端时却用了 4xxxx 端口?
后端·nginx
JustHappy5 小时前
我汇总了身边朋友的经历才发现,其实第一份实习是最难找的......
前端·后端·面试
uhakadotcom5 小时前
在python 的 工程化架构中 ,什么是 薄包装器层?
后端·面试·github