Bug覆盤:接口異步返回的重要性

前言

最近接收了一個老項目,突然甲方 QA 報了一個 bug,連續請求 60 次,成功 8 次,後面的 52 次全部失敗,而且成功的 case 返回時間普遍較長。看了日誌,並非業務上的異常。這讓剛畢業沒什麼經驗的我,頓時陷入了沉思。但回過神來考慮了一下,大膽才猜測,可能是網絡問題或者是併發請求上的問題。

但其實業務異常相對容易排查,而網絡或者併發的問題會相對難一些,剛好自己對於服務器服務器請求處理的流程也不太清楚,所以就花了時間看了下,最後基本斷定是接口是實現方式出現了問題,重新改寫成異步接口後問題基本解決。

所以今天就打算覆盤一下,聊聊 tomcat 處理請求的流程,心裏好有個數,以及對於某些場合下接口異步返回的重要性。

Tomcat 請求流程

項目是 Spring Boot 開發的,默認用 jar 包部署,實際上就是運行在一個內嵌的 tomcat 中,所以下面就簡單理一下 tomcat 處理併發請求的基本流程。這裏不具體涉及到相關的組件以及源碼,僅僅是梳理過程。

基本的 HTTP 請求處理的過程如下圖,其中 Connector 和 Engine 是 tomcat 內置的組件。

  1. Connector 會監聽響應的端口,例如 80 或 443,新的 HTTP 請求過來後會做響應的處理。
  2. Connector 接受請求後,將其封裝成 Request 對象,並創建線程來處理。tomcat 默認可以處理的併發數爲 200 個(通過 maxThreads 參數設置),實際的處理速度取決於我們自己實現的服務程序;超出 maxThreads 部分,tomcat 仍然不斷接收,但最多不能超過 maxConnector 設置的數,默認 1w 個;超過 maxConnector 的部分,tomcat 仍然不斷接收,但不做處理,放入 Connector 創建的一個隊列中,但最多不能超過 acceptCount,超過則拒絕(也就是我們所說的,服務器卡死、掛了)。
  3. 配置時 Connector 會與 Engine 進行綁定,新創建的線程會在 pipeline 中有序等待 Engine 進行處理,其中就包含了 servlet 和 Spring MVC 的處理流程。
  4. Engine 處理完成後會將結果返回給對應的 Connector,再做進一步封裝後返回給 HTTP 請求的一方。

清楚了上述流程之後,基本上對於服務器如何處理併發請求有了一個基本的概念,當併發量大的時候,可以對上述參數進行改動,以適應自己的項目。

接口異步返回的重要性

在回到之前講的項目上來,可以看到 tomcat 默認配置就已經具有不小的併發量了,並且在 Spring Boot 中 Controller 是單例的,且每個請求的處理互不相關,但是爲什麼接口返回的速度仍然不似預期呢?這其實和這個項目的業務時有關的。

這個接口是對算法的集成,發起請求後需要通過 HTTP 調用算法處理返回結果,請求調用的速度遠大於接口處理的速度,再者算法依賴於獨佔的 GPU,也就意味着一個請求在處理時,其他請求必須等待。而之前實現的接口是同步的,且設置的算法接口返回的 timeout 爲 15s,因此當請求積累到一定數量時,後續等待時間超過 15s,直接返回了異常的結果,導致後續請求全部失敗。

顯然,在處理速度低於請求速度的接口,並且依賴資源是獨佔或者很緊張的場景下,通過同步的形式返回接口是不可取的。由於接口占用的資源有限,可以理解成將此接口加上了一個 synchronized,後續請求過來都會無限制等待,或者設置了 timeout,無限制拒絕服務,這兩種情況都不是我們想要的。

異步就是一種更優雅的形式,請求發送後,接口的調用者可以繼續幹別的事,請求處理完後會自動通知給調用者。並且在 Java 中的實現也是比較簡單的,直接創建一個線程池來接收請求就可以了,線程池自帶阻塞隊列已經很好地幫我們處理排隊這個場景,分佈式場景則需要考慮用 redis 或者成熟的 mq 框架來進行調度了。調用者額外需要實現一個 callback 接口來接收處理完後的結果。這樣再多的請求都能夠有序的獲取到處理的結果,無非是耗時的長短問題罷了。

@RequestMapping(value = "handleTask", method = RequestMethod.POST)
public RestResult handleTask(HttpServletRequest request,String callbackUrl) {
    mServerPool.submit((Runnable) SpringUtil.getBean("imageTask", callbackUrl));
    return new RestResult();
}

其實,用過支付寶支付 API 的開發者應該很熟悉這個套路,因爲阿里也是這麼在做的,發起支付後,用戶有一段時間可以確認支付,因此這個過程並非實時返回的,所有會有一個 callback 接口,用於實現用戶支付完成的後的業務邏輯,當用戶完成支付後,支付寶服務器會回調到這個接口,完成最終的一個業務。

總結

以前一直覺得異步、併發很抽象,學習的時候也總是那麼幾個 demo(交叉輸出、生產者消費者 etc.),但是真正遇到這麼一個場景的時候,發現一切都是水到渠成的。只有在不斷的實踐中,才能調整對某一編程思想的認識,有新的體會。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章