蘇寧會員任務平台:基於異步化的性能優化實踐

尋夢新聞LINE@每日推播熱門推薦文章,趣聞不漏接❤️

加入LINE好友

蘇寧會員任務平臺:基於異步化的性能優化實踐

背景

蘇寧會員任務平台是覆蓋聚合電商、體育、金融、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 結果之間的依賴性。實際開發中,我們經常需要達成以下目的:

1. 將多個異步計算的結果合併成一個

2. 等待Future集合中的所有任務都完成

3. Future完成事件(即,任務完成以後觸發執行動作)

Future雖然可以做到獲取異步執行結果的需求,但是它沒有提供通知的機制,我們無法得知Future什麼時候完成。

要麼使用阻塞,在future.get()的地方等待future返回的結果,這時又變成同步操作。要麼使用isDone()輪詢地判斷Future是否完成,這樣會耗費CPU的資源。

Guava的Listenable Future對其做了改進,支持註冊一個任務執行結束後回調函數。

ListenableFuture<String> listenableFuture =

listeningExecutor.submit(new Callable<String>() {

@Override

public String call() throws Exception {

return “”;

}

});

Futures.addCallback(ListenableFuture<V>,FutureCallback<V>, Executor)

其中FutureCallback是一個包含onSuccess(V),onFailure(Throwable)的接口:

Futures.addCallback(ListenableFuture, new FutureCallback<Object>() {

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 task

completableFuture.complete(new Object());//api method

completableFuture.thenApply(Function f); //api method

completableFuture.thenAccept(Consumer c); //api method

CompletableFuture 提出了CompletionStage的概念,代表異步計算過程中的某一個階段,一個階段完成以後可能會觸發另外一個階段。

一個階段的計算執行可以是一個Function,Consumer或者Runnable。比如:

stage.thenApply(x -> square(x)).thenAccept(x -> System.out.print(x)).thenRun(() -> System.out.println());

一個階段的執行可能是被單個階段的完成觸發,也可能是由多個階段一起觸發。

與Guava ListenableFuture相比,CompletableFuture不僅可以在任務完成時註冊回調通知,而且可以指定任意線程,做到了真正的異步非阻塞。

Servlet 3.0:

蘇寧會員任務平臺:基於異步化的性能優化實踐

傳統Servlet 2.x web容器處理http請求時是為每一個請求分配一個線程,處理完請求再釋放線程,如果請求處理的比較慢或者請求過多,就可能達到線程池達到上限,這時候後續的用戶請求就會處於等待狀態或者超時,這里用戶請求和處理請求是一個線程,Servlet 3.0 開始提供了AsyncContext用來支持異步處理請求,主要是把請求線程和工作線程分開,將耗時的業務處理工作交給另外一個線程來完成。

@WebServlet(urlPatterns = “/servlet3”,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(“/index.jsp”);

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】

蘇寧會員任務平臺:基於異步化的性能優化實踐

總結:

利用異步化來提升系統性能是一個整體、全鏈路的工作,僅僅依靠業務上的異步化,或者服務層的異步化遠遠不夠,隨著不同技術方案的選擇及演進,對異步非阻塞模型有了更深入的了解之後,從前台用戶請求到後端服務層處理,根據一整條鏈路的上每一層場景的不同,需要選取不同的異步化技術方案,才能達到系統整體性能提升的目的。

About 尋夢園
尋夢園是台灣最大的聊天室及交友社群網站。 致力於發展能夠讓會員們彼此互動、盡情分享自我的平台。 擁有數百間不同的聊天室 ,讓您隨時隨地都能找到志同道合的好友!