當我們在處理慢接口問題時,經常會使用多線程技術,將能夠并行處理的任務拆分到不同的線程中處理,等任務處理完成后,再收集各線程的處理結果,進行后續的處理。整體思路如下圖所示:
圖片
這樣可以將并行部分的總耗時從 sum 降為 max,從而大幅降低接口的響應時間。
訂單詳情頁耗時嚴重,p99 將近3秒,已經驗證影響用戶體驗,本次迭代小艾專門對該接口進行優化。迭代剛上線,該接口的響應時間大幅降低,p99 降低到 800 毫秒以內,大家紛紛向小艾發來祝賀。但好景不長,隨著流量的增加,接口響應時間也在逐漸變長,p99 超過 5 秒,最后系統拋出大量的 RejectedExecutionException 異常,這個接口不可用。最終,QA伙伴火速進行回滾操作,系統恢復正常。
系統恢復后,小艾仔細查看系統監控,CPU使用率并不高,內存也處于正常水位,接口性能居然比優化前還差,真心不知道哪里出了問題。
優化前代碼:
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);}
優化前耗時:
圖片
優化后代碼:
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); }
優化后耗時:
圖片
可見采用并行優化后,接口的響應時間從 4 秒 將至 1 秒,效果還是非常明顯的。
但,繼續加大請求量,系統便出現問題,如下圖所示:
圖片
在流量逐漸增加的過程中,從日志中可以得到以下信息:
初期耗時穩定,基本在 1 秒左右
接口耗時逐漸增大,甚至遠超串行處理的耗時(大于 4 秒)
有些請求直接拋出 RejectedExecutionException 異常
從代碼中并未發現任何問題,設計思路也非常清晰,其核心問題在線程池使用上,項目線程池配置如下:
int coreSize = Runtime.getRuntime().availableProcessors();executorService = new ThreadPoolExecutor(coreSize, coreSize * 5, 5L, TimeUnit.MINUTES, new LinkedBlockingQueue<Runnable>(1024) );
核心配置為:
在這個配置下,我們推演下以上的三個現象。
如下圖所示:
圖片
整體流程如下:
這正是想要的執行結果,任務被并行執行,大幅降低接口耗時。
隨著流量的增加,所有的核心線程都處于忙碌狀態,此時新任務將進入等待隊列,具體如下:
圖片
整體流入如下:
任務在隊列中等待線程調度時間
任務分配到線程后,任務實際執行時間
主線程等待時間 = 隊列等待時間 + 任務執行時間。當任務隊列非常長時,整體時間將遠超串行執行時間。
流量繼續增加,線程池的任務隊列已滿并且線程數量也達到上限,此時會觸發拒絕策略,具體如下:
圖片
線程池默認拒絕策略為:AbortPolicy,直接拋出 RejectedExecutionException,從而觸發接口異常。
還有更可怕的情況,就是部分提交,也就是主線程已經成功提交幾個任務,如下圖所示:
圖片
核心流程如下:
前面已經分析的很清楚,問題的本質就是線程池資源分配不合理,核心參數設置錯誤:
除線程池參數問題外,還有個小問題:主線程完成任務提交后處于等待狀態,未執行任何有意義的操作,存在資源浪費。
改進線程池如下所示:
int coreSize = Runtime.getRuntime().availableProcessors();executorService = new ThreadPoolExecutor(coreSize, coreSize * 5, 5L, TimeUnit.MINUTES, new SynchronousQueue<>(), new ThreadPoolExecutor.CallerRunsPolicy() );
線程池配置如下:
在這個配置下,及時線程池中的所有資源全部耗盡,也只會降級到串行執行,不會讓系統變的更糟糕。
新配置下,系統表現如下:
圖片
在最差的情況下也僅僅與串行執行耗時一致。
總體來說就一句話:線程池有資源可用,那就為主線程分擔部分壓力;如果沒有資源可用,那就由主線程獨自完成。
上面提到一個小問題,在資源充足情況下,所有任務均有線程池線程完成,主線程一致處于等待狀態,存在一定的資源浪費。
如下圖所示:
圖片
3 個任務耗費 4 個線程資源:
為了充分利用線程資源,可以讓主線程負責執行任意一個任務。如下圖所示:
圖片
主線程不在盲目等待,也負責一個任務的執行,這樣 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);}
主線程執行不同的任務,會對接口的響應時間產生影響嗎?
不會,并行執行整體耗時為 max(任務耗時),主線程必須獲取全部結果才能運行,所以必須等待這么長時間。
代碼倉庫:https://gitee.com/litao851025/learnFromBug
代碼地址:https://gitee.com/litao851025/learnFromBug/tree/master/src/main/java/com/geekhalo/demo/thread/paralleltask
本文鏈接:http://www.www897cc.com/showinfo-26-66204-0.html【故障現場】多線程性能優化最大的坑,99%人都不自知
聲明:本網頁內容旨在傳播知識,若有侵權等問題請及時與本網聯系,我們將在第一時間刪除處理。郵件:2376512515@qq.com
上一篇: CSS 滾動驅動動畫實現圓弧滾動條