天天看點

自研Java協程在騰訊的生産實踐

自研Java協程在騰訊的生産實踐

導讀 / Introduction

本文是今年QCon java專場《Java協程在騰訊的生産實踐》主題分享,分享團隊為騰訊大資料JVM團隊。本文主要介紹協程的産生背景、java協程的發展曆程、社群官方協程Project Loom的設計與實作,以及騰訊自研協程Kona Fiber的産生背景、設計與實作、性能測試和業務實踐。

1. 協程産生的背景

Kona

1.1 線程模型

最經典的程式設計模型是線程模型,它是作業系統層面對cpu的抽象。由于線程模型是一種同步程式設計模型,它直覺、易于了解,是以使用線程模型的開發效率高。但是對于IO密集型的程式,由于每次IO操作都需要Blocking目前線程,是以會産生一次線程切換,線程切換需要在使用者态和核心态之間切換,且線程切換需要儲存目前線程的執行上下文,一次線程切換的開銷在10微秒量級。是以,對于IO密集型程式,cpu很大一部分都用于線程切換,導緻cpu的使用率不高。

線程模型的第二個問題是,對于IO密集且高并發的程式,如果不采用異步程式設計模型,通常是一個線程對應一個并發(因為假設一個線程去做資料庫通路操作,線程就被阻塞了,就不能執行其他任務了,隻能用另一個線程去執行)。是以對于高并發的程式來說,需要建立大量線程。作業系統為了相容各種各樣的程式設計語言、執行邏輯,是以作業系統線程預留的棧記憶體通常較大,一般是8M。由于線程占用的記憶體較大,即使不考慮cpu使用率的問題,一台機器也很難建立太多線程(隻考慮線程棧的記憶體開銷,1萬個線程就需要80G記憶體),是以很難在不使用異步程式設計架構的情況下,僅靠線程模型支援IO密集型+高并發程式。

1.2 異步模型

異步程式設計模型是一種程式設計語言架構的抽象,它可以彌補線程模型對于IO密集型+高并發程式支援的短闆。它通過複用一個線程,例如線程在做io操作導緻阻塞之前,通過回調函數調用到另一個邏輯單元,完成類似線程切換的操作;異步模型的執行效率很高,因為相比線程切換,它直接調用了一個回調函數;但是它的開發門檻較高,需要程式員自己了解哪裡可能産生io操作,哪裡需要調用回調函數,如何定期檢查io操作是否做完;另外,由于線程的調用棧由一些邏輯上并不相關的子產品組成,是以一旦出現crash之類的問題,調用棧比較難以了解,維護成本也較高。

1.3 協程

圖1.1展示了線程模型和異步模型在生産效率和執行效率的對比圖。可以看到,線程模型的生産效率是最高的,同時它對于IO密集型+高并發程式的執行效率較差。異步模型正好相反,它的生産效率較差,但是如果實作的很完美,它的執行效率是最高的。

協程的出現,是為了平衡線程模型和異步模型的生産效率和執行效率;首先,協程可以讓程式員按照線程模型去編寫同步代碼,同時,盡可能降低線程切換的開銷。

自研Java協程在騰訊的生産實踐

圖1.1

2. Java協程的發展曆程

Kona

上面分析了協程産生的背景,那麼Java協程的現狀是怎樣的呢?

首先,由于Java生态豐富的異步架構,緩解了協程的緊迫性,使用者可以用異步架構去解決IO密集型+高并發的程式。

如圖2.1所示,列出了Java協程的發展曆程。

自研Java協程在騰訊的生産實踐

圖2.1

2.1 JKU

Java協程最早是JKU發表的論文+提供的patch。JKU的協程是一種有棧的協程,即每個協程都有自己獨立的調用棧。不同于作業系統線程,由于JKU的協程隻需要考慮java代碼的執行,根據java代碼執行時的特點,通常隻需要較小的棧就可以滿足需要,通常JKU的協程隻需要不超過256K的棧。

2.2 Quasar/Kotlin

Quasar和Kotlin是之後出現的方案,它們都不需要修改java虛拟機,隻需要修改上層java代碼。它們都是無棧協程,所謂無棧協程,是指協程在suspend狀态時不需要儲存調用棧。那麼如果協程切換出去以後,沒有儲存調用棧,下次恢複執行時,如何讀取調用關系呢?Quasar和Kotlin在切換時,會回溯目前協程的調用棧,然後根據這個調用棧生成一個狀态機,下次恢複執行時,根據這個狀态機恢複執行狀态;無棧協程通常不能在任意點切換,隻能在被标記的函數切換,因為隻有被标記的函數才能生成對應的狀态機,Quasar需要加一個@Suspendable的annotation标記可以切換的函數,Kotlin需要在函數定義時加上suspend關鍵字标記可以切換的函數。

2.3 Project Loom

Project Loom是Openjdk社群推出的官方協程實作,它從立項到今天已經超過3.5年,目前已經包含27個Committer,超過180個author,3200+commits。

圖2.1列出了Loom一些重要的時間點。Loom在17年底立項,18年初正式對外推出。19年7月,它釋出了第一個EA(Early Access)Build,這時候它的實作還是一個Fiber類。在19年10月的時候,它的接口發生了一個較大的變動,将Fiber類去掉,新增了VirtualThread類,作為Thread類的子類,相容所有Thread的操作。這時候它的基本思想已經比較清晰了,就是協程是線程的一個子類,支援線程的所有操作,使用者可以完全按照線程的方式使用協程。

Loom作為Openjdk的官方實作,它的目标是提供一個Java協程的系統解決方案,相容已有Java規範、JVM特性(例如ZGC、jvmti、jfr等),最終目标是對整個Java生态的全面相容。對Java生态的全面相容既是Loom的優勢,同時也是它的挑戰。因為Java已經發展了20多年,Loom要在最底層新加入一個協程的概念,需要适配的東西非常多,例如龐大而複雜的标準類庫,大量JVM特性。是以,截至目前,Loom仍然有非常多的事情要做,目前還沒有達到一個真正可用的狀态,圖2.1最右邊的部分是我們的一個推測,我們猜測到jdk18或jdk19的時候,Loom或許可以加上一個Experimental标記,提供給使用者使用。

3. Loom的實作架構

Kona

圖3.1列出了Loom引入的一些新的概念,其中最重要的概念就是Virtual Thread,也就是協程。Loom的官方表述為:“Virtual threads are just threads that are scheduled by the Java virtual machine rather than the operating system”。站在使用者的角度,可以把協程了解為線程,按照線程的方式使用,這也是Loom最重要的設計。

自研Java協程在騰訊的生産實踐

圖3.1

除了Virtual Thread以外,Loom還新增了Scope Variable和Structured Concurrency的概念,後文會對它們分别進行介紹。

3.1 Loom的基本原理

如圖3.2所示,展示了一個協程的生命周期。最初,執行VirtualThread.start()方法建立一個協程,等待被排程;當協程被排程執行以後,開始執行使用者指定的業務代碼,在執行代碼的過程中可能會去通路資料庫/通路網絡,這些IO操作最後都會調用到底層的一個Park操作。Park可以了解為協程讓出執行權限,且目前不能被排程執行。IO結束後會調用Unpark,Unpark之後協程可以被排程執行。在Park操作時,需要執行一個freeze操作,這個操作主要是将目前協程的執行狀态,也就是調用棧儲存起來。當協程Unpark且被排程時,會執行一個thaw操作,它是freeze的對稱操作,主要是把之前freeze儲存的調用棧恢複到執行線程上。

自研Java協程在騰訊的生産實踐

圖3.2

圖3.3展示了freeze和thaw具體完成的内容。首先看freeze操作,調用棧的上半部分從ForkJoinWorkerThread.run到Continuation.enterSpecial都是類庫裡面的調用。從A開始才是使用者的業務代碼,假設使用者調用了函數A,函數A又調用了函數B,函數B又調用了函數C,之後函數C有一個資料庫通路操作,導緻協程讓出執行權限(執行了一個yield操作),接下來調用到freeze儲存目前協程的執行狀态(調用棧);這時會首先産生一個stack walk的動作,Loom會從目前調用棧的最底層逐漸向上周遊,一直周遊到Continuation.enterSpecial為止。将所有周遊到的棧中的oop都儲存一個refStack中,這樣做可以保證協程的棧在GC時不作為root。

通常,thread的棧都會被GC當作root來處理,且處理root時通常是需要stop-the-world的,如果協程的棧和線程的棧一樣,也被當作root處理,那麼由于協程可以支援到幾百萬甚至上千萬的量級,這會導緻stop-the-world的時間變長。是以stack walk和refStack的設計(将協程棧上的内容拷貝到一個refStack中),可以在concurrent階段處理協程的棧,不會由于協程數量的增多影響GC的暫停時間。

自研Java協程在騰訊的生産實踐

圖3.3

另一方面,每次freeze時,Loom都會将協程目前的執行棧拷貝到java heap中,即本例中的A、B、C和yield被單獨拷貝出來,這樣做可以保證協程的棧的記憶體真正做到按需使用。

當協程被Unpark且被排程時,協程執行thaw操作。thaw主要是将之前儲存在java heap裡的協程棧恢複到執行棧上,Loom在這裡引入了lazy copy的優化。所謂lazy copy,是指Loom通過大量的profiling,發現大部分程式在io操作以後,還會緊跟着産生又一次io。具體到目前的例子,函數C在觸發一次io操作以後,很可能還會産生一次io操作,而不是執行完畢傳回到函數B,然後函數B也執行完畢傳回到函數A。這樣的話,在每次thaw的時候,就沒必要把所有的調用棧都拷貝回去,而隻需要拷貝一部分,然後在拷貝的調用棧尾部加一個return barrier,如果函數确實傳回到return barrier,可以通過return barrier觸發繼續拷貝調用棧的動作;這樣每次freeze和thaw的時候,都隻需要拷貝一小部分内容,大大提升了切換性能。

在freeze和thaw的時候間隔裡,有可能觸發過GC導緻oop被relocate。是以thaw的時候需要執行一個restore oop的動作,確定不會出現記憶體通路異常。

3.2 VirtualThread的使用

  • 排程器:

協程可以了解為一種使用者态線程,在使用者态執行時,由于不能直接通路CPU,是以隻能用線程代替cpu。是以,協程需要一個使用者态的排程器,排程器中包含實體線程,協程被排程器放在實體線程上執行。如果使用者不指定排程器,預設排程器是ForkJoinPool,Loom針對ForkJoinPool做了很多關于協程排程的優化。

  • 直接建立VirtualThread:
Thread thread = Thread.ofVirtual().start(() -> System.out.println("Hello"));thread.join()           

複制

相比于建立Thread,隻需要增加一個ofVirtual()的函數調用,建立的就是協程。

如果使用者想要自己寫一個針對自己業務模型更有效的排程器,可以通過調用scheduler()函數指定排程器,代碼如下所示:

Thread thread = Thread.ofVirtual().scheduler(CUSTOM_SCHEDULER).start(() -> System.out.println("Hello"));thread.join()           

複制

  • 建立協程池

除了直接使用協程以外,還可以通過建立協程池的方式使用協程,例如建立一個包含10000個協程的協程池,将任務送出給協程池執行。對應代碼如下:

ThreadFactory factoryif (UseFiber == false) {    factory = Thread.ofPlatform().factory();} else {    factory = Thread.ofVirtual().factory();}ExecutorService e = Executors.newFixedThreadPool(ThreadCount, factory);           

複制

在建立ThreadPool時,如果傳入的factory是ofPlatform()的factory,對應的就是線程池,如果是ofVirtual()的factory,對應的就是協程池。

3.3 VirtualThread Pin

Pin的狀态指的是VirtualThread在freeze時無法讓出Carrier Thread(協程執行時挂載的實體線程)。主要有兩種情況下會導緻Pin:

  • VirtualThread的調用棧包含JNI frame。因為JNI調用的實作是C++代碼,可以做的事情非常多,例如它可以儲存目前Carrier Thread的Thread ID,如果這時切換出去,那麼下一次執行時,如果另一個Carrier Thread來執行這個協程,将會産生邏輯錯誤(Carrier Thread的ID不一緻);
  • VirtualThread持有synchronized鎖。這是java早期鎖的實作帶來的限制,因為java的synchronized鎖的owner是目前的Carrier Thread,如果一個協程持有一把鎖,但是鎖的owner被認為是目前的Carrier Thread,那麼如果接下來這個Carrier Thread又去執行另一個協程,可能另一個協程也被認為擁有了鎖,這可能導緻同步的語義發生混亂,産生各種錯誤。

偶爾出現的Pin并不是一個很嚴重的問題,隻要排程器中始終有實體線程負責執行協程就可以。如果排程器中所有的實體線程都被Pin住,可能會對吞吐量産生較大影響。Loom針對預設的排程器ForkJoinPool做了優化,如果發現所有的實體線程都被Pin住,就會額外建立一些實體線程,保證協程的執行不受太大影響。使用者如果想要徹底消除Pin,可以按照如圖3.4的方式,通過-Djdk.tracePinnedThreads選項定位産生Pin的調用棧。

自研Java協程在騰訊的生産實踐

圖3.4

3.4 Structured Concurrency

結構化并發的設計初衷是友善管理協程的生命周期。結構化并發的基本想法是,調用一個method A,通常并不關心method A是由一個協程按部就班的執行,還是将method A劃分為100個子任務,由100個協程同時執行。下面的代碼是使用結構化并發的一個小例子:

ThreadFactory factory = Thread.ofVirtual().factory();try (ExecutorService executor = Executors.newThreadExecutor(factory)) {    executor.submit(task1);    executor.submit(task2);}           

複制

目前執行的協程會在try代碼段結束的位置等待,直到try對應的代碼段執行結束,就好像try代碼段中的内容是由目前協程按部就班的執行一樣。結構化并發本質上是一種文法糖,友善将一個大任務劃分為多個小任務,由多個協程同時執行。有了結構化并發,很自然的會産生結構化的中斷和結構化的異常處理。結構化的中斷,是指try代碼段的逾時管理,例如使用者想要try中的task1和task2在30秒内完成,如果30秒内沒有完成則産生一個中斷,結束執行。結構化的異常處理,是指try中的内容被劃分為2個子任務,如果子任務産生異常,對于異常處理來說,可以做到和一個協程順序執行的做法相同。

3.5 Scope Variable

Scope Variable是Loom社群增加協程以後,對原有ThreadLocal的重新思考。Scope Variable可以了解為輕量的、結構化的Thread Local。由于Thread Local是全局有效的、非結構化的資料,是以一旦修改就會覆寫掉之前的值。Scope Variable是一種結構化的Thread Local,它的作用範圍僅限一個Code Blob,下面的代碼是Scope Variable的一個測試用例,根據assert資訊可以看出Scope Variable如果在Code Blob之外就會自動失效。

public void testRunWithBinding6() {    ScopeLocal<String> name = ScopeLocal.inheritableForType(String.class);    ScopeLocal.where(name, "fred", () -> {        assertTrue(name.isBound());        assertTrue("fred".equals(name.get()));
        ScopeLocal.where(name, "joe", () -> {            assertTrue(name.isBound());            assertTrue("joe".equals(name.get()));            ensureInherited(name);        });
        assertTrue(name.isBound());        assertTrue("fred".equals(name.get()));        ensureInherited(name);    });}           

複制

Scope Variable的另一個好處是,它可以和結構化并發巧妙結合起來。結構化并發通常會将一個大任務劃分為多個子任務,如果子任務的個數非常多,例如一個大任務被劃分成1000個子任務,那麼如果采用Inherit Thread Local來複制父協程的Thread Local到子協程上,由于Thread Local是mutable的,是以子協程也隻能拷貝父協程的Thread Local。在子協程非常多的情況下,這種拷貝開銷很大。Scope Variable為了應對這種情況,僅僅在子協程上增加一個父協程的引用,而不需要額外的拷貝開銷。

4. 為什麼需要Kona Fiber?

Kona

通過前面的分析可以看出,Loom的設計是非常完善的,對各種情況都有充分的考慮,使用者隻需要等待Loom成熟以後使用Loom即可。那麼,為什麼騰訊還需要自研一個協程Kona Fiber呢?

我們通過和大量業務的溝通,分析出目前業務對協程的三個主要需求:

  • 在JDK8/JDK11上可用:目前大量業務還是基于JDK8/JDK11進行開發的,而Loom作為Openjdk社群的前沿特性,基于社群的最前沿版本進行開發,讓很多還在使用舊版本JDK的業務無法使用;
  • 代碼的可演進性:使用者希望基于JDK8/JDK11修改的協程代碼,未來更新到社群最新版本的時候,能夠在不修改代碼的情況,切換到社群官方的協程Loom;
  • 切換性能的需求:目前Loom的實作,由于有stack walk和stack copy的操作,導緻切換效率還有一定提升空間,使用者希望有更好的切換效率。

基于這三點需求,我們設計并實作了Kona Fiber。

5. Kona Fiber的實作

Kona

5.1 Kona Fiber與Loom的異同

圖5.1展示了Kona Fiber和Loom的共性以及差異點,中間黃色的部分是Kona Fiber和Loom都支援的,兩邊的藍色部分是Kona Fiber和Loom的差異點。

自研Java協程在騰訊的生産實踐

圖5.1

首先看共性的部分:

  • Loom最重要的設計是VirtualThread,對于Virtual Thread的接口,KonaFiber是全部支援的,使用者可以用相同的一套接口使用Loom和Kona Fiber。
  • Loom針對ForkJoinPool做了很多優化,包括前面提到的自動擴充Carrier Thread,Kona Fiber也對這一部分優化進行了移植和适配。
  • 對于Test Case,由于Kona Fiber的接口與Loom接口的一緻性,理想情況下可以不加任何修改,直接運作Loom的Test Case。當然,實際運作Loom的Test Case時,仍然需要少量修改,這些修改主要是由于大版本的差異。因為Loom是基于最新版本(jdk18),而Kona Fiber是基于jdk8,如果Loom的Test Case包含了jdk8不支援的特性,例如jdk8不支援var的變量定義,那麼仍需要少量的适配。目前,Loom的大部分Test Case我們已經移植到了Kona Fiber,對應的檔案目錄(相對于jdk的根目錄)為jdk/test/java/lang/VirtualThread/loom。

對于差異的部分,首先是性能方面,由于Kona Fiber是stackful的方案,在切換性能上會優于Loom,在記憶體開銷上也會比Loom多一些,這部分的詳細資料會在下一章進行介紹。其次,由于Loom引入了一些新的概念,這些概念雖然可以讓程式員更好的使用協程,但是這些概念的成熟以及被程式員廣泛接受,都需要一定的時間。未來如果使用者對Scope Variable、Structure Concurrency有共性需求,Kona Fiber也會慮引入這些概念,目前還是以開箱即用為目标,暫不支援這些新特性。

5.2 Kona Fiber的實作架構

如圖5.2所示,展示了一個Kona Fiber協程的生命周期。

自研Java協程在騰訊的生産實踐

圖5.2

第一個步驟和第二個步驟與Loom相同,即協程被建立、協程被排程執行。當協程真正被排程執行時,才會在runtime建立協程的資料結構以及協程的棧。建立成功以後,傳回到java層執行使用者代碼。接下來如果在使用者代碼中遇到IO操作(例如資料庫通路),會導緻協程被Park,是以會進入到runtime,這時在runtime會執行一個stack switch的過程,切換到另一個協程執行。IO結束時(例如資料庫通路完成),會喚醒協程繼續執行。

自研Java協程在騰訊的生産實踐

圖5.3

如圖5.3所示,展示了Kona Fiber在stack switch時的具體實作。協程可以了解為使用者态的線程,又因為Kona Fiber的每個協程都有一個獨立的棧,是以協程切換本質上隻需要切換rsp和rbp指針即可。因為,Kona Fiber的切換開銷相比于Loom的stack walk和stack copy要小一些,在理論上會有一個更好的性能。接下來會有詳細的資料對Kona Fiber和Loom進行比較。

6. Kona Fiber的性能資料

Kona

圖6.1展示了Kona Fiber、Loom和JKU的切換性能資料,橫軸代表協程個數,縱軸表示每秒切換的次數。

自研Java協程在騰訊的生産實踐

圖6.1

可以看到,Kona Fiber的性能優于Loom,且當協程個數較多時,JKU的性能也好于Loom。前文叙述過,由于Loom在切換時需要做stack copy和stack walk,是以導緻切換性能會差一點。

自研Java協程在騰訊的生産實踐

圖6.2

圖6.2展示了Kona Fiber、Loom和JKU在建立30000個協程時的記憶體開銷,無論是runtime直接使用的實體記憶體,還是JavaHeap的記憶體使用,Loom都是最優的(占用記憶體最少),當然這也得益于Loom的stack copy的實作,可以做到對記憶體真正的按需使用。由于Kona Fiber相容了Loom Pin的概念,是以相比JKU去掉了很多不需要的資料結構,在記憶體使用上優于JKU。從記憶體使用總量上,雖然Kona Fiber占用的記憶體多于Loom,但是30000個協程總體占用不到1G記憶體(runtime占用的記憶體加上Java Heap占用的記憶體),對于大多數業務也是可接受的。

自研Java協程在騰訊的生産實踐

圖6.3

自研Java協程在騰訊的生産實踐

圖6.4

圖6.3、圖6.4分别展示了Kona Fiber和Loom在使用預設排程器ForkJoinPool時的排程性能。其中橫軸表示排程器中Carrier Thread的個數,縱軸表示排程器每秒完成切換操作的次數,不同顔色的線代表不同個數的協程。可以看到,Loom在協程個數較多時,性能抖動較明顯。Kona Fiber在不同協程個數的情況下,性能表現都很穩定。這種差異的産生,可能還是與Loom在切換時要做stack walk和stack copy有關。

注:

1. 所有關于Loom的性能資料,都是基于2020年9月Loom的代碼。

2. 所有的性能測試用例都可以在開源代碼中擷取,對應的目錄(相對于jdk的根目錄)為demo/fiber

7. Kona Fiber的業務落地

DATA

7.1 業務協程化改造

如果一個業務想要從線程切換到協程,通常需要以下三個步驟:

1.将建立線程改為建立協程;将線程池改為協程池。第一步非常簡單,隻需要将線程的使用替換為協程的使用(按照3.2小節“Virtual Thread的使用”進行替換即可)

2.将部分同步接口替換為異步架構。目前Kona Fiber仍有一些接口不支援,例如Socket、JDBC相關的接口。使用這些接口,将導緻協程不能正常切換(Blocking在native代碼或者Pin住),協程會退化成線程,那麼協程的優勢也就不存在了。目前Kona Fiber對網絡和資料庫的一些原生接口不支援,好在一般網絡和資料庫操作都可以找到對應的異步架構,例如網絡操作有Netty,資料庫有異步的redis。圖7.1以替換Netty為例,介紹了如何利用異步架構有效的使用協程。首先,建立一個CompletableFuture,然後将任務送出給Netty,接下來目前協程調用Future.get()等待Netty執行完成。在Netty執行完成的回調函數裡調用Future.complete(),這樣協程就可以恢複執行。

自研Java協程在騰訊的生産實踐

圖7.1

3. 協程性能優化:Pin的解決。3.3小節介紹過Pin,Pin雖然不會導緻整個系統死鎖,但是頻繁的Pin仍然會顯著降低業務的吞吐量。對于Pin的解決,主要是兩個方面,即産生Pin的兩個原因:synchronized鎖和native frame。第一步,在調試階段開啟-Djdk.tracePinnedThreads,這樣可以找到所有引起Pin的調用棧。如果Pin是由于業務代碼使用synchronized鎖引起的,那麼隻需要将synchronized鎖替換成ReentrantLock即可;如果Pin是由于包含native frame或者第三方代碼包含synchronized鎖引起的,那麼隻能通過将任務送出到一個獨立線程池的方法來解決,這樣可以保證協程的執行不受影響。

7.2 企點開放平台-統一推送服務

企點開放平台-統一推送服務是騰訊公司的業務,天然存在高并發的需求。最初業務方嘗試使用WebFlux響應式程式設計,但由于業務方存在較多第三方外包人員,且WebFlux的開發、維護難度較高,導緻業務方放棄使用WebFlux。後來,在了解了騰訊内部自研的Kona Fiber後,業務方果斷選擇嘗試切換Kona Fiber。

業務方針對Kona Fiber的适配,主要是通過nio+Future替換bio,将所有阻塞操作替換為nio,當阻塞操作完成時執行Future.complete()喚醒協程;業務方回報的替換協程的工作量為:三人天、200+行的代碼适配、測試工作。最終相比Servlet的線程方案,系統整體吞吐量提升了60%。

下面的代碼是業務方修改的代碼片段,分别表示建立一個協程池,以及通過加一個annotation讓函數運作在協程池上:

@Bean(name = "asyncExecutor")public Executor asyncExecutor() {  ThreadFactory threadFactory = Thread.ofVirtual().factory();  return Executors.newFixedThreadPool(fiberCount, threadFactory);}

@Async("asyncExecutor")public CompletableFuture<ResponseEntity<ResponseDTO>> direct() {  ···}           

複制

7.3 SLG遊戲背景服務

SLG(Simulation Game)遊戲主要是一些政策類遊戲,這類遊戲不像一些對戰類遊戲對實時性要求非常高。政策類遊戲的特點是邏輯複雜,且遊戲業務通常都有高并發、高性能需求。業務方基于Kona Fiber定制了一種單并發的協程排程器。

如圖7.2所示,多個協程隻運作在一個playerService Thread上,由于Carrier Thread隻有一個線程,是以同一時刻隻有一個運作在playerService Thread的協程可以執行;這樣做可以省去很多同步操作,提高開發者的程式設計效率。替換了Kona Fiber以後,系統的整體吞吐量也相比線程方案提高了35%。

自研Java協程在騰訊的生産實踐

圖7.2

右側的battleService Thread存在通路資料庫請求,業務方使用的是騰訊自研的Tcaplus資料庫,為了避免協程退化成線程,業務方将資料庫操作送出到一個獨立的線程池來執行。

7.4 trpc-java

trpc是騰訊公司自研的高性能、新老架構易互通、便于業務測試的RPC架構。一些使用trpc-java的使用者對高并發+IO密集型程式有需求。在協程出現以前,他們都隻能通過trpc-java提供的異步架構解決性能問題。異步架構雖然可以解決高并發+IO密集型程式的性能問題,但是由于它對開發者要求較高,很多時候使用者一不小心就會将異步代碼寫成同步代碼,導緻性能下降。

目前,trpc-java已經結合Kona Fiber推出了trpc-java協程版本,使用者可以按照編寫同步代碼的方式,獲得異步代碼的性能。目前已有大資料特征中台業務、trpc-網關業務正在适配協程。

8. Future Plan

Kona

  1. Kona Fiber在Kona8上的源碼github連結:

    https://github.com/Tencent/TencentKona-8/tree/KonaFiber

  2. Kona Fiber在Kona11上的源碼github連結:

    https://github.com/Tencent/TencentKona-11/tree/KonaFiber

  3. 持續跟進Loom社群,将Loom的優化移植到Kona Fiber。

參考文獻

  1. https://wiki.openjdk.java.net/display/loom/Getting+started
  2. https://wiki.openjdk.java.net/display/loom/Troubleshooting
  3. http://cr.openjdk.java.net/~rpressler/loom/loom/sol1_part2.html

傳送門

Kona 8對外開源版本,歡迎star:

https://github.com/Tencent/TencentKona-8

Kona11對外開源版本,歡迎star:

https://github.com/Tencent/TencentKona-11

往期精選

自研Java協程在騰訊的生産實踐
自研Java協程在騰訊的生産實踐
自研Java協程在騰訊的生産實踐

- 标題圖來源:Pexels -

自研Java協程在騰訊的生産實踐

掃碼關注 | 即刻了解騰訊大資料技術動态