自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

【故障現(xiàn)場】多線程性能優(yōu)化最大的坑,99%人都不自知

開發(fā) 前端
訂單詳情頁耗時嚴重,p99 將近3秒,已經(jīng)驗證影響用戶體驗,本次迭代小艾專門對該接口進行優(yōu)化。迭代剛上線,該接口的響應時間大幅降低,p99 降低到 800 毫秒以內(nèi),大家紛紛向小艾發(fā)來祝賀。但好景不長,隨著流量的增加,接口響應時間也在逐漸變長,p99 超過 5 秒,最后系統(tǒng)拋出大量的 RejectedExecutionException 異常,這個接口不可用。最終,QA伙伴火速進行回滾操作,系統(tǒng)恢

1. 問題&分析

當我們在處理慢接口問題時,經(jīng)常會使用多線程技術(shù),將能夠并行處理的任務拆分到不同的線程中處理,等任務處理完成后,再收集各線程的處理結(jié)果,進行后續(xù)的處理。整體思路如下圖所示:

圖片圖片

這樣可以將并行部分的總耗時從 sum 降為 max,從而大幅降低接口的響應時間。

1.1. 案例

訂單詳情頁耗時嚴重,p99 將近3秒,已經(jīng)驗證影響用戶體驗,本次迭代小艾專門對該接口進行優(yōu)化。迭代剛上線,該接口的響應時間大幅降低,p99 降低到 800 毫秒以內(nèi),大家紛紛向小艾發(fā)來祝賀。但好景不長,隨著流量的增加,接口響應時間也在逐漸變長,p99 超過 5 秒,最后系統(tǒng)拋出大量的 RejectedExecutionException 異常,這個接口不可用。最終,QA伙伴火速進行回滾操作,系統(tǒng)恢復正常。

系統(tǒng)恢復后,小艾仔細查看系統(tǒng)監(jiān)控,CPU使用率并不高,內(nèi)存也處于正常水位,接口性能居然比優(yōu)化前還差,真心不知道哪里出了問題。

優(yōu)化前代碼:

public RestResult<OrderDetailVO> getOrderDetail(@PathVariable Long orderId){
        Stopwatch stopwatch = Stopwatch.createStarted();
        OrderService.Order order = this.orderService.getById(orderId);
        if (order == null){
            return RestResult.success(null);
        }
        OrderDetailVO orderDetail = new OrderDetailVO();
        orderDetail.setUser(userService.getById(order.getUserId()));
        orderDetail.setAddress(addressService.getById(order.getUserAddressId()));
        orderDetail.setCoupon(couponService.getById(order.getCouponId()));
        orderDetail.setProduct(productService.getById(order.getProductId()));
        log.info("串行 Cost {} ms", stopwatch.stop().elapsed(TimeUnit.MILLISECONDS));
        return RestResult.success(orderDetail);
}

優(yōu)化前耗時:

圖片圖片

優(yōu)化后代碼:

public RestResult<OrderDetailVO> getOrderDetailNew(@PathVariable Long orderId){
        Stopwatch stopwatch = Stopwatch.createStarted();
        OrderService.Order order = this.orderService.getById(orderId);
        if (order == null){
            return RestResult.success(null);
        }
        Future<UserService.User> userFuture = this.executorService.submit(() -> userService.getById(order.getUserId()));
        Future<AddressService.Address> addressFuture = this.executorService.submit(() -> addressService.getById(order.getUserAddressId()));
        Future<CouponService.Coupon> couponFuture = this.executorService.submit(() -> couponService.getById(order.getCouponId()));
        Future<ProductService.Product> productFuture = this.executorService.submit(() -> productService.getById(order.getProductId()));

        OrderDetailVO orderDetail = new OrderDetailVO();
        orderDetail.setUser(getFutureValue(userFuture));
        orderDetail.setProduct(getFutureValue(productFuture));
        orderDetail.setAddress(getFutureValue(addressFuture));
        orderDetail.setCoupon(getFutureValue(couponFuture));
        log.info("并行 Cost {} ms", stopwatch.stop().elapsed(TimeUnit.MILLISECONDS));
        return RestResult.success(orderDetail);
    }

優(yōu)化后耗時:

圖片圖片

可見采用并行優(yōu)化后,接口的響應時間從 4 秒 將至 1 秒,效果還是非常明顯的。

但,繼續(xù)加大請求量,系統(tǒng)便出現(xiàn)問題,如下圖所示:

圖片圖片

圖片

在流量逐漸增加的過程中,從日志中可以得到以下信息:

初期耗時穩(wěn)定,基本在 1 秒左右

接口耗時逐漸增大,甚至遠超串行處理的耗時(大于 4 秒)

有些請求直接拋出 RejectedExecutionException 異常

1.2. 問題分析

從代碼中并未發(fā)現(xiàn)任何問題,設計思路也非常清晰,其核心問題在線程池使用上,項目線程池配置如下:

int coreSize = Runtime.getRuntime().availableProcessors();
executorService = new ThreadPoolExecutor(coreSize, coreSize * 5,
        5L, TimeUnit.MINUTES,
        new LinkedBlockingQueue<Runnable>(1024)
        );

核心配置為:

  1. 核心線程數(shù)為 cpu 核數(shù)
  2. 最大線程數(shù)為 cpu 核數(shù)的 5 倍
  3. 空閑線程存活時間為 5 分鐘
  4. 任務隊列為 LinkedBlockingQueue 大小為 1024

在這個配置下,我們推演下以上的三個現(xiàn)象。

1.2.1. 線程資源充足

如下圖所示:

圖片圖片

整體流程如下:

  1. 主線程向線程池提交 Task
  2. 由于線程處于空閑狀態(tài),立即接受并處理問題
  3. 線程池線程處理完任務,將最終的處理結(jié)果寫回到 Future
  4. 主線程等待所有任務執(zhí)行完成,獲取所有執(zhí)行結(jié)果,然后執(zhí)行后續(xù)流程

這正是想要的執(zhí)行結(jié)果,任務被并行執(zhí)行,大幅降低接口耗時。

1.2.2. 任務進入等待隊列

隨著流量的增加,所有的核心線程都處于忙碌狀態(tài),此時新任務將進入等待隊列,具體如下:

圖片圖片

整體流入如下:

  1. 主線程向線程池提交任務
  2. 由于沒有核心線程可用,任務被放置到任務隊列
  3. 主線程進入等待狀態(tài),等待時間包括兩部分:

任務在隊列中等待線程調(diào)度時間

任務分配到線程后,任務實際執(zhí)行時間

  1. 如果前面等待的任務非常多,那等待時間將變的非常長

主線程等待時間 = 隊列等待時間 + 任務執(zhí)行時間。當任務隊列非常長時,整體時間將遠超串行執(zhí)行時間。

1.2.3. 資源耗盡觸發(fā)拒絕策略

流量繼續(xù)增加,線程池的任務隊列已滿并且線程數(shù)量也達到上限,此時會觸發(fā)拒絕策略,具體如下:

圖片圖片

線程池默認拒絕策略為:AbortPolicy,直接拋出 RejectedExecutionException,從而觸發(fā)接口異常。

還有更可怕的情況,就是部分提交,也就是主線程已經(jīng)成功提交幾個任務,如下圖所示:

圖片圖片

核心流程如下:

  1. 主線程已經(jīng)成功提交兩個任務
  2. 在提交第三個任務時,由于資源不夠觸發(fā)拒絕策略,拋出異常導致主線程提前結(jié)束
  3. 已經(jīng)成功提交的任務仍舊會被線程執(zhí)行,由于主線程已經(jīng)退出,執(zhí)行結(jié)果沒有任何意義,從而白白浪費系統(tǒng)資源

2. 解決方案

前面已經(jīng)分析的很清楚,問題的本質(zhì)就是線程池資源分配不合理,核心參數(shù)設置錯誤:

  1. 隊列設置錯誤。在該場景下,需要充分利用線程資源,將任務放入隊列會增加任務在隊列的等待時間,隊列長度越大對系統(tǒng)的傷害越大;
  2. 拒絕策略設置錯誤。直接拋出異常會中斷主流程,導致部分無效任務(無意義任務)提交,白白浪費系統(tǒng)資源;

除線程池參數(shù)問題外,還有個小問題:主線程完成任務提交后處于等待狀態(tài),未執(zhí)行任何有意義的操作,存在資源浪費。

2.1. 線程池改進方案

改進線程池如下所示:

int coreSize = Runtime.getRuntime().availableProcessors();
executorService = new ThreadPoolExecutor(coreSize, coreSize * 5,
        5L, TimeUnit.MINUTES,
        new SynchronousQueue<>(),
        new ThreadPoolExecutor.CallerRunsPolicy()
        );

線程池配置如下:

  1. 核心線程數(shù)不變,仍舊是 cpu 數(shù);
  2. 最大線程數(shù)不變,仍舊是 cpu 數(shù)的5倍;
  3. 空閑線程存活時間不變,仍舊是 5 分鐘;
  4. 使用 SynchronousQueue 替代 LinkedBlockingQueue(1024)。SynchronousQueue 是一個特殊的隊列,其最大容量是1。也就是說,任何一次插入操作都必須等待一個相應的刪除操作,反之亦然。如果沒有相應的操作正在進行,則該線程將被阻塞;
  5. 指定拒絕策略為 CallerRunsPolicy。當線程池資源不夠時,由主線程來執(zhí)行任務;

在這個配置下,及時線程池中的所有資源全部耗盡,也只會降級到串行執(zhí)行,不會讓系統(tǒng)變的更糟糕。

新配置下,系統(tǒng)表現(xiàn)如下:

圖片圖片

在最差的情況下也僅僅與串行執(zhí)行耗時一致。

總體來說就一句話:線程池有資源可用,那就為主線程分擔部分壓力;如果沒有資源可用,那就由主線程獨自完成。

2.1. 充分利用主線程

上面提到一個小問題,在資源充足情況下,所有任務均有線程池線程完成,主線程一致處于等待狀態(tài),存在一定的資源浪費。

如下圖所示:

圖片圖片

3 個任務耗費 4 個線程資源:

  1. 線程池3個線程負責執(zhí)行任務
  2. 主線程等待執(zhí)行結(jié)果,一直處于阻塞狀態(tài)

為了充分利用線程資源,可以讓主線程負責執(zhí)行任意一個任務。如下圖所示:

圖片圖片

主線程不在盲目等待,也負責一個任務的執(zhí)行,這樣 3 個任務只需 3 個線程即可。

代碼上也非常簡單,具體如下:

public RestResult<OrderDetailVO> getOrderDetailNew(@PathVariable Long orderId){
    Stopwatch stopwatch = Stopwatch.createStarted();
    OrderService.Order order = this.orderService.getById(orderId);
    if (order == null){
        return RestResult.success(null);
    }
    Future<UserService.User> userFuture = this.executorService.submit(() -> userService.getById(order.getUserId()));
    Future<AddressService.Address> addressFuture = this.executorService.submit(() -> addressService.getById(order.getUserAddressId()));
    Future<CouponService.Coupon> couponFuture = this.executorService.submit(() -> couponService.getById(order.getCouponId()));
//        Future<ProductService.Product> productFuture = this.executorService.submit(() -> productService.getById(order.getProductId()));

    OrderDetailVO orderDetail = new OrderDetailVO();
    // 由主線程負責運行
    orderDetail.setProduct(productService.getById(order.getProductId()));

    orderDetail.setUser(getFutureValue(userFuture));
    orderDetail.setAddress(getFutureValue(addressFuture));
    orderDetail.setCoupon(getFutureValue(couponFuture));
    log.info("并行 Cost {} ms", stopwatch.stop().elapsed(TimeUnit.MILLISECONDS));
    return RestResult.success(orderDetail);
}

主線程執(zhí)行不同的任務,會對接口的響應時間產(chǎn)生影響嗎?

不會,并行執(zhí)行整體耗時為 max(任務耗時),主線程必須獲取全部結(jié)果才能運行,所以必須等待這么長時間。

  1. 如果主線程運行的任務不是最耗時任務,則需要等待最耗時任務執(zhí)行完成才能執(zhí)行后續(xù)邏輯;
  2. 如果主線程運行的是最耗時任務,則其他線程已經(jīng)執(zhí)行完成并提前釋放資源;

3. 示例&源碼

代碼倉庫:https://gitee.com/litao851025/learnFromBug

代碼地址:https://gitee.com/litao851025/learnFromBug/tree/master/src/main/java/com/geekhalo/demo/thread/paralleltask


責任編輯:武曉燕 來源: geekhalo
相關(guān)推薦

2024-09-29 09:27:10

2024-09-27 09:31:25

2025-04-14 09:31:03

2022-12-15 19:27:33

多線程代碼性能

2019-12-26 09:56:34

Java多線程內(nèi)部鎖

2024-03-18 09:24:12

RocketMQ消息模型分布式

2024-11-05 16:29:57

2022-07-20 07:45:15

多線程程序性能

2010-01-28 09:55:05

性能優(yōu)化

2009-06-12 16:55:10

VPN客戶端故障

2018-10-25 15:55:44

Java多線程鎖優(yōu)化

2025-04-16 10:10:00

互聯(lián)網(wǎng)DNS網(wǎng)絡

2024-01-29 09:22:59

死鎖線程池服務

2024-03-28 12:51:00

Spring異步多線程

2018-07-03 10:49:22

性能故障排查

2025-02-25 12:00:00

Java線程開發(fā)

2019-06-06 14:21:32

SQL過濾測試

2023-04-27 08:35:20

Webpack 4性能優(yōu)化

2021-10-15 06:49:37

MySQL

2023-05-31 08:19:23

Webpack4Webpack 5
點贊
收藏

51CTO技術(shù)棧公眾號