阿里巴巴高級Java面試題 續(xù)3

6.(七)-典型配置舉例1
以下配置主要針對分代垃圾回收算法而言。

堆大小設(shè)置
年輕代的設(shè)置很關(guān)鍵
JVM中最大堆大小有三方面限制:相關(guān)操作系統(tǒng)的數(shù)據(jù)模型(32-bt還是64-bit)限制;系統(tǒng)的可用虛擬內(nèi)存限制;系統(tǒng)的可用物理內(nèi)存限制。32位系統(tǒng)下,一般限制在1.5G~2G;64為操作系統(tǒng)對內(nèi)存無限制。在Windows Server 2003 系統(tǒng),3.5G物理內(nèi)存,JDK5.0下測試,最大可設(shè)置為1478m。
典型設(shè)置:
java -Xmx3550m -Xms3550m -Xmn2g –Xss128k
-Xmx3550m:設(shè)置JVM最大可用內(nèi)存為3550M。
-Xms3550m:設(shè)置JVM初始內(nèi)存為3550m。此值可以設(shè)置與-Xmx相同,以避免每次垃圾回收完成后JVM重新分配內(nèi)存。
-Xmn2g:設(shè)置年輕代大小為2G。整個堆大小=年輕代大小 + 年老代大小 + 持久代大小。持久代一般固定大小為64m,所以增大年輕代后,將會減小年老代大小。此值對系統(tǒng)性能影響較大,Sun官方推薦配置為整個堆的3/8。
-Xss128k:設(shè)置每個線程的堆棧大小。JDK5.0以后每個線程堆棧大小為1M,以前每個線程堆棧大小為256K。更具應(yīng)用的線程所需內(nèi)存大小進行調(diào)整。在相同物理內(nèi)存下,減小這個值能生成更多的線程。但是操作系統(tǒng)對一個進程內(nèi)的線程數(shù)還是有限制的,不能無限生成,經(jīng)驗值在3000~5000左右。

java -Xmx3550m -Xms3550m -Xss128k -XX:NewRatio=4 -XX:SurvivorRatio=4 -XX:MaxPermSize=16m -XX:MaxTenuringThreshold=0
-XX:NewRatio=4:設(shè)置年輕代(包括Eden和兩個Survivor區(qū))與年老代的比值(除去持久代)。設(shè)置為4,則年輕代與年老代所占比值為1:4,年輕代占整個堆棧的1/5
-XX:SurvivorRatio=4:設(shè)置年輕代中Eden區(qū)與Survivor區(qū)的大小比值。設(shè)置為4,則兩個Survivor區(qū)與一個Eden區(qū)的比值為2:4,一個Survivor區(qū)占整個年輕代的1/6
-XX:MaxPermSize=16m:設(shè)置持久代大小為16m。
-XX:MaxTenuringThreshold=0:設(shè)置垃圾最大年齡。如果設(shè)置為0的話,則年輕代對象不經(jīng)過Survivor區(qū),直接進入年老代。對于年老代比較多的應(yīng)用,可以提高效率。如果將此值設(shè)置為一個較大值,則年輕代對象會在Survivor區(qū)進行多次復(fù)制,這樣可以增加對象再年輕代的存活時間,增加在年輕代即被回收的概論。

回收器選擇
JVM給了三種選擇:串行收集器、并行收集器、并發(fā)收集器,但是串行收集器只適用于小數(shù)據(jù)量的情況,所以這里的選擇主要針對并行收集器和并發(fā)收集器。默認(rèn)情況下,JDK5.0以前都是使用串行收集器,如果想使用其他收集器需要在啟動時加入相應(yīng)參數(shù)。JDK5.0以后,JVM會根據(jù)當(dāng)前系統(tǒng)配置進行判斷。
吞吐量優(yōu)先的并行收集器
如上文所述,并行收集器主要以到達(dá)一定的吞吐量為目標(biāo),適用于科學(xué)技術(shù)和后臺處理等。
典型配置:
java -Xmx3800m -Xms3800m -Xmn2g -Xss128k -XX:+UseParallelGC -XX:ParallelGCThreads=20
-XX:+UseParallelGC:選擇垃圾收集器為并行收集器。此配置僅對年輕代有效。即上述配置下,年輕代使用并發(fā)收集,而年老代仍舊使用串行收集。
-XX:ParallelGCThreads=20:配置并行收集器的線程數(shù),即:同時多少個線程一起進行垃圾回收。此值最好配置與處理器數(shù)目相等。
java -Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:+UseParallelGC -XX:ParallelGCThreads=20 -XX:+UseParallelOldGC
-XX:+UseParallelOldGC:配置年老代垃圾收集方式為并行收集。JDK6.0支持對年老代并行收集。
java -Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:+UseParallelGC -XX:MaxGCPauseMillis=100
-XX:MaxGCPauseMillis=100:設(shè)置每次年輕代垃圾回收的最長時間,如果無法滿足此時間,JVM會自動調(diào)整年輕代大小,以滿足此值。
n java -Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:+UseParallelGC -XX:MaxGCPauseMillis=100 -XX:+UseAdaptiveSizePolicy
-XX:+UseAdaptiveSizePolicy:設(shè)置此選項后,并行收集器會自動選擇年輕代區(qū)大小和相應(yīng)的Survivor區(qū)比例,以達(dá)到目標(biāo)系統(tǒng)規(guī)定的最低相應(yīng)時間或者收集頻率等,此值建議使用并行收集器時,一直打開。

響應(yīng)時間優(yōu)先的并發(fā)收集器
如上文所述,并發(fā)收集器主要是保證系統(tǒng)的響應(yīng)時間,減少垃圾收集時的停頓時間。適用于應(yīng)用服務(wù)器、電信領(lǐng)域等。
典型配置:
java -Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:ParallelGCThreads=20 -XX:+UseConcMarkSweepGC -XX:+UseParNewGC
-XX:+UseConcMarkSweepGC:設(shè)置年老代為并發(fā)收集。測試中配置這個以后,-XX:NewRatio=4的配置失效了,原因不明。所以,此時年輕代大小最好用-Xmn設(shè)置。
-XX:+UseParNewGC: 設(shè)置年輕代為并行收集??膳cCMS收集同時使用。JDK5.0以上,JVM會根據(jù)系統(tǒng)配置自行設(shè)置,所以無需再設(shè)置此值。
java -Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:+UseConcMarkSweepGC -XX:CMSFullGCsBeforeCompaction=5 -XX:+UseCMSCompactAtFullCollection
-XX:CMSFullGCsBeforeCompaction:由于并發(fā)收集器不對內(nèi)存空間進行壓縮、整理,所以運行一段時間以后會產(chǎn)生“碎片”,使得運行效率降低。此值設(shè)置運行多少次GC以后對內(nèi)存空間進行壓縮、整理。
-XX:+UseCMSCompactAtFullCollection:打開對年老代的壓縮。可能會影響性能,但是可以消除碎片

輔助信息
JVM提供了大量命令行參數(shù),打印信息,供調(diào)試使用。主要有以下一些:
-XX:+PrintGC:輸出形式:[GC 118250K->113543K(130112K), 0.0094143 secs] [Full GC 121376K->10414K(130112K), 0.0650971 secs]
-XX:+PrintGCDetails:輸出形式:[GC [DefNew: 8614K->781K(9088K), 0.0123035 secs] 118250K->113543K(130112K), 0.0124633 secs] [GC [DefNew: 8614K->8614K(9088K), 0.0000665 secs][Tenured: 112761K->10414K(121024K), 0.0433488 secs] 121376K->10414K(130112K), 0.0436268 secs]
-XX:+PrintGCTimeStamps -XX:+PrintGC:PrintGCTimeStamps可與上面兩個混合使用
輸出形式:11.851: [GC 98328K->93620K(130112K), 0.0082960 secs]

-XX:+PrintGCApplicationConcurrentTime:打印每次垃圾回收前,程序未中斷的執(zhí)行時間。可與上面混合使用。輸出形式:Application time: 0.5291524 seconds
-XX:+PrintGCApplicationStoppedTime:打印垃圾回收期間程序暫停的時間??膳c上面混合使用。輸出形式:Total time for which application threads were stopped: 0.0468229 seconds
** -XX:PrintHeapAtGC: ** 打印GC前后的詳細(xì)堆棧信息。輸出形式:

34.702: [GC {Heap before gc invocations=7:
def new generation   total 55296K, used 52568K [0x1ebd0000, 0x227d0000, 0x227d0000)
eden space 49152K,  99% used [0x1ebd0000, 0x21bce430, 0x21bd0000)
from space 6144K,  55% used [0x221d0000, 0x22527e10, 0x227d0000)
to   space 6144K,   0% used [0x21bd0000, 0x21bd0000, 0x221d0000)
tenured generation   total 69632K, used 2696K [0x227d0000, 0x26bd0000, 0x26bd0000)
the space 69632K,   3% used [0x227d0000, 0x22a720f8, 0x22a72200, 0x26bd0000)
compacting perm gen  total 8192K, used 2898K [0x26bd0000, 0x273d0000, 0x2abd0000)
   the space 8192K,  35% used [0x26bd0000, 0x26ea4ba8, 0x26ea4c00, 0x273d0000)
ro space 8192K,  66% used [0x2abd0000, 0x2b12bcc0, 0x2b12be00, 0x2b3d0000)
rw space 12288K,  46% used [0x2b3d0000, 0x2b972060, 0x2b972200, 0x2bfd0000)
34.735: [DefNew: 52568K->3433K(55296K), 0.0072126 secs] 55264K->6615K(124928K)Heap after gc invocations=8:
def new generation   total 55296K, used 3433K [0x1ebd0000, 0x227d0000, 0x227d0000)
eden space 49152K,   0% used [0x1ebd0000, 0x1ebd0000, 0x21bd0000)
  from space 6144K,  55% used [0x21bd0000, 0x21f2a5e8, 0x221d0000)
  to   space 6144K,   0% used [0x221d0000, 0x221d0000, 0x227d0000)
tenured generation   total 69632K, used 3182K [0x227d0000, 0x26bd0000, 0x26bd0000)
the space 69632K,   4% used [0x227d0000, 0x22aeb958, 0x22aeba00, 0x26bd0000)
compacting perm gen  total 8192K, used 2898K [0x26bd0000, 0x273d0000, 0x2abd0000)
   the space 8192K,  35% used [0x26bd0000, 0x26ea4ba8, 0x26ea4c00, 0x273d0000)
   ro space 8192K,  66% used [0x2abd0000, 0x2b12bcc0, 0x2b12be00, 0x2b3d0000)
   rw space 12288K,  46% used [0x2b3d0000, 0x2b972060, 0x2b972200, 0x2bfd0000)
}
, 0.0757599 secs]

-Xloggc:filename:與上面幾個配合使用,把相關(guān)日志信息記錄到文件以便分析。

7.(八)-典型配置舉例2
常見配置匯總

堆設(shè)置
-Xms:初始堆大小
-Xmx:最大堆大小
-XX:NewSize=n:設(shè)置年輕代大小
-XX:NewRatio=n:設(shè)置年輕代和年老代的比值。如:為3,表示年輕代與年老代比值為1:3,年輕代占整個年輕代年老代和的1/4
-XX:SurvivorRatio=n:年輕代中Eden區(qū)與兩個Survivor區(qū)的比值。注意Survivor區(qū)有兩個。如:3,表示Eden:Survivor=3:2,一個Survivor區(qū)占整個年輕代的1/5
-XX:MaxPermSize=n:設(shè)置持久代大小

收集器設(shè)置
-XX:+UseSerialGC:設(shè)置串行收集器
-XX:+UseParallelGC:設(shè)置并行收集器
-XX:+UseParalledlOldGC:設(shè)置并行年老代收集器
-XX:+UseConcMarkSweepGC:設(shè)置并發(fā)收集器

垃圾回收統(tǒng)計信息
-XX:+PrintGC
-XX:+PrintGCDetails
-XX:+PrintGCTimeStamps
-Xloggc:filename

并行收集器設(shè)置
-XX:ParallelGCThreads=n:設(shè)置并行收集器收集時使用的CPU數(shù)。并行收集線程數(shù)。
-XX:MaxGCPauseMillis=n:設(shè)置并行收集最大暫停時間
-XX:GCTimeRatio=n:設(shè)置垃圾回收時間占程序運行時間的百分比。公式為1/(1+n)

并發(fā)收集器設(shè)置
-XX:+CMSIncrementalMode:設(shè)置為增量模式。適用于單CPU情況。
-XX:ParallelGCThreads=n:設(shè)置并發(fā)收集器年輕代收集方式為并行收集時,使用的CPU數(shù)。并行收集線程數(shù)。

調(diào)優(yōu)總結(jié)
年輕代大小選擇
響應(yīng)時間優(yōu)先的應(yīng)用:盡可能設(shè)大,直到接近系統(tǒng)的最低響應(yīng)時間限制(根據(jù)實際情況選擇)。在此種情況下,年輕代收集發(fā)生的頻率也是最小的。同時,減少到達(dá)年老代的對象。
吞吐量優(yōu)先的應(yīng)用:盡可能的設(shè)置大,可能到達(dá)Gbit的程度。因為對響應(yīng)時間沒有要求,垃圾收集可以并行進行,一般適合8CPU以上的應(yīng)用。

年老代大小選擇
響應(yīng)時間優(yōu)先的應(yīng)用:年老代使用并發(fā)收集器,所以其大小需要小心設(shè)置,一般要考慮并發(fā)會話率和會話持續(xù)時間等一些參數(shù)。如果堆設(shè)置小了,可以會造成內(nèi)存碎片、高回收頻率以及應(yīng)用暫停而使用傳統(tǒng)的標(biāo)記清除方式;如果堆大了,則需要較長的收集時間。最優(yōu)化的方案,一般需要參考以下數(shù)據(jù)獲得:

  1. 并發(fā)垃圾收集信息
  2. 持久代并發(fā)收集次數(shù)
  3. 傳統(tǒng)GC信息
  4. 花在年輕代和年老代回收上的時間比例
    減少年輕代和年老代花費的時間,一般會提高應(yīng)用的效率

吞吐量優(yōu)先的應(yīng)用
一般吞吐量優(yōu)先的應(yīng)用都有一個很大的年輕代和一個較小的年老代。原因是,這樣可以盡可能回收掉大部分短期對象,減少中期的對象,而年老代盡存放長期存活對象。

較小堆引起的碎片問題
因為年老代的并發(fā)收集器使用標(biāo)記、清除算法,所以不會對堆進行壓縮。當(dāng)收集器回收時,他會把相鄰的空間進行合并,這樣可以分配給較大的對象。但是,當(dāng)堆空間較小時,運行一段時間以后,就會出現(xiàn)“碎片”,如果并發(fā)收集器找不到足夠的空間,那么并發(fā)收集器將會停止,然后使用傳統(tǒng)的標(biāo)記、清除方式進行回收。如果出現(xiàn)“碎片”,可能需要進行如下配置:

  1. -XX:+UseCMSCompactAtFullCollection:使用并發(fā)收集器時,開啟對年老代的壓縮。
    2.-XX:CMSFullGCsBeforeCompaction=0:上面配置開啟的情況下,這里設(shè)置多少次Full GC后,對年老代進行壓縮。

8.(九)-新一代的垃圾回收算法
垃圾回收的瓶頸
傳統(tǒng)分代垃圾回收方式,已經(jīng)在一定程度上把垃圾回收給應(yīng)用帶來的負(fù)擔(dān)降到了最小,把應(yīng)用的吞吐量推到了一個極限。但是他無法解決的一個問題,就是Full GC所帶來的應(yīng)用暫停。在一些對實時性要求很高的應(yīng)用場景下,GC暫停所帶來的請求堆積和請求失敗是無法接受的。這類應(yīng)用可能要求請求的返回時間在幾百甚至幾十毫秒以內(nèi),如果分代垃圾回收方式要達(dá)到這個指標(biāo),只能把最大堆的設(shè)置限制在一個相對較小范圍內(nèi),但是這樣有限制了應(yīng)用本身的處理能力,同樣也是不可接收的。

分代垃圾回收方式確實也考慮了實時性要求而提供了并發(fā)回收器,支持最大暫停時間的設(shè)置,但是受限于分代垃圾回收的內(nèi)存劃分模型,其效果也不是很理想。

為了達(dá)到實時性的要求(其實Java語言最初的設(shè)計也是在嵌入式系統(tǒng)上的),一種新垃圾回收方式呼之欲出,它既支持短的暫停時間,又支持大的內(nèi)存空間分配。可以很好的解決傳統(tǒng)分代方式帶來的問題。

增量收集的演進
增量收集的方式在理論上可以解決傳統(tǒng)分代方式帶來的問題。增量收集把對堆空間劃分成一系列內(nèi)存塊,使用時,先使用其中一部分(不會全部用完),垃圾收集時把之前用掉的部分中的存活對象再放到后面沒有用的空間中,這樣可以實現(xiàn)一直邊使用邊收集的效果,避免了傳統(tǒng)分代方式整個使用完了再暫停的回收的情況。

當(dāng)然,傳統(tǒng)分代收集方式也提供了并發(fā)收集,但是他有一個很致命的地方,就是把整個堆做為一個內(nèi)存塊,這樣一方面會造成碎片(無法壓縮),另一方面他的每次收集都是對整個堆的收集,無法進行選擇,在暫停時間的控制上還是很弱。而增量方式,通過內(nèi)存空間的分塊,恰恰可以解決上面問題。

Garbage Firest(G1)
這部分的內(nèi)容主要參考這里,這篇文章算是對G1算法論文的解讀。我也沒加什么東西了。

目標(biāo)
從設(shè)計目標(biāo)看G1完全是為了大型應(yīng)用而準(zhǔn)備的。
支持很大的堆
高吞吐量
--支持多CPU和垃圾回收線程
--在主線程暫停的情況下,使用并行收集
--在主線程運行的情況下,使用并發(fā)收集
實時目標(biāo):可配置在N毫秒內(nèi)最多只占用M毫秒的時間進行垃圾回收
當(dāng)然G1要達(dá)到實時性的要求,相對傳統(tǒng)的分代回收算法,在性能上會有一些損失。

算法詳解

image.png

G1可謂博采眾家之長,力求到達(dá)一種完美。他吸取了增量收集優(yōu)點,把整個堆劃分為一個一個等大小的區(qū)域(region)。內(nèi)存的回收和劃分都以region為單位;同時,他也吸取了CMS的特點,把這個垃圾回收過程分為幾個階段,分散一個垃圾回收過程;而且,G1也認(rèn)同分代垃圾回收的思想,認(rèn)為不同對象的生命周期不同,可以采取不同收集方式,因此,它也支持分代的垃圾回收。為了達(dá)到對回收時間的可預(yù)計性,G1在掃描了region以后,對其中的活躍對象的大小進行排序,首先會收集那些活躍對象小的region,以便快速回收空間(要復(fù)制的活躍對象少了),因為活躍對象小,里面可以認(rèn)為多數(shù)都是垃圾,所以這種方式被稱為Garbage First(G1)的垃圾回收算法,即:垃圾優(yōu)先的回收。

回收步驟:

初始標(biāo)記(Initial Marking)
G1對于每個region都保存了兩個標(biāo)識用的bitmap,一個為previous marking bitmap,一個為next marking bitmap,bitmap中包含了一個bit的地址信息來指向?qū)ο蟮钠鹗键c。

開始Initial Marking之前,首先并發(fā)的清空next marking bitmap,然后停止所有應(yīng)用線程,并掃描標(biāo)識出每個region中root可直接訪問到的對象,將region中top的值放入next top at mark start(TAMS)中,之后恢復(fù)所有應(yīng)用線程。

觸發(fā)這個步驟執(zhí)行的條件為:
G1定義了一個JVM Heap大小的百分比的閥值,稱為h,另外還有一個H,H的值為(1-h)*Heap Size,目前這個h的值是固定的,后續(xù)G1也許會將其改為動態(tài)的,根據(jù)jvm的運行情況來動態(tài)的調(diào)整,在分代方式下,G1還定義了一個u以及soft limit,soft limit的值為H-u*Heap Size,當(dāng)Heap中使用的內(nèi)存超過了soft limit值時,就會在一次clean up執(zhí)行完畢后在應(yīng)用允許的GC暫停時間范圍內(nèi)盡快的執(zhí)行此步驟;
在pure方式下,G1將marking與clean up組成一個環(huán),以便clean up能充分的使用marking的信息,當(dāng)clean up開始回收時,首先回收能夠帶來最多內(nèi)存空間的regions,當(dāng)經(jīng)過多次的clean up,回收到?jīng)]多少空間的regions時,G1重新初始化一個新的marking與clean up構(gòu)成的環(huán)。

并發(fā)標(biāo)記(Concurrent Marking)
按照之前Initial Marking掃描到的對象進行遍歷,以識別這些對象的下層對象的活躍狀態(tài),對于在此期間應(yīng)用線程并發(fā)修改的對象的以來關(guān)系則記錄到remembered set logs中,新創(chuàng)建的對象則放入比top值更高的地址區(qū)間中,這些新創(chuàng)建的對象默認(rèn)狀態(tài)即為活躍的,同時修改top值。

最終標(biāo)記暫停(Final Marking Pause)
當(dāng)應(yīng)用線程的remembered set logs未滿時,是不會放入filled RS buffers中的,在這樣的情況下,這些remebered set logs中記錄的card的修改就會被更新了,因此需要這一步,這一步要做的就是把應(yīng)用線程中存在的remembered set logs的內(nèi)容進行處理,并相應(yīng)的修改remembered sets,這一步需要暫停應(yīng)用,并行的運行。

存活對象計算及清除(Live Data Counting and Cleanup)
值得注意的是,在G1中,并不是說Final Marking Pause執(zhí)行完了,就肯定執(zhí)行Cleanup這步的,由于這步需要暫停應(yīng)用,G1為了能夠達(dá)到準(zhǔn)實時的要求,需要根據(jù)用戶指定的最大的GC造成的暫停時間來合理的規(guī)劃什么時候執(zhí)行Cleanup,另外還有幾種情況也是會觸發(fā)這個步驟的執(zhí)行的:

G1采用的是復(fù)制方法來進行收集,必須保證每次的”to space”的空間都是夠的,因此G1采取的策略是當(dāng)已經(jīng)使用的內(nèi)存空間達(dá)到了H時,就執(zhí)行Cleanup這個步驟;

對于full-young和partially-young的分代模式的G1而言,則還有情況會觸發(fā)Cleanup的執(zhí)行,full-young模式下,G1根據(jù)應(yīng)用可接受的暫停時間、回收young regions需要消耗的時間來估算出一個yound regions的數(shù)量值,當(dāng)JVM中分配對象的young regions的數(shù)量達(dá)到此值時,Cleanup就會執(zhí)行;partially-young模式下,則會盡量頻繁的在應(yīng)用可接受的暫停時間范圍內(nèi)執(zhí)行Cleanup,并最大限度的去執(zhí)行non-young regions的Cleanup。

展望
以后JVM的調(diào)優(yōu)或許跟多需要針對G1算法進行調(diào)優(yōu)了。

9.(十)-調(diào)優(yōu)方法
Jconsole,jProfile,VisualVM
Jconsole : jdk自帶,功能簡單,但是可以在系統(tǒng)有一定負(fù)荷的情況下使用。對垃圾回收算法有很詳細(xì)的跟蹤。詳細(xì)說明參考這里

JProfiler:商業(yè)軟件,需要付費。功能強大。詳細(xì)說明參考這里

VisualVM:JDK自帶,功能強大,與JProfiler類似。推薦。

如何調(diào)優(yōu)
觀察內(nèi)存釋放情況、集合類檢查、對象樹
上面這些調(diào)優(yōu)工具都提供了強大的功能,但是總的來說一般分為以下幾類功能

堆信息查看

image.png

可查看堆空間大小分配(年輕代、年老代、持久代分配)
提供即時的垃圾回收功能
垃圾監(jiān)控(長時間監(jiān)控回收情況)

image.png

查看堆內(nèi)類、對象信息查看:數(shù)量、類型等

image.png

對象引用情況查看

有了堆信息查看方面的功能,我們一般可以順利解決以下問題:
--年老代年輕代大小劃分是否合理
--內(nèi)存泄漏
--垃圾回收算法設(shè)置是否合理

線程監(jiān)控

image.png

線程信息監(jiān)控:系統(tǒng)線程數(shù)量。
線程狀態(tài)監(jiān)控:各個線程都處在什么樣的狀態(tài)下

image.png

Dump線程詳細(xì)信息:查看線程內(nèi)部運行情況
死鎖檢查

熱點分析

image.png

CPU熱點:檢查系統(tǒng)哪些方法占用的大量CPU時間
內(nèi)存熱點:檢查哪些對象在系統(tǒng)中數(shù)量最大(一定時間內(nèi)存活對象和銷毀對象一起統(tǒng)計)

這兩個東西對于系統(tǒng)優(yōu)化很有幫助。我們可以根據(jù)找到的熱點,有針對性的進行系統(tǒng)的瓶頸查找和進行系統(tǒng)優(yōu)化,而不是漫無目的的進行所有代碼的優(yōu)化。

快照
快照是系統(tǒng)運行到某一時刻的一個定格。在我們進行調(diào)優(yōu)的時候,不可能用眼睛去跟蹤所有系統(tǒng)變化,依賴快照功能,我們就可以進行系統(tǒng)兩個不同運行時刻,對象(或類、線程等)的不同,以便快速找到問題

舉例說,我要檢查系統(tǒng)進行垃圾回收以后,是否還有該收回的對象被遺漏下來的了。那么,我可以在進行垃圾回收前后,分別進行一次堆情況的快照,然后對比兩次快照的對象情況。

內(nèi)存泄漏檢查
內(nèi)存泄漏是比較常見的問題,而且解決方法也比較通用,這里可以重點說一下,而線程、熱點方面的問題則是具體問題具體分析了。

內(nèi)存泄漏一般可以理解為系統(tǒng)資源(各方面的資源,堆、棧、線程等)在錯誤使用的情況下,導(dǎo)致使用完畢的資源無法回收(或沒有回收),從而導(dǎo)致新的資源分配請求無法完成,引起系統(tǒng)錯誤。

內(nèi)存泄漏對系統(tǒng)危害比較大,因為他可以直接導(dǎo)致系統(tǒng)的崩潰。

需要區(qū)別一下,內(nèi)存泄漏和系統(tǒng)超負(fù)荷兩者是有區(qū)別的,雖然可能導(dǎo)致的最終結(jié)果是一樣的。內(nèi)存泄漏是用完的資源沒有回收引起錯誤,而系統(tǒng)超負(fù)荷則是系統(tǒng)確實沒有那么多資源可以分配了(其他的資源都在使用)。

年老代堆空間被占滿
異常: java.lang.OutOfMemoryError: Java heap space
說明:

image.png

這是最典型的內(nèi)存泄漏方式,簡單說就是所有堆空間都被無法回收的垃圾對象占滿,虛擬機無法再在分配新空間。

如上圖所示,這是非常典型的內(nèi)存泄漏的垃圾回收情況圖。所有峰值部分都是一次垃圾回收點,所有谷底部分表示是一次垃圾回收后剩余的內(nèi)存。連接所有谷底的點,可以發(fā)現(xiàn)一條由底到高的線,這說明,隨時間的推移,系統(tǒng)的堆空間被不斷占滿,最終會占滿整個堆空間。因此可以初步認(rèn)為系統(tǒng)內(nèi)部可能有內(nèi)存泄漏。(上面的圖僅供示例,在實際情況下收集數(shù)據(jù)的時間需要更長,比如幾個小時或者幾天)

解決:
這種方式解決起來也比較容易,一般就是根據(jù)垃圾回收前后情況對比,同時根據(jù)對象引用情況(常見的集合對象引用)分析,基本都可以找到泄漏點。

持久代被占滿
異常:java.lang.OutOfMemoryError: PermGen space
說明:
Perm空間被占滿。無法為新的class分配存儲空間而引發(fā)的異常。這個異常以前是沒有的,但是在Java反射大量使用的今天這個異常比較常見了。主要原因就是大量動態(tài)反射生成的類不斷被加載,最終導(dǎo)致Perm區(qū)被占滿。

更可怕的是,不同的classLoader即便使用了相同的類,但是都會對其進行加載,相當(dāng)于同一個東西,如果有N個classLoader那么他將會被加載N次。因此,某些情況下,這個問題基本視為無解。當(dāng)然,存在大量classLoader和大量反射類的情況其實也不多。

解決:
1. -XX:MaxPermSize=16m
2. 換用JDK。比如JRocket。

堆棧溢出
異常:java.lang.StackOverflowError
說明:這個就不多說了,一般就是遞歸沒返回,或者循環(huán)調(diào)用造成

線程堆棧滿
異常:Fatal: Stack size too small
說明:java中一個線程的空間大小是有限制的。JDK5.0以后這個值是1M。與這個線程相關(guān)的數(shù)據(jù)將會保存在其中。但是當(dāng)線程空間滿了以后,將會出現(xiàn)上面異常。
解決:增加線程棧大小。-Xss2m。但這個配置無法解決根本問題,還要看代碼部分是否有造成泄漏的部分。

系統(tǒng)內(nèi)存被占滿
異常:java.lang.OutOfMemoryError: unable to create new native thread
說明:
這個異常是由于操作系統(tǒng)沒有足夠的資源來產(chǎn)生這個線程造成的。系統(tǒng)創(chuàng)建線程時,除了要在Java堆中分配內(nèi)存外,操作系統(tǒng)本身也需要分配資源來創(chuàng)建線程。因此,當(dāng)線程數(shù)量大到一定程度以后,堆中或許還有空間,但是操作系統(tǒng)分配不出資源來了,就出現(xiàn)這個異常了。

分配給Java虛擬機的內(nèi)存愈多,系統(tǒng)剩余的資源就越少,因此,當(dāng)系統(tǒng)內(nèi)存固定時,分配給Java虛擬機的內(nèi)存越多,那么,系統(tǒng)總共能夠產(chǎn)生的線程也就越少,兩者成反比的關(guān)系。同時,可以通過修改-Xss來減少分配給單個線程的空間,也可以減少系統(tǒng)總共生產(chǎn)的線程數(shù)。

解決:
1.重新設(shè)計系統(tǒng)減少線程數(shù)量。
2.線程數(shù)量不能減少的情況下,通過-Xss減小單個線程大小。以便能生產(chǎn)更多的線程。

10.(十一)-反思
垃圾回收的悖論
所謂“成也蕭何敗蕭何”。Java的垃圾回收確實帶來了很多好處,為開發(fā)帶來了便利。但是在一些高性能、高并發(fā)的情況下,垃圾回收確成為了制約Java應(yīng)用的瓶頸。目前JDK的垃圾回收算法,始終無法解決垃圾回收時的暫停問題,因為這個暫停嚴(yán)重影響了程序的相應(yīng)時間,造成擁塞或堆積。這也是后續(xù)JDK增加G1算法的一個重要原因。

當(dāng)然,上面是從技術(shù)角度出發(fā)解決垃圾回收帶來的問題,但是從系統(tǒng)設(shè)計方面我們就需要問一下了:
我們需要分配如此大的內(nèi)存空間給應(yīng)用嗎?
我們是否能夠通過有效使用內(nèi)存而不是通過擴大內(nèi)存的方式來設(shè)計我們的系統(tǒng)呢?
我們的內(nèi)存中都放了什么內(nèi)存中需要放什么呢?個人認(rèn)為,內(nèi)存中需要放的是你的應(yīng)用需要在不久的將來再次用到到的東西。想想看,如果你在將來不用這些東西,何必放內(nèi)存呢?放文件、數(shù)據(jù)庫不是更好?這些東西一般包括:

  1. 系統(tǒng)運行時業(yè)務(wù)相關(guān)的數(shù)據(jù)。比如web應(yīng)用中的session、即時消息的session等。這些數(shù)據(jù)一般在一個用戶訪問周期或者一個使用過程中都需要存在。
  2. 緩存。緩存就比較多了,你所要快速訪問的都可以放這里面。其實上面的業(yè)務(wù)數(shù)據(jù)也可以理解為一種緩存。
  3. 線程。

因此,我們是不是可以這么認(rèn)為,如果我們不把業(yè)務(wù)數(shù)據(jù)和緩存放在JVM中,或者把他們獨立出來,那么Java應(yīng)用使用時所需的內(nèi)存將會大大減少,同時垃圾回收時間也會相應(yīng)減少。
我認(rèn)為這是可能的。

解決之道

數(shù)據(jù)庫、文件系統(tǒng)
把所有數(shù)據(jù)都放入數(shù)據(jù)庫或者文件系統(tǒng),這是一種最為簡單的方式。在這種方式下,Java應(yīng)用的內(nèi)存基本上等于處理一次峰值并發(fā)請求所需的內(nèi)存。數(shù)據(jù)的獲取都在每次請求時從數(shù)據(jù)庫和文件系統(tǒng)中獲取。也可以理解為,一次業(yè)務(wù)訪問以后,所有對象都可以進行回收了。

這是一種內(nèi)存使用最有效的方式,但是從應(yīng)用角度來說,這種方式很低效。

內(nèi)存-硬盤映射
上面的問題是因為我們使用了文件系統(tǒng)帶來了低效。但是如果我們不是讀寫硬盤,而是寫內(nèi)存的話效率將會提高很多。

數(shù)據(jù)庫和文件系統(tǒng)都是實實在在進行了持久化,但是當(dāng)我們并不需要這樣持久化的時候,我們可以做一些變通——把內(nèi)存當(dāng)硬盤使。

內(nèi)存-硬盤映射很好很強大,既用了緩存又對Java應(yīng)用的內(nèi)存使用又沒有影響。Java應(yīng)用還是Java應(yīng)用,他只知道讀寫的還是文件,但是實際上是內(nèi)存。

這種方式兼得的Java應(yīng)用與緩存兩方面的好處。memcached的廣泛使用也正是這一類的代表。

同一機器部署多個JVM
這也是一種很好的方式,可以分為縱拆和橫拆。縱拆可以理解為把Java應(yīng)用劃分為不同模塊,各個模塊使用一個獨立的Java進程。而橫拆則是同樣功能的應(yīng)用部署多個JVM。

通過部署多個JVM,可以把每個JVM的內(nèi)存控制一個垃圾回收可以忍受的范圍內(nèi)即可。但是這相當(dāng)于進行了分布式的處理,其額外帶來的復(fù)雜性也是需要評估的。另外,也有支持分布式的這種JVM可以考慮,不要要錢哦:)

程序控制的對象生命周期
這種方式是理想當(dāng)中的方式,目前的虛擬機還沒有,純屬假設(shè)。即:考慮由編程方式配置哪些對象在垃圾收集過程中可以直接跳過,減少垃圾回收線程遍歷標(biāo)記的時間。

這種方式相當(dāng)于在編程的時候告訴虛擬機某些對象你可以在*時間后在進行收集或者由代碼標(biāo)識可以收集了(類似C、C++),在這之前你即便去遍歷他也是沒有效果的,他肯定是還在被引用的。

這種方式如果JVM可以實現(xiàn),個人認(rèn)為將是一個飛躍,Java即有了垃圾回收的優(yōu)勢,又有了C、C++對內(nèi)存的可控性。

線程分配
Java的阻塞式的線程模型基本上可以拋棄了,目前成熟的NIO框架也比較多了。阻塞式IO帶來的問題是線程數(shù)量的線性增長,而NIO則可以轉(zhuǎn)換成為常數(shù)線程。因此,對于服務(wù)端的應(yīng)用而言,NIO還是唯一選擇。不過,JDK7中為我們帶來的AIO是否能讓人眼前一亮呢?我們拭目以待。

其他的JDK
本文說的都是Sun的JDK,目前常見的JDK還有JRocket和IBM的JDK。其中JRocket在IO方面比Sun的高很多,不過Sun JDK6.0以后提高也很大。而且JRocket在垃圾回收方面,也具有優(yōu)勢,其可設(shè)置垃圾回收的最大暫停時間也是很吸引人的。不過,系統(tǒng)Sun的G1實現(xiàn)以后,在這方面會有一個質(zhì)的飛躍。

十、分布式系統(tǒng)設(shè)計原理與方案

一直在思考分布式系統(tǒng)設(shè)計的問題,業(yè)務(wù)對象原封不動的情況下部署在客戶端和服務(wù)器端,可以根據(jù)配置文件選擇是連接服務(wù)器還是連接本地的數(shù)據(jù)庫,這個問題讓我絞盡腦汁,我總是設(shè)想的客戶端與服務(wù)器端通信的方式是最低端的Socket?;藘蓚€晚上研究CSLA.NET框架關(guān)于數(shù)據(jù)門戶這塊代碼,才發(fā)現(xiàn)問題的關(guān)鍵所在:客戶端與服務(wù)器端通信不能采用最低端的Socket,而要用高端的WebService、.NET Remoting或者是自己定義一種協(xié)議等,只要它們支持客戶端直接根據(jù)服務(wù)器端的服務(wù)URL、類名、方法名和方法參數(shù)四個信息就可以調(diào)用服務(wù)器對應(yīng)的類和方法就行。

說明:本文中所表達(dá)的思想與CSLA.NET有很大區(qū)別,不要看了本文就以為是CSLA.NET的設(shè)計思想,也不要以為本文錯誤的解釋了CSLA.NET,這不是一篇介紹CSLA.NET的文章,但純思想上它們是相同的。

分布式系統(tǒng)的部署
平常我們都說三層架構(gòu)

接下來我要把三層變的更簡單點,兩層,數(shù)據(jù)訪問層合并到業(yè)務(wù)層,統(tǒng)稱為業(yè)務(wù)層,因為我們面對的問題不是分層的問題,而是分布式系統(tǒng)中各層應(yīng)該怎么部署的問題。在CSLA.NET書中也說到業(yè)務(wù)層和數(shù)據(jù)訪問層放到同一臺機器上可以提高性能和容錯性。因此他們倆的合并不影響分布式系統(tǒng)的部署。

不過要解釋的是數(shù)據(jù)庫系統(tǒng)(CSLA.NET中說的數(shù)據(jù)存儲和管理層)并沒有考慮到三層中來,也就是它不包含在數(shù)據(jù)訪問層中,如果把它算進來,那么它是在數(shù)據(jù)訪問層之下單獨存在的。

綜上,在分布式系統(tǒng)部署角度考慮的分層實際是三層:界面層、業(yè)務(wù)層(包含數(shù)據(jù)訪問層的業(yè)務(wù)層)、數(shù)據(jù)存儲層。

下面舉例說明可能的部署情景,帶陰影的框框表示一臺機器,虛線框表示根據(jù)使用場合可有可無,虛橫線表示從此處劃開單獨出服務(wù)器。在B/S應(yīng)用中,Web瀏覽器為客戶端,其他全部為服務(wù)器。在C/S應(yīng)用中,處在最上層的界面層+業(yè)務(wù)層為客戶端,其他為服務(wù)器。

非分布式系統(tǒng)的部署

單機版
兩三臺機器

分布式系統(tǒng)的部署

分布式的Web系統(tǒng)
分布式的C/S系統(tǒng)

有幾點要說明:
1. 客戶端上的驗證等業(yè)務(wù)邏輯是不可信的,因此任何一種部署都需要服務(wù)器端包含業(yè)務(wù)層;
2. 為了開發(fā)、維護和部署中的高度可伸縮性,圖中的各業(yè)務(wù)層所包含的代碼都是一模一樣的;
3. 因為第2點,所以我遇到了業(yè)務(wù)層的同一個操作是與其他機器上的業(yè)務(wù)層通信還是訪問數(shù)據(jù)庫這個難題。

解決業(yè)務(wù)層的數(shù)據(jù)訪問問題
1.這個問題是關(guān)鍵問題,也就是上面幾點說明中的第3個問題,為了解決這個問題我們引入數(shù)據(jù)門戶的概念。
2.下面以WebService為例說明:界面層訪問本機的業(yè)務(wù)對象的增刪改查中的“查”方法時,跳過數(shù)據(jù)庫的查詢操作,訪問另一臺機器中的同一個業(yè)務(wù)對象類的“查”方法。

image.png

以上是向另一臺機器發(fā)送請求,該請求并不直接調(diào)用另一臺機器上的業(yè)務(wù)對象類的“查”方法,而是將要調(diào)用的業(yè)務(wù)對象和方法參數(shù)信息轉(zhuǎn)為一個“二進制包”,作為參數(shù)去調(diào)用另一臺機器上通用的“查”方法,另一臺機器上的“查”方法再解開這個包,然后去調(diào)用解開的包中所表示的業(yè)務(wù)對象類型,下面的靜態(tài)圖是另一臺機器接受到請求后的工作。

image.png

又有些說明:
1. 關(guān)于原理都已在圖中做了描述,不另寫大段文字解釋了;
2. 上面兩個圖中,除了“實際業(yè)務(wù)對象類”以外的部分全部屬于架構(gòu)或者框架部分;
3. 如果用OO的思想去審查上面的兩個圖,你一定會為這糟糕的設(shè)計而抱怨,這里只是為了盡可能簡單的表述分布式系統(tǒng)的工作原理,你可以采用策略模式使數(shù)據(jù)門戶不改變的情況下適應(yīng)各種請求響應(yīng)場合,采用工廠模式實現(xiàn)不同的請求響應(yīng)場合的切換。

關(guān)于數(shù)據(jù)庫的分布
  為了解決數(shù)據(jù)庫服務(wù)器的負(fù)擔(dān),我們可能希望把數(shù)據(jù)分布存儲在多個服務(wù)器上,我設(shè)想的數(shù)據(jù)庫分布方案是,各服務(wù)器上的數(shù)據(jù)庫在結(jié)構(gòu)上一模一樣,而表里的數(shù)據(jù)存儲到不同服務(wù)器上,這樣數(shù)據(jù)訪問層在查數(shù)據(jù)的時候分別向所有數(shù)據(jù)庫服務(wù)器發(fā)送同樣的sql命令,然后數(shù)據(jù)訪問層得到數(shù)據(jù)后整合,這樣減輕每臺服務(wù)器的工作量。亦或者根據(jù)表里的某個代表性的字段(如:省份)分布數(shù)據(jù)到不同服務(wù)器。

十一、分布式系統(tǒng)設(shè)計系列 -- 基本原理及高可用策略

【分布式系統(tǒng)中的概念】
三元組
其實,分布式系統(tǒng)說白了,就是很多機器組成的集群,靠彼此之間的網(wǎng)絡(luò)通信,擔(dān)當(dāng)?shù)慕巧赡懿煌?,共同完成同一個事情的系統(tǒng)。如果按”實體“來劃分的話,就是如下這幾種:
1、節(jié)點 -- 系統(tǒng)中按照協(xié)議完成計算工作的一個邏輯實體,可能是執(zhí)行某些工作的進程或機器
2、網(wǎng)絡(luò) -- 系統(tǒng)的數(shù)據(jù)傳輸通道,用來彼此通信。通信是具有方向性的。
3、存儲 -- 系統(tǒng)中持久化數(shù)據(jù)的數(shù)據(jù)庫或者文件存儲。

image.png

狀態(tài)特性
各個節(jié)點的狀態(tài)可以是“無狀態(tài)”或者“有狀態(tài)的”.
一般認(rèn)為,節(jié)點是偏計算和通信的模塊,一般是無狀態(tài)的。這類應(yīng)用一般不會存儲自己的中間狀態(tài)信息,比如Nginx,一般情況下是轉(zhuǎn)發(fā)請求而已,不會存儲中間信息。另一種“有狀態(tài)”的,如mysql等數(shù)據(jù)庫,狀態(tài)和數(shù)據(jù)全部持久化到磁盤等介質(zhì)。
“無狀態(tài)”的節(jié)點一般我們認(rèn)為是可隨意重啟的,因為重啟后只需要立刻工作就好?!坝袪顟B(tài)”的則不同,需要先讀取持久化的數(shù)據(jù),才能開始服務(wù)。所以,“無狀態(tài)”的節(jié)點一般是可以隨意擴展的,“有狀態(tài)”的節(jié)點需要一些控制協(xié)議來保證擴展。

系統(tǒng)異常
異常,可認(rèn)為是節(jié)點因為某種原因不能工作,此為節(jié)點異常。還有因為網(wǎng)絡(luò)原因,臨時、永久不能被其他節(jié)點所訪問,此為網(wǎng)絡(luò)異常。在分布式系統(tǒng)中,要有對異常的處理,保證集群的正常工作。

【分布式系統(tǒng)與單節(jié)點的不同】
1、從linux write()系統(tǒng)調(diào)用說起
眾所周知,在unix/linux/mac(類Unix)環(huán)境下,兩個機器通信,最常用的就是通過socket連接對方。傳輸數(shù)據(jù)的話,無非就是調(diào)用write()這個系統(tǒng)調(diào)用,把一段內(nèi)存緩沖區(qū)發(fā)出去。但是可以進一步想一下,write()之后能確認(rèn)對方收到了這些數(shù)據(jù)嗎?

答案肯定是不能,原因就是發(fā)送數(shù)據(jù)需要走內(nèi)核->網(wǎng)卡->鏈路->對端網(wǎng)卡->內(nèi)核,這一路徑太長了,所以只能是異步操作。write()把數(shù)據(jù)寫入內(nèi)核緩沖區(qū)之后就返回到應(yīng)用層了,具體后面何時發(fā)送、怎么發(fā)送、TCP怎么做滑動窗口、流控都是tcp/ip協(xié)議棧內(nèi)核的事情了。

所以在應(yīng)用層,能確認(rèn)對方受到了消息只能是對方應(yīng)用返回數(shù)據(jù),邏輯確認(rèn)了這次發(fā)送才認(rèn)為是成功的。這就區(qū)別與單系統(tǒng)編程,大部分系統(tǒng)調(diào)用、庫調(diào)用只要返回了就說明已經(jīng)確認(rèn)完成了。

2、TCP/IP協(xié)議是“不可靠”的
教科書上明確寫明了互聯(lián)網(wǎng)是不可靠的,TCP實現(xiàn)了可靠傳輸。何來“不可靠”呢?先來看一下網(wǎng)絡(luò)交互的例子,有A、B兩個節(jié)點,之間通過TCP連接,現(xiàn)在A、B都想確認(rèn)自己發(fā)出的任何一條消息都能被對方接收并反饋,于是開始了如下操作:
A->B發(fā)送數(shù)據(jù),然后A需要等待B收到數(shù)據(jù)的確認(rèn),B收到數(shù)據(jù)后發(fā)送確認(rèn)消息給A,然后B需要等待A收到數(shù)據(jù)的確認(rèn),A收到B的數(shù)據(jù)確認(rèn)消息后再次發(fā)送確認(rèn)消息給B,然后A又去需要等待B收到的確認(rèn)。。。死循環(huán)了!!

其實,這就是著名的“拜占庭將軍”問題:http://baike.baidu.com/link?url=6iPrbRxHLOo9an1hT-s6DvM5kAoq7RxclIrzgrS34W1fRq1h507RDWJOxfhkDOcihVFRZ2c7ybCkUosWQeUoS_

所以,通信雙方是“不可能”同時確認(rèn)對方受到了自己的信息。而教科書上定義的其實是指“單向”通信是成立的,比如A向B發(fā)起Http調(diào)用,收到了HttpCode 200的響應(yīng)包,這只能確認(rèn),A確認(rèn)B收到了自己的請求,并且B正常處理了,不能確認(rèn)的是B確認(rèn)A受到了它的成功的消息。

3、不可控的狀態(tài)
在單系統(tǒng)編程中,我們對系統(tǒng)狀態(tài)是非??煽氐?。比如函數(shù)調(diào)用、邏輯運算,要么成功,要么失敗,因為這些操作被框在一個機器內(nèi)部,cpu/總線/內(nèi)存都是可以快速得到反饋的。開發(fā)者可以針對這兩個狀態(tài)很明確的做出程序上的判斷和后續(xù)的操作。

而在分布式的網(wǎng)絡(luò)環(huán)境下,這就變得微妙了。比如一次rpc、http調(diào)用,可能成功、失敗,還有可能是“超時”,這就比前者的狀態(tài)多了一個不可控因素,導(dǎo)致后面的代碼不是很容易做出判斷。試想一下,用A用支付寶向B轉(zhuǎn)了一大筆錢,當(dāng)他按下“確認(rèn)”后,界面上有個圈在轉(zhuǎn)啊轉(zhuǎn),然后顯示請求超時了,然后A就抓狂了,不知道到底錢轉(zhuǎn)沒轉(zhuǎn)過去,開始確認(rèn)自己的賬戶、確認(rèn)B的賬戶、打電話找客服等等。

所以分布式環(huán)境下,我們的其實要時時刻刻考慮面對這種不可控的“第三狀態(tài)”設(shè)計開發(fā),這也是挑戰(zhàn)之一。

4、視”異?!盀椤闭!?br> 單系統(tǒng)下,進程/機器的異常概率十分小。即使出現(xiàn)了問題,可以通過人工干預(yù)重啟、遷移等手段恢復(fù)。但在分布式環(huán)境下,機器上千臺,每幾分鐘都可能出現(xiàn)宕機、死機、網(wǎng)絡(luò)斷網(wǎng)等異常,出現(xiàn)的概率很大。所以,這種環(huán)境下,進程core掉、機器掛掉都是需要我們在編程中認(rèn)為隨時可能出現(xiàn)的,這樣才能使我們整個系統(tǒng)健壯起來,所以”容錯“是基本需求。

異常可以分為如下幾類:
節(jié)點錯誤:
一般是由于應(yīng)用導(dǎo)致,一些coredump和系統(tǒng)錯誤觸發(fā),一般重新服務(wù)后可恢復(fù)。
硬件錯誤:
由于磁盤或者內(nèi)存等硬件設(shè)備導(dǎo)致某節(jié)點不能服務(wù),需要人工干預(yù)恢復(fù)。
網(wǎng)絡(luò)錯誤:
由于點對點的網(wǎng)絡(luò)抖動,暫時的訪問錯誤,一般拓?fù)浞€(wěn)定后或流量減小可以恢復(fù)。
網(wǎng)絡(luò)分化:
網(wǎng)絡(luò)中路由器、交換機錯誤導(dǎo)致網(wǎng)絡(luò)不可達(dá),但是網(wǎng)絡(luò)兩邊都正常,這類錯誤比較難恢復(fù),并且需要在開發(fā)時特別處理?!具@種情況也會比較前面的問題較難處理】

image.png

【分布式系統(tǒng)特性】
CAP是分布式系統(tǒng)里最著名的理論,wiki百科如下
Consistency (all nodes see the same data at the same time)
Availability (a guarantee that every request receives a response about whether it was successful or failed)
Partition tolerance (the system continues to operate despite arbitrary message loss or failure of part of the system)
早些時候,國外的大牛已經(jīng)證明了CAP三者是不能兼得,很多實踐也證明了。
本人就不挑戰(zhàn)權(quán)威了,感興趣的同學(xué)可以自己Google。本人以自己的觀點總結(jié)了一下:

一致性
描述當(dāng)前所有節(jié)點存儲數(shù)據(jù)的統(tǒng)一模型,分為強一致性和弱一致性:
強一致性描述了所有節(jié)點的數(shù)據(jù)高度一致,無論從哪個節(jié)點讀取,都是一樣的。無需擔(dān)心同一時刻會獲得不同的數(shù)據(jù)。是級別最高的,實現(xiàn)的代價比較高

image.png

弱一致性又分為單調(diào)一致性和最終一致性:
1、單調(diào)一致性強調(diào)數(shù)據(jù)是按照時間的新舊,單調(diào)向最新的數(shù)據(jù)靠近,不會回退,如:數(shù)據(jù)存在三個版本v1->v2->v3,獲取只能向v3靠近(如取到的是v2,就不可能再次獲得v1)
2、最終一致性強調(diào)數(shù)據(jù)經(jīng)過一個時間窗口之后,只要多嘗試幾次,最終的狀態(tài)是一致的,是最新的數(shù)據(jù)

image.png

強一致性的場景,就好像交易系統(tǒng),存取錢的+/-操作必須是馬上一致的,否則會令很多人誤解。
弱一致性的場景,大部分就像web互聯(lián)網(wǎng)的模式,比如發(fā)了一條微博,改了某些配置,可能不會馬上生效,但刷新幾次后就可以看到了,其實弱一致性就是在系統(tǒng)上通過業(yè)務(wù)可接受的方式換取了一些系統(tǒng)的低復(fù)雜度和可用性。

可用性
保證系統(tǒng)的正??蛇\行性,在請求方看來,只要發(fā)送了一個請求,就可以得到恢復(fù)無論成功還是失?。ú粫瑫r)!

分區(qū)容忍性
在系統(tǒng)某些節(jié)點或網(wǎng)絡(luò)有異常的情況下,系統(tǒng)依舊可以繼續(xù)服務(wù)。
這通常是有負(fù)載均衡和副本來支撐的。例如計算模塊異??赏ㄟ^負(fù)載均衡引流到其他平行節(jié)點,存儲模塊通過其他幾點上的副本來對外提供服務(wù)。

擴展性
擴展性是融合在CAP里面的特性,我覺得此處可以單獨講一下。擴展性直接影響了分布式系統(tǒng)的好壞,系統(tǒng)開發(fā)初期不可能把系統(tǒng)的容量、峰值都考慮到,后期肯定牽扯到擴容,而如何做到快而不太影響業(yè)務(wù)的擴容策略,也是需要考慮的。(后面在介紹數(shù)據(jù)分布時會著重討論這個問題)

【分布式系統(tǒng)設(shè)計策略】
1、重試機制
一般情況下,寫一段網(wǎng)絡(luò)交互的代碼,發(fā)起rpc或者h(yuǎn)ttp,都會遇到請求超時而失敗情況??赡苁蔷W(wǎng)絡(luò)抖動(暫時的網(wǎng)絡(luò)變更導(dǎo)致包不可達(dá),比如拓?fù)渥兏?或者對端掛掉。這時一般處理邏輯是將請求包在一個重試循環(huán)塊里,如下:

int retry = 3;  
while(!request() && retry--)  
    sched_yield();   // or usleep(100) 

此種模式可以防止網(wǎng)絡(luò)暫時的抖動,一般停頓時間很短,并重試多次后,請求成功!但不能防止對端長時間不能連接(網(wǎng)絡(luò)問題或進程問題)

2、心跳機制
心跳顧名思義,就是以固定的頻率向其他節(jié)點匯報當(dāng)前節(jié)點狀態(tài)的方式。收到心跳,一般可以認(rèn)為一個節(jié)點和現(xiàn)在的網(wǎng)絡(luò)拓?fù)涫橇己玫?。?dāng)然,心跳匯報時,一般也會攜帶一些附加的狀態(tài)、元數(shù)據(jù)信息,以便管理。如下圖:

image.png

但心跳不是萬能的,收到心跳可以確認(rèn)ok,但是收不到心跳卻不能確認(rèn)節(jié)點不存在或者掛掉了,因為可能是網(wǎng)絡(luò)原因倒是鏈路不通但是節(jié)點依舊在工作。
所以切記,”心跳“只能告訴你正常的狀態(tài)是ok,它不能發(fā)現(xiàn)節(jié)點是否真的死亡,有可能還在繼續(xù)服務(wù)。(后面會介紹一種可靠的方式 -- Lease機制)

3、副本
副本指的是針對一份數(shù)據(jù)的多份冗余拷貝,在不同的節(jié)點上持久化同一份數(shù)據(jù),當(dāng)某一個節(jié)點的數(shù)據(jù)丟失時,可以從副本上獲取數(shù)據(jù)。數(shù)據(jù)副本是分布式系統(tǒng)解決數(shù)據(jù)丟失異常的僅有的唯一途徑。當(dāng)然對多份副本的寫入會帶來一致性和可用性的問題,比如規(guī)定副本數(shù)為3,同步寫3份,會帶來3次IO的性能問題。還是同步寫1份,然后異步寫2份,會帶來一致性問題,比如后面2份未寫成功其他模塊就去讀了(下個小結(jié)會詳細(xì)討論如果在副本一致性中間做取舍)。

4、中心化/無中心化
系統(tǒng)模型這方面,無非就是兩種:
中心節(jié)點,例如mysql的MSS單主雙從、MongDB Master、HDFS NameNode、MapReduce JobTracker等,有1個或幾個節(jié)點充當(dāng)整個系統(tǒng)的核心元數(shù)據(jù)及節(jié)點管理工作,其他節(jié)點都和中心節(jié)點交互。這種方式的好處顯而易見,數(shù)據(jù)和管理高度統(tǒng)一集中在一個地方,容易聚合,就像領(lǐng)導(dǎo)者一樣,其他人都服從就好。簡單可行。
但是缺點是模塊高度集中,容易形成性能瓶頸,并且如果出現(xiàn)異常,就像群龍無首一樣。

無中心化的設(shè)計,例如cassandra、zookeeper,系統(tǒng)中不存在一個領(lǐng)導(dǎo)者,節(jié)點彼此通信并且彼此合作完成任務(wù)。好處在于如果出現(xiàn)異常,不會影響整體系統(tǒng),局部不可用。缺點是比較協(xié)議復(fù)雜,而且需要各個節(jié)點間同步信息。

【分布式系統(tǒng)設(shè)計實踐】
基本的理論和策略簡單介紹這么多,后面本人會從工程的角度,細(xì)化說一下”數(shù)據(jù)分布“、"副本控制"和"高可用協(xié)議"

在分布式系統(tǒng)中,無論是計算還是存儲,處理的對象都是數(shù)據(jù),數(shù)據(jù)不存在于一臺機器或進程中,這就牽扯到如何多機均勻分發(fā)數(shù)據(jù)的問題,此小結(jié)主要討論"哈希取模",”一致性哈?!?,”范圍表劃分“,”數(shù)據(jù)塊劃分“

1、哈希取模:
哈希方式是最常見的數(shù)據(jù)分布方式,實現(xiàn)方式是通過可以描述記錄的業(yè)務(wù)的id或key(比如用戶 id),通過Hash函數(shù)的計算求余。余數(shù)作為處理該數(shù)據(jù)的服務(wù)器索引編號處理。如圖:

image.png

這樣的好處是只需要通過計算就可以映射出數(shù)據(jù)和處理節(jié)點的關(guān)系,不需要存儲映射。難點就是如果id分布不均勻可能出現(xiàn)計算、存儲傾斜的問題,在某個節(jié)點上分布過重。并且當(dāng)處理節(jié)點宕機時,這種”硬哈?!暗姆绞綍苯訉?dǎo)致部分?jǐn)?shù)據(jù)異常,還有擴容非常困難,原來的映射關(guān)系全部發(fā)生變更。

此處,如果是”無狀態(tài)“型的節(jié)點,影響比較小,但遇到”有狀態(tài)“的存儲節(jié)點時,會發(fā)生大量數(shù)據(jù)位置需要變更,發(fā)生大量數(shù)據(jù)遷移的問題。這個問題在實際生產(chǎn)中,可以通過按2的冪的機器數(shù),成倍擴容的方式來緩解,如圖:

image.png

不過擴容的數(shù)量和方式后收到很大限制。下面介紹一種”自適應(yīng)“的方式解決擴容和容災(zāi)的問題。

2、一致性哈希:
一致性哈希 -- Consistent Hash 是使用一個哈希函數(shù)計算數(shù)據(jù)或數(shù)據(jù)特征的哈希值,令該哈希函數(shù)的輸出值域為一個封閉的環(huán),最大值+1=最小值。將節(jié)點隨機分布到這個環(huán)上,每個節(jié)點負(fù)責(zé)處理從自己開始順時針至下一個節(jié)點的全部哈希值域上的數(shù)據(jù),如圖:

image.png

一致性哈希的優(yōu)點在于可以任意動態(tài)添加、刪除節(jié)點,每次添加、刪除一個節(jié)點僅影響一致性哈希環(huán)上相鄰的節(jié)點。 為了盡可能均勻的分布節(jié)點和數(shù)據(jù),一種常見的改進算法是引入虛節(jié)點的概念,系統(tǒng)會創(chuàng)建許多虛擬節(jié)點,個數(shù)遠(yuǎn)大于當(dāng)前節(jié)點的個數(shù),均勻分布到一致性哈希值域環(huán)上。讀寫數(shù)據(jù)時,首先通過數(shù)據(jù)的哈希值在環(huán)上找到對應(yīng)的虛節(jié)點,然后查找到對應(yīng)的real節(jié)點。這樣在擴容和容錯時,大量讀寫的壓力會再次被其他部分節(jié)點分?jǐn)偅饕鉀Q了壓力集中的問題。如圖:

image.png

3、數(shù)據(jù)范圍劃分:
有些時候業(yè)務(wù)的數(shù)據(jù)id或key分布不是很均勻,并且讀寫也會呈現(xiàn)聚集的方式。比如某些id的數(shù)據(jù)量特別大,這時候可以將數(shù)據(jù)按Group劃分,從業(yè)務(wù)角度劃分比如id為010000,已知8000以上的id可能訪問量特別大,那么分布可以劃分為[[08000],[80009000],[90001000]]。將小訪問量的聚集在一起。

這樣可以根據(jù)真實場景按需劃分,缺點是由于這些信息不能通過計算獲取,需要引入一個模塊存儲這些映射信息。這就增加了模塊依賴,可能會有性能和可用性的額外代價。

4、數(shù)據(jù)塊劃分:
許多文件系統(tǒng)經(jīng)常采用類似設(shè)計,將數(shù)據(jù)按固定塊大小(比如HDFS的64MB),將數(shù)據(jù)分為一個個大小固定的塊,然后這些塊均勻的分布在各個節(jié)點,這種做法也需要外部節(jié)點來存儲映射關(guān)系。

由于與具體的數(shù)據(jù)內(nèi)容無關(guān),按數(shù)據(jù)量分布數(shù)據(jù)的方式一般沒有數(shù)據(jù)傾斜的問題,數(shù)據(jù)總是被均勻切分并分布到集群中。當(dāng)集群需要重新負(fù)載均衡時,只需通過遷移數(shù)據(jù)塊即可完成。
如圖:

image.png

大概說了一下數(shù)據(jù)分布的具體實施,后面根據(jù)這些分布,看看工程中各個節(jié)點間如何相互配合、管理,一起對外服務(wù)。

1、paxos
paxos很多人都聽說過了,這是唯一一個被認(rèn)可的在工程中證實的強一致性、高可用的去中心化分布式協(xié)議。

雖然論文里提到的概念比較復(fù)雜,但基本流程不難理解。本人能力有限,這里只簡單的闡述一下基本原理:
Paxos 協(xié)議中,有三類角色:
Proposer:Proposer 可以有多個,Proposer 提出議案,此處定義為value。不同的 Proposer 可以提出不同的甚至矛盾的 value,例如某個 Proposer 提議“將變量a設(shè)置為x1” ,另一個 Proposer 提議“將變量a設(shè)置為x2” ,但對同一輪 Paxos過程,最多只有一個 value 被批準(zhǔn)。
Acceptor: 批準(zhǔn)者。 Acceptor 有 N 個, Proposer 提出的 value 必須獲得超過半數(shù)(N/2+1)的 Acceptor批準(zhǔn)后才能通過。Acceptor 之間對等獨立。
Learner:學(xué)習(xí)者。Learner 學(xué)習(xí)被批準(zhǔn)的 value。所謂學(xué)習(xí)就是通過讀取各個 Proposer 對 value的選擇結(jié)果, 如果某個 value 被超過半數(shù) Proposer 通過, 則 Learner 學(xué)習(xí)到了這個 value。從而學(xué)習(xí)者需要至少讀取 N/2+1 個 Accpetor,至多讀取 N 個 Acceptor 的結(jié)果后,能學(xué)習(xí)到一個通過的 value。

paxos在開源界里比較好的實現(xiàn)就是zookeeper(類似Google chubby),zookeeper犧牲了分區(qū)容忍性,在一半節(jié)點宕機情況下,zookeeper就不可用了??梢蕴峁┲行幕渲霉芾硐掳l(fā)、分布式鎖、選主等消息隊列等功能。其中前兩者依靠了Lease機制來實現(xiàn)節(jié)點存活感知和網(wǎng)絡(luò)異常檢測。

2、Lease機制
Lease英文含義是”租期“、”承諾“。在分布式環(huán)境中,此機制描述為:
Lease 是由授權(quán)者授予的在一段時間內(nèi)的承諾。授權(quán)者一旦發(fā)出 lease,則無論接受方是否收到,也無論后續(xù)接收方處于何種狀態(tài),只要 lease 不過期,授權(quán)者一定遵守承諾,按承諾的時間、內(nèi)容執(zhí)行。接收方在有效期內(nèi)可以使用頒發(fā)者的承諾,只要 lease 過期,接收方放棄授權(quán),不再繼續(xù)執(zhí)行,要重新申請Lease。

image.png
image.png

Lease用法舉例1:
現(xiàn)有一個類似DNS服務(wù)的系統(tǒng),數(shù)據(jù)的規(guī)律是改動很少,大量的讀操作。客戶端從服務(wù)端獲取數(shù)據(jù),如果每次都去服務(wù)器查詢,則量比較大。可以把數(shù)據(jù)緩存在本地,當(dāng)數(shù)據(jù)有變動的時候重新拉取?,F(xiàn)在服務(wù)器以lease的形式,把數(shù)據(jù)和lease一同推送給客戶端,在lease中存放承諾該數(shù)據(jù)的不變的時間,然后客戶端就可以一直放心的使用這些數(shù)據(jù)(因為這些數(shù)據(jù)在服務(wù)器不會發(fā)生變更)。如果有客戶端修改了數(shù)據(jù),則把這些數(shù)據(jù)推送給服務(wù)器,服務(wù)器會阻塞一直到已發(fā)布的所有l(wèi)ease都已經(jīng)超時用完,然后后面發(fā)送數(shù)據(jù)和lease時,更新現(xiàn)在的數(shù)據(jù)。

這里有個優(yōu)化可以做,當(dāng)服務(wù)器收到數(shù)據(jù)更新需要等所有已經(jīng)下發(fā)的lease超時的這段時間,可以直接發(fā)送讓數(shù)據(jù)和lease失效的指令到客戶端,減小服務(wù)器等待時間,如果不是所有的lease都失效成功,則退化為前面的等待方案(概率小)。

Lease用法舉例2:
現(xiàn)有一個系統(tǒng),有三個角色,選主模塊Manager,唯一的Master,和其他salver節(jié)點。slaver都向Maganer注冊自己,并由manager選出唯一的Master節(jié)點并告知其他slaver節(jié)點。當(dāng)網(wǎng)絡(luò)出現(xiàn)異常時,可能是Master和Manager之間的鏈路斷了,Master認(rèn)為Master已經(jīng)死掉了,則會再選出一個Master,但是原來的Master對其他網(wǎng)絡(luò)鏈路可能都還是正常的,原來的Master認(rèn)為自己還是主節(jié)點,繼續(xù)服務(wù)。這時候系統(tǒng)中就出現(xiàn)了”雙主“,俗稱”腦裂“。
解決這個問題的方式可以通過Lease,來規(guī)定節(jié)點可以當(dāng)Master的時間,如果沒有可用的Lease,則自動退化為Slaver。如果出現(xiàn)”雙主“,原Master會因為Lease到期而放棄當(dāng)Master,退化為Slaver,恢復(fù)了一個Master的情況。

3、選主算法
有些時候出于系統(tǒng)某些特性,可以在有取舍的情況下,實現(xiàn)一些類似Lease的選主的方案,可見本人另一篇文章:http://blog.csdn.net/gugemichael/article/details/8964834

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 八、深入理解java異常處理機制 引子try…catch…finally恐怕是大家再熟悉不過的語句了, 你的答案是...
    壹點零閱讀 1,730評論 0 0
  • 1.一些概念 1.1.數(shù)據(jù)類型 Java虛擬機中,數(shù)據(jù)類型可以分為兩類:基本類型和引用類型?;绢愋偷淖兞勘4嬖?..
    落落落落大大方方閱讀 4,815評論 4 86
  • 從三月份找實習(xí)到現(xiàn)在,面了一些公司,掛了不少,但最終還是拿到小米、百度、阿里、京東、新浪、CVTE、樂視家的研發(fā)崗...
    時芥藍(lán)閱讀 42,787評論 11 349
  • Java SE 基礎(chǔ): 封裝、繼承、多態(tài) 封裝: 概念:就是把對象的屬性和操作(或服務(wù))結(jié)合為一個獨立的整體,并盡...
    Jayden_Cao閱讀 2,234評論 0 8
  • 吃過晚飯,全家人在一起閑聊。聊著聊著,便聊到了弟弟的學(xué)習(xí)上。 弟弟馬上就要參加中考了。他的學(xué)業(yè)屬于中游,如果再努力...
    漪漪麻麻417閱讀 239評論 0 0

友情鏈接更多精彩內(nèi)容