苏宁11.11:如何基于异步化打造会员任务平台?
本文為『InfoQ x 蘇寧 2018 雙十一』技術特別策劃系列文章之一。
背景
蘇寧會員任務平臺是覆蓋聚合電商、體育、金融、PPTV、直播、紅孩子等各個業態,平臺會實時獲取用戶的畫像信息來計算用戶在客群中的分布及畫像屬性,從而實時判斷用戶是否滿足相關場景下任務,若滿足相關場景以后可以領取任務下所有獎項;任務類型包含了訂單紅包、母嬰、Super會員、直播、雙簽、金融升級存等等。在大促特別是雙十一期間,任務中心產品對于各個業態的引流,會員的留存及轉化來說是一個重要的工具。
問題
因任務平臺業務邏輯復雜、實時性要求高,涉及多個外圍系統服務及數據調用;一期系統上線后部分功能遇到性能問題,例如聚合頁打開時間過長,首先聚合頁上要展示用戶能看到的任務列表,以及當前用戶是否達到領取條件,其次每個任務需要展示的狀態依賴于后臺多種信息的聚合,包括不在有效時間范圍內、當前時段庫存、可供領取的總庫存、領取頻次等。復雜邏輯和實時要求導致TPS在上線壓測的時候沒有能夠達到一個理想預期效果。
即將到來的”雙十一”流量高峰, 可以預見會使得超過現有的任務系統的TPS的峰值, 從而導致任務系統在”雙十一”的場景下很容易觸碰到性能瓶頸,影響用戶體驗;因此需要對蘇寧任務平臺的核心功能做性能優化, 提升實時性復雜業務邏輯場景下的性能, 以便于應對任務平臺的流量暴漲以及雙十一流量高峰。
定位
現有的每個任務可能依賴于多個異構系統的服務或者數據,例如直播任務及訂單任務來自于不同的系統的服務,并且有些場景是基于外圍系統的數據進行邏輯計算,有些則是通過服務接口調用的方式。
代碼示例:
public ResultDTO checkAndGetInfo() { A a = getA(); B b = getB(); C c = getC(); ...... ResultDTO result = computeResult(a, b, c ...); return resultDTO;}由于頁面實時性要求高,邏輯復雜,對于某個任務是否展示需要調用多個外圍接口,響應時間不可控,理論上根據任務的復雜性可能涉及多個客群,調用次數及響應時間不可控。性能主要在響應時間不可控。
某個任務狀態要調用多個本地接口或者外圍接口。
主要思路:異步,緩存,線程池
針對以上定位到位問題,考慮到實時調用外圍接口的方案會導致響應時間不可控,采用NIO的思想,對整個調用鏈進行梳理,盡量異步化調用,同時增加適當過期時間的緩存,達到性能優化的目的。
在一期設計的時候已經從業務邏輯的角度做了拆分,將不同生命周期的邏輯異步化處理,例如獎勵是通過kafka推送到獎勵資源系統異步發放的。
上述從業務生命周期角度分析,通過切分業務流程,達到優化的方式已經不能滿足系統性能需求,需要從技術上考慮更細粒度的異步化處理方式。
優化方案的選擇及演進
Kilim
Kilim是一個java的協程框架,利用字節碼技術編織技術將普通代碼轉化為支持協程的代碼,當時是基于同步的思路下,想利用協程優化同步并發處理的能力。經過調研業界實踐應用相對較少,因此考慮到項目開發周期等因素,沒有采用Kilim方案。
Guava Listenable Future:
JDK 5引入了Future模式。 Future接口是Java多線程Future模式的實現,在java.util.concurrent包中,可以來進行異步計算。
Future模式是多線程設計常用的一種設計模式。Future模式可以理解成:有一個任務,提交給了Future,Future替我完成這個任務。期間我自己可以去做任何想做的事情。一段時間之后,我就便可以從Future那兒取出結果。
ExecutorService executor = ...;Future f = executor.submit(...);f.get();Future接口可以構建異步應用,但依然有其局限性。它很難直接表述多個Future 結果之間的依賴性。實際開發中,我們經常需要達成以下目的:
Future雖然可以實現獲取異步執行結果的需求,但是它沒有提供通知的機制,我們無法得知Future什么時候完成。
要么使用阻塞,在future.get()的地方等待future返回的結果,這時又變成同步操作。要么使用isDone()輪詢地判斷Future是否完成,這樣會耗費CPU的資源。
Guava的Listenable Future對其做了改進,支持注冊一個任務執行結束后回調函數。
ListenableFuture\u0026lt;String\u0026gt; listenableFuture = listeningExecutor.submit(new Callable\u0026lt;String\u0026gt;() { @Override public String call() throws Exception { return \u0026quot;\u0026quot;; }});Futures.addCallback(ListenableFuture\u0026lt;V\u0026gt;,FutureCallback\u0026lt;V\u0026gt;, Executor)其中FutureCallback是一個包含onSuccess(V),onFailure(Throwable)的接口:
Futures.addCallback(ListenableFuture, new FutureCallback\u0026lt;Object\u0026gt;() { public void onSuccess(Object result) { // do something on success } public void onFailure(Throwable thrown) { // do something on failure }});這也是一開始試驗的方案,確定好了異步化的思路,自然聯想到了增強版的Listenable Future,雖然在任務完成時可以回調函數通知,但是仍然是阻塞的,主線程仍然要等待異步線程完成任務通知。
Completable Future
Java8的CompletableFuture參考了Guava的ListenableFuture的思路,CompletableFuture能夠將回調放到與任務不同的線程中執行,也能將回調作為繼續執行的同步函數,在與任務相同的線程中執行。它避免了傳統回調最大的問題,那就是能夠將控制流分離到不同的事件處理器中。
CompletableFuture彌補了Future模式的缺點。在異步的任務完成后,需要用其結果繼續操作時,無需等待。可以直接通過thenAccept、thenApply、thenCompose等方式將前面異步處理的結果交給另外一個異步事件處理線程來處理。
CompletableFuture completableFuture = new CompletableFuture();completableFuture.whenComplete(new BiConsumer() { @Override public void accept(Object o, Object o2) { //handle complete }}); // complete the taskcompletableFuture.complete(new Object());//api methodcompletableFuture.thenApply(Function f); //api methodcompletableFuture.thenAccept(Consumer c); //api methodCompletableFuture 提出了CompletionStage的概念,代表異步計算過程中的某一個階段,一個階段完成以后可能會觸發另外一個階段。
一個階段的計算執行可以是一個Function,Consumer或者Runnable。比如:
stage.thenApply(x -\u0026gt; square(x)).thenAccept(x -\u0026gt; System.out.print(x)).thenRun(() -\u0026gt; System.out.println());一個階段的執行可能是被單個階段的完成觸發,也可能是由多個階段一起觸發。
與Guava ListenableFuture相比,CompletableFuture不僅可以在任務完成時注冊回調通知,而且可以指定任意線程,實現了真正的異步非阻塞。
Servlet 3.0
傳統Servlet 2.x web容器處理http請求時是為每一個請求分配一個線程,處理完請求再釋放線程,如果請求處理的比較慢或者請求過多,就可能達到線程池達到上限,這時候后續的用戶請求就會處于等待狀態或者超時,這里用戶請求和處理請求是一個線程,Servlet 3.0 開始提供了AsyncContext用來支持異步處理請求,主要是把請求線程和工作線程分開,將耗時的業務處理工作交給另外一個線程來完成。
@WebServlet(urlPatterns = \u0026quot;/servlet3\u0026quot;,asyncSupported = true)public class Servlet3 extends HttpServlet { public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { //在子線程中執行業務調用,并由其負責輸出響應,主線程退出 AsyncContext ctx = request.startAsync(); new Thread(new Executor(ctx)).start(); } public void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { doGet(request, response); }}class Executor implements Runnable { private AsyncContext ctx = null; public Executor(AsyncContext ctx){ this.ctx = ctx; } public void run(){ try { Thread.sleep(3000); ServletRequest request = ctx.getRequest(); ctx.dispatch(\u0026quot;/index.jsp\u0026quot;); ctx.complete(); } catch (Exception e) { e.printStackTrace(); } }}最終方案
最終選定Completable Future + Servlet 3.0的方案,前臺web接口層采用Serlvet 3.0,后臺服務層采用Completable Future。
驗證
優化前壓測數據:
圖1:在訪問聚合頁100并發情況下的數據,TPS值3235
【圖1】
圖2:在訪問聚合頁200并發情況下的數據,TPS值3322,在用戶并發量增加的時候,因依賴外部接口服務和原有的系統設計接口調用方法導致TPS基本不會隨并發量的增加而提高。
【圖2】
優化后壓測數據
在訪問聚合頁100并發情況下的數據,TPS值5869,相對于優化之前的TPS有明顯的提升。
【圖3】
在訪問聚合頁150并發情況下的數據TPS值8581,在提高并發量的時TPS有顯著的提高,說明優化后的效果很明顯,也證實了優化方案是可行的。
【圖4】
總結
利用異步化來提升系統性能是一個整體、全鏈路的工作,僅僅依靠業務上的異步化,或者服務層的異步化遠遠不夠,隨著不同技術方案的選擇及演進,對異步非阻塞模型有了更深入的了解之后,從前臺用戶請求到后端服務層處理,根據一整條鏈路的上每一層場景的不同,需要選取不同的異步化技術方案,才能達到系統整體性能提升的目的。
作者
葛蘇杰,現擔任蘇寧易購IT總部技術經理職位,從事多年的電商系統2C業務開發,對于高可用、高并發的分布式系統的JVM性能調優、SQL優化、Cache、NIO、NGINX等相關技術有豐富的經驗。
總結
以上是生活随笔為你收集整理的苏宁11.11:如何基于异步化打造会员任务平台?的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 什么才是真正的PUSHMAIL?
- 下一篇: 【POWERBI】GDP数据