蘇寧會員任務平臺:基於非同步化的效能優化實踐
背景
蘇寧會員任務平臺是覆蓋聚合電商、體育、金融、PPTV、直播、紅孩子等各個業態,平臺會實時獲取使用者的畫像資訊來計算使用者在客群中的分佈及畫像屬性,從而實時判斷使用者是否滿足相關場景下任務,若滿足相關場景以後可以領取任務下所有獎項;任務型別包含了訂單紅包、母嬰、Super會員、直播、雙籤、金融升級存等等。在大促特別是雙十一期間,任務中心產品對於各個業態的引流,會員的留存及轉化來說是一個重要的工具。
問題
因任務平臺業務邏輯複雜、實時性要求高,涉及多個外圍系統服務及資料呼叫;一期系統上線後部分功能遇到效能問題,例如聚合頁開啟時間過長,首先聚合頁上要展示使用者能看到的任務列表,以及當前使用者是否達到領取條件,其次每個任務需要展示的狀態依賴於後臺多種資訊的聚合,包括不在有效時間範圍內、當前時段庫存、可供領取的總庫存、領取頻次等。複雜邏輯和實時要求導致TPS在上線壓測的時候沒有能夠達到一個理想預期效果。
即將到來的”雙十一”流量高峰, 可以預見會使得超過現有的任務系統的TPS的峰值, 從而導致任務系統在”雙十一”的場景下很容易觸碰到效能瓶頸,影響使用者體驗;因此需要對蘇寧任務平臺的核心功能做效能優化, 提升實時性複雜業務邏輯場景下的效能, 以便於應對任務平臺的流量暴漲以及雙十一流量高峰。
定位
現有的每個任務可能依賴於多個異構系統的服務或者資料,例如直播任務及訂單任務來自於不同的系統的服務,並且有些場景是基於外圍系統的資料進行邏輯計算,有些則是通過服務介面呼叫的方式。
程式碼示例:
public ResultDTO checkAndGetInfo() { A a = getA(); B b = getB(); C c = getC(); ...... ResultDTO result = computeResult(a, b, c ...); return resultDTO; }
由於頁面實時性要求高,邏輯複雜,對於某個任務是否展示需要呼叫多個外圍介面,響應時間不可控,理論上根據任務的複雜性可能涉及多個客群,呼叫次數及響應時間不可控。效能主要在響應時間不可控。
某個任務狀態要呼叫多個本地介面或者外圍介面。
主要思路:非同步,快取,執行緒池
針對以上定位到位問題,考慮到實時呼叫外圍介面的方案會導致響應時間不可控,採用IO/">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雖然可以實現獲取非同步執行結果的需求,但是它沒有提供通知的機制,我們無法得知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 ""; } });
其中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
圖2:在訪問聚合頁200併發情況下的資料,TPS值3322,在使用者併發量增加的時候,因依賴外部介面服務和原有的系統設計介面呼叫方法導致TPS基本不會隨併發量的增加而提高。
優化後壓測資料:
在訪問聚合頁100併發情況下的資料,TPS值5869,相對於優化之前的TPS有明顯的提升。
在訪問聚合頁150併發情況下的資料TPS值8581,在提高併發量的時TPS有顯著的提高,說明優化後的效果很明顯,也證實了優化方案是可行的。
總結
利用非同步化來提升系統性能是一個整體、全鏈路的工作,僅僅依靠業務上的非同步化,或者服務層的非同步化遠遠不夠,隨著不同技術方案的選擇及演進,對非同步非阻塞模型有了更深入的瞭解之後,從前臺使用者請求到後端服務層處理,根據一整條鏈路的上每一層場景的不同,需要選取不同的非同步化技術方案,才能達到系統整體效能提升的目的。
作者簡介
葛蘇傑,現擔任蘇寧易購IT總部技術經理職位,從事多年的電商系統2C業務開發,對於高可用、高併發的分散式系統的JVM效能調優、SQL優化、Cache、NIO、NGINX等相關技術有豐富的經驗。