構建高效能高併發Java系統
場景
這裡指的高效能高併發伺服器是一個有狀態的服務,可以理解成web或者socket伺服器,每個業務在這個服務上執行後是有狀態的。比如一次電信業務,設計使用者請求資源分配,網路頻寬分配,billing認證等。這些狀態需要保留在伺服器端,稱為session。該系統的特點是session資訊寫入量大,更新訪問頻繁。
1,使用非同步通訊
非同步通訊顯然可以更快的返回響應。從實際經驗看,對高吞吐伺服器更大的好處是,系統中的某一服務出現問題後往往出現雪崩似的服務宕機。這很多都是由於採用同步通訊,需要等待其他服務同步通訊結束後,其佔用資源才能得到釋放。而這些資源往往是socket連線、執行緒、資料庫連線等比較重的資源。因此請慎重使用同步通訊。如果你真的需要他,可以用個mock同步。正如Tim Yang所說:很多遠端服務呼叫是在關鍵路徑中,它可以容忍失敗,但是不能容忍堵塞。
2,使用NIO
NIO幾乎是Java cluster的基石。大量分散式開源專案Thrift, ZooKeeper等都基於此項技術。其好處是用較低的系統開銷處理大量訊息。在Intel(R) Pentium(R) 4 CPU 3.00GHz/1G記憶體的普通PC上跑基於NIO/TCP的RTSP測試,每秒處理1K個RTSP訊息是沒有問題的。關於NIO的架構可以參考開源專案MINA,但要注意的是,不要直接用處理NIO訊息的執行緒處理邏輯。
3,儘量不使用鎖
如果你在高效能伺服器邏輯中看到同步鎖,就要小心了。我們希望伺服器可以同時併發的儘量多的處理請求,而同步鎖恰恰摁住了業務的咽喉。同時如果執行緒設計有問題而導致大量執行緒爭奪同一把鎖,最壞的情況(超過1K+執行緒)我曾見過JVM要用幾個小時來排程一個執行緒佔鎖(為什麼?請牛人解釋)。解決的辦法就是:1,儘量不用。可能麼?在一定程度上是可能的,比如我曾使用一致性雜湊演算法解決資源sticky的問題。用演算法替代同步,是一個思路;2,儘量減少鎖的範圍,事同步鎖影響的邏輯越少越好。效果也很明顯;3,使用資料庫更新替代同步也是一個思路,雖然我自己不是很喜歡,但是從實際效果看使用資料庫,特別是用儲存過程。在壓力不大的情況也是個簡單有效的方法。
4,減少GC
Full GC對Java伺服器效能的影響是致命的,特別是當JVM管理著較大記憶體時。即使是在小型應用,例如Old區只分配512M記憶體,一次Full GC都可能耗時2~3秒,這意味著你所有的服務都會中斷。儘量減少Full GC的次數絕對是我們的目標。
首先,合理的配置GC引數,採用ParallelGC和 ConcMarkSweepGC,即併發和增量GC。CMS,全稱Concurrent Low Pause Collector,CMS用兩次短暫停來替代序列標記整理演算法的長暫停。
第二,打出GC log,在效能測試階段,檢查你的GC是否OK。系統線上測試時也可以提供寶貴的track。
-verbose:gc -XX:+PrintGCDetails -XX:+PrintGCTimeStamps
給出一個實際伺服器GC引數為例:
1 |
JAVA_OPTS=" -verbose:gc -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+DisableExplicitGC |
2 |
JAVA_OPTS=" -XX:+UseParNewGC -XX:+UseConcMarkSweepGC -XX:ParallelGCThreads=8 |
3 |
JAVA_OPTS="-Xms256m -Xmx640m -XX:NewSize=128m -XX:MaxNewSize=256m -Xss128k |
4 |
JAVA_OPTS=" -XX:PermSize=64m -XX:MaxPermSize=128m |
5 |
JAVA_OPTS=" -XX:SurvivorRatio=14 -XX:TargetSurvivorRatio=90 -XX:MaxTenuringThreshold=16 |
5,資料庫前Cache
用queue快取資料,一次寫入多個數據。可以用多個執行緒同時寫入資料庫。在實際系統中,用多個執行緒同時讀取cache並寫入Mysql是效率很高的。
6,增加資料庫索引
看起來很弱對吧?可是我真的遇到過靠加個簡單的索引解決過一個很嚴重的效能問題。系統瓶頸的多數問題都存在於資料庫和執行緒的使用不當上,所以還是要強調一下。
7,Master-Slave Mysql
Mysql有一篇很有名的文章來解釋在Master-Slave結構下提高讀效率的文章。大致就是說Mysql基準測試下max_read=1200次/s。在讀寫比例為9:1的情況下,通常寫的時間比讀的時間多一倍。假設有N個slave,如果只用slave來讀,公式如下:
Max reads = 1200 – 2 * writes
Max Reads = 9 * writes / (N + 1) (讀為9倍的寫,平均到N+1個Mysql上)
9 * writes / (N + 1) + 2 *writes = 1200
Writes = 1200/ (2 + 9/(N+1))
結論是N=1,每秒增加到184次寫
N = 8,增加到400次寫
其中需要考慮的是複製多份binlog所佔的網路頻寬和對Master Mysql的影響。但我們在實際應用中認為mysql master-slave binlog增量同步是非常迅速且沒有察覺到對效能的負面影響。但一般來說,既然memcached可以很好解決read問題,很少有team做這麼大規模的mysql cluster。但小於等於4個還是可以考慮的。
總結,合理的使用非同步通訊,執行緒排程,優化GC和解決資料庫瓶頸往往可以是構建高效能高併發Java訊息處理伺服器的利器。但這僅限於邏輯業務相對簡單的即時通訊系統,如果是設計大量cache排程,比如網頁排程。或者是設計海量資料處理伺服器,則需要考慮更多的問題。