J2EE集群原理
什么是集群呢?总的来说,集群包括两个概念:“负载均衡”(load balancing)和“ 失败接管 ”(failover)
多个客户端同时发出请求,位于前端的负载均衡器根据特定算法,将请求分担给比较空闲的机器,从而实现较高性能和较好的扩展性
当客户端连续向某个服务器发出请求时,该服务器可能处理到一半就宕机了,失败接管系统能够检测出有问题的服务器,将后续的请求转发至其他可用的机器,从而实现容错功能
那么,哪些对象可以被集群呢,答案是:“可以被部署在分布式拓扑的组件 ”
因此,负载均衡和失败接管会发生在哪些 J2EE代码中呢?“仅当你调用分布式对象的方法时 ”
客户端和目标服务器不在一个 JVM上,他们之间通过标准的网络协议进行通讯,这也就给集群提供了用武之地,实现集群效果的设备可以放在边界上对通讯做一些处理。
J2EE的分布式技术包括: JSP,JDBC,EJB,JNDI,WEB SERVICE等等
一. Web层的集群实现
这是最重要最基本的 J2EE集群功能,网络层集群技术包括:Web负载均衡和HTTPSession失败接管
Web负载均衡
负载均衡器可以是一个硬件设备,比如 F5 Load Balancer,也可以是另外一台服务器加一个负载均衡的插件,甚至一个 Linux的嵌入式设备都能胜任。通常,负载均衡包括以下几个特点:
1.实现负载均衡算法
常用的算法有: Round-Robin, Random 和 Weight Based,算法的最终目标是尽量使每台服务器的负载达到平衡,但以上算法只是根据每台服务器接收的请求来进行均衡,因此都不能完全达到理想化的目标。有些复杂的算法可以在分发请求之前检测机器的性能,从而决定要由哪台机器来处理请求。
2.健康监测
一旦某台机器宕机了,负载均衡器要能够及时发现情况,并且将请求转交给其他可用的服务器,保证做到“ failover”
3.Session stickiness
即是让一次 session会话的请求都尽量交给一台机器处理,这样省去了服务器之间交换 session数据的开销
HTTPSession失败接管
当session进行到一半时,如果服务器挂了,我们就要想办法让session在另外一台机器上继续进行,而不是让用户重新来过。下面图六解释了实现HTTPSession Failover的原理:每次的session都会分配一个唯一的id,这个id以cookies的形式存放在客户端中,负载均衡器通过id分辨请求是属于哪个session的。在第四步中,服务器 A以某种定义好的方式,定期将 session数据保存起来,一旦出问题,负载均衡器会自动通知另一台服务器 B,让它取出保存的数据,接 A的班
要实现以上基本功能,首先要支持以下特性:
1.全局session id
每个jvm内部都会为每次session维护一个唯一的id,但多个jvm之间的session id会不会重复就很难说了,所以负载均衡器要能够协调各个jvm,使每个session id都能全局唯一。
2.如何备份session
这个跟具体厂商有关,后面会讲到
3.备份的频度和粒度
这个事关负载均衡的性能,因为备份数据的过程直接占用cpu、网络和IO性能
I.数据库方式备份
这是最简单的实现方式,数据库还能在整个系统都当掉时依旧保存好 session 数据,可靠性高,几乎所有 J2EE 实现都提供了这种方式,当然,你的 session 数据必须是可序列化的。但是,这种方式一般推荐不要在 session 中存放过大过多的数据,因为数据库的事务过程相当费资源,但这样也大大限制了它的使用范围,毕竟我们有时必须要在 session 中存放一些大对象
II.内存拷贝方式
这种方式好处很多,首先它省去了数据库连接和事务的开销,其次由于备份的数据已经放在内存中了,也就省去了从数据库恢复的过程
“JavaGroup”是当前tomcat和jboss集群方案的通讯层协议,本质上是一个可靠的组间通讯和管理的toolkit,它的核心功能在于“ Group membership protocols” 和“message multicast”,用在集群上非常适合,关于JavaGroup的更多内容,请参考 http://www.jgroups.org/javagroupsnew/docs/index.html
III. Tomcat的实现:多服务器之间互相拷贝内存
虽然实现起来容易,但这种方法的弊端也显而易见,当节点增加时,拷贝量就要成倍增长,性能有时还不如不集群
IV.Weblogic, Jboss 和 WebSphere 的实现:双服务器拷贝
虽然这种方法的性能和扩展性都很好,但也有不少弊端
1 负载均衡器的复杂度加大,因为要记住每个服务器的备份者是谁
2 除了处理请求外,每个服务器还得自己维护备份开销
3 平时大量的内存都被用于备份数据,会增加 jvm 的垃圾收集频率,间接影响性能
4 一旦某个服务器长时间挂掉,那么另一台服务器的负荷就会翻倍,可能也被一起拖垮
为了克服以上缺点,各个厂商都有自己的不同解决方案
V. IBM 的方案:中央服务器
很像是之前的数据库方式吧,只是把数据库换成了一台专门备份的服务器,综合了数据库和内存方式的优点。
1 将备份和请求处理分开,增强了系统的鲁棒性
2 所有的备份数据都在专门的服务器上,节省了其他服务器的内存
3 所有服务器都能存取备份机器上的数据,任何服务器宕机了,负载都能平滑的分配到剩余所有服务器上,而不至于加重某台机器的负担
4 比起数据库连接,备份服务器使用的 socket 连接更加轻量级
比起直接的双服务器互拷内存,这种方式效率还是比较低的,因为还要有个恢复的过程,而且在管理上也增加了系统的复杂度。此外,备份服务器本身也很有可能成为瓶颈。
VI. Sun 的实现:专用数据库
表面上就是数据库的方式,但实际上, Sun JES 应用服务器使用的称为“ HADB ”的数据库是专门为 session 的备份做了优化的,其大部分数据都是直接存放在内存中
* Session 备份的性能考虑
集群中每台服务器都有多个 web 应用,每个应用又同时有数以千记的用户 session ,这些 session 的备份和恢复都会耗费大量资源。更恐怖的是, session 一直在变,比如失效了,比如增加或修改某个属性了,等等。所以如何备份 session 是考验系统架构水平的
何时备份?
不管是用数据库还是内存方式,下面两点都是比较常见的考虑
1. 按 web 请求,即每次请求都进行备份更新,这样可以保证备份的数据是最新的
2. 固定时间备份,虽然不能保证备份的 session 是最新的,但却可以提高性能
备份粒度
1.全部备份,最保险也是最慢的方法
2.备份修改过的 session 。通常的做法是检测 ”HTTPSession.setAttribute()” 或 “HTTPSession.removeAttribute()” 的调用,当然你要保证 session 的状态只能由这两个方法更改,尽管这不是 J2EE 规范的要求。
3.备份修改过的 session 的属性,最节省性能的做法。但是有一点很重要:防止交叉引用。如下图, session 中包含 school 对象,指向一个 student 对象。当 school 对象某个属性改变后,被备份到 AS2 中,它此时引用的是旧的 student 对象。接着 student 对象被单独改变了,也进行了备份,但是 school 中仍然引用旧的 student 。因此这种方法对 web 容器的设计要求很高,尽管它的性能是最好的。
图十三: session 复制中的交叉引用
VII.其他 失效接管的实现
以上不论是数据库还是内存复制,归根结底都要使用 Java 的序列化机制,这给 web 容器的性能造成了不小的影响,因此有些应用服务器使用别的方式进行 session 备份
JRun with Jini
JRun 4 使用 jini 实现集群, jini 的详细介绍请参考 http://java.sun.com/products/jini/2_0index.html
Tangosol with Distributed Cache
Tangosol Coherence™ 提供了一个分布式数据管理平台,可以嵌入到大部分 J2EE 系统中,此外还提供一个分布式缓存系统,能够在多台 jvm 上有效的共享缓存,详情请参考 http://www.tangosol.com/
二. JNDI集群
Jndi集群对于EJB也是非常重要的,因为几乎所有的EJB都是从JNDI调用开始的
I. 共享全局JNDI树
Weblogic和JBOSS都使用一个全局的、共享的、分布在整个集群系统的JNDI树,对象被绑定到全局上下文,使用ip多播方式拷贝JNDI数据
图十四:全局共享JNDI
集群中的每个节点都有自己的命名服务器,并且自动保存其他所有节点的JNDI数据,因此这种结构具有高度可靠性
实际中,集群JNDI树主要有两个用途:一是用于部署,你只需要将某个EJB部署到一台服务器上,系统会自动将其拷贝到其他节点。二是在运行中你可以用JNDI存取自己的对象,这些自定义对象同样会被自动拷贝到其他节点。
II.独立的JNDI树
Sun JES, IBM Websphere和其他厂商使用的是独立的JNDI树,各个节点拥有自己独立的JNDI,而不会关心其他节点的JNDI。但这并不意味着它们不能实现集群,关键是每台服务器上的配置要相同,应用也要相同,这样的话通过代理agent就能实现高性能的集群了
Sun JES 和 IBM Websphere都在每个节点处安装了一个agent,由console负责协调各个agent
但这种方式不支持动态绑定运行时生成的对象,设计者的理由是:JNDI本身就是作为管理外部资源的中间层,运行时对象绑定不在JNDI的职责范围内,如果真的有这种需要,可以使用LDAP或者具有HA功能的数据库。Sun和IBM都有自己的LDAP实现
III. 中央型JNDI树
少数厂商采用中央型的JNDI,所有节点都去一个某个指定的JNDI服务器上获取资源,当然这对于客户端来说是透明的。不过这种方式会大大增加安装和管理的复杂性,因此被大部分厂商放弃
怎样开始连接JNDI呢
使用JNDI,你必须知道提供JNDI服务的域名或IP以及端口,在全局和独立型JNDI树中,都存在着多个JNDI服务器,客户端要连接哪一个呢,负载均衡和失败恢复又是如何实现的呢?
技术上来说,可以在客户端和JNDI服务器之间放一个硬件或软件实现的负载均衡器,来实现上述要求,不过大部分厂商都有更加简单的方法:
* SUN JES 和JBOSS 能够令“java.naming.provider.url”这一JNDI属性支持用逗号分隔多个地址,例如,“java.naming.provider.url=server1:1100,server2:1100,server3:1100,server4:1100”,客户端会逐个搜索,直到找到一个可用的为止
* JBOSS还支持自动找寻功能,即令“java.naming.provider.url”属性为空,客户端会自动以网络多播的方式寻找一个JNDI服务器作为查询的起始点
三. EJB集群原理
EJB衍生自分布式计算技术,服务器组件或富客户端都能够以标准协议(RMI/IIOP)调用远程的EJB,并且RMI/IIOP技术能够屏蔽底层网络,使得EJB的调用对客户透明化
如图,客户端不是直接和EJB打交道,而是通过stub来代理。Stub负责利用RMI找到远程的EJB并进行调用。EJB的调用过程分为下面三步(EJB2.0)
1.从JNDI中查找EJBHOME stub
2.利用home stub创建一个EJB Object stub
3.利用EJB Object stub调用EJB的方法
在JNDI查找时,可以利用上文说到的方法进行负载均衡;各厂商也会有自己专门的技术对stub的使用进行负载均衡,一般有以下三种方式:
Smart stub
我们知道,stub可以通过JNDI来获得和创建,甚至可以直接下载相应的class文件来在运行时动态生成,而无须在客户端本地的classpath中声明
如图十七,Weblogic 和JBOSS通过在stub的代码中嵌入特殊的代码来实现集群,这个stub的确相当smart,它包含了所有能够访问的集群服务器节点、内置了专门的集群算法来决定把请求发给何处,甚至当集群的拓扑有变时(比如增加了节点),能动态改变自身来适应新的结构,无须手工干预
Smart stub有以下几个好处:
1.节省大量服务器资源
2.由于负载均衡是由客户端处理,因此可以防止在服务器端放置负载均衡器而可能导致的单点失败
3.Stub可以动态下载安装,意味着无须手工维护
IIOP Runtime Library
Sun JES采用另一种方法,它直接修改客户端的IIOP运行时库
如图十八,sun直接把负载均衡逻辑转移到了IIOP库中,从而使stub能够保持“小而轻”,同时由于IIOP是底层库,能够更加有效地获取和使用JVM提供的资源。但正是由于底层库有所不同,使得JES与其他J2EE服务器打交道时可能会出现一些问题
Interceptor Proxy
IBM Websphere使用Location Service Daemon (LSD)作为一个拦截器代理来实现EJB集群
Figure 19: Interceptor Proxy
使用这种机制,stub中包含至LSD的路由信息,而不是直接去找服务器节点,这样LSD就能获得所有请求并进行负载均衡了,不过这样会大大增加管理和维护的成本
EJB的集群支持
调用EJB方法的过程中我们要和两个stub打交道,一个是home stub,另一个是object stub,因此可以在两个层面上实现负载均衡和失败恢复
1.EJBHOME STUB的集群实现
由于EJBHOME STUB本身不包括任何客户端信息,无论从哪个服务器上获得的EJBHOME STUB都是一样的,因此当客户端调用EJBHOME STUB的create等方法时,就能利用一些负载均衡的算法选择合适的服务器节点
2.EJBOBJECT STUB的集群实现
EJBOBJECT STUB包含业务接口,而且其本身也能够含有集群节点的信息,但也不是所有的方法调用都能够进行负载均衡式的路由,得看EJB的类型是什么
无状态会话bean最容易实现负载均衡了,因为它本身不包含特定的客户信息
有状态会话bean略有不同,因为它本身包含客户端的会话信息,因此有状态bean的集群实现本质上和HTTP session 无异,一般情况下客户端的stub都是一直与某个节点上的EJB组件打交道的,除非中途出问题了才会将请求转发到备用的节点上
实体bean本质上也是无状态的。表面上看可以采用和无状态会话bean一样的方式集群,但实际上很少厂商会对实体bean做集群。因为实体bean一般都是被其他会话bean调用的,因此通常都使用本地接口通讯,实在没有集群的必要
四.JMS和数据库连接的集群
目前一些数据库产品已经可以集群了,你可以部署成多份,每个节点之间可以同步。但是JDBC本质上是有状态连接,和底层的socket紧密绑定。当某个JDBC连接突然中断了,与之相关的对象也就费了,因此很难对JDBC集群。Weblogic使用一种JDBC multipool,可以在JDBC断开情况下,方便地进行重新连接
JMS的负载均衡和失败恢复只在JMS broker上有实现,很少有厂商在JMS destination 的消息上实现了负载均衡
五.关于J2EE集群的神话
失败恢复能够防止所有错误吗?——错!
JBOSS的文档花了整整一章的内容来提醒你:“你真的需要HTTP SESSION复制吗”,有时候不用失败恢复也能以经济的方式获得高可靠性。更何况,失败恢复并没有你想象中的那么可靠
你也许认为失败恢复能够在节点宕机时保护你的session数据,但你得清楚,这种保障是有代价的。
回想一下作者在定义“失败恢复”时,前提条件是“在两个方法调用之间”!也就是说只有在第一个方法成功返回之后、第二个方法调用开始之前,失败恢复才能起作用
假设某个方法处理到一半时服务器不幸挂掉了,客户端一般只能收到错误消息。除非你这个方法恰好是“idempotent”的(即多次调用的结果都能保持一致,不会对环境造成任何改变,比如getter方法),那么有些聪明的负载均衡器会尝试找其他节点去调用这个方法
所以说“idempotent”这个概念非常重要,因为客户端压根不知道是在什么地方失败的,而如果这个方法不是idempotent,那么系统就可能处于一种不一致的状态,很危险
你也许认为事务性的方法就是idempotent的,毕竟事务可以回滚。但其实事务远远不能涵盖整个远程调用的范围,比如服务器成功执行某事务性方法后,返回结果的过程中网络崩溃了呢?
在比较严格的系统设计中,你根本不能令所有方法都idempotent,你能做的就是利用failover,尽可能减少错误而不是彻底杜绝错误。以某个网上商城为例:每台服务器都同时处理100个用户请求,当某台服务器崩溃时,如果没有失败恢复,那么你会得罪一百个用户;而如果有失败恢复,可能只有不到20个用户会发飙。你自己要权衡:
1.是得罪一百个客户还是得罪20个客户?
2.有没有失败恢复的服务器的价格差别
独立的应用程序可以无缝地迁移到分布式平台上?——错!
这只是某些厂商的广告而已,不可轻信。如果是大型系统,那么设计之初就应该全盘考虑集群可能造成的影响.
HTTP SESSION
正如前文所述,HTTP SESSION的集群会受到你使用的服务器的诸多限制。首先是可序列化的要求,在很多MVC架构中,session被用来存储一些不可序列化的对象(比如servlet context);其次,序列化、特别是数据库方式的序列化非常耗费资源,因此要集群就尽量不要用session存储大对象。如果是内存拷贝的方式,那么就要考虑内存的限制和交叉引用的问题(交叉引用请参考前文);最后,你在集群环境下改变session的属性时,必须使用“setAttribute”方法,而在单机环境下却没这个限制。这么做的主要原因是让你的应用服务器能够检测到属性改变,及时地备份已修改的属性
缓存
大部分流行的应用服务器都会使用缓存来提升性能,但缓存一般都是为单机环境设计的。集群环境下如果使用缓存,那么缓存之间的拷贝花费的性能将比缓存带来的好处还多,适得其反
静态变量
有一种很流行的J2EE设计模式“singleton(单体)”,是使用静态变量的,这在单机环境下适用,但到了集群就不行了,道理很简单,每个JVM都有自己的静态对象,“单体”也就失去意义了。比如要统计在线用户数时,经常用一个静态变量来存储,但是在集群环境下这种方法显然失去作用。要使用静态对象,最好把它放到一个数据库中,才能实现全局的“单体”
外部资源
尽管J2EE规范并不推荐,但外部IO操作还是有用的,比如用来存放用户上传的文件。在集群中,服务器是不能通过另一台服务器把本地文件直接存放到别的机器上的,那么还是要依靠数据库来统一存放文件,或者你可以使用SAN这种集中式文件仓库
专有服务
有些专有服务是只限于单机环境的,比如定时器服务(timer);再比如某些事件触发类型的服务,如初始化服务,邮件提醒服务也属于此列。这些服务一般针对一个特定条件只发生一次,拿去集群没什么意义。JBOSS的“clustered singleton facility”就是用来保证所有服务器运行且只运行一次某种服务
分布式系统比并列式系统更灵活?——未必
尽管EJB生来就是为了分布式、解耦合,但很多框架认为把EJB层和web层放在一起也未尝不是好事,或许更好
如图20,负载均衡器先将请求分发到适当服务器的web层,web层进一步判断调用何处的EJB,这等于是做了两次的负载均衡和失败恢复。很多人认为这种设计并不好:
1. 首先,第二次的转发根本没有必要,每个服务器都有自己的web和ejb层,比起只调用内部的ejb,web层调用其他ejb层没有任何的好处
2. 第二次的失败恢复也是没有必要的,大部分厂商都把web容器和ejb容器实现为在同一个jvm上跑,那样一旦web容器挂了,ejb容器很难独善其身
3. 损失性能,这个不必解释了,大量的服务器之间的调用对性能肯定有影响。
实际上,大部分厂商都让web容器优先选择同一个服务器上的ejb容器,这种方式称作“并列式”,是分布式的一种特例,如下图
这样引出一个有趣的问题:既然都放一块了,为什么不直接用ejb的本地接口呢?虽然本地接口可以提高性能,但它却把web和ejb紧耦合在一起了,负载均衡机制也就没有办法对其进行优化,特别是当你要变成分布式的时候
此外很不幸的是,在集群环境下本地接口的使用经常受到限制,因为有本地接口的ejb通常是不可序列化的。所以有些应用服务器,比如sun JES,对本地接口的ejb做了特殊处理使其可以被序列化,从而保存到诸如session中去
还有一个问题是,既然大部分情况下并列式的性能都比较好,那还要分布式干什么呢?其实在某些情况下还非得用分布式不可:
1. 除了web容器,富客户端也要调用ejb组件
2. Web容器和ejb容器所处的安全级别不同,它们物流上也被放在不同的地方,中间加上一个防火墙
3.Ejb和web层的极度不对称。指的是复杂度的不对称,比如ejb层有大规模的运算,那就放在一台高级的服务器上,而web容器只需放在一台pc上即可.
六.结论
集群与单机环境是有很大不同的,各厂商的集群实现也不同。最好在项目开始就考虑到集群,使得你的系统更有扩展性,根据自身的需求选择最合适的应用服务器,并且选购第三方软件时也要考虑到对集群的支持。最后,合适的架构可以让你受益于集群而不是让集群成为你的噩梦