官方彩票投注

代碼改變世界

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

2019-12-20 14:39  Fururur  閱讀(...)  評論(...收藏

前言

最近接收了一個老項目,突然甲方 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.),但是真正遇到這么一個場景的時候,發現一切都是水到渠成的。只有在不斷的實踐中,才能調整對某一編程思想的認識,有新的體會。