推送服務
還記得一年半前,做的一個項目需要用到 Android 推送服務。和 iOS 不同,Android 生態中沒有統一的推送服務。Google 雖然有 Google Cloud Messaging ,但是連國外都沒統一,更別說國內了,直接被牆。
所以之前在 Android 上做推送大部分只能靠輪詢。而我們之前在技術調研的時候,搜到了 jPush 的博客,上面介紹了一些他們的技術特點,他們主要做的其實就是移動網絡下的長連接服務。單機 50W-100W 的連接的確是嚇我一跳!后來我們也采用了他們的免費方案,因為是一個受眾面很小的產品,所以他們的免費版夠我們用了。一年多下來,運作穩定,非常不錯!
時隔兩年,換了部門后,竟然接到了一項任務,優化公司自己的長連接服務端。
再次搜索網上技術資料后才發現,相關的很多難點都被攻破,網上也有了很多的總結文章,單機 50W-100W 的連接完全不是夢,其實人人都可以做到。但是光有連接還不夠,QPS 也要一起上去。
所以,這篇文章就是匯總一下利用 Netty 實現長連接服務過程中的各種難點和可優化點。
Netty 是什么
Netty: http://netty.io/
Netty is an asynchronous event-driven network application framework for rapid development of maintainable high performance protocol servers & clients.
官方的解釋最精准了,期中最吸引人的就是高性能了。但是很多人會有這樣的疑問:直接用 NIO 實現的話,一定會更快吧?就像我直接手寫 JDBC 雖然代碼量大了點,但是一定比 iBatis 快!
但是,如果了解 Netty 后你才會發現,這個還真不一定!
利用 Netty 而不用 NIO 直接寫的優勢有這些:
-
高性能高擴展的架構設計,大部分情況下你只需要關注業務而不需要關注架構
-
Zero-Copy
技術盡量減少內存拷貝 -
為 Linux 實現 Native 版 Socket
-
寫同一份代碼,兼容 java 1.7 的 NIO2 和 1.7 之前版本的 NIO
-
Pooled Buffers
大大減輕Buffer
和釋放Buffer
的壓力 -
……
特性太多,大家可以去看一下《Netty in Action》這本書了解更多。
另外,Netty 源碼是一本很好的教科書!大家在使用的過程中可以多看看它的源碼,非常棒!
瓶頸是什么
想要做一個長鏈服務的話,最終的目標是什么?而它的瓶頸又是什么?
其實目標主要就兩個:
-
更多的連接
-
更高的 QPS
所以,下面就針對這連個目標來說說他們的難點和注意點吧。
更多的連接
非阻塞 IO
其實無論是用 Java NIO 還是用 Netty,達到百萬連接都沒有任何難度。因為它們都是非阻塞的 IO,不需要為每個連接創建一個線程了。
欲知詳情,可以搜索一下BIO
,NIO
,AIO
的相關知識點。
Java NIO 實現百萬連接
這段代碼只會接受連過來的連接,不做任何操作,僅僅用來測試待機連接數極限。
大家可以看到這段代碼是 NIO 的基本寫法,沒什么特別的。
Netty 實現百萬連接
這段其實也是非常簡單的 Netty 初始化代碼。同樣,為了實現百萬連接根本沒有什么特殊的地方。
瓶頸到底在哪
上面兩種不同的實現都非常簡單,沒有任何難度,那有人肯定會問了:實現百萬連接的瓶頸到底是什么?
其實只要 java 中用的是非阻塞 IO(NIO 和 AIO 都算),那么它們都可以用單線程來實現大量的 Socket 連接。 不會像 BIO 那樣為每個連接創建一個線程,因為代碼層面不會成為瓶頸。
其實真正的瓶頸是在 Linux 內核配置上,默認的配置會限制全局最大打開文件數(Max Open Files)還會限制進程數。 所以需要對 Linux 內核配置進行一定的修改才可以。
這個東西現在看似很簡單,按照網上的配置改一下就行了,但是大家一定不知道第一個研究這個人有多難。
這里直接貼幾篇文章,介紹了相關配置的修改方式:
構建C1000K的服務器
(http://www.ideawu.net/blog/archives/740.html)
100萬並發連接服務器筆記之1M並發連接目標達成(http://www.blogjava.net/yongboy/archive/2013/04/11/397677.html)
淘寶技術分享 HTTP長連接200萬嘗試及調優
(http://www.linuxde.net/2013/08/15150.html)
如何驗證
讓服務器支持百萬連接一點也不難,我們當時很快就搞定了一個測試服務端,但是最大的問題是,我怎么去驗證這個服務器可以支撐百萬連接呢?
我們用 Netty 寫了一個測試客戶端,它同樣用了非阻塞 IO ,所以不用開大量的線程。 但是一台機器上的端口數是有限制的,用root
權限的話,最多也就 6W 多個連接了。 所以我們這里用 Netty 寫一個客戶端,用盡單機所有的連接吧。
代碼同樣很簡單,只要連上就行了,不需要做任何其他的操作。
這樣只要找到一台電腦啟動這個程序即可。這里需要注意一點,客戶端最好和服務端一樣,修改一下 Linux 內核參數配置。
怎么去找那么多機器
按照上面的做法,單機最多可以有 6W 的連接,百萬連接起碼需要17台機器!
如何才能突破這個限制呢?其實這個限制來自於網卡。 我們后來通過使用虛擬機,並且把虛擬機的虛擬網卡配置成了橋接模式解決了問題。
根據物理機內存大小,單個物理機起碼可以跑4-5個虛擬機,所以最終百萬連接只要4台物理機就夠了。
討巧的做法
除了用虛擬機充分壓榨機器資源外,還有一個非常討巧的做法,這個做法也是我在驗證過程中偶然發現的。
根據 TCP/IP 協議,任何一方發送FIN
后就會啟動正常的斷開流程。而如果遇到網絡瞬斷的情況,連接並不會自動斷開。
那我們是不是可以這樣做?
-
啟動服務端,千萬別設置 Socket 的
keep-alive
屬性,默認是不設置的 -
用虛擬機連接服務器
-
強制關閉虛擬機
-
修改虛擬機網卡的 MAC 地址,重新啟動並連接服務器
-
服務端接受新的連接,並保持之前的連接不斷
我們要驗證的是服務端的極限,所以只要一直讓服務端認為有那么多連接就行了,不是嗎?
經過我們的試驗后,這種方法和用真實的機器連接服務端的表現是一樣的,因為服務端只是認為對方網絡不好罷了,不會將你斷開。
另外,禁用keep-alive
是因為如果不禁用,Socket 連接會自動探測連接是否可用,如果不可用會強制斷開。
更高的 QPS
由於 NIO 和 Netty 都是非阻塞 IO,所以無論有多少連接,都只需要少量的線程即可。而且 QPS 不會因為連接數的增長而降低(在內存足夠的前提下)。
而且 Netty 本身設計得足夠好了,Netty 不是高 QPS 的瓶頸。那高 QPS 的瓶頸是什么?
是數據結構的設計!
如何優化數據結構
首先要熟悉各種數據結構的特點是必需的,但是在復雜的項目中,不是用了一個集合就可以搞定的,有時候往往是各種集合的組合使用。
既要做到高性能,還要做到一致性,還不能有死鎖,這里難度真的不小…
我在這里總結的經驗是,不要過早優化。優先考慮一致性,保證數據的准確,然后再去想辦法優化性能。
因為一致性比性能重要得多,而且很多性能問題在量小和量大的時候,瓶頸完全會在不同的地方。 所以,我覺得最佳的做法是,編寫過程中以一致性為主,性能為輔;代碼完成后再去找那個 TOP1,然后去解決它!
解決 CPU 瓶頸
在做這個優化前,先在測試環境中去狠狠地壓你的服務器,量小量大,天壤之別。
有了壓力測試后,就需要用工具來發現性能瓶頸了!
我喜歡用的是 VisualVM,打開工具后看抽樣器(Sample),根據自用時間(Self Time (CPU))倒序,排名第一的就是你需要去優化的點了!
備注:Sample 和 Profiler 有什么區別?前者是抽樣,數據不是最准但是不影響性能;后者是統計准確,但是非常影響性能。 如果你的程序非常耗 CPU,那么盡量用 Sample,否則開啟 Profiler 后降低性能,反而會影響准確性。
還記得我們項目第一次發現的瓶頸竟然是ConcurrentLinkedQueue
這個類中的size()
方法。 量小的時候沒有影響,但是Queue
很大的時候,它每次都是從頭統計總數的,而這個size()
方法我們又是非常頻繁地調用的,所以對性能產生了影響。
size()
的實現如下:
后來我們通過額外使用一個AtomicInteger
來計數,解決了問題。但是分離后豈不是做不到高一致性呢? 沒關系,我們的這部分代碼關心最終一致性,所以只要保證最終一致就可以了。
總之,具體案例要具體分析,不同的業務要用不同的實現。
解決 GC 瓶頸
GC 瓶頸也是 CPU 瓶頸的一部分,因為不合理的 GC 會大大影響 CPU 性能。
這里還是在用 VisualVM,但是你需要裝一個插件:VisualGC
有了這個插件后,你就可以直觀的看到 GC 活動情況了。
按照我們的理解,在壓測的時候,有大量的 New GC 是很正常的,因為有大量的對象在創建和銷毀。
但是一開始有很多 Old GC 就有點說不過去了!
后來發現,在我們壓測環境中,因為 Netty 的 QPS 和連接數關聯不大,所以我們只連接了少量的連接。內存分配得也不是很多。
而 JVM 中,默認的新生代和老生代的比例是1:2,所以大量的老生代被浪費了,新生代不夠用。
通過調整 -XX:NewRatio
后,Old GC 有了顯著的降低。
但是,生產環境又不一樣了,生產環境不會有那么大的 QPS,但是連接會很多,連接相關的對象存活時間非常長,所以生產環境更應該分配更多的老生代。
總之,GC 優化和 CPU 優化一樣,也需要不斷調整,不斷優化,不是一蹴而就的。
其他優化
如果你已經完成了自己的程序,那么一定要看看《Netty in Action》作者的這個網站:Netty Best Practices a.k.a Faster == Better(http://normanmaurer.me/presentations/2014-facebook-eng-netty/slides.html)。
相信你會受益匪淺,經過里面提到的一些小小的優化后,我們的整體 QPS 提升了很多。
最后一點就是,java 1.7 比 java 1.6 性能高很多!因為 Netty 的編寫風格是事件機制的,看似是 AIO。 可 java 1.6 是沒有 AIO 的,java 1.7 是支持 AIO 的,所以如果用 java 1.7 的話,性能也會有顯著提升。
最后成果
經過幾周的不斷壓測和不斷優化了,我們在一台16核、120G內存(JVM只分配8G)的機器上,用 java 1.6 達到了60萬的連接和20萬的QPS。
其實這還不是極限,JVM 只分配了8G內存,內存配置再大一點連接數還可以上去;
QPS 看似很高,System Load Average 很低,也就是說明瓶頸不在 CPU 也不在內存,那么應該是在 IO 了! 上面的 Linux 配置是為了達到百萬連接而配置的,並沒有針對我們自己的業務場景去做優化。
因為目前性能完全夠用,線上單機 QPS 最多才 1W,所以我們先把精力放在了其他地方。 相信后面我們還會去繼續優化這塊的性能,期待 QPS 能有更大的突破!
小編語:
netty適用於快速開發高性能,高可用的網絡服務,在IT行業有着比較廣泛的應用,如分布式服務框架 Dubbo,就是用netty作為基礎通信組件。netty可以作為java程序員進階的一個研究方向。
推薦資料如下:
netty線程模型
http://www.infoq.com/cn/articles/netty-threading-model/
grpc原理解析
http://shift-alt-ctrl.iteye.com/blog/2292862