Web服務(wù)耗時接口同步請求異步處理解決方案

背景

生產(chǎn)環(huán)境存在一些接口强重,其因為后端服務(wù)涉及到大量數(shù)據(jù)庫讀寫操作,因此接口非常耗時膳叨。比如商品導(dǎo)入功能,經(jīng)過事務(wù)拆分痘系、拆分查詢并組裝數(shù)據(jù)等手段對功能進行了優(yōu)化菲嘴,在不改變業(yè)務(wù)設(shè)計的前提下幾乎無任何優(yōu)化空間。
容器(tomcat)中線程的數(shù)量是一定的汰翠,容器處理大量耗時請求時龄坪,勢必會影響其他接口的正常訪問。
因此复唤,在不改變業(yè)務(wù)的前提下健田,在高并發(fā)場景提高商品服務(wù)的吞吐率優(yōu)化是非常必要的。

技術(shù)實現(xiàn)方案

采用Spring MVC異步處理方案(適用于耗時同步交易場景)佛纫。Spring MVC異步處理實現(xiàn)方案通常支持3種方式:

  • Callable實現(xiàn)
  • WebAsyncTask實現(xiàn)
  • DefferedResult實現(xiàn)

我們使用DefferedResult + 線程池 + 阻塞隊列LinkedBlockingQueue 實現(xiàn)請求的異步處理同步響應(yīng)妓局。

關(guān)于DeferredResult

DeferredResult從 Spring 3.2 開始可用,有助于將長時間運行的計算從 http-worker 線程卸載到單獨的線程呈宇。
盡管另一個線程會占用一些資源進行計算好爬,但工作線程在此期間不會被阻塞并且可以處理傳入的其他客戶端請求。
異步請求處理模型非常有用甥啄,因為它有助于在高負載期間很好地擴展應(yīng)用程序抵拘,尤其是對于 IO 密集型操作。

DeferredResult處理流程

DeferredResult的處理過程與Callback類似型豁,不一樣的地方在于它的結(jié)果不是DeferredResult直接返回的僵蛛,而是由其它線程通過同步的方式設(shè)置到該對象中。它的執(zhí)行過程如下所示:

  • 客戶端請求服務(wù)
    SpringMVC調(diào)用Controller迎变,Controller返回一個DeferredResult對象
  • SpringMVC調(diào)用ruquest.startAsync
  • DispatcherServlet以及Filters等從應(yīng)用服務(wù)器線程中結(jié)束(釋放容器線程)充尉,但Response仍舊是打開狀態(tài),也就是說暫時還不返回給客戶端
  • 異步線程處理實際業(yè)務(wù)并將結(jié)果設(shè)置到DeferredResult中衣形,SpringMVC將請求發(fā)送給應(yīng)用服務(wù)器繼續(xù)處理
  • DispatcherServlet再次被調(diào)用并且繼續(xù)處理DeferredResult中的結(jié)果驼侠,最終將其返回給客戶端。

重要技術(shù)點

線程池

線程池維護多個線程谆吴,等待監(jiān)督管理者分配可并發(fā)執(zhí)行的任務(wù)倒源。這種做法,一方面避免了處理任務(wù)時創(chuàng)建銷毀線程開銷的代價句狼,另一方面避免了線程數(shù)量膨脹導(dǎo)致的過分調(diào)度問題笋熬,保證了對內(nèi)核的充分利用。

LinkedBlockingQueue

LinkedBlockingQueue實現(xiàn)是線程安全的腻菇,實現(xiàn)了先進先出等特性胳螟,是作為生產(chǎn)者消費者的首選昔馋,LinkedBlockingQueue 可以指定容量,也可以不指定糖耸,不指定的話秘遏,默認最大是Integer.MAX_VALUE,其中主要用到put和take方法嘉竟,put方法在隊列滿的時候會阻塞直到有隊列成員被消費邦危,take方法在隊列空的時候會阻塞,直到有隊列成員被放進來舍扰。這里需要注意直接使用LinkedBlockingQueue阻塞隊列作為線程池會存在一個問題倦蚪,當workcount > corePool時優(yōu)先進入隊列排隊,因此當請求并發(fā)過多會導(dǎo)致請求緩慢妥粟,甚至因為隊列過多出現(xiàn)內(nèi)存溢出(JDK是先排隊再漲線程池)

網(wǎng)絡(luò)上基本找得到的DeferredResult相關(guān)的技術(shù)博客或文章都是直接創(chuàng)建線程或使用LinkedBlockingQueue的線程池审丘,實際在壓力測試過程當模擬接口并發(fā)到50就出現(xiàn)大量延遲,比不優(yōu)化時性能還差勾给。有興趣的可以直接使用LinkedBlockingQueue作為線程池隊列壓力測試看看效果滩报。

Tomcat的線程池

org.apache.tomcat.util.threads.TaskQueue
org.apache.tomcat.util.threads.ThreadPoolExecutor

因為我們優(yōu)化的是web接口請求,不能因為LinkedBlockingQueue的排隊導(dǎo)致接口出現(xiàn)大量延遲和緩慢播急,因此我們在實現(xiàn)過程不直接使用LinkedBlockingQueue作為線程池的阻塞隊列脓钾,而是使用tomcat的線程池TaskQueue,TaskQueue繼承了JDK的LinkedBlockingQueue 并擴展了JDK線程池的功能桩警,主要體現(xiàn)在兩點:

  • Tomcat的ThreadPoolExecutor使用的TaskQueue可训,是無界的LinkedBlockingQueue,但是通過taskQueue的offer方法覆蓋了LinkedBlockingQueue的offer方法捶枢,改寫了規(guī)則握截,使得線程池能在任務(wù)較多的情況下增長線程池數(shù)量——JDK是先排隊再漲線程池,Tomcat則是先漲線程池再排隊烂叔。
  • Tomcat的ThreadPoolExecutor改寫了execute方法谨胞,當任務(wù)被reject時,捕獲異常蒜鸡,并強制入隊胯努。

代碼實現(xiàn)

創(chuàng)建處理耗時任務(wù)的線程池

public static ThreadPoolExecutor executor = null;

  private TaskQueue taskqueue;

  protected int maxQueueSize = Integer.MAX_VALUE;

  protected int threadPriority = 5;
  protected boolean daemon = true;
  protected String namePrefix = "testsleep-";
  protected int minSpareThreads = 25;
  protected int maxThreads = 200;
  protected int maxIdleTime = 60000;
  protected long threadRenewalDelay = 1000L;
  protected boolean prestartminSpareThreads = false;


  /**
   * 初始化時啟動監(jiān)聽請求隊列
   */
  @PostConstruct
  public void init() {
    /*cachedThreadPool = new ThreadPoolExecutor(4,
        50,
        0,
        TimeUnit.MILLISECONDS,
        new LinkedBlockingQueue<>(50),
        r -> new Thread(r));*/

    // 任務(wù)隊列:這里你看到的是一個無界隊列,但是隊列里面進行了特殊處理
    taskqueue = new TaskQueue(maxQueueSize);
    TaskThreadFactory tf = new TaskThreadFactory(namePrefix,daemon, threadPriority);
    // 創(chuàng)建線程池逢防,這里的ThreadPoolExecutor是Tomcat繼承自JDK的ThreadPoolExecutor
    executor = new ThreadPoolExecutor(
        minSpareThreads, maxThreads, // 核心線程數(shù)與最大線程數(shù)
        maxIdleTime, TimeUnit.MILLISECONDS, // 默認6萬毫秒的超時時間叶沛,也就是一分鐘
        taskqueue, tf); // 玄機在任務(wù)隊列的設(shè)置
    executor.setThreadRenewalDelay(threadRenewalDelay);
    if (prestartminSpareThreads) {
      executor.prestartAllCoreThreads(); // 預(yù)熱所有的核心線程
    }
    taskqueue.setParent(executor);


  }

重構(gòu)請求,使用DeferredResult實現(xiàn)異步處理

這里我們直接使用Thread.sleep模擬一個耗時任務(wù)

@GetMapping("/users-anon/test/{testkey}")
  public DeferredResult<Result<String>> testSleep(@PathVariable String testkey) {

    //return service.testSleep(); //直接調(diào)用耗時業(yè)務(wù)處理

    DeferredResult<Result<String>> output = new DeferredResult<>(1000 * 30L);

    output.onTimeout(() ->
        output.setErrorResult(
            ResponseEntity.status(HttpStatus.REQUEST_TIMEOUT)
                .body("Request timeout occurred.")));

    log.info("[ TestSleepController ] 接到請求");

    //轉(zhuǎn)到后臺線程
    QueueListener.executor.execute(() -> {
      log.info("開始執(zhí)行耗時任務(wù):{}", System.currentTimeMillis());
      try {
        Thread.sleep(6000);
      } catch (InterruptedException e) {
      }
      log.info("執(zhí)行耗時任務(wù)結(jié)束:{}", System.currentTimeMillis());
      output.setResult(Result.succeed());
    });


    log.info("[ TestSleepController ] 返回DeferredResult忘朝,并釋放容器線程.");

    return output;


  }

壓力測試

機器參數(shù)

CentOS Linux release 7.3.1611 (Core) 1核 2.30GHz 8G內(nèi)存

服務(wù)部署

Docker容器

測試結(jié)果

單接口測試

在測試DeferredResult前灰署,我們先模擬一個耗時接口,并對接口進行壓力測試:
100線程 循環(huán)一次

200線程 循環(huán)一次

500線程 循環(huán)一次

當耗時接口在100、200并發(fā)下接口基本正常氓侧,當達到500時接口響應(yīng)出現(xiàn)明顯的遲緩脊另。

混合接口測試

我們建立兩個線程組导狡,一個是正常的耗時任務(wù)A(線程組2)约巷,一個是使用DeferredResult優(yōu)化的耗時任務(wù)B(線程組1)。
任務(wù)A旱捧、B 各50線程 循環(huán)一次
壓測結(jié)果如下:


整體響應(yīng)都差不多独郎。

任務(wù)A 50線程 循環(huán)一次 任務(wù)B 500線程 循環(huán)一次
DeferredResult優(yōu)化的耗時任務(wù)壓測結(jié)果:

正常任務(wù)壓測結(jié)果:


當任務(wù)BDeferredResult優(yōu)化后的接口并發(fā)爆發(fā)式增長后,接口的響應(yīng)仍和優(yōu)化前一樣出現(xiàn)大范圍的延遲枚赡,但是任務(wù)A的接口響應(yīng)并未收到影響氓癌。

結(jié)果分析

通過壓測結(jié)果分析我們可以得出DeferredResult優(yōu)化的耗時任務(wù)雖然不能提示耗時接口本身的響應(yīng)速度,但是能極大減少耗時任務(wù)對服務(wù)容器線程的占用贫橙,提升應(yīng)用在高并發(fā)場景下本身的吞吐量贪婉。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市卢肃,隨后出現(xiàn)的幾起案子疲迂,更是在濱河造成了極大的恐慌,老刑警劉巖莫湘,帶你破解...
    沈念sama閱讀 216,591評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件尤蒿,死亡現(xiàn)場離奇詭異,居然都是意外死亡幅垮,警方通過查閱死者的電腦和手機腰池,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,448評論 3 392
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來忙芒,“玉大人示弓,你說我怎么就攤上這事『侨” “怎么了奏属?”我有些...
    開封第一講書人閱讀 162,823評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長甘桑。 經(jīng)常有香客問我拍皮,道長,這世上最難降的妖魔是什么跑杭? 我笑而不...
    開封第一講書人閱讀 58,204評論 1 292
  • 正文 為了忘掉前任铆帽,我火速辦了婚禮,結(jié)果婚禮上德谅,老公的妹妹穿的比我還像新娘爹橱。我一直安慰自己,他們只是感情好窄做,可當我...
    茶點故事閱讀 67,228評論 6 388
  • 文/花漫 我一把揭開白布愧驱。 她就那樣靜靜地躺著慰技,像睡著了一般。 火紅的嫁衣襯著肌膚如雪组砚。 梳的紋絲不亂的頭發(fā)上吻商,一...
    開封第一講書人閱讀 51,190評論 1 299
  • 那天,我揣著相機與錄音糟红,去河邊找鬼艾帐。 笑死,一個胖子當著我的面吹牛盆偿,可吹牛的內(nèi)容都是我干的柒爸。 我是一名探鬼主播,決...
    沈念sama閱讀 40,078評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼事扭,長吁一口氣:“原來是場噩夢啊……” “哼捎稚!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起求橄,我...
    開封第一講書人閱讀 38,923評論 0 274
  • 序言:老撾萬榮一對情侶失蹤今野,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后谈撒,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體腥泥,經(jīng)...
    沈念sama閱讀 45,334評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,550評論 2 333
  • 正文 我和宋清朗相戀三年啃匿,在試婚紗的時候發(fā)現(xiàn)自己被綠了蛔外。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,727評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡溯乒,死狀恐怖夹厌,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情裆悄,我是刑警寧澤矛纹,帶...
    沈念sama閱讀 35,428評論 5 343
  • 正文 年R本政府宣布,位于F島的核電站光稼,受9級特大地震影響或南,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜艾君,卻給世界環(huán)境...
    茶點故事閱讀 41,022評論 3 326
  • 文/蒙蒙 一采够、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧冰垄,春花似錦蹬癌、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,672評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽隅要。三九已至,卻和暖如春董济,著一層夾襖步出監(jiān)牢的瞬間步清,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,826評論 1 269
  • 我被黑心中介騙來泰國打工感局, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留尼啡,地道東北人暂衡。 一個月前我還...
    沈念sama閱讀 47,734評論 2 368
  • 正文 我出身青樓询微,卻偏偏與公主長得像,于是被迫代替她去往敵國和親狂巢。 傳聞我的和親對象是個殘疾皇子撑毛,可洞房花燭夜當晚...
    茶點故事閱讀 44,619評論 2 354

推薦閱讀更多精彩內(nèi)容