http連接池未設置獲取連接超時時間導致服務死機

一、故障過程回顧

2020年1月3日早上7:30收到pay-xx服務連接超時告警,發現問題後馬上進行排查,根據鏈路日誌發現,我們請求有通過http發送給第三方,將請求日誌發給第三方,但第三方反饋未收到該請求。瞬間懵逼了, 我們有發送請求,但第三方沒有收到,請求中途掉包了 ?網絡出現了問題?但是我們部分請求是可以送達第三方的,開始懷疑是第三方對我們出口的ip做了白名單限制 ? 7:50 左右我們一個服務節點開始出現波測告警,該告警的意思是一個服務節點已經死機。於是趕快聯繫運維重新部署,pay-xx服務。 8:30重新部署服務後,整個請求恢復正常。
爲了排查問題,我們然運維保留了死機節點,並導出了jvm堆棧。

二、問題排查

1)我將出問題的節點出口ip給第三方排查是否由於網關ip白名單訪問限制導致問題,第三運維反饋ip白名單正常。
2)我開始找出問題時候http訪問的狀態,發現在服務告警前,http請求第三方返回了很多504以後, 我們服務開始出現異常;
在這裏插入圖片描述
3)第三方排查確實7:29至7:30 一段時間沒有收到我們的請求,那這個http 504狀態表示的超時,是什麼地方返回的呢?看第三方網關監控7:30分他們可以正常收到請求,但爲什麼我們的一個節點會出現死機呢?
在這裏插入圖片描述
4)第三方用的是阿里雲cdn加速,進過與阿里一起排查發現7:29至7:30,我們收到的504狀態碼,是阿里網關內部超時返回的
在這裏插入圖片描述
5)第三方優化了cdn 中tcp 超時時間
在這裏插入圖片描述
6)問題的根源算是找到了,是由於阿里網關內部超時導致,我們服務7:29至7:30大量請求超時。
7)問題起因算是找到了, 但我們的服務爲什麼沒有,沒有對連接做超時斷開呢 ?於是我開始看項目代碼。

 public Map<String, String> send(HttpRequest request) throws ClientException, BusinessException {
        Stopwatch stopwatch = Stopwatch.createStarted();
        logger.info("Start send http request...");

        //設置超時時間,如果request沒設置超時時間,取profile裏的
        int socketTimeout = request.getSocketTimeout() > 0 ? request.getSocketTimeout() :10000;
        int connectTimeout = request.getConnectTimeout() > 0 ? request.getConnectTimeout() : 10000;
        RequestConfig config = RequestConfig.custom().setSocketTimeout(socketTimeout).setConnectTimeout(connectTimeout)
                .build();

        logger.info("Http request params: uri={}, headers={}, params={} ", request.getUri(),
                JSONArray.toJSONString(request.getHeaders()), SensitiveUtils.toJson(request.getParamsMap()));

        if (logger.isDebugEnabled() && Strings.nullToEmpty(request.getBodyText()).length() > 0) {
            logger.debug("Http request body is {}", request.getBodyText());
        }
        //請求調用
        CloseableHttpResponse httpResponse;
        if (request.getMethodType() == MethodType.GET) {
            httpResponse = sendGet(request, config);
        } else if (request.getMethodType() == MethodType.POST) {
            httpResponse = sendPost(request, config);
        } else {
            throw new ClientException(ClientErrorCode.METHOD_TYPE_NOT_SUPPORT);
        }
        //校驗http狀態碼
        checkHttpStatus(httpResponse);
        Map<String, String> response = new HashMap<>();
     
        } catch (Exception e) {
            logger.error("Close the response inputStream error, error={}", e);
            throw new ServerException(ClientErrorCode.CLOSE_RESPONSE_CONTENT_ERROR);
        } finally {
            try {
                httpResponse.close();
            } catch (IOException e) {
                logger.error("Close the response error, error={}", e);
            }
            logger.info("Http request response: uri={}, response={}, time={} ", request.getUri(),
                    SensitiveUtils.toJson(response), stopwatch.elapsed(TimeUnit.MILLISECONDS));
        }

        return response;
    }

從上面代碼看來都有設置SocketTimeout、ConnectTimeout的超時時間,按理我們請求第三方如果超時,就會出現超時。在代碼裏面我們有打印error日誌,但是我在日誌系統中查了並無明顯的error日誌。那說明我們http請求是沒有出現異常。
在這裏插入圖片描述
8)開始排查爲什麼我們服務沒有異常的原因,我們在日誌中看到一直在發請求,但一直沒有http返回
在這裏插入圖片描述
在這裏插入圖片描述

9) 讓運維查看了故障節點日誌, 也未發現異常,我們用的是docker容器,如果宿主機出現問題,其它服務器也會有異常告警, 說明容器是沒有問題的。
10)通過故障節點堆棧信息分析,http請求導致Tomcat線程池出現了問題。
在這裏插入圖片描述
在這裏插入圖片描述
11)上分析已經明顯知道,是由於http請求堆積太多導致,服務線程池飆升,導致節點死機。那問題又回到爲爲什麼大量http請求,堆積後沒有超時失敗呢 ?然後開始google 發現 apache httpclient 4.5 用連接池技術,新增了一個超時時間設置,默認設置的是-1 ;
setConnectionRequestTimeout:設置從connect Manager獲取Connection 超時時間,單位毫秒。這個屬性是新加的屬性,因爲目前版本是可以共享連接池的。
緊接去看httpclient的源碼發現了坑。 獲取Connection有判斷ConnectionRequestTimeout時間,默認值-1會被設置爲0 ,0表示永久不失效。

  final HttpClientConnection managedConn;
        try {
            final int timeout = config.getConnectionRequestTimeout();
            managedConn = connRequest.get(timeout > 0 ? timeout : 0, TimeUnit.MILLISECONDS);
        } catch(final InterruptedException interrupted) {
            Thread.currentThread().interrupt();
            throw new RequestAbortedException("Request aborted", interrupted);
  } 

尷尬,永久不失效,代表的就是:如果沒有獲取到Connection,線程會一直等待,一直未發送到第三方。這就是我們看到爲什麼發送日誌,是正常的但是,沒有返回的Response並且第三方也沒有收到請求。

三、優化方案

1) 上面已經找到問題, 我開始驗證,我的想法。 我先寫了一個server端, 並且讓所有請求都暫停1分鐘,模擬第三方返回延遲 。

    AtomicInteger count = new AtomicInteger();
    
    @RequestMapping(path = "/{id}", method = RequestMethod.GET)
    @ResponseBody
    public Coffee getById(@PathVariable Long id) {
    
       int counts = count.incrementAndGet();
        log.info("counts {}:", counts);
        log.info("request {}:", id);
        try {
            Thread.sleep(60000);
        }catch (Exception e){
            e.printStackTrace();
        }
        Coffee coffee = coffeeService.getCoffee(id);
        return coffee;
    }

2)我用故障服務代碼寫了一個http請求

public class HttpTimeOutCase {
     // 用3000個線程模擬併發
    public static void main(String args[]) {
        for (int i = 0; i < 3000; i++) {
            try {
                R r = new R();
                System.err.println(r.getName());
                r.start();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        System.err.println("finish");
    }

    protected static class R extends Thread {
        @Override
        public void run() {
            try {
                Map<String, String> map = new HashMap<>();
                String url = "http://localhost:8080/coffee/" + 1;
                HttpRequest request = new HttpRequest(url, MethodType.GET, map);
                request.setParamType(ParamType.X_WWW_FORM_URLENCODED);
                Map<String, String> rspData = HttpClientsInstance.INSTANCE.getInstance().send(request);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}

3)我們先試一下沒有設置ConnectionRequestTimeout的情況,沒有獲取到connection的線程一直等待中
在這裏插入圖片描述
每個請求,都是一個線程,所以看到線程數量最高的時候是2200個,如果是生產環境節點,這個線程數容器肯定死機了。
在這裏插入圖片描述
最後我們消費端將3000個請求都消費掉
在這裏插入圖片描述
4)我們試一下設置ConnectionRequestTimeout的情況,沒有獲取到connection的線程20s後超時
在這裏插入圖片描述
處理了400個請求, 其它併發的2600線程在客戶端已經超時,現在最大連接數是200,連接超時時間是10s,服務端延遲時間是10s,所以處理了 2*200條請求。設置獲取連接超時時間,可以避免服務死機,但可能導致部分請求失敗。
5)在我們容器中Tomcat線程數默認是1000,所以將ConnectionRequestTimeout設置爲50s

四、總結

1)整個故障排查的思路不夠清晰,導致排查了很久
2)如果現在重新來排查

  1. 查看日誌,是否有正常發送http請求
  2. 聯繫第三方排查是否接收到請求
  3. 故障節點出現死機的時候,應該聯繫運維馬上重新發布一組服務,優先恢復業務
  4. 應該在故障節點上ping /wget 第三方域名,檢查網絡
  5. 用jps 和 jstack 命令查看線程情況
  6. 用jmap 導出堆棧信息
發佈了103 篇原創文章 · 獲贊 20 · 訪問量 2萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章