现象:

偶现点击某功能按钮,10秒后才有响应。大多数时间不会出现此现象,但是出现后不会恢复。

服务日志:

org.apache.http.conn.ConnectionPoolTimeoutException: Timeout waiting for connection from pool

image.png
KongClient为公司内部UGC审核提供的Java SDK,依赖为Kong-sdk。
能力提供方需要在下个月才能排期修复,所以计划先行排查修复以解决问题。

查看相关代码:

请求报错位置
image.png
无法正常获取连接。
image.png

通过 this.httpClient = HttpClients.createDefault();创建httpClient。
其中连接池默认参数
image.png
最大连接数 maxTotal 20
每个域名最大并发连接数 maxPerRoute 2

修复:

基于源码修复,拷贝KongClient代码为DocsKongClient。

1、需要手动修改连接池配置:

image.png
自定义连接池配置,超时时间、最大连接数、每路最大连接数,增加闲置连接超时释放机制,便于异常时自动恢复。
具体数值按照业务场景和能力提供方具体情况进行调整。

2、手动释放请求连接和响应。

在默认情况下,Apache HttpClient 4.x 会自动将连接返回到连接池中,你不需要手动释放连接到连接池。HttpClient 会根据连接的 keep-alive 策略来确定何时可以关闭连接或将其返回到连接池。
为了避免异常情况影响,在使用连接后手动释放。
image.png
3.增加timeout、maxConnTotal和maxConnPerRoute配置项,可在nacos中调整连接池配置。
替换KongClient为DocsKongClient进行调用。

并发测试

原线程池配置并发测试,复现异常,多个请求处于pending状态。
image.png
修改配置后并发测试
增加配置

connectionManager.setMaxTotal(100);
connectionManager.setDefaultMaxPerRoute(50);

image.png
未发现并发异常,正常完成请求

上线观察

上线持续观察相关功能日志正常。
image.png

文本同步审核调用的优化:

受限于产品方案和交互问题,决策使用同步审核,审核未通过的内容无法进行分享、分发等。如何快速审核以不影响用户体验成为一个问题。

相关条件限制:

  • 审核接口限制文本长度1万字。
  • 审核1万字的文本耗时2600ms
  • 90%的文档内容在五千字以下,部分文本长度达数万字。

如何优化?

  • 需要对超长文本进行分割,以满足接口条件限制。
  • 需要测试审核接口响应时间,选取合适的文本长度分批调用,在响应时间和请求次数之间做均衡,同时满足多数短文本需求和少量长文本需求。
  • 使用并行的方式调用接口,降低长文本的审核耗时时间。
  • 分割长文本时每段适当冗余,避免相邻两段文本内容的丢失,如每段长度为4千字,第二段从3990开始截取。
  • 分段后有一段文本审核不通过即为全文不通过
    相关处理代码
    ThreadPoolExecutor executor = new ThreadPoolExecutor(
            10,
            10,
            5,
            TimeUnit.SECONDS,
            new LinkedBlockingQueue<>()
    );

    @Override
    public boolean textCheck(String fid, String title, String content) {
        //拆分长文本
        String[] textArray = splitText(content, 4000);
        List<Future<Boolean>> futures = new ArrayList<>();
        for (String text : textArray) {
            //分段处理, 审核接口content最多10000字
            JSONObject jsonObject = new JSONObject();
            jsonObject.put("name", title);
            jsonObject.put("content", text);
            JSONArray jsonArray = new JSONArray();
            jsonArray.add(jsonObject);
            JSONObject param = new JSONObject();
            param.put("content_list", jsonArray);
            param.put("bizId", ugcContentCheckProperties.getBizId());
            param.put("productId", ugcContentCheckProperties.getProductId());
            //透传签名参数
            String requestBody = JSON.toJSONString(param);
            Future<Boolean> checkFuture = executor.submit(new Callable<Boolean>() {
                @Override
                public Boolean call() throws Exception {
                    // 此处仅展示了post方法,还有更多携带header、queryString的方法请查阅KongClient类
                    try {
                        KongResponse response = docsKongClient.post(ugcContentCheckProperties.getTextCheckApi(), requestBody);
                        //对response做处理
                    } catch (Exception e) {
                        log.error("UGC审核请求异常 body={}", requestBody, e);
                    }
                    return true;
                }
            });
            futures.add(checkFuture);
        }
        for (Future<Boolean> future:futures) {
            try {
                //只要一个文本检测违规就算违规
                if (!future.get()){
                    return false;
                }
            } catch (InterruptedException e) {
                log.error("InterruptedException", e);
            } catch (ExecutionException e) {
                log.error("ExecutionException", e);
            }
        }
        return true;
    }