為什麼選擇Java語言用作高頻交易?-Jad

banq發表於2020-10-27

在高頻交易的世界中,自動化應用程式每天處理數億個市場訊號,並在全球各個交易所傳送成千上萬的訂單。
為了保持競爭力,反應時間必須始終保持在微秒內,特別是在異常高峰(例如“黑天鵝”事件)期間。
在典型的體系結構中,金融交易訊號將轉換為單一的內部市場資料格式(交易所使用各種協議(例如TCP / IP,UDP多播)和多種格式(例如二進位制,SBE,JSON,FIX等)。那些normalised的訊息然後被髮送到演算法的伺服器,統計引擎,使用者介面,日誌伺服器,和所有種資料庫(儲存器內,物理,分散式)。沿這條路徑進行的任何延遲都可能帶來昂貴的後果,例如基於舊價格制定策略或過早到達市場的訂單。
為了獲得這些關鍵的微秒時間,大多數玩家都在昂貴的硬體上進行了投資:配備超頻水冷CPU的伺服器池(到2020年,您可以購買具有56個5.6 GHz核心和1 TB RAM的伺服器),在主要交換資料中心的配置,納秒級網路交換機,專用的次洋線(Hibernian Express是主要提供商),甚至是微波網路。常見的情況是,具有作業系統旁路的高度定製的Linux核心使資料直接從網路卡“跳轉”到應用程式,IPC(程式間通訊)甚至FPGA(可程式設計的單用途晶片)。
對於程式語言,C ++似乎是伺服器端應用程式的自然競爭者:它速度很快,儘可能接近機器程式碼,並且一旦為目標平臺編譯,就提供了恆定的處理時間。
 
我們做出了不同的選擇。
在過去的14年中,我們一直在使用Java並使用出色且價格合理的硬體進行FX演算法交易空間編碼方面的競賽。
由於團隊規模小,資源有限以及熟練的開發人員缺乏工作市場,因此Java意味著我們可以快速新增軟體改進,因為Java生態系統比C派生產品具有更快的上市時間。可以在早上討論改進措施,並在下午在生產中實施,測試和釋出改進。
與需要幾周甚至幾個月的軟體更新時間的大型公司相比,這是一個關鍵優勢。在一個漏洞可以在幾秒鐘內抹掉全年利潤的領域中,我們還沒有準備好在質量上做出妥協。我們使用許多開源庫和專案實施了嚴格的敏捷環境,包括Jenkins,Maven,單元測試,夜間構建和Jira。
使用Java,開發人員可以專注於直觀的物件導向的業務邏輯,而不必像C ++中那樣除錯一些晦澀的記憶體Core轉儲或管理指標。而且,由於Java強大的內部記憶體管理,初級程式設計師也可以在第一天以有限的風險增加價值。
 
憑藉良好的設計模式和簡潔的編碼習慣,可以使用Java達到C ++低延遲
例如,Java將最佳化和編譯在應用程式執行期間觀察到的最佳路徑,但是C ++會預先編譯所有內容,因此即使未使用的方法也仍將是最終可執行二進位制檔案的一部分。
但是,這裡有一個問題,一個主要問題要啟動過程。使Java如此強大和令人愉悅的語言的原因還在於它的劣勢(至少對於微秒敏感的應用程式而言),即Java虛擬機器 (JVM):
  • Java隨心所欲地編譯程式碼(僅在時間編譯器或JIT中進行),這意味著它第一次遇到某些程式碼時,會導致編譯延遲。
  • Java管理記憶體的方式是在其“堆”空間中分配記憶體塊。每隔一段時間,它將清理該空間並移走舊物體,以便為新物體騰出空間。主要問題是為了準確計數,應用程式執行緒需要暫時“凍結”。此過程稱為垃圾收集(GC)。

GC是低延遲應用程式開發人員可能會先驗丟棄Java的主要原因。
市場上有一些Java虛擬機器:

  1. 最常見和標準的是Oracle Hotspot JVM,它在Java社群中被廣泛使用,主要是出於歷史原因。
  2. 對於要求非常高的應用程式,Azul Systems提供了一個很棒的替代方案,稱為Zing
  3. Zing是標準Oracle Hotspot JVM的強大替代品。Zing解決了GC暫停和JIT編譯問題。

讓我們研究一下使用Java和可能的解決方案所固有的一些問題。
 

瞭解Java的即時編譯器
像C ++這樣的語言被稱為編譯語言,因為所提供的程式碼完全是二進位制的,可以直接在CPU上執行。
PHP或Perl之所以稱為解釋型,是因為直譯器(安裝在目標計算機上)會在執行時編譯每一行程式碼。
Java介於兩者之間。它將程式碼編譯為Java位元組碼,然後在認為合適的情況下將其編譯為二進位制。
Java在啟動時不編譯程式碼的原因與長期效能最佳化有關。透過觀察應用程式的執行並分析實時方法呼叫和類初始化,Java可以編譯經常呼叫的程式碼部分。它甚至可能根據經驗做出一些假設(這部分程式碼永遠不會被呼叫,或者該物件始終是String)。
因此,實際的編譯程式碼非常快。但是存在三個缺點:

  1. 在最佳化和編譯某個方法之前,必須先呼叫該方法一定次數才能達到編譯閾值(該限制是可配置的,但通常約為10,000次呼叫)。在此之前,未最佳化的程式碼不會以“全速”執行。在獲得更快的編譯速度和獲得高質量的編譯速度之間存在折衷(如果假設錯誤,則會產生重新編譯的費用)。
  2. Java應用程式重新啟動時,我們回到第一個平方,必須等待再次達到該閾值。
  3. 某些應用程式(例如我們的應用程式)具有一些不常見但很關鍵的方法,它們只會被呼叫幾次,但是在執行時需要非常快(考慮到風險或止損過程僅在緊急情況下才呼叫)。

Azul Zing透過讓其JVM在其所謂的概要檔案中“儲存”已編譯方法和類的狀態來解決這些問題。名為ReadyNow! 的獨特功能意味著Java應用程式即使在重啟後也始終以最佳速度執行。
當您使用現有概要檔案重新啟動應用程式時,Azul JVM會立即撤回其先前的決定並直接編譯概述的方法,從而解決了Java預熱問題。
此外,您可以在開發環境中建立概要檔案,以模仿生產行為。然後,知道所有關鍵路徑都已編譯和最佳化,然後可以將最佳化的概要檔案部署到生產中。
在1%的時間內,熱點JVM產生的延遲是Zing JVM的16倍。
 

解決垃圾回收(GC)暫停
第二個問題,在垃圾回收期間,整個應用程式可能凍結幾毫秒到幾秒鐘之間的任何時間(延遲隨著程式碼複雜性和堆大小而增加),更糟糕的是,您無法控制何時發生。
儘管對於許多Java應用程式來說,暫停應用程式幾毫秒甚至幾秒鐘是可以接受的,但對於低延遲應用程式(無論是在汽車,航空航天,醫療還是金融領域)來說,這是一場災難。
GC的影響是Java開發人員中的一個大話題。完整的垃圾回收通常稱為“世界停止停頓”,因為它凍結了整個應用程式。
多年以來,許多GC演算法都試圖降低吞吐量(在實際的應用程式邏輯上而不是在垃圾回收上花費了多少CPU)與GC 暫停(我可以承受暫停應用​​程式多長時間?)。
從Java 9開始,G1收集器已成為預設的GC,其主要思想是根據使用者提供的時間目標分割GC暫停。它通常提供較短的暫停時間,但以降低吞吐量為代價。另外,暫停時間隨著堆的大小而增加。
Java提供了許多設定來調整其垃圾收集(通常是JVM),從堆大小到收集演算法,以及分配給GC的執行緒數。因此,常見的情況是看到Java應用程式配置了過多的自定義選項。
許多開發人員(包括我們的開發人員)已轉向各種技術來完全避免使用GC。主要是,如果我們建立的物件更少,那麼以後需要清除的物件就會更少。
一種舊的(並且仍在使用)的技術是使用可重用物件的物件池。例如,資料庫連線池將儲存對10個開啟的連線的引用,這些連線可以按需使用。
多執行緒通常需要鎖,這會導致同步延遲和暫停(尤其是如果它們共享資源)。一種流行的設計是環形緩衝區 ring buffer佇列系統,其中許多執行緒在無鎖設定中進行讀寫操作(請參閱disruptor )。
出於無奈,一些專家甚至選擇完全覆蓋Java記憶體管理並自己管理記憶體分配,這在解決一個問題的同時,還帶來了更多的複雜性和風險。
在這種情況下,很明顯我們應該考慮其他JVM,因此我們決定嘗試Azul Zing JVM
很快,我們就可以實現極高的吞吐量,而暫停時間卻可以忽略不計。
這是因為Zing使用稱為C4(連續併發壓縮收集器)的唯一收集器,該收集器無論Java堆大小(最大為8 TB)如何,都可以進行無中斷的垃圾收集。
這是透過在應用程式仍在執行時同時對映和壓縮記憶體來實現的。
此外,它不需要任何程式碼更改,並且不需要冗長的配置即可立即獲得延遲和速度方面的改進。
在這種情況下,Java程式設計師可以享受兩全其美的優勢,即Java的簡單性(無需對建立新物件抱有幻想)以及Zing的基礎效能,從而可以在整個系統中實現高度可預測的延遲。
由於GC容易,通用GC日誌分析,我們可以quicky在一個真正的自動交易程式比較兩者的JVM(在模擬環境中)。
在我們的應用程式中,Zing的GC大約是標準Oracle Hotspot JVM的180倍。
更為令人印象深刻的是,雖然GC暫停通常對應於實際的應用程式暫停時間,但Zing智慧GC通常並行進行,實際中斷最少或沒有。
 

相關文章