低延遲系統的Java實踐

在很久很久以前,如果有人讓我用Java語言開發一個低延遲系統,我肯定會用迷茫的眼神望着他,然後說“are you kidding me?”。然而隨着Java語言的日臻完善以及JVM性能的極速提升,使得用Java語言開發低延遲(不要和實時系統搞混)系統越來越成爲可能,其中就包括最典型的交易(支付)系統。當然作爲系統架構師,他們會嘗試使用一些成熟分佈式架構方案(通常是整合一些商業或開源項目),通過利用冗餘計算資源以及異步通信方式提高應用程序的吞吐量和響應率,使其到達低延遲系統的標準,這在社區中有大量的實踐案例,包括淘寶,京東、XX系等。然而我的興趣愛好是研究Java語言本身能爲低延遲應用開發帶來什麼,在開發低延遲系統中我們有哪些實踐可以參照,這纔是本文的討論重點。關於低延遲系統和實時系統的區別不再贅述,作爲架構師的你們應該比我清楚的多。

作爲低延遲系統,比如交易系統,應該有2個比較重要的參數指標:吞吐量和響應率(當然還有其他重要指標)。吞吐量表達了系統在單位時間內所處理的請求量;而響應率則表達了單次請求所消耗的單位時間。這2個指標基本能判斷出一個交易系統是否“足夠快”。當我們在使用Java語言開發低延遲系統時,應該放棄一些我們之前約定俗成的規則,其中就包括了我們一直信奉的Java編程原則——面向對象。有人肯定會說我,“這不是扯蛋嗎?那你還用Java幹啥?”。其實我們並不會放棄Java面向對象的思維方式,而是在使用的方式上有所改變而已。Java設計之初就是純面向對象的,記得之前所有Java入門書中都會有一句名言:“在Java世界,一切皆對象!”。有點跑題了,寫這篇文章也是因爲之前看到了一篇文章《Using Java in Low Latency Environments》,在這篇文章中幾位大師討論了有關於Java在低延遲環境中的使用方法,有些原則非常值得參考,再結合自己實踐工作中的一些經驗的積累,所以總結了三條最最重要的基本準則,以供同學們參考。

如果你是一個Java老手,肯定對JVM或是Java語言的各種特性瞭如指掌。JVM的內存釋放是由GC自動完成的,程序員無法直接控制和干預GC的執行(有人會說,不是有System.gc()可以執行垃圾收集嘛,那就請你好好的去看一下Java Doc吧),這也是我對Java最大的詬病之一。我們都知道,當JVM在執行GC時,不管是YGC還是FULL GC,JVM都將阻塞其他所有正在執行的線程,雖然這個時間已經從分鐘級別降低到了毫秒級別,但是作爲低延遲系統還是會受其影響,從而降低系統的響應率。這種情況直到Oracle推出帶有並行GC的JVM之前都會一直存在,爲了避免這種情況,大師們的解決方案是降低GC的頻率,將GC控制在每天一次或是幾天一次,那到底這麼做呢?大師們爲我們指明瞭一條明路。那就是環保——儘量少產生“垃圾”或不產生“垃圾”,簡單講就是少使用堆對象(用new關鍵字實例化的對象),甚至包括String對象。好吧,小夥伴們都驚呆了,你是要我去寫C代碼嘛!!還好,我會C不會因此而失業——開個玩笑。其實大師們想表達的意思是對象複用技術,這種技術可以大量減少堆對象的產生。在我現在的交易系統開發中,基本不會關注對象的複用,字符串對象更是當做了基本類型來使用。其實作爲交易系統,業務邏輯非常的複雜,各種邏輯判斷,上百個交易業務屬性,再加上對面向對象技術以及設計模型的迷戀,勢必會引起堆對象的泛濫從而導致GC的頻繁執行。所以爲了減少“垃圾”的產生,我們必須在對象的設計和使用上做一些約束,例如,用基本類型(short、int、long、double等)替換包裝對象、減小對象的規模(不要嵌套對象太多)、用數組替換Java集合、使用對象池複用對象(例如:commons-pool庫)、減少第三方類庫的使用等等。當然我們所做的一切都比不上來自GC自身的改進,所以真心希望oracle儘快的推出可以並行的垃圾收集器,使其不再成爲我們既愛又恨的關注點。

其次對低延遲系統具有影響的就是Java的內存模型,即JMM。Java內存模型定義了可見性和原子性,爲了保證這兩項實現,我們必須使用同步,在Java中,所有線程的同步必須爭奪唯一的一把鎖,因此在需要低延遲的環境中鎖競爭會大大的影響吞吐量和響應率。在交易系統中,當請求量急劇上升時,鎖的競爭將更加的激烈,從而導致大量的線程阻塞或是餓死。那如何來規避這種情況的發生呢?那就是使用無鎖技術或無等待技術,具體而言就是在同步塊中不加鎖或是減小加鎖的代碼範圍。我們可以避免使用synchronized或是使用ReentrantLock來自己控制鎖的範圍。ReentrantLock允許我們在代碼塊上加鎖,但是必須要注意不要忘記釋放鎖。舉一個例子,假設我們的交易系統中有一個共享的數據,每個寫請求方法都需要用synchronized關鍵字加以同步,否則就會發生數據異常。現在我們用無鎖技術來規避synchronized,實現很簡單,就是使用一個隊列,將所有寫請求先放入隊列中,然後由一個線程循環隊列,將寫請求寫入共享數據中。其實這就是我們常說的“單一寫原則”,另外異步處理也是一種“單一寫原則”的具體化實現。

IO可以說是影響低延遲系統性能最爲關鍵的因素之一,而網絡IO更是各種IO調用的重中之重。在交易系統中網絡IO無法避免,我們必須通過以太網從其他應用程序中獲取資源,比如:數據庫,消息系統等,同時我們又會通過以太網向其他應用系統輸出服務,比如:交易通知等。所以,網絡質量將直接影響到交易系統的吞吐量和響應時間。在廣域網環境中,網絡傳輸需要時間、爲了保證TCP/IP可靠協議必須重新發送丟失的數據包,交換機或路由器也會產生網絡阻塞,這些完全不可預知的問題都將影響到低延遲系統的性能,IO的延遲無時無刻的在考驗着我們的忍耐底線。到目前爲止,我們還沒有一個絕對可行的方案來解決所有由IO引起的問題,但還是有一些指導建議值得我們去借鑑。我們可以通過預加載資源來最大限度的減少IO開銷,例如,在應用程序啓動的時候加載配置文件或其他資源文件等。這裏需要注意的是,加載的資源不能在應用程序中被垃圾回收,讓其存在於堆內存的P區中是一個不錯的選擇。另一方面,從JDK7開始,Java提供了SDP的支持,SDP協議可以大大的提升網絡IO的性能,所謂SDP就是Sockets Direct Protocol,即套接字直聯協議。它不同於傳統TCP/IP協議,它需要硬件的支持,即InfiniBand網絡設備。SDP可以直接訪問遠程主機的內存,不再需要通過ISO的7層模型來進行數據的傳輸,所以它的效率要比以太網的TCP/IP協議高很多。我們用一張圖就可以非常清楚的對比SDP協議和以太網協議的本質區別.。(此圖從infoq上摘錄,非本人版權,特此聲明)

從上圖中可以看到,Java7提供的SDP協議是直接和物理層打交道,數據不再像之前的Java6那種以太網的方式要經過ISO的各層。當然,對於開發人員來說這一切都是是完全透明的,我們還是在使用非常熟悉的java.net.*包中各種API進行網絡應用程序的開發,所有的一切全部交給JDK。是不是覺得很Cool呢!不過本人還未對此進行過嘗試,因爲我們公司目前還是以太網,並沒有InfiniBand網絡設備,所以SDP技術還有待驗證。

綜上所述,在用Java做低延遲系統開發時,我們應該從三個方面着手製定優化方案,第一,有效減少垃圾收集的執行頻率;第二,有效的使用鎖機制或根本不用鎖;第三,減少IO(重點是網絡IO)的等待處理時間。當然除此之外還有一些其他的小技巧,比如,不使用第三方庫、不使用反射庫(java.lang.reflect)、優化代碼執行路徑、用DirectByteBuffers創建數據結構等等。

好像寫了那麼多自我感覺乾貨不是太多,其實我只是想拋磚引玉,通過這樣一篇文章能夠激發出更多的碰撞和辯論,低延遲系統的開發是一個大課題,其複雜程度遠遠超過本文所講述的內容,所以希望更多的人能參與進來,把磚頭扔向我。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章