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

前言

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

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

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

Tomcat 請求流程

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

基本的 HTTP 請求處理的過程以下圖,其中 Connector 和 Engine 是 tomcat 內置的組件。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,無限制拒絕服務,這兩種狀況都不是咱們想要的。app

異步就是一種更優雅的形式,請求發送後,接口的調用者能夠繼續幹別的事,請求處理完後會自動通知給調用者。而且在 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.),可是真正遇到這麼一個場景的時候,發現一切都是水到渠成的。只有在不斷的實踐中,才能調整對某一編程思想的認識,有新的體會。

相關文章
相關標籤/搜索