為什么我們選擇Java開(kāi)發(fā)高頻交易系統(tǒng)?
過(guò)去 14 年,我們一直用 Java 開(kāi)發(fā)外匯算法交易系統(tǒng),并使用了很棒但價(jià)格實(shí)惠的硬件。這一切是怎樣實(shí)現(xiàn)的?
在高頻交易領(lǐng)域,自動(dòng)化應(yīng)用程序每天需要處理數(shù)億個(gè)市場(chǎng)交易信號(hào),并在全球各交易所之間發(fā)送成千上萬(wàn)的訂單。
為了保持競(jìng)爭(zhēng)力,響應(yīng)時(shí)間必須始終保持在微秒級(jí),特別是在發(fā)生類似“黑天鵝”事件的異常高峰期。
在一個(gè)典型的架構(gòu)中,金融市場(chǎng)的交易信號(hào)被轉(zhuǎn)換成內(nèi)部的市場(chǎng)數(shù)據(jù)格式 (使用各種協(xié)議,如 TCP/IP、UDP 組播和多種格式,如二進(jìn)制、SBE、JSON、FIX 等)。
這些規(guī)范化的消息被發(fā)送到算法服務(wù)器、統(tǒng)計(jì)引擎、用戶界面、日志服務(wù)器和各種類型的數(shù)據(jù)庫(kù) (內(nèi)存數(shù)據(jù)庫(kù)、物理數(shù)據(jù)庫(kù)、分布式數(shù)據(jù)庫(kù))。
這條路徑上的任何一個(gè)延遲都有可能帶來(lái)嚴(yán)重后果(比如基于舊價(jià)格做出戰(zhàn)略決策或訂單到達(dá)交易市場(chǎng)的時(shí)間太遲),并為此付出慘重代價(jià)。
為了加快至關(guān)重要的幾微秒,大多數(shù)券商投入了昂貴的硬件:服務(wù)器組配備了超頻液冷的 CPU(在 2020 年,你可以買到單臺(tái)配備了 56 核 5.6 GHz CPU 和 1 TB 內(nèi)存的服務(wù)器)、靠近交易所的數(shù)據(jù)中心、納秒級(jí)高端網(wǎng)絡(luò)交換機(jī)、海底專線 (Hibernian Express 是一家主要的供應(yīng)商),甚至是微波網(wǎng)絡(luò)。
我們經(jīng)??吹礁叨榷ㄖ频目梢岳@過(guò)操作系統(tǒng)的 Linux 內(nèi)核,數(shù)據(jù)可以直接從網(wǎng)卡“跳轉(zhuǎn)”到應(yīng)用程序、IPC(進(jìn)程間通信),甚至是 FPGA(可編程單用途芯片)。
在編程語(yǔ)言方面,C++ 似乎是服務(wù)器端應(yīng)用程序的天然競(jìng)爭(zhēng)者:它速度快,與機(jī)器碼非常接近,而且一旦針對(duì)目標(biāo)平臺(tái)進(jìn)行編譯,就可以提供恒定的處理時(shí)間。
但是,我們做了一個(gè)不一樣的選擇。
在過(guò)去的 14 年里,我們一直在用 Java 開(kāi)發(fā)外匯算法交易系統(tǒng),并使用了很棒但價(jià)格實(shí)惠的硬件。
由于團(tuán)隊(duì)規(guī)模小,資源有限,技術(shù)能力強(qiáng)的開(kāi)發(fā)人員難找,所以使用 Java 意味著我們可以快速地改進(jìn)軟件功能,因?yàn)?Java 生態(tài)系統(tǒng)比 C 語(yǔ)言生態(tài)系統(tǒng)的發(fā)布速度更快。上午討論功能改進(jìn),下午就可以實(shí)現(xiàn)、測(cè)試并發(fā)布到生產(chǎn)環(huán)境。
與那些需要幾周甚至幾個(gè)月才能發(fā)布更新的大公司相比,這是一個(gè)關(guān)鍵的優(yōu)勢(shì)。在高頻交易領(lǐng)域,一個(gè)漏洞可以在幾秒鐘內(nèi)抹掉一整年的利潤(rùn),所以我們不打算在質(zhì)量上做任何妥協(xié)。我們搭建了一個(gè)嚴(yán)格的敏捷開(kāi)發(fā)環(huán)境,包括 Jenkins、Maven、單元測(cè)試、夜晚構(gòu)建和 Jira,使用了很多開(kāi)源庫(kù)和項(xiàng)目。
使用 Java,開(kāi)發(fā)人員可以專注于直觀的面向?qū)ο髽I(yè)務(wù)邏輯,而不是浪費(fèi)時(shí)間去調(diào)試一些晦澀的內(nèi)存核心轉(zhuǎn)儲(chǔ)或管理 C++ 指針。而且,由于 Java 強(qiáng)大的內(nèi)存管理能力,即使是初級(jí)程序員也可以在第一天加入項(xiàng)目時(shí)為系統(tǒng)帶來(lái)價(jià)值,而且風(fēng)險(xiǎn)很小。
有了良好的設(shè)計(jì)模式和干凈的編碼習(xí)慣,Java 的速度可與 C++ 相媲美。
例如,Java 會(huì)優(yōu)化和編譯在應(yīng)用程序運(yùn)行期間觀察到的最佳路徑,但 C++ 會(huì)預(yù)先編譯所有東西,因此即使未被使用的方法也會(huì)成為可執(zhí)行二進(jìn)制文件的一部分。
但是,Java 有一個(gè)問(wèn)題,它讓 Java 成為一門強(qiáng)大且令人喜愛(ài)的編程語(yǔ)言,但也成了 Java 的缺點(diǎn)之一 (至少對(duì)于微秒級(jí)應(yīng)用程序來(lái)說(shuō))——Java 虛擬機(jī) (JVM):
- Java 在運(yùn)行過(guò)程中編譯代碼 (JIT),這意味著當(dāng)它第一次運(yùn)行某些代碼時(shí),會(huì)有編譯延遲。
- Java 管理內(nèi)存的方式是在“堆”空間中分配內(nèi)存塊。每隔一段時(shí)間,它就會(huì)清理空間,移除舊對(duì)象,為新對(duì)象騰出空間。主要的問(wèn)題是,為了進(jìn)行準(zhǔn)確的計(jì)數(shù),應(yīng)用程序線程需要暫時(shí)“凍結(jié)”。這個(gè)過(guò)程稱為垃圾回收 (GC)。
GC 是低延遲應(yīng)用程序開(kāi)發(fā)人員可能會(huì)放棄 Java 的主要原因。
市場(chǎng)上有一些可用的 Java 虛擬機(jī)。
最常見(jiàn)的是 Oracle Hotspot JVM,它在 Java 社區(qū)中被廣泛使用,主要是一些歷史原因。
對(duì)于非常苛刻的應(yīng)用程序,有一個(gè)很棒的替代方案,也就是 Azul Systems 的 Zing。
Zing 是標(biāo)準(zhǔn) Oracle Hotspot JVM 的一個(gè)強(qiáng)大的替代品。Zing 解決了 GC 停頓和 JIT 編譯問(wèn)題。
接下來(lái),讓我們來(lái)研究一下 Java 的一些固有問(wèn)題和可能的解決方案。
1. 了解 Java 的 JIT 編譯器
像 C++ 這樣的編程語(yǔ)言被稱為編譯型語(yǔ)言,因?yàn)榘l(fā)布的代碼完全是二進(jìn)制的,可以直接在 CPU 上執(zhí)行。
PHP 或 Perl 被稱為解釋型語(yǔ)言,因?yàn)榻忉屍?(安裝在目標(biāo)機(jī)器上) 會(huì)在運(yùn)行時(shí)編譯每一行代碼。
Java 介于兩者之間,它將代碼編譯成 Java 字節(jié)碼,并在必要時(shí)再將其編譯成二進(jìn)制的。
Java 不在啟動(dòng)時(shí)編譯代碼的原因與后續(xù)的性能優(yōu)化有關(guān)。通過(guò)觀察應(yīng)用程序運(yùn)行并分析實(shí)時(shí)方法調(diào)用和類初始化情況,Java 對(duì)經(jīng)常被調(diào)用的代碼部分進(jìn)行編譯。它甚至可能會(huì)根據(jù)經(jīng)驗(yàn)做出一些假設(shè) (某些代碼永遠(yuǎn)不會(huì)被調(diào)用,或者某個(gè)對(duì)象始終是一個(gè)字符串)。
編譯過(guò)的代碼執(zhí)行速度非常快,但有三個(gè)缺點(diǎn):
- 一個(gè)方法需要被調(diào)用一定次數(shù)才能達(dá)到編譯閾值,然后才能被編譯和優(yōu)化 (這個(gè)閾值是可配置的,通常在 10000 次左右)。在此之前,未優(yōu)化的代碼不會(huì)“全速”運(yùn)行。在更快的編譯和高質(zhì)量的編譯之間存在折衷 (如果假設(shè)是錯(cuò)誤的,就會(huì)發(fā)生編譯成本)。
- 當(dāng) Java 應(yīng)用程序重新啟動(dòng)時(shí),我們又回到了起點(diǎn),必須等待再次達(dá)到閾值。
- 有些應(yīng)用程序有一些不常被調(diào)用但很關(guān)鍵的方法,這些方法只會(huì)被調(diào)用幾次,但在被調(diào)用時(shí)需要非??臁?/li>
Zing 通過(guò)讓它的 JVM“保存”已編譯的方法和類的狀態(tài)(也就是所謂的 profile)來(lái)解決這些問(wèn)題。這個(gè)獨(dú)特的功能叫做 ReadyNow,也就是說(shuō) Java 應(yīng)用程序可以始終以最佳速度運(yùn)行,即使是在重啟之后。
當(dāng)你使用已有的 profile 重新啟動(dòng)應(yīng)用程序,Azul JVM 會(huì)立即收回以前的決策并直接編譯重要的方法,以解決 Java 的預(yù)熱問(wèn)題。
此外,你也可以在開(kāi)發(fā)環(huán)境中構(gòu)建一個(gè) profile 來(lái)模擬生產(chǎn)行為。優(yōu)化后的 profile 能部署到生產(chǎn)環(huán)境中,并知道所有關(guān)鍵路徑都已經(jīng)過(guò)編譯和優(yōu)化。
下圖顯示了交易應(yīng)用程序 (在模擬環(huán)境中) 的最大延遲。
Hotspot JVM 的延時(shí)峰值是顯而易見(jiàn)的,而 Zing 的延時(shí)保持得相當(dāng)穩(wěn)定。
百分比分布表明,在 1% 的時(shí)間內(nèi),Hotspot JVM 發(fā)生的延遲是 Zing JVM 的 16 倍。
2. 解決垃圾回收停頓問(wèn)題
第二個(gè)問(wèn)題是在垃圾回收期間,整個(gè)應(yīng)用程序可能會(huì)停頓幾毫秒到幾秒鐘 (延遲會(huì)隨著代碼復(fù)雜性和堆大小的增加而增加),更糟糕的是,你無(wú)法控制這種情況何時(shí)發(fā)生。
雖然對(duì)很多 Java 應(yīng)用程序來(lái)說(shuō),暫停應(yīng)用程序幾毫秒甚至幾秒是可以接受的,但對(duì)于低延遲應(yīng)用程序來(lái)說(shuō),這是一場(chǎng)災(zāi)難,無(wú)論是在汽車、航空航天、醫(yī)療還是金融領(lǐng)域。
GC 影響對(duì)于 Java 開(kāi)發(fā)人員來(lái)說(shuō)是一個(gè)很大話題,F(xiàn)ull GC 通常也叫作“停止世界的停頓(stop-the-world)”,因?yàn)樗鼤?huì)凍結(jié)整個(gè)應(yīng)用程序。
多年來(lái),有很多 GC 算法都試圖降低吞吐量 (有多少 CPU 時(shí)間用于應(yīng)用程序邏輯執(zhí)行而不是垃圾回收) 和 GC 停頓 (我可以暫停應(yīng)用程序多長(zhǎng)時(shí)間)。
從 Java 9 發(fā)布以來(lái),G1 一直是默認(rèn)的垃圾回收器,其主要思想是根據(jù)用戶提供的時(shí)間目標(biāo)對(duì) GC 停頓進(jìn)行劃分。它通常提供較短的停頓時(shí)間,但以降低吞吐量為代價(jià)。此外,停頓時(shí)間隨著堆的大小而增加。
Java 提供了大量的設(shè)置參數(shù),從堆大小到回收算法以及分配給 GC 的線程數(shù)。因此,Java 應(yīng)用程序通常會(huì)配置大量的參數(shù):
很多開(kāi)發(fā)人員通過(guò)各種技術(shù)來(lái)避免 GC。最主要的是,如果我們少創(chuàng)建一些對(duì)象,那么后續(xù)要清除的對(duì)象就越少。
一種古老的 (仍然在使用) 技術(shù)是使用對(duì)象池。例如,數(shù)據(jù)庫(kù)連接池可以保存 10 個(gè)已經(jīng)打開(kāi)的數(shù)據(jù)庫(kù)連接,以便在需要時(shí)使用。
多線程通常需要鎖,這會(huì)導(dǎo)致同步延遲和停頓 (特別是當(dāng)它們共享資源時(shí))。一種流行的方式是使用環(huán)形緩沖隊(duì)列系統(tǒng),多個(gè)線程可以在一個(gè)無(wú)鎖的環(huán)境中(請(qǐng)參考 disruptor)進(jìn)行讀寫(xiě)操作。
https://lmax-exchange.github.io/disruptor/
一些專家甚至處于無(wú)奈而選擇完全覆蓋 Java 的內(nèi)存管理機(jī)制,由自己來(lái)管理內(nèi)存分配,這雖然解決了問(wèn)題,但也帶來(lái)了更多的復(fù)雜性和風(fēng)險(xiǎn)。
因此,我們需要考慮使用其他 JVM,于是我們決定嘗試 Azul Zing JVM。
很快,我們就能夠在幾乎無(wú)停頓的情況下實(shí)現(xiàn)很高的吞吐量。
這是因?yàn)?Zing 使用了一個(gè)叫作 C4(Continuously Concurrent Compacting Collector,連續(xù)并發(fā)壓縮回收器) 的垃圾回收器,它可以進(jìn)行無(wú)停頓的垃圾回收,而不管 Java 堆有多大 (可以達(dá)到 8TB)。
這是通過(guò)在應(yīng)用程序運(yùn)行時(shí)并發(fā)映射和壓縮內(nèi)存來(lái)實(shí)現(xiàn)的。
此外,它不需要修改代碼,而且延遲和速度方面的改進(jìn)都是開(kāi)箱即用的,不需要進(jìn)行繁雜的配置。
Java 程序員可以享受到兩方面的好處:Java 的簡(jiǎn)單性 (不需要擔(dān)心創(chuàng)建太多的新對(duì)象) 和 Zing 的底層性能,允許系統(tǒng)中出現(xiàn)高度可預(yù)測(cè)的延遲。
GCeasy 提供了通用 GC 日志分析器,我們可以在真實(shí)的自動(dòng)交易應(yīng)用程序 (在模擬環(huán)境中) 中快速地對(duì) JVM 進(jìn)行比較。
https://gceasy.io/
在我們的應(yīng)用程序中,使用 Zing 的 GC 大約比使用標(biāo)準(zhǔn) Oracle Hotspot JVM 的 GC 快 180 倍。
更令人印象深刻的是,GC 停頓通常對(duì)應(yīng)于實(shí)際的應(yīng)用程序停頓時(shí)間,而 Zing 的 GC 通常是并行發(fā)生的,實(shí)際的停頓很少,甚至沒(méi)有停頓。
總之,在享受 Java 的簡(jiǎn)單和特性的同時(shí),仍然有可能實(shí)現(xiàn)高性能和低延遲。C++ 一般用于開(kāi)發(fā)特定的底層組件,如驅(qū)動(dòng)程序、數(shù)據(jù)庫(kù)、編譯器和操作系統(tǒng),但大多數(shù)現(xiàn)實(shí)生活中的應(yīng)用程序可以使用 Java 開(kāi)發(fā),甚至是要求很高的應(yīng)用程序。
這就是為什么 Java 是排名第一的編程語(yǔ)言(根據(jù) Oracle 的說(shuō)法),并擁有數(shù)百萬(wàn)開(kāi)發(fā)者,在全世界有超過(guò) 510 億個(gè) Java 虛擬機(jī)。