6.(七)-典型配置举例1
以下配置主要针对分代垃圾回收算法而言。
堆大小设置
年轻代的设置很关键
JVM中最大堆大小有三方面限制:相关操作系统的数据模型(32-bt还是64-bit)限制;系统的可用虚拟内存限制;系统的可用物理内存限制。32位系统下,一般限制在1.5G~2G;64为操作系统对内存无限制。在Windows Server 2003 系统,3.5G物理内存,JDK5.0下测试,最大可设置为1478m。
典型设置:
java -Xmx3550m -Xms3550m -Xmn2g –Xss128k
-Xmx3550m:设置JVM最大可用内存为3550M。
-Xms3550m:设置JVM初始内存为3550m。此值可以设置与-Xmx相同,以避免每次垃圾回收完成后JVM重新分配内存。
-Xmn2g:设置年轻代大小为2G。整个堆大小=年轻代大小 + 年老代大小 + 持久代大小。持久代一般固定大小为64m,所以增大年轻代后,将会减小年老代大小。此值对系统性能影响较大,Sun官方推荐配置为整个堆的3/8。
-Xss128k:设置每个线程的堆栈大小。JDK5.0以后每个线程堆栈大小为1M,以前每个线程堆栈大小为256K。更具应用的线程所需内存大小进行调整。在相同物理内存下,减小这个值能生成更多的线程。但是操作系统对一个进程内的线程数还是有限制的,不能无限生成,经验值在3000~5000左右。
java -Xmx3550m -Xms3550m -Xss128k -XX:NewRatio=4 -XX:SurvivorRatio=4 -XX:MaxPermSize=16m -XX:MaxTenuringThreshold=0
-XX:NewRatio=4:设置年轻代(包括Eden和两个Survivor区)与年老代的比值(除去持久代)。设置为4,则年轻代与年老代所占比值为1:4,年轻代占整个堆栈的1/5
-XX:SurvivorRatio=4:设置年轻代中Eden区与Survivor区的大小比值。设置为4,则两个Survivor区与一个Eden区的比值为2:4,一个Survivor区占整个年轻代的1/6
-XX:MaxPermSize=16m:设置持久代大小为16m。
-XX:MaxTenuringThreshold=0:设置垃圾最大年龄。如果设置为0的话,则年轻代对象不经过Survivor区,直接进入年老代。对于年老代比较多的应用,可以提高效率。如果将此值设置为一个较大值,则年轻代对象会在Survivor区进行多次复制,这样可以增加对象再年轻代的存活时间,增加在年轻代即被回收的概论。
回收器选择
JVM给了三种选择:串行收集器、并行收集器、并发收集器,但是串行收集器只适用于小数据量的情况,所以这里的选择主要针对并行收集器和并发收集器。默认情况下,JDK5.0以前都是使用串行收集器,如果想使用其他收集器需要在启动时加入相应参数。JDK5.0以后,JVM会根据当前系统配置进行判断。
吞吐量优先的并行收集器
如上文所述,并行收集器主要以到达一定的吞吐量为目标,适用于科学技术和后台处理等。
典型配置:
java -Xmx3800m -Xms3800m -Xmn2g -Xss128k -XX:+UseParallelGC -XX:ParallelGCThreads=20
-XX:+UseParallelGC:选择垃圾收集器为并行收集器。此配置仅对年轻代有效。即上述配置下,年轻代使用并发收集,而年老代仍旧使用串行收集。
-XX:ParallelGCThreads=20:配置并行收集器的线程数,即:同时多少个线程一起进行垃圾回收。此值最好配置与处理器数目相等。
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:设置每次年轻代垃圾回收的最长时间,如果无法满足此时间,JVM会自动调整年轻代大小,以满足此值。
n java -Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:+UseParallelGC -XX:MaxGCPauseMillis=100 -XX:+UseAdaptiveSizePolicy
-XX:+UseAdaptiveSizePolicy:设置此选项后,并行收集器会自动选择年轻代区大小和相应的Survivor区比例,以达到目标系统规定的最低相应时间或者收集频率等,此值建议使用并行收集器时,一直打开。
响应时间优先的并发收集器
如上文所述,并发收集器主要是保证系统的响应时间,减少垃圾收集时的停顿时间。适用于应用服务器、电信领域等。
典型配置:
java -Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:ParallelGCThreads=20 -XX:+UseConcMarkSweepGC -XX:+UseParNewGC
-XX:+UseConcMarkSweepGC:设置年老代为并发收集。测试中配置这个以后,-XX:NewRatio=4的配置失效了,原因不明。所以,此时年轻代大小最好用-Xmn设置。
-XX:+UseParNewGC: 设置年轻代为并行收集。可与CMS收集同时使用。JDK5.0以上,JVM会根据系统配置自行设置,所以无需再设置此值。
java -Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:+UseConcMarkSweepGC -XX:CMSFullGCsBeforeCompaction=5 -XX:+UseCMSCompactAtFullCollection
-XX:CMSFullGCsBeforeCompaction:由于并发收集器不对内存空间进行压缩、整理,所以运行一段时间以后会产生“碎片”,使得运行效率降低。此值设置运行多少次GC以后对内存空间进行压缩、整理。
-XX:+UseCMSCompactAtFullCollection:打开对年老代的压缩。可能会影响性能,但是可以消除碎片
辅助信息
JVM提供了大量命令行参数,打印信息,供调试使用。主要有以下一些:
-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:打印每次垃圾回收前,程序未中断的执行时间。可与上面混合使用。输出形式:Application time: 0.5291524 seconds
-XX:+PrintGCApplicationStoppedTime:打印垃圾回收期间程序暂停的时间。可与上面混合使用。输出形式:Total time for which application threads were stopped: 0.0468229 seconds
** -XX:PrintHeapAtGC: ** 打印GC前后的详细堆栈信息。输出形式:
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:与上面几个配合使用,把相关日志信息记录到文件以便分析。
7.(八)-典型配置举例2
常见配置汇总
堆设置
-Xms:初始堆大小
-Xmx:最大堆大小
-XX:NewSize=n:设置年轻代大小
-XX:NewRatio=n:设置年轻代和年老代的比值。如:为3,表示年轻代与年老代比值为1:3,年轻代占整个年轻代年老代和的1/4
-XX:SurvivorRatio=n:年轻代中Eden区与两个Survivor区的比值。注意Survivor区有两个。如:3,表示Eden:Survivor=3:2,一个Survivor区占整个年轻代的1/5
-XX:MaxPermSize=n:设置持久代大小
收集器设置
-XX:+UseSerialGC:设置串行收集器
-XX:+UseParallelGC:设置并行收集器
-XX:+UseParalledlOldGC:设置并行年老代收集器
-XX:+UseConcMarkSweepGC:设置并发收集器
垃圾回收统计信息
-XX:+PrintGC
-XX:+PrintGCDetails
-XX:+PrintGCTimeStamps
-Xloggc:filename
并行收集器设置
-XX:ParallelGCThreads=n:设置并行收集器收集时使用的CPU数。并行收集线程数。
-XX:MaxGCPauseMillis=n:设置并行收集最大暂停时间
-XX:GCTimeRatio=n:设置垃圾回收时间占程序运行时间的百分比。公式为1/(1+n)
并发收集器设置
-XX:+CMSIncrementalMode:设置为增量模式。适用于单CPU情况。
-XX:ParallelGCThreads=n:设置并发收集器年轻代收集方式为并行收集时,使用的CPU数。并行收集线程数。
调优总结
年轻代大小选择
响应时间优先的应用:尽可能设大,直到接近系统的最低响应时间限制(根据实际情况选择)。在此种情况下,年轻代收集发生的频率也是最小的。同时,减少到达年老代的对象。
吞吐量优先的应用:尽可能的设置大,可能到达Gbit的程度。因为对响应时间没有要求,垃圾收集可以并行进行,一般适合8CPU以上的应用。
年老代大小选择
响应时间优先的应用:年老代使用并发收集器,所以其大小需要小心设置,一般要考虑并发会话率和会话持续时间等一些参数。如果堆设置小了,可以会造成内存碎片、高回收频率以及应用暂停而使用传统的标记清除方式;如果堆大了,则需要较长的收集时间。最优化的方案,一般需要参考以下数据获得:
- 并发垃圾收集信息
- 持久代并发收集次数
- 传统GC信息
- 花在年轻代和年老代回收上的时间比例
减少年轻代和年老代花费的时间,一般会提高应用的效率
吞吐量优先的应用
一般吞吐量优先的应用都有一个很大的年轻代和一个较小的年老代。原因是,这样可以尽可能回收掉大部分短期对象,减少中期的对象,而年老代尽存放长期存活对象。
较小堆引起的碎片问题
因为年老代的并发收集器使用标记、清除算法,所以不会对堆进行压缩。当收集器回收时,他会把相邻的空间进行合并,这样可以分配给较大的对象。但是,当堆空间较小时,运行一段时间以后,就会出现“碎片”,如果并发收集器找不到足够的空间,那么并发收集器将会停止,然后使用传统的标记、清除方式进行回收。如果出现“碎片”,可能需要进行如下配置:
- -XX:+UseCMSCompactAtFullCollection:使用并发收集器时,开启对年老代的压缩。
2.-XX:CMSFullGCsBeforeCompaction=0:上面配置开启的情况下,这里设置多少次Full GC后,对年老代进行压缩。
8.(九)-新一代的垃圾回收算法
垃圾回收的瓶颈
传统分代垃圾回收方式,已经在一定程度上把垃圾回收给应用带来的负担降到了最小,把应用的吞吐量推到了一个极限。但是他无法解决的一个问题,就是Full GC所带来的应用暂停。在一些对实时性要求很高的应用场景下,GC暂停所带来的请求堆积和请求失败是无法接受的。这类应用可能要求请求的返回时间在几百甚至几十毫秒以内,如果分代垃圾回收方式要达到这个指标,只能把最大堆的设置限制在一个相对较小范围内,但是这样有限制了应用本身的处理能力,同样也是不可接收的。
分代垃圾回收方式确实也考虑了实时性要求而提供了并发回收器,支持最大暂停时间的设置,但是受限于分代垃圾回收的内存划分模型,其效果也不是很理想。
为了达到实时性的要求(其实Java语言最初的设计也是在嵌入式系统上的),一种新垃圾回收方式呼之欲出,它既支持短的暂停时间,又支持大的内存空间分配。可以很好的解决传统分代方式带来的问题。
增量收集的演进
增量收集的方式在理论上可以解决传统分代方式带来的问题。增量收集把对堆空间划分成一系列内存块,使用时,先使用其中一部分(不会全部用完),垃圾收集时把之前用掉的部分中的存活对象再放到后面没有用的空间中,这样可以实现一直边使用边收集的效果,避免了传统分代方式整个使用完了再暂停的回收的情况。
当然,传统分代收集方式也提供了并发收集,但是他有一个很致命的地方,就是把整个堆做为一个内存块,这样一方面会造成碎片(无法压缩),另一方面他的每次收集都是对整个堆的收集,无法进行选择,在暂停时间的控制上还是很弱。而增量方式,通过内存空间的分块,恰恰可以解决上面问题。
Garbage Firest(G1)
这部分的内容主要参考这里,这篇文章算是对G1算法论文的解读。我也没加什么东西了。
目标
从设计目标看G1完全是为了大型应用而准备的。
支持很大的堆
高吞吐量
--支持多CPU和垃圾回收线程
--在主线程暂停的情况下,使用并行收集
--在主线程运行的情况下,使用并发收集
实时目标:可配置在N毫秒内最多只占用M毫秒的时间进行垃圾回收
当然G1要达到实时性的要求,相对传统的分代回收算法,在性能上会有一些损失。
算法详解
G1可谓博采众家之长,力求到达一种完美。他吸取了增量收集优点,把整个堆划分为一个一个等大小的区域(region)。内存的回收和划分都以region为单位;同时,他也吸取了CMS的特点,把这个垃圾回收过程分为几个阶段,分散一个垃圾回收过程;而且,G1也认同分代垃圾回收的思想,认为不同对象的生命周期不同,可以采取不同收集方式,因此,它也支持分代的垃圾回收。为了达到对回收时间的可预计性,G1在扫描了region以后,对其中的活跃对象的大小进行排序,首先会收集那些活跃对象小的region,以便快速回收空间(要复制的活跃对象少了),因为活跃对象小,里面可以认为多数都是垃圾,所以这种方式被称为Garbage First(G1)的垃圾回收算法,即:垃圾优先的回收。
回收步骤:
初始标记(Initial Marking)
G1对于每个region都保存了两个标识用的bitmap,一个为previous marking bitmap,一个为next marking bitmap,bitmap中包含了一个bit的地址信息来指向对象的起始点。
开始Initial Marking之前,首先并发的清空next marking bitmap,然后停止所有应用线程,并扫描标识出每个region中root可直接访问到的对象,将region中top的值放入next top at mark start(TAMS)中,之后恢复所有应用线程。
触发这个步骤执行的条件为:
G1定义了一个JVM Heap大小的百分比的阀值,称为h,另外还有一个H,H的值为(1-h)*Heap Size,目前这个h的值是固定的,后续G1也许会将其改为动态的,根据jvm的运行情况来动态的调整,在分代方式下,G1还定义了一个u以及soft limit,soft limit的值为H-u*Heap Size,当Heap中使用的内存超过了soft limit值时,就会在一次clean up执行完毕后在应用允许的GC暂停时间范围内尽快的执行此步骤;
在pure方式下,G1将marking与clean up组成一个环,以便clean up能充分的使用marking的信息,当clean up开始回收时,首先回收能够带来最多内存空间的regions,当经过多次的clean up,回收到没多少空间的regions时,G1重新初始化一个新的marking与clean up构成的环。
并发标记(Concurrent Marking)
按照之前Initial Marking扫描到的对象进行遍历,以识别这些对象的下层对象的活跃状态,对于在此期间应用线程并发修改的对象的以来关系则记录到remembered set logs中,新创建的对象则放入比top值更高的地址区间中,这些新创建的对象默认状态即为活跃的,同时修改top值。
最终标记暂停(Final Marking Pause)
当应用线程的remembered set logs未满时,是不会放入filled RS buffers中的,在这样的情况下,这些remebered set logs中记录的card的修改就会被更新了,因此需要这一步,这一步要做的就是把应用线程中存在的remembered set logs的内容进行处理,并相应的修改remembered sets,这一步需要暂停应用,并行的运行。
存活对象计算及清除(Live Data Counting and Cleanup)
值得注意的是,在G1中,并不是说Final Marking Pause执行完了,就肯定执行Cleanup这步的,由于这步需要暂停应用,G1为了能够达到准实时的要求,需要根据用户指定的最大的GC造成的暂停时间来合理的规划什么时候执行Cleanup,另外还有几种情况也是会触发这个步骤的执行的:
G1采用的是复制方法来进行收集,必须保证每次的”to space”的空间都是够的,因此G1采取的策略是当已经使用的内存空间达到了H时,就执行Cleanup这个步骤;
对于full-young和partially-young的分代模式的G1而言,则还有情况会触发Cleanup的执行,full-young模式下,G1根据应用可接受的暂停时间、回收young regions需要消耗的时间来估算出一个yound regions的数量值,当JVM中分配对象的young regions的数量达到此值时,Cleanup就会执行;partially-young模式下,则会尽量频繁的在应用可接受的暂停时间范围内执行Cleanup,并最大限度的去执行non-young regions的Cleanup。
展望
以后JVM的调优或许跟多需要针对G1算法进行调优了。
9.(十)-调优方法
Jconsole,jProfile,VisualVM
Jconsole : jdk自带,功能简单,但是可以在系统有一定负荷的情况下使用。对垃圾回收算法有很详细的跟踪。详细说明参考这里
JProfiler:商业软件,需要付费。功能强大。详细说明参考这里
VisualVM:JDK自带,功能强大,与JProfiler类似。推荐。
如何调优
观察内存释放情况、集合类检查、对象树
上面这些调优工具都提供了强大的功能,但是总的来说一般分为以下几类功能
堆信息查看
可查看堆空间大小分配(年轻代、年老代、持久代分配)
提供即时的垃圾回收功能
垃圾监控(长时间监控回收情况)
查看堆内类、对象信息查看:数量、类型等
对象引用情况查看
有了堆信息查看方面的功能,我们一般可以顺利解决以下问题:
--年老代年轻代大小划分是否合理
--内存泄漏
--垃圾回收算法设置是否合理
线程监控
线程信息监控:系统线程数量。
线程状态监控:各个线程都处在什么样的状态下
Dump线程详细信息:查看线程内部运行情况
死锁检查
热点分析
CPU热点:检查系统哪些方法占用的大量CPU时间
内存热点:检查哪些对象在系统中数量最大(一定时间内存活对象和销毁对象一起统计)
这两个东西对于系统优化很有帮助。我们可以根据找到的热点,有针对性的进行系统的瓶颈查找和进行系统优化,而不是漫无目的的进行所有代码的优化。
快照
快照是系统运行到某一时刻的一个定格。在我们进行调优的时候,不可能用眼睛去跟踪所有系统变化,依赖快照功能,我们就可以进行系统两个不同运行时刻,对象(或类、线程等)的不同,以便快速找到问题
举例说,我要检查系统进行垃圾回收以后,是否还有该收回的对象被遗漏下来的了。那么,我可以在进行垃圾回收前后,分别进行一次堆情况的快照,然后对比两次快照的对象情况。
内存泄漏检查
内存泄漏是比较常见的问题,而且解决方法也比较通用,这里可以重点说一下,而线程、热点方面的问题则是具体问题具体分析了。
内存泄漏一般可以理解为系统资源(各方面的资源,堆、栈、线程等)在错误使用的情况下,导致使用完毕的资源无法回收(或没有回收),从而导致新的资源分配请求无法完成,引起系统错误。
内存泄漏对系统危害比较大,因为他可以直接导致系统的崩溃。
需要区别一下,内存泄漏和系统超负荷两者是有区别的,虽然可能导致的最终结果是一样的。内存泄漏是用完的资源没有回收引起错误,而系统超负荷则是系统确实没有那么多资源可以分配了(其他的资源都在使用)。
年老代堆空间被占满
异常: java.lang.OutOfMemoryError: Java heap space
说明:
这是最典型的内存泄漏方式,简单说就是所有堆空间都被无法回收的垃圾对象占满,虚拟机无法再在分配新空间。
如上图所示,这是非常典型的内存泄漏的垃圾回收情况图。所有峰值部分都是一次垃圾回收点,所有谷底部分表示是一次垃圾回收后剩余的内存。连接所有谷底的点,可以发现一条由底到高的线,这说明,随时间的推移,系统的堆空间被不断占满,最终会占满整个堆空间。因此可以初步认为系统内部可能有内存泄漏。(上面的图仅供示例,在实际情况下收集数据的时间需要更长,比如几个小时或者几天)
解决:
这种方式解决起来也比较容易,一般就是根据垃圾回收前后情况对比,同时根据对象引用情况(常见的集合对象引用)分析,基本都可以找到泄漏点。
持久代被占满
异常:java.lang.OutOfMemoryError: PermGen space
说明:
Perm空间被占满。无法为新的class分配存储空间而引发的异常。这个异常以前是没有的,但是在Java反射大量使用的今天这个异常比较常见了。主要原因就是大量动态反射生成的类不断被加载,最终导致Perm区被占满。
更可怕的是,不同的classLoader即便使用了相同的类,但是都会对其进行加载,相当于同一个东西,如果有N个classLoader那么他将会被加载N次。因此,某些情况下,这个问题基本视为无解。当然,存在大量classLoader和大量反射类的情况其实也不多。
解决:
1. -XX:MaxPermSize=16m
2. 换用JDK。比如JRocket。
堆栈溢出
异常:java.lang.StackOverflowError
说明:这个就不多说了,一般就是递归没返回,或者循环调用造成
线程堆栈满
异常:Fatal: Stack size too small
说明:java中一个线程的空间大小是有限制的。JDK5.0以后这个值是1M。与这个线程相关的数据将会保存在其中。但是当线程空间满了以后,将会出现上面异常。
解决:增加线程栈大小。-Xss2m。但这个配置无法解决根本问题,还要看代码部分是否有造成泄漏的部分。
系统内存被占满
异常:java.lang.OutOfMemoryError: unable to create new native thread
说明:
这个异常是由于操作系统没有足够的资源来产生这个线程造成的。系统创建线程时,除了要在Java堆中分配内存外,操作系统本身也需要分配资源来创建线程。因此,当线程数量大到一定程度以后,堆中或许还有空间,但是操作系统分配不出资源来了,就出现这个异常了。
分配给Java虚拟机的内存愈多,系统剩余的资源就越少,因此,当系统内存固定时,分配给Java虚拟机的内存越多,那么,系统总共能够产生的线程也就越少,两者成反比的关系。同时,可以通过修改-Xss来减少分配给单个线程的空间,也可以减少系统总共生产的线程数。
解决:
1.重新设计系统减少线程数量。
2.线程数量不能减少的情况下,通过-Xss减小单个线程大小。以便能生产更多的线程。
10.(十一)-反思
垃圾回收的悖论
所谓“成也萧何败萧何”。Java的垃圾回收确实带来了很多好处,为开发带来了便利。但是在一些高性能、高并发的情况下,垃圾回收确成为了制约Java应用的瓶颈。目前JDK的垃圾回收算法,始终无法解决垃圾回收时的暂停问题,因为这个暂停严重影响了程序的相应时间,造成拥塞或堆积。这也是后续JDK增加G1算法的一个重要原因。
当然,上面是从技术角度出发解决垃圾回收带来的问题,但是从系统设计方面我们就需要问一下了:
我们需要分配如此大的内存空间给应用吗?
我们是否能够通过有效使用内存而不是通过扩大内存的方式来设计我们的系统呢?
我们的内存中都放了什么内存中需要放什么呢?个人认为,内存中需要放的是你的应用需要在不久的将来再次用到到的东西。想想看,如果你在将来不用这些东西,何必放内存呢?放文件、数据库不是更好?这些东西一般包括:
- 系统运行时业务相关的数据。比如web应用中的session、即时消息的session等。这些数据一般在一个用户访问周期或者一个使用过程中都需要存在。
- 缓存。缓存就比较多了,你所要快速访问的都可以放这里面。其实上面的业务数据也可以理解为一种缓存。
- 线程。
因此,我们是不是可以这么认为,如果我们不把业务数据和缓存放在JVM中,或者把他们独立出来,那么Java应用使用时所需的内存将会大大减少,同时垃圾回收时间也会相应减少。
我认为这是可能的。
解决之道
数据库、文件系统
把所有数据都放入数据库或者文件系统,这是一种最为简单的方式。在这种方式下,Java应用的内存基本上等于处理一次峰值并发请求所需的内存。数据的获取都在每次请求时从数据库和文件系统中获取。也可以理解为,一次业务访问以后,所有对象都可以进行回收了。
这是一种内存使用最有效的方式,但是从应用角度来说,这种方式很低效。
内存-硬盘映射
上面的问题是因为我们使用了文件系统带来了低效。但是如果我们不是读写硬盘,而是写内存的话效率将会提高很多。
数据库和文件系统都是实实在在进行了持久化,但是当我们并不需要这样持久化的时候,我们可以做一些变通——把内存当硬盘使。
内存-硬盘映射很好很强大,既用了缓存又对Java应用的内存使用又没有影响。Java应用还是Java应用,他只知道读写的还是文件,但是实际上是内存。
这种方式兼得的Java应用与缓存两方面的好处。memcached的广泛使用也正是这一类的代表。
同一机器部署多个JVM
这也是一种很好的方式,可以分为纵拆和横拆。纵拆可以理解为把Java应用划分为不同模块,各个模块使用一个独立的Java进程。而横拆则是同样功能的应用部署多个JVM。
通过部署多个JVM,可以把每个JVM的内存控制一个垃圾回收可以忍受的范围内即可。但是这相当于进行了分布式的处理,其额外带来的复杂性也是需要评估的。另外,也有支持分布式的这种JVM可以考虑,不要要钱哦:)
程序控制的对象生命周期
这种方式是理想当中的方式,目前的虚拟机还没有,纯属假设。即:考虑由编程方式配置哪些对象在垃圾收集过程中可以直接跳过,减少垃圾回收线程遍历标记的时间。
这种方式相当于在编程的时候告诉虚拟机某些对象你可以在*时间后在进行收集或者由代码标识可以收集了(类似C、C++),在这之前你即便去遍历他也是没有效果的,他肯定是还在被引用的。
这种方式如果JVM可以实现,个人认为将是一个飞跃,Java即有了垃圾回收的优势,又有了C、C++对内存的可控性。
线程分配
Java的阻塞式的线程模型基本上可以抛弃了,目前成熟的NIO框架也比较多了。阻塞式IO带来的问题是线程数量的线性增长,而NIO则可以转换成为常数线程。因此,对于服务端的应用而言,NIO还是唯一选择。不过,JDK7中为我们带来的AIO是否能让人眼前一亮呢?我们拭目以待。
其他的JDK
本文说的都是Sun的JDK,目前常见的JDK还有JRocket和IBM的JDK。其中JRocket在IO方面比Sun的高很多,不过Sun JDK6.0以后提高也很大。而且JRocket在垃圾回收方面,也具有优势,其可设置垃圾回收的最大暂停时间也是很吸引人的。不过,系统Sun的G1实现以后,在这方面会有一个质的飞跃。
十、分布式系统设计原理与方案
一直在思考分布式系统设计的问题,业务对象原封不动的情况下部署在客户端和服务器端,可以根据配置文件选择是连接服务器还是连接本地的数据库,这个问题让我绞尽脑汁,我总是设想的客户端与服务器端通信的方式是最低端的Socket。花了两个晚上研究CSLA.NET框架关于数据门户这块代码,才发现问题的关键所在:客户端与服务器端通信不能采用最低端的Socket,而要用高端的WebService、.NET Remoting或者是自己定义一种协议等,只要它们支持客户端直接根据服务器端的服务URL、类名、方法名和方法参数四个信息就可以调用服务器对应的类和方法就行。
说明:本文中所表达的思想与CSLA.NET有很大区别,不要看了本文就以为是CSLA.NET的设计思想,也不要以为本文错误的解释了CSLA.NET,这不是一篇介绍CSLA.NET的文章,但纯思想上它们是相同的。
分布式系统的部署
平常我们都说三层架构
接下来我要把三层变的更简单点,两层,数据访问层合并到业务层,统称为业务层,因为我们面对的问题不是分层的问题,而是分布式系统中各层应该怎么部署的问题。在CSLA.NET书中也说到业务层和数据访问层放到同一台机器上可以提高性能和容错性。因此他们俩的合并不影响分布式系统的部署。
不过要解释的是数据库系统(CSLA.NET中说的数据存储和管理层)并没有考虑到三层中来,也就是它不包含在数据访问层中,如果把它算进来,那么它是在数据访问层之下单独存在的。
综上,在分布式系统部署角度考虑的分层实际是三层:界面层、业务层(包含数据访问层的业务层)、数据存储层。
下面举例说明可能的部署情景,带阴影的框框表示一台机器,虚线框表示根据使用场合可有可无,虚横线表示从此处划开单独出服务器。在B/S应用中,Web浏览器为客户端,其他全部为服务器。在C/S应用中,处在最上层的界面层+业务层为客户端,其他为服务器。
非分布式系统的部署
分布式系统的部署
有几点要说明:
1. 客户端上的验证等业务逻辑是不可信的,因此任何一种部署都需要服务器端包含业务层;
2. 为了开发、维护和部署中的高度可伸缩性,图中的各业务层所包含的代码都是一模一样的;
3. 因为第2点,所以我遇到了业务层的同一个操作是与其他机器上的业务层通信还是访问数据库这个难题。
解决业务层的数据访问问题
1.这个问题是关键问题,也就是上面几点说明中的第3个问题,为了解决这个问题我们引入数据门户的概念。
2.下面以WebService为例说明:界面层访问本机的业务对象的增删改查中的“查”方法时,跳过数据库的查询操作,访问另一台机器中的同一个业务对象类的“查”方法。
以上是向另一台机器发送请求,该请求并不直接调用另一台机器上的业务对象类的“查”方法,而是将要调用的业务对象和方法参数信息转为一个“二进制包”,作为参数去调用另一台机器上通用的“查”方法,另一台机器上的“查”方法再解开这个包,然后去调用解开的包中所表示的业务对象类型,下面的静态图是另一台机器接受到请求后的工作。
又有些说明:
1. 关于原理都已在图中做了描述,不另写大段文字解释了;
2. 上面两个图中,除了“实际业务对象类”以外的部分全部属于架构或者框架部分;
3. 如果用OO的思想去审查上面的两个图,你一定会为这糟糕的设计而抱怨,这里只是为了尽可能简单的表述分布式系统的工作原理,你可以采用策略模式使数据门户不改变的情况下适应各种请求响应场合,采用工厂模式实现不同的请求响应场合的切换。
关于数据库的分布
为了解决数据库服务器的负担,我们可能希望把数据分布存储在多个服务器上,我设想的数据库分布方案是,各服务器上的数据库在结构上一模一样,而表里的数据存储到不同服务器上,这样数据访问层在查数据的时候分别向所有数据库服务器发送同样的sql命令,然后数据访问层得到数据后整合,这样减轻每台服务器的工作量。亦或者根据表里的某个代表性的字段(如:省份)分布数据到不同服务器。
十一、分布式系统设计系列 -- 基本原理及高可用策略
【分布式系统中的概念】
三元组
其实,分布式系统说白了,就是很多机器组成的集群,靠彼此之间的网络通信,担当的角色可能不同,共同完成同一个事情的系统。如果按”实体“来划分的话,就是如下这几种:
1、节点 -- 系统中按照协议完成计算工作的一个逻辑实体,可能是执行某些工作的进程或机器
2、网络 -- 系统的数据传输通道,用来彼此通信。通信是具有方向性的。
3、存储 -- 系统中持久化数据的数据库或者文件存储。
状态特性
各个节点的状态可以是“无状态”或者“有状态的”.
一般认为,节点是偏计算和通信的模块,一般是无状态的。这类应用一般不会存储自己的中间状态信息,比如Nginx,一般情况下是转发请求而已,不会存储中间信息。另一种“有状态”的,如mysql等数据库,状态和数据全部持久化到磁盘等介质。
“无状态”的节点一般我们认为是可随意重启的,因为重启后只需要立刻工作就好。“有状态”的则不同,需要先读取持久化的数据,才能开始服务。所以,“无状态”的节点一般是可以随意扩展的,“有状态”的节点需要一些控制协议来保证扩展。
系统异常
异常,可认为是节点因为某种原因不能工作,此为节点异常。还有因为网络原因,临时、永久不能被其他节点所访问,此为网络异常。在分布式系统中,要有对异常的处理,保证集群的正常工作。
【分布式系统与单节点的不同】
1、从linux write()系统调用说起
众所周知,在unix/linux/mac(类Unix)环境下,两个机器通信,最常用的就是通过socket连接对方。传输数据的话,无非就是调用write()这个系统调用,把一段内存缓冲区发出去。但是可以进一步想一下,write()之后能确认对方收到了这些数据吗?
答案肯定是不能,原因就是发送数据需要走内核->网卡->链路->对端网卡->内核,这一路径太长了,所以只能是异步操作。write()把数据写入内核缓冲区之后就返回到应用层了,具体后面何时发送、怎么发送、TCP怎么做滑动窗口、流控都是tcp/ip协议栈内核的事情了。
所以在应用层,能确认对方受到了消息只能是对方应用返回数据,逻辑确认了这次发送才认为是成功的。这就区别与单系统编程,大部分系统调用、库调用只要返回了就说明已经确认完成了。
2、TCP/IP协议是“不可靠”的
教科书上明确写明了互联网是不可靠的,TCP实现了可靠传输。何来“不可靠”呢?先来看一下网络交互的例子,有A、B两个节点,之间通过TCP连接,现在A、B都想确认自己发出的任何一条消息都能被对方接收并反馈,于是开始了如下操作:
A->B发送数据,然后A需要等待B收到数据的确认,B收到数据后发送确认消息给A,然后B需要等待A收到数据的确认,A收到B的数据确认消息后再次发送确认消息给B,然后A又去需要等待B收到的确认。。。死循环了!!
其实,这就是著名的“拜占庭将军”问题:http://baike.baidu.com/link?url=6iPrbRxHLOo9an1hT-s6DvM5kAoq7RxclIrzgrS34W1fRq1h507RDWJOxfhkDOcihVFRZ2c7ybCkUosWQeUoS_
所以,通信双方是“不可能”同时确认对方受到了自己的信息。而教科书上定义的其实是指“单向”通信是成立的,比如A向B发起Http调用,收到了HttpCode 200的响应包,这只能确认,A确认B收到了自己的请求,并且B正常处理了,不能确认的是B确认A受到了它的成功的消息。
3、不可控的状态
在单系统编程中,我们对系统状态是非常可控的。比如函数调用、逻辑运算,要么成功,要么失败,因为这些操作被框在一个机器内部,cpu/总线/内存都是可以快速得到反馈的。开发者可以针对这两个状态很明确的做出程序上的判断和后续的操作。
而在分布式的网络环境下,这就变得微妙了。比如一次rpc、http调用,可能成功、失败,还有可能是“超时”,这就比前者的状态多了一个不可控因素,导致后面的代码不是很容易做出判断。试想一下,用A用支付宝向B转了一大笔钱,当他按下“确认”后,界面上有个圈在转啊转,然后显示请求超时了,然后A就抓狂了,不知道到底钱转没转过去,开始确认自己的账户、确认B的账户、打电话找客服等等。
所以分布式环境下,我们的其实要时时刻刻考虑面对这种不可控的“第三状态”设计开发,这也是挑战之一。
4、视”异常“为”正常“
单系统下,进程/机器的异常概率十分小。即使出现了问题,可以通过人工干预重启、迁移等手段恢复。但在分布式环境下,机器上千台,每几分钟都可能出现宕机、死机、网络断网等异常,出现的概率很大。所以,这种环境下,进程core掉、机器挂掉都是需要我们在编程中认为随时可能出现的,这样才能使我们整个系统健壮起来,所以”容错“是基本需求。
异常可以分为如下几类:
节点错误:
一般是由于应用导致,一些coredump和系统错误触发,一般重新服务后可恢复。
硬件错误:
由于磁盘或者内存等硬件设备导致某节点不能服务,需要人工干预恢复。
网络错误:
由于点对点的网络抖动,暂时的访问错误,一般拓扑稳定后或流量减小可以恢复。
网络分化:
网络中路由器、交换机错误导致网络不可达,但是网络两边都正常,这类错误比较难恢复,并且需要在开发时特别处理。【这种情况也会比较前面的问题较难处理】
【分布式系统特性】
CAP是分布式系统里最著名的理论,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)
早些时候,国外的大牛已经证明了CAP三者是不能兼得,很多实践也证明了。
本人就不挑战权威了,感兴趣的同学可以自己Google。本人以自己的观点总结了一下:
一致性
描述当前所有节点存储数据的统一模型,分为强一致性和弱一致性:
强一致性描述了所有节点的数据高度一致,无论从哪个节点读取,都是一样的。无需担心同一时刻会获得不同的数据。是级别最高的,实现的代价比较高
弱一致性又分为单调一致性和最终一致性:
1、单调一致性强调数据是按照时间的新旧,单调向最新的数据靠近,不会回退,如:数据存在三个版本v1->v2->v3,获取只能向v3靠近(如取到的是v2,就不可能再次获得v1)
2、最终一致性强调数据经过一个时间窗口之后,只要多尝试几次,最终的状态是一致的,是最新的数据
强一致性的场景,就好像交易系统,存取钱的+/-操作必须是马上一致的,否则会令很多人误解。
弱一致性的场景,大部分就像web互联网的模式,比如发了一条微博,改了某些配置,可能不会马上生效,但刷新几次后就可以看到了,其实弱一致性就是在系统上通过业务可接受的方式换取了一些系统的低复杂度和可用性。
可用性
保证系统的正常可运行性,在请求方看来,只要发送了一个请求,就可以得到恢复无论成功还是失败(不会超时)!
分区容忍性
在系统某些节点或网络有异常的情况下,系统依旧可以继续服务。
这通常是有负载均衡和副本来支撑的。例如计算模块异常可通过负载均衡引流到其他平行节点,存储模块通过其他几点上的副本来对外提供服务。
扩展性
扩展性是融合在CAP里面的特性,我觉得此处可以单独讲一下。扩展性直接影响了分布式系统的好坏,系统开发初期不可能把系统的容量、峰值都考虑到,后期肯定牵扯到扩容,而如何做到快而不太影响业务的扩容策略,也是需要考虑的。(后面在介绍数据分布时会着重讨论这个问题)
【分布式系统设计策略】
1、重试机制
一般情况下,写一段网络交互的代码,发起rpc或者http,都会遇到请求超时而失败情况。可能是网络抖动(暂时的网络变更导致包不可达,比如拓扑变更)或者对端挂掉。这时一般处理逻辑是将请求包在一个重试循环块里,如下:
int retry = 3;
while(!request() && retry--)
sched_yield(); // or usleep(100)
此种模式可以防止网络暂时的抖动,一般停顿时间很短,并重试多次后,请求成功!但不能防止对端长时间不能连接(网络问题或进程问题)
2、心跳机制
心跳顾名思义,就是以固定的频率向其他节点汇报当前节点状态的方式。收到心跳,一般可以认为一个节点和现在的网络拓扑是良好的。当然,心跳汇报时,一般也会携带一些附加的状态、元数据信息,以便管理。如下图:
但心跳不是万能的,收到心跳可以确认ok,但是收不到心跳却不能确认节点不存在或者挂掉了,因为可能是网络原因倒是链路不通但是节点依旧在工作。
所以切记,”心跳“只能告诉你正常的状态是ok,它不能发现节点是否真的死亡,有可能还在继续服务。(后面会介绍一种可靠的方式 -- Lease机制)
3、副本
副本指的是针对一份数据的多份冗余拷贝,在不同的节点上持久化同一份数据,当某一个节点的数据丢失时,可以从副本上获取数据。数据副本是分布式系统解决数据丢失异常的仅有的唯一途径。当然对多份副本的写入会带来一致性和可用性的问题,比如规定副本数为3,同步写3份,会带来3次IO的性能问题。还是同步写1份,然后异步写2份,会带来一致性问题,比如后面2份未写成功其他模块就去读了(下个小结会详细讨论如果在副本一致性中间做取舍)。
4、中心化/无中心化
系统模型这方面,无非就是两种:
中心节点,例如mysql的MSS单主双从、MongDB Master、HDFS NameNode、MapReduce JobTracker等,有1个或几个节点充当整个系统的核心元数据及节点管理工作,其他节点都和中心节点交互。这种方式的好处显而易见,数据和管理高度统一集中在一个地方,容易聚合,就像领导者一样,其他人都服从就好。简单可行。
但是缺点是模块高度集中,容易形成性能瓶颈,并且如果出现异常,就像群龙无首一样。
无中心化的设计,例如cassandra、zookeeper,系统中不存在一个领导者,节点彼此通信并且彼此合作完成任务。好处在于如果出现异常,不会影响整体系统,局部不可用。缺点是比较协议复杂,而且需要各个节点间同步信息。
【分布式系统设计实践】
基本的理论和策略简单介绍这么多,后面本人会从工程的角度,细化说一下”数据分布“、"副本控制"和"高可用协议"
在分布式系统中,无论是计算还是存储,处理的对象都是数据,数据不存在于一台机器或进程中,这就牵扯到如何多机均匀分发数据的问题,此小结主要讨论"哈希取模",”一致性哈希“,”范围表划分“,”数据块划分“
1、哈希取模:
哈希方式是最常见的数据分布方式,实现方式是通过可以描述记录的业务的id或key(比如用户 id),通过Hash函数的计算求余。余数作为处理该数据的服务器索引编号处理。如图:
这样的好处是只需要通过计算就可以映射出数据和处理节点的关系,不需要存储映射。难点就是如果id分布不均匀可能出现计算、存储倾斜的问题,在某个节点上分布过重。并且当处理节点宕机时,这种”硬哈希“的方式会直接导致部分数据异常,还有扩容非常困难,原来的映射关系全部发生变更。
此处,如果是”无状态“型的节点,影响比较小,但遇到”有状态“的存储节点时,会发生大量数据位置需要变更,发生大量数据迁移的问题。这个问题在实际生产中,可以通过按2的幂的机器数,成倍扩容的方式来缓解,如图:
不过扩容的数量和方式后收到很大限制。下面介绍一种”自适应“的方式解决扩容和容灾的问题。
2、一致性哈希:
一致性哈希 -- Consistent Hash 是使用一个哈希函数计算数据或数据特征的哈希值,令该哈希函数的输出值域为一个封闭的环,最大值+1=最小值。将节点随机分布到这个环上,每个节点负责处理从自己开始顺时针至下一个节点的全部哈希值域上的数据,如图:
一致性哈希的优点在于可以任意动态添加、删除节点,每次添加、删除一个节点仅影响一致性哈希环上相邻的节点。 为了尽可能均匀的分布节点和数据,一种常见的改进算法是引入虚节点的概念,系统会创建许多虚拟节点,个数远大于当前节点的个数,均匀分布到一致性哈希值域环上。读写数据时,首先通过数据的哈希值在环上找到对应的虚节点,然后查找到对应的real节点。这样在扩容和容错时,大量读写的压力会再次被其他部分节点分摊,主要解决了压力集中的问题。如图:
3、数据范围划分:
有些时候业务的数据id或key分布不是很均匀,并且读写也会呈现聚集的方式。比如某些id的数据量特别大,这时候可以将数据按Group划分,从业务角度划分比如id为010000,已知8000以上的id可能访问量特别大,那么分布可以划分为[[08000],[80009000],[90001000]]。将小访问量的聚集在一起。
这样可以根据真实场景按需划分,缺点是由于这些信息不能通过计算获取,需要引入一个模块存储这些映射信息。这就增加了模块依赖,可能会有性能和可用性的额外代价。
4、数据块划分:
许多文件系统经常采用类似设计,将数据按固定块大小(比如HDFS的64MB),将数据分为一个个大小固定的块,然后这些块均匀的分布在各个节点,这种做法也需要外部节点来存储映射关系。
由于与具体的数据内容无关,按数据量分布数据的方式一般没有数据倾斜的问题,数据总是被均匀切分并分布到集群中。当集群需要重新负载均衡时,只需通过迁移数据块即可完成。
如图:
大概说了一下数据分布的具体实施,后面根据这些分布,看看工程中各个节点间如何相互配合、管理,一起对外服务。
1、paxos
paxos很多人都听说过了,这是唯一一个被认可的在工程中证实的强一致性、高可用的去中心化分布式协议。
虽然论文里提到的概念比较复杂,但基本流程不难理解。本人能力有限,这里只简单的阐述一下基本原理:
Paxos 协议中,有三类角色:
Proposer:Proposer 可以有多个,Proposer 提出议案,此处定义为value。不同的 Proposer 可以提出不同的甚至矛盾的 value,例如某个 Proposer 提议“将变量a设置为x1” ,另一个 Proposer 提议“将变量a设置为x2” ,但对同一轮 Paxos过程,最多只有一个 value 被批准。
Acceptor: 批准者。 Acceptor 有 N 个, Proposer 提出的 value 必须获得超过半数(N/2+1)的 Acceptor批准后才能通过。Acceptor 之间对等独立。
Learner:学习者。Learner 学习被批准的 value。所谓学习就是通过读取各个 Proposer 对 value的选择结果, 如果某个 value 被超过半数 Proposer 通过, 则 Learner 学习到了这个 value。从而学习者需要至少读取 N/2+1 个 Accpetor,至多读取 N 个 Acceptor 的结果后,能学习到一个通过的 value。
paxos在开源界里比较好的实现就是zookeeper(类似Google chubby),zookeeper牺牲了分区容忍性,在一半节点宕机情况下,zookeeper就不可用了。可以提供中心化配置管理下发、分布式锁、选主等消息队列等功能。其中前两者依靠了Lease机制来实现节点存活感知和网络异常检测。
2、Lease机制
Lease英文含义是”租期“、”承诺“。在分布式环境中,此机制描述为:
Lease 是由授权者授予的在一段时间内的承诺。授权者一旦发出 lease,则无论接受方是否收到,也无论后续接收方处于何种状态,只要 lease 不过期,授权者一定遵守承诺,按承诺的时间、内容执行。接收方在有效期内可以使用颁发者的承诺,只要 lease 过期,接收方放弃授权,不再继续执行,要重新申请Lease。
Lease用法举例1:
现有一个类似DNS服务的系统,数据的规律是改动很少,大量的读操作。客户端从服务端获取数据,如果每次都去服务器查询,则量比较大。可以把数据缓存在本地,当数据有变动的时候重新拉取。现在服务器以lease的形式,把数据和lease一同推送给客户端,在lease中存放承诺该数据的不变的时间,然后客户端就可以一直放心的使用这些数据(因为这些数据在服务器不会发生变更)。如果有客户端修改了数据,则把这些数据推送给服务器,服务器会阻塞一直到已发布的所有lease都已经超时用完,然后后面发送数据和lease时,更新现在的数据。
这里有个优化可以做,当服务器收到数据更新需要等所有已经下发的lease超时的这段时间,可以直接发送让数据和lease失效的指令到客户端,减小服务器等待时间,如果不是所有的lease都失效成功,则退化为前面的等待方案(概率小)。
Lease用法举例2:
现有一个系统,有三个角色,选主模块Manager,唯一的Master,和其他salver节点。slaver都向Maganer注册自己,并由manager选出唯一的Master节点并告知其他slaver节点。当网络出现异常时,可能是Master和Manager之间的链路断了,Master认为Master已经死掉了,则会再选出一个Master,但是原来的Master对其他网络链路可能都还是正常的,原来的Master认为自己还是主节点,继续服务。这时候系统中就出现了”双主“,俗称”脑裂“。
解决这个问题的方式可以通过Lease,来规定节点可以当Master的时间,如果没有可用的Lease,则自动退化为Slaver。如果出现”双主“,原Master会因为Lease到期而放弃当Master,退化为Slaver,恢复了一个Master的情况。
3、选主算法
有些时候出于系统某些特性,可以在有取舍的情况下,实现一些类似Lease的选主的方案,可见本人另一篇文章:http://blog.csdn.net/gugemichael/article/details/8964834