Java EE
集群技术初探
越来越多的任务关键型与大规模应用运行在
Java EE
技术上。有的应用例如银行的或者是记费系统需要越来越高的高可用性
(HA)
,另外一些应用比如像
Google
或者是
Yahoo
则需要越来越好的可扩展性。
eBay
在
99
年的
22
小时宕机,造成
230
万笔交易的中断,影响其股价下跌
9.2%
。这个示例充分证明了高可用性和可扩展性在今天变得越来越重要。
Java EE
的集群技术是为了提供高可用和有容错能力的可扩展服务。但是由于
Java EE
缺少此方面的规范,所以
Java EE
的供应商都使用不同的技术来实现集群,为此对开发人员和架构师带来一定的问题,例如:
- 有集群功能选项的Java EE服务器为什么会那么贵?(可能比没有的要贵10倍)
- 我的程序按照标准Java EE规范编写,为何不能在集群环境下运行?
- 为何我的程序在集群环境下运行得更慢?
- 为何要移植在集群环境下的程序那么复杂?
所以,我们通过探讨
Java EE
集群环境的实现方式来理解并解决上述问题。
一些基本的概念
在真正开始探讨实现技术之前,我们先来了解一些基本的概念。了解这些概念不仅是理解
Java EE
集群技术的基础,而且勾勒出了构建集群的不同技术实现的关键点,更易于理解实现。
可扩展性
(scalability)
在一些大规模的系统中,不太好预估确切的在线用户数和他们的行为。可扩展性即指系统能够支撑用户数量激增的情况。立马能想到的提高可扩展性
(
在线并发会话
)
的办法是在服务器上增加资源
(
内存、
CPU……)
。集群技术是解决这个问题的另一途径。集群支持一组服务器从逻辑上作为单独的服务器运行,但共同分担系统负载。
高可用性
(high availability)
采用单一服务器解决方案
(
增加内存、
CPU
等
)
来解决可扩展性的问题并非完美,因为系统失败集中于一点
(
单一服务器
)
上。任务关键型的应用
(
银行的或者是计费系统
)
不能容忍哪怕只有一分钟的宕机。所以需要这些服务在任何时刻都必须可以访问。集群技术是达到这类高可用要求的解决方案,它通过在集群中提供附加的服务器来保证一旦其中的某个服务器宕机时服务的延续性。
负载均衡
(load balance)
负载均衡是集群技术中重要的一部分。通过将请求分发到不同的服务器,它提供了可同时获得高可用性和更高性能的服务的方法。简单的负载均衡器可以是一个
Servlet
或一些插件,复杂的负载均衡器可能是高级的嵌入了
SSL
加速器的硬件设备。除了分发请求之外,负载均衡器还负责执行一些重要的任务:
- “会话持久”用来确保用户会话生命周期在一台服务器上的完整性。
- “心跳”来检查失败的服务器。
- 在某些情况下,负载均衡器需要参与“失败转移”的过程,下文中会提及。
容错
(fault tolerance)
高可用环境下的数据不见得肯定是正确的数据。在
Java EE
集群中,当一个服务器实例宕机
(
以后统称
“
失败
”)
后,服务通过集群中别的服务器依然可用。但是在失败的那台服务器中正在处理的请求也许就不能获取正确的数据了。所以,容错技术可始终保证在特定的错误环境下也能执行正确的行为。
失败转移
(Failover)
失败转移是集群当中的另一项实现容错的重要技术。失败转移通过在集群中选择另一节点,将能延续失败节点上的服务。失败转移可通过代码指定或者自动通过底层平台的通信链路进行自动切换。
幂等方法
(Idempotent methods)
幂等函数,或幂等方法,是指可以使用相同参数重复执行,并能获得相同结果的函数。这些函数不会影响系统状态,也不用担心重复执行会对系统造成改变。例如,
“getUsername()”
函数就是一个幂等函数,
“deleteFile()”
函数就不是。
“
幂等
”
是
HTTP Session
和
EJB
失败转移中的一个重要概念。
Java EE
集群
总的来讲,
Java EE
集群技术包括
“
负载均衡
”
和
“
失败转移
”
两个部分。

在上图中,
“
负载均衡
”
即指由许多客户端对象
(Client Object)
同时请求目标对象
(Target Object)
。负载均衡器位于调用方和被调用方中间,可以将请求分发至功能相同但位置不同的目标对象中。高性能和高可用性都可通过这种方法实现。

如上图,
“
失败转移
”
的工作方式和负载均衡不同。通常,客户端对象可成功请求目标对象。如果目标对象在请求过程中失败,
“
失败转移
”
系统将监测到失败,并将后续的请求重定向至另一个可用的对象上。可通过这种方法实现容错。
那么,什么类型的对象可以集群呢?在
Java
代码中如何体现负载均衡和失败转移的点呢?实际上,并不是所有的对象都可以被集群起来,负载均衡和失败转移也不可能在任意一行
Java
代码上发生。

查看上述代码。
A
类中
business()
方法的代码可以被负载均衡或失败转移么?我们的答案是不会。对于负载均衡和失败转移来说,都需要在调用方和被调用方之间有一个监听器来负责分发或重定向对不同对象的方法调用。
A
类和
B
类的对象运行在相同的
JVM
中,并且紧密关联,我们很难在方法调用之间添加分发逻辑。
所以,什么类型的对象可以被集群起来呢?答案是只有那些能够被部署在分布式拓扑环境下的组件对象。
所以,何时产生负载均衡和失败转移的行为呢?答案是只有在调用分布式对象的方法时。

如上图所示,在分布式环境下,调用方和被调用方被明确分隔在不同的运行时容器中。可通过不同的
JVM
、进程或计算机来进行分隔。
当客户端调用目标对象时,即在目标对象的容器中执行方法
(
所以称之为
“
分布式
”)
。客户端和目标对象通过标准的网络协议通信。通过这些特性,可使用一些机制来干预方法调用的路径达到负载均衡和失败转移的目的。
在上图中,浏览器可能通过
HTTP
协议调用远程
JSP
对象。
JSP
在
Web
服务器中执行,浏览器并不关心执行过程,它只等待结果。在这种情况下,即可在浏览器和
Web
服务器中间加上能实现负载均衡和失败转移功能的组件。在
Java EE
中,分布式技术包括:
JSP(Servlet)
、
JDBC
、
EJB
、
JNDI
、
Web
服务等。负载均衡和失败转移在调用这些分布式方法的时候发生。下面,我们就来深入探讨这些技术细节。
Web
层集群实现
Web
层的集群技术是
Java EE
集群技术中最基础且最重要的部分。
Web
集群技术包括:
Web
负载均衡与
HTTPSession
失败转移。
Web
负载均衡
Java EE
的供应商通过多种方式实现
Web
负载均衡。总的来说,负载均衡器在浏览器和
Web
服务器之间进行监听,如下图:

诸如
F5
负载均衡器这样的硬件产品可以担当这个工作,也有可能是一个有负载均衡插件的另一个
Web
服务器。无论使用什么技术,负载均衡器通常具有以下特点:
- 实现了负载均衡的算法
当客户端请求到来 时,负载均衡器决定将此请求分发到后端的哪台服务器实例上。经常使用的算法包括轮询、随机和基于权重的算法。负载均衡器试图均衡每个服务器实例上的工作负 载,但上述算法都不可能真正实现理想的均衡,因为他们仅仅考虑到了分发到特定服务器实例的请求数量。使用复杂算法的负载均衡器通过特殊算法在分发请求之前 将探测每台服务器的工作负载。
- 心跳线检查
当某个服务器实例失败时,负载均衡器需要检测到该失败并取消分发到该实例的请求。负载均衡器也需要监控失败的服务器何时重新开始服务,并开始对其分发请求。
- 会话持久
几乎所有的
Web
应用都使用会话状态来记录诸如登录、购物车等信息。因为
HTTP
协议本身是无状态的,会话状态需要保存起来并与浏览器会话关联。在负载均衡的状态下,最佳选择是将请求分发到与该浏览器会话的上次请求同一的服务器实例上。否则,可能会导致应用工作不正确。
因为会话状态是在
Web
服务器实例的内存空间中存储的,所以
“
会话持久
”
就相当重要了。但当一个服务器实例失败后,所有该实例中的会话信息就会丢失。所以,我们即需要会话的失败转移。
HTTPSession
失败转移
主流的
Java EE
供应商都实现了
HTTPSession
失败转移。如下图,当浏览器在第一和第二步访问有状态的
Web
应用时,在该服务器的内存中会创建会话对象。与此同时,在第三步将能唯一确定会话对象的
HTTPSession ID
发送至浏览器。浏览器将该
ID
作为
cookie
存储,并在下次请求该
Web
应用时将此
cookie
发送至
Web
服务器。为了支持会话失败转移,
Web
服务器中的会话对象将被备份下来
(
第四步
)
,以备服务器失败时防止会话信息丢失。负载均衡器将检测到实例的失败
(
第五步和第六步
)
,并将后续请求分发到其他服务器实例上
(
第七步
)
。由于会话对象已经被备份下来了,处理请求的新服务器实例就能恢复该会话的信息,继续正确处理会话。

要实现上述功能,需要引入以下内容:
- 全局HTTPSession ID
上面已经讲过,
HTTPSession ID
用来标识特定服务器实例中的内存会话对象。在
Java EE
中
,
HTTPSession ID
根据
JVM
实例来产生。每个
JVM
实例能驻留多个
Web
应用,每个应用能为不同的用户保存
HTTPSession
。
HTTPSession ID
是在当前
JVM
实例中访问相关会话对象的关键。在会话失败转移的实现中,不同的
JVM
实例不能生成相同的
HTTPSession ID
。如若不然,在失败转移发生时,就不知道哪个对象是应该恢复的对象。所以,需要引入全局
HTTPSession ID
机制。
- 备份会话状态的机制
后续的描述中会有这方面的内容。将来我还着手写一片比较各主流服务器中会话备份机制的具体实现的文章。
- 备份频率和粒度
HTTPSession
状态的备份会造成额外开销,比如
CPU
时间周期、网络带宽和磁盘或数据库
I/O
。备份操作的频率和粒度的选择都可能影响集群的性能。
数据库持久化办法
一些的
Java EE
集群产品都支持使用
JDBC
通 过关系型数据库备份会话状态。在下图中,显示了该方法仅需让服务器实例序列化会话内容并在适当的时候将他们保存在数据库中。当失败转移发生时,另一可用的 服务器实例接管失败的服务器实例,并从数据库中恢复所有的会话状态。对象的序列化是其中的一个关键点,这样内存中的会话数据就可以得到持久化并可进行移 植。

通常,执行数据库的事务会造成一些额外的开销,所以本方法的主要缺陷在于当并发数据量较大时可能无法提供需要的可扩展性。大部分采用数据库会话持久化方法的应用服务器供应商都建议尽量减少
HTTPSession
存储的对象,但这又可能影响到
web
应用的架构和设计,尤其是在需要使用
HTTPSession
存储用户数据的时候。
采用数据库持久化方法的优点在于:
- 易于实现。
- 由于数据库对集群中的所有节点都共享,所以,可以实现对会话数据的失败转移。
- 即使在集群的所有节点都瘫痪的情况下,会话数据依然能够得到保留。
内存复制办法
Tomcat
、
JBoss
、
WebLogic
、
Websphere
之流的
Java EE
服务器都提供了另一种实现:内存复制。

在上图中描述了使用内存复制技术来实现会话状态备份的场景。本方法的性能很好。之余数据库持久化技术,在原服务器和一台或多台备份服务器之间进行直接内存复制对网络通信的影响很小。但是,不同厂商的服务器对内存复制技术的应用方式不同
(
在下面有具体描述
)
,有的厂商采用的技术就不再需要
“
恢复
”
的过程了,即会话数据备份时已驻留在备份服务器的内存中,当备份服务器接管时,所有的数据已经存在。
“JavaGroups”
是目前
JBoss
和
Tomcat
集群使用的通信层。
JavaGroups
是一套可靠的组合通信和管理工具包。其中的核心功能,如
“
组员协议
”
与
“
消息多播
”
技术,是支持集群正常工作的重要基础。具体内容可以参考
http://www.jgroups.org/javagroupsnew/docs/index.html
。
Tomcat
:多服务器复制
有很多种内存复制的办法,第一种就是将会话数据复制到集群中的所有节点上。
Tomcat 5
就是使用者中方法实现的。

在上图中,当一个特定服务器实例的会话改变时,它将数据备份到所有其他服务器 上。当该服务器实例失败后,负载均衡器能选择其他可用的服务器实例进行接管。但此方法在可扩展性上存在一定局限。如果集群中的实例数量较多,就不能忽略网 络通信的额外开销,可能严重影响网络通信性能并成为应用性能的瓶颈。
WebLogic
、
JBoss
、
WebSphere
:服务器配对复制
考虑到性能和可扩展性的因素,
WebLogic
、
JBoss
和
WebSphere
都使用了另一种内存复制的技术:每个服务器实例选择另一专门的备份实例来存储会话信息,如下图:

使用这种方法,每个服务器实例都有与其匹配的备份服务器。本方法在更多实例被添加到集群中时消除了可扩展性的问题。
虽然本方法也能实现高性能的会话失败转移和高可扩展性,但其依然具有以下局限性:
- 增加了负载均衡器的复杂度。当服务器实例失败时,负载均衡器需要找出该服务器的匹配备份服务器。这就影响到负载均衡器的选择范围,在这样的要求下一些硬件负载均衡器就不能使用了。
- 除了正常处理请求外,服务器还需要承担复制的工作。这可能影响到服务器的吞吐量,因为需要将一些CPU时钟周期分配用来做复制的工作。
- 在正常的处理过程中(没有失败转移法生的情况下),备份服务器中存储的备份会话信息浪费了大量的服务器内存,这会对JVM的GC(垃圾回收) 产生额外的开销。
- 由于集群中的服务器是配对复制的,所以当主服务器上失败后,负载均衡器就将该对服务器的所有请求转移到配对的备份服务器上。备份服务器于是就会处理很多额外的请求,可能造成备份服务器性能问题。
为了克服上述问题,各厂商都纷纷出招。
WebLogic
为了克服最后一个问题,将复制配对定义从服务器粒度上降低到会话粒度上。当一个服务器实例失败后,其上的会话被分散转移至备份服务器中,并将均衡失败后的负载分配。
IBM
:集中状态服务器
WebSphere
有另外一种方案来进行内存复制:将所有会话信息集中备份到一台状态服务器
(
我记得
Sybase
在其第一个
J2EE
服务器产品
EAServer
或
Jaguar CTS
中就采用本方法实现集群,目前最新的版本是
6
,不知道有没有改变
)
,如下图:

该方案和数据库持久化的方案很像。不同点在于本方法指定一台
“
会话备份服务器
”
来代替数据库。这种方案结合了数据库持久化方案和内存复制方法的优点:
- 将请求处理和会话备份处理分离,这样能让集群更健壮。
- 所有会话数据将被备份到一台特定的服务器上,不需要其他服务器浪费内存空间存储会话数据。
- 由于会话备份服务器是集群中所有节点都共享的,所以会话的失败转移可顺利完成。所以,可在集群中使用大多数软硬件负载均衡器,更为重要的是,服务器实例失败时,其请求负载将被均衡分散。
- 和数据库连接比较,应用服务器和会话备份服务器之间的网络通信更为轻量,所有比数据库持久化方案具有更好的可扩展性和性能。
但是,由于需要对失败服务器的会话数据进行恢复,其性能不如直接配对内存复制方案的优越。同时,单独的会话备份服务器也增加了管理的难度,也可能由于备份服务器单一的原因造成性能影响。在会话备份服务器宕机的情况下,集群就不能进行正常工作。
SUN
:特定数据库方案

SUN JES
应用服务器如上图所示,采用不同的方式实现会话失败转移。从表面上看,这种方法和数据库持久化方法一样,他们都采用了一个关系型数据库通过
JDBC
连接来存储和访问所有会话数据。但是从内部来看,
JES
使用的是
HADB
,其是专门被优化用来存储访问会话数据的,并将大部分数据都存储在内存中。所以,可能更与集中式状态服务器的解决方案接近。
性能问题
考虑一下这样的场景:一台
Web
服务器可能驻留了很多
Web
应用,每个
Web
应 用都同时有几百个并发用户,每个用户将生成特定的浏览器会话。所有这些会话信息都将被备份下来以防服务器实例失败。更有甚者,会话时时刻刻都在改变:会话 创建、过期;会话中的属性添加、修改与移除;为了判断会话何时过期,会话最后修改时间也随着访问时间而改变。所以性能就成为会话失败转移解决方案中的关键 问题。厂商总会为你提供一些可调参数来对服务器行为进行调节来满足系统性能的需求。
何时备份会话
当客户端处理请求时,会话数据每次都会改变。由于性能因素,并不能对会话进行 实时备份。选择备份频率是相互权衡的结果。如果备份动作过于频繁,性能就会受到很大影响;但是如果备份动作的间隔时间太长,则当服务器失败发生时就可能丢 失更多会话信息。对于所有的备份方案,包括数据库和内存复制方案,都可采用以下几种常用的选项来决定备份频率:
- 根据Web方法。
会话状态是在每个
Web
请求的最后,在向客户端返回应答前备份的。这种模式最大程度保证了在失败时的会话信息是最新的。
- 定期
会话状态定期进行备份。这种模式不能保证会话信息处于最新状态。虽然如此,这种方法能提供卓越的性能提升因为并不是每个请求都对状态进行备份。
备份粒度
在备份会话时,也需要决定到底保存多少会话信息。不同产品采用的办法通常有:
- 全会话
每次都备份整个会话状态。这种模式提供了对会话数据正确备份与分布式
Web
应用的最佳保证。这种方法简单易行,在内存复制方案和数据库持久化方案中都是缺省选项。
- 修改过的会话
如果会话修改了,就备份整个会话。如果调用了
HTTPSession.setAttribute()
或
HTTPSession.removeAttribute()
方法,则就会话就被修改了。虽然
Java EE
的规范没有明确规定一定要使用这两个方法对会话进行修改,但为了保证集群工作正确,必须确保用这两个方法去修改会话。只备份修改过的会话降低了会话备份的数量。在备份间隔期间只进行了读操作的会话并不进行重新备份,这就比全会话备份模式的性能更好。
- 修改过的属性
这种情况只备份修改过的会话属性。这就进一步将备份的会话数据降低到了最小程度。本方法是网络通信量最小,性能最好的方法。为了让采用本方法的集群工作正常,必须遵循一些规则。首先,每次对会话状态的修改都要调用
setAttribute()
方法,仅仅是被修改的对象被序列化并备份。其次,确保属性之间没有交叉引用。每个键
(key)
对应的不同对象是分别被序列化和备份的。如果交叉引用的情况存在,序列化和反序列化的可能错误。例如,在下图中所示的内存复制集群中有一个
“school”
对象和
“student
”
对象在会话中,并且
school
对象有一个针对
student
对象的引用。
School
对象在某时刻被修改并被备份到备份服务器。在序列化和反序列化后,恢复了的
school
对象将保存引用到
student
对象的引用。但是
student
对象和
school
对象是分开修改的,当
student
对象被恢复到备份服务器的内存中后,它也失去了和
school
对象的连接。虽然本方法带来了最佳的性能,上述的局限性对
Web
应用架构和设计的影响相当大,尤其是如果需要使用会话存储复杂结构的数据。

其他失败转移的实施方法
在我上一部分的描述中,备份的粒度对于性能是非常重要的。然而,现在的实施办法
(
数据库持久化和内存复制的方案
)
都是使用
Java
对象序列化技术来传输
Java
对象。这种方法是重量级的,会影响系统性能并局限
Web
应用的架构和设计。一些
Java EE
的厂商在寻找特殊的途径来实现轻量级的,小如指印的模式,并同时提供合理粒度的分布式对象共享机制来提升集群的性能。
JRun
和
Jini
JRun 4
使用
Jini
技术提供了内置的集群解决方案。
Jini
是分布式计算的产物,它支持创建在单一分布式计算空间中设备和软件组件的
“
联邦
”(federation)
。
Jini
提供了如查找、注册和租借的分布式系统服务,在集群环境下非常有用。另一种叫
JavaSpace
的技术也构建于
Jini
上提供诸如处理、共享和移植的功能,同样在集群实现中具有相当价值。详细内容可参考:
http://java.sun.com/products/jini/2_0index.html
。
Tangosol
使用分布式缓存
Tangosol Coherence
提供了分布式数据管理平台,它可通过将目前流行的
Java EE
容器嵌入其中来提供集群环境。
Tangosol Coherence
同样也提供了分布式缓存系统将分布在不同
JVM
实例中的
Java
对象高效地共享起来。详细内容可参考:
http://www.tangosol.com/
。
JNDI
集群的实现
Java EE
规范要求
Java EE
容易需要提供对
JNDI
规范的实现。在
Java EE
应用中,
JNDI
的主要角色就是提供了一个中间层,资源可通过较为透明的方式获得。这使
Java EE
组件提高了重用度。
具有完整功能的集群
JNDI
是
Java EE
集群的重要内容,因为几乎每个
EJB
组件都从查找
(lookup)
其
JNDI
树的
home
接口开始。各个厂商实现
JNDI
集群的方法不同,主要是根据其集群的结构而定。
全局共享
JNDI
树
WebLogic
和
JBoss
都使用集群范围全局共享的
JNDI
上下文以便客户端能查找并绑定对象。绑定到全局
JNDI
上下文的内容将被基于
IP
的多播技术复制到整个集群中,这样当一个服务器实例宕机时,还能保证绑定对象可获得。

如上图所示,全局共享的
JNDI
树实际上包含了每个节点上所有本地的
JNDI
的集合。集群中的每个节点都有其自己的
JNDI
命名服务器,它将所有的信息复制到集群中其它所有的命名服务器中。这样,每个命名服务器的树中都有其他命名服务器的对象拷贝。这样的冗余结构使全局
JNDI
树实现了高可用。
实际上,集群的
JNDI
树有两个用途。管理员可以用它来进行部署任务。在一台服务器上部署了
EJB
模块或配置了
JDBC/JMS
服务后,所有的
JNDI
树都会被复制到其它服务器实例。在应用运行期间,程序用
JNDI
的
API
访问
JNDI
树来存储和检索对象,所以自定义的对象也被全局复制。
独立
JNDI
虽然
JBoss
和
WebLogic
采用了全局共享
JNDI
,但
SUN JES
、
IBM WebsPhere
和其他一些服务器采用了独立
JNDI
树技术。在独立
JNDI
树集群环境下的成员服务器不知道或者并不关心集群中的其他服务器。那这样岂不是意味着不对
JNDI
进行集群?因为几乎每个
EJB
访问都从
JNDI
树中查找其
home
接口开始,所以如果不对
JNDI
树进行集群就会丧失集群的功能。
实际上,独立
JNDI
树依然能实现高可用,只要
Java EE
的应用都是同构的。我们将其称为同构集群,因为所有的实例配置相同,部署了同样的应用。在这样的条件下,一个叫
“
代理
”
的特殊管理工具可用来实现高可用,如下图:

SUN JES
和
IBM WebSphere
都将代理安装在集群的每个实例上。当部署
EJB
模块并绑定其他
JNDI
服务时,管理终端通过对所有代理发送命令来达到与全局共享
JNDI
树同样的效果。
但是独立
JNDI
解决方案将不支持复制由运行的应用绑定和检索的判定对象。原因如下:在
Java EE
应用中,
JNDI
扮演的角色是对管理外部资源提供中间层,而不是运行时数据的存储。如果有需要的话,各自的
LDAP
服务器或有
HA
功能的数据库能够实现。
Sun
和
IBM
都有它们的
LDAP
服务器产品,而且都已经随着集群特性分发。
集中式
JNDI
一些
Java EE
产品使用集中式的
JNDI
树,命名服务器驻留在一台服务器上,所有服务器实例将
EJB
组件和其他管理对象注册至命名服务器中。
命名服务器本身实现了高可用,对客户端透明。所有客户端通过单独的命名服务器查找
EJB
组件。这种结构加大了安装和管理的复杂度,所以不为大多数厂商采用。
对
JNDI
服务器的初始访问
当客户端访问
JNDI
服务器时,需要知道
hostname/IP
地址和远程
JNDI
服务器端口号。在全局共享与独立
JNDI
树解决方案中,均有多个
JNDI
服务器。那么客户端会先连接哪台
JNDI
服务器呢?如何实现负载均衡和失败转移呢?
通常,软件或硬件负载均衡器可在远程客户端和所有的
JNDI
服务器之间执行负载均衡和失败转移的任务。但是几乎没有厂商采用这样的办法,而使用一些简单的解决方案。
- Sun JES和JBoss通过“java.naming.provider.url”让JNDI设置接收由逗号分离的URL列表来实现集群。例如,java.naming.provider.url=server1:1100,server2:1100,server3:1100,server4:1100
客户端将尝试逐个与列表中的每台服务器,一旦连接上之后就停止尝试。
- JBoss也实现了自动恢复的功能。当属性字符串“java.naming.providing.url”为空时,客户端将尝试通过网络多播调用恢复引导JNDI服务器。
EJB
集群的实现
EJB
是
Java EE
重要的组成部分,实现
EJB
的集群也是最具挑战的部分。
EJB
技术也来源于分布式计算,可在独立的服务器上运行。
Web
服务器组件或胖客户端能从其他计算机通过标准
RMI/IIOP
协议访问
EJB
组件。调用远程
EJB
的方法和调用本地
Java
对象的方法一样。实际上,
RMI-IIOP
完全实现了对本地
/
远程调用的透明性。

上图展示了调用远程
EJB
的机制。当客户端要调用
EJB
时,不能直接调用
EJB
,它需要调用称为
“
存根
(stub)”
的本地对象,其接口与远程对象一致,充当着远程对象代理作用。
Stub
负责本地接收方法调用并将其传递到网络另一端的远程
EJB
上去。
Stub
在客户端
JVM
中运行,并通过
RMI/IIOP
向远程网络查找真实对象。
我们通过了解如何在代码中调用
EJB
来解释
EJB
集群的实现。如欲调用
EJB
,需要:
- 从JNDI服务器中查找EJBHome存根。
- 通过EJBHome存根查找或创建EJB对象,返回EJBObject存根。
- 通过EJBObject存根调用EJB方法。
负载均衡和失败转移可在
JNDI
查找的时候发生。当通过
EJB
存根
(
无论
EJBHome
还是
EJBObject)
调用方法时,厂商已通过不同的方法实现了
EJB
的负载均衡和失败转移。
智能存根
由于客户端通过存根对象访问远程
EJB
,存根对象可通过检索
JNDI
树获得,甚至可能让客户端透明地通过下载任意一台
web
服务器上的类文件也可获得。所以,存根的特点有:
存根可以在运行时动态地或通过编程的手段生成。存根的定义,即类文件并不需要在客户端环境的
classpath
下,也不需要包含在客户端运行时的
JAR
包中
(
因为可通过下载的方法获得
)
。

如上图,
BEA WebLogic
和
JBoss
采用在存根代码中添加特定功能来实现
EJB
的集群。这些代码透明运行在客户端。这种技术称之为智能存根技术。
智能存根之所以智能是因为它包含了能访问的目标实例。它能侦测到目标实例的失败,并使用复杂的负载均衡和失败转移的逻辑将请求转发至其他目标。此外,如果集群的拓扑图发生了变化
(
例如,有新实例加入或实例移除
)
,存根能自动更新目标列表来反映最新的变化情况。
在存根中实现集群的优点如下:
- 因为EJB存根运行在客户端,所以节省了很多服务器端的资源。
- 负载均衡器整合在了客户端代码中,并与客户端生命周期息息相关。这就避免了单点负载均衡器失败的情况。如果负载均衡器失败了,客户端也极有可能失败,所以对系统并没有太大影响。
- 存根能自动动态下载更新,最大程度降低了维护量。
IIOP
运行时库
Sun JES
应用服务器通过另一途径实现
EJB
集群。负载均衡和失败转移逻辑在
IIOP
运行时库中实现。例如,
JES
修改了
ORBSocketFactory
的实现,让其支持集群,如下图。

修改过的
ORBSocketFactory
拥有执行负载均衡和失败转移的所有逻辑和算法,同时也保持了存根的简洁。因为实在运行时库中实现的,所以比起在存根中实现的办法来说它能更容易获取系统资源。但是这种方法在客户端需要指定的运行库,在与别的
Java EE
产品整合时可能会有一些麻烦。
监听代理
(Interceptor Proxy)
IBM WebSphere
引入了位置服务后台线程
(LSD
,
Location Service Daemon)
,其作为
EJB
客户端的监听代理,如下图所示。

在这种方法中,客户端从
JNDI
中查找并获得存根。存根包含了通向
LSD
的路由信息而不是直接到
EJB
驻留的应用服务器。
LSD
接收所有的请求并根据负载均衡和失败转移策略来决定将它们分别发送至哪台实例中。本方法使集群的安装和维护工作量加大。
对
EJB
的集群支持
要调用
EJB
的方法,需要两种类型的存根对象:一个是
EJBHome
接口另一个是
EJBObject
接口。这就意味着对
EJB
的负载均衡和失败转移可能在两个阶段发生:
- 当客户端使用EJBHome存根创建并查找EJB对象时
- 当客户端使用EJBObject存根来调用EJB方法时
EJBHome
存根的集群支持
EJBHome
接口用来创建或查找在
EJB
容器中的
EJB
实例,
EJBHome
存根是
EJBHome
接口的客户端代理。
EJBHome
接口不保持客户端的任何状态。所以,不同
EJB
容器的
EJBHome
接口对客户端来说都是相同的。当客户端调用
create()
或
find()
方法时,
home
存根根据负载均衡和失败转移算法从复制列表中选择一个服务器,并把对
home
接口的调用传递到那台服务器上。
EJBObject
存根的集群支持
当
EJBHome
接口创建
EJB
实例时,它将
EJBObject
存根返回客户端让用户调用
EJB
方法。系统已经有了一个集群中可用服务器的列表,在这些服务器上都部署了
EJB
组件,但是根据
EJB
的类型,不能将由
EJBObject
存根发起的方法调用发送至判断服务器实例的
EJBObject
接口。
无状态的会话
bean
是情况最简单的:正因为没有保存状态,所有
EJB
实例都可考虑为相同的,所以从
EJBObject
来的方法调用可被负载均衡或失败转移至任何参与的服务器实例上。
有状态的会话
bean
就不太一样了。有状态的会话
bean
需要保持特定客户端连续请求时的会话状态信息。总的来说,对有状态的会话
bean
实施集群和对
HTTPSession
实施集群差不多。通常,
EJBObject
存根不会将请求转发至之前服务的实例之外的其他实例;它们通常会一直使用
EJBObject
创建时的实例,可以称之为
“
主实例
”
。在处理过程中,状态信息需要从主实例备份至其他服务器。如果主实例失败了,其他备份服务器将进行接管。
实体
bean
从根本上说还是无状态的,尽管它也处理有状态的请求。实体
bean
本身将所有信息数据备份至数据库中。感觉好像对于实体
bean
来说,负载均衡和失败转移都较容易实现。但实际上,实体
bean
不是总能得到负载均衡和失败转移的。正如设计模式中的建议,实体
bean
总是被包装在会话
bean
后。因此,大多数对实体
bean
的访问发生在正在处理的会话
bean
的本地接口中,而不是远程客户端。这就让负载均衡和失败转移失去了意义。
对
JMS
和数据库连接的集群支持
在
Java EE
中,除了
JSP
、
Servlet
、
JNDI
和
EJB
之外还有其他的分布式对象。这些对象可能不能在集群实施中得到全面支持。
现在,一些数据库产品,例如
Oracle RAC
和
Sybase SDC
支持集群环境并可部署成多点同步的数据库实例。但是,
JDBC
是高度有状态的协议,其需要保存事务状态并紧密维护客户端和服务器的
socket
连接。所以,比较难实现集群。如果
JDBC
连接失败了,所有与连接相关的
JDBC
对象都会失败,而重新连接需要客户端代码的支持。
BEA WebLogic
使用
JDBC
连接池来降低重新连接的复杂度。
JMS
在大多数
Java EE
服务器中都支持,但不是完全支持。负载均衡和失败转移只在
JMS broker
上实现了,一些产品还支持
JMS
消息目的地的失败转移功能。
关于
Java EE
集群的误区
失败转移能完全避免错误。
——
否
在
JBoss
的文档中,我看见一段警告:
“
真的需要
HTTPSession
复制么?
”
当然,有的时候不带失败转移的高可用解决方案是可接受,而且也很划算。而且,失败转移的功能并不像想象中的那样强大。
那到底失败转移能带来些什么呢?有的人认为失败转移能避免错误。事实上,如果没有失败转移,当服务器失败时会因会话数据的丢失产生错误;如果有会话失败转移的话,会话数据能被恢复到另外一台服务器实例中,客户端可能都没有察觉失败。这是真的,但绝对是有条件的!
回忆一下对
“
失败转移
”
的定义。失败转移的时机是
“
介于方法调用之间的
”
。这就意味着连续两次调用远程对象的方法,只有当第一个方法调用完毕且第二个调用请求还未送出时才可能发生失败转移。
所以,当正在进行方法调用的时候失败了怎么办呢?答案是:处理过程中止,客户端看见错误消息提示
(
除非方法是幂等方法
)
。只有方法是幂等方法的情况,一些负载均衡器才能试图失败转移这些方法到别的实例。
幂等为何如此重要?因为客户端并不知道服务器何时失败的
(
在方法刚开始调用或者快要调用完成的时候
)
。如果是非幂等方法,则两次调用就会两次改变系统状态,系统就会处于不一致的状态。
在复杂应用中,不太可能把所有的方法都变成幂等方法。所以,只能通过失败转移减少错误,而不可能从根本上避免错误。
未采用集群技术的应用能顺利地透明迁移至集群环境中。
——
否
虽然一些厂商宣称其
Java EE
产品的灵活性,但是我奉劝大家不要相信他们。实际上,需要从一开始的设计阶段就考虑到集群的因素,并在开发和测试阶段去进行验证。
HTTPSession
在集群环境下,根据会话失败转移使用的机制,对
HTTPSession
有很多限制。首先就是限制在
HTTPSession
中存储的对象必须是可序列化的。有些
MVC
的框架使用
HTTPSession
存储一些非序列化对象
(
如
Servlet
上下文、
Local EJB
接口和
web
服务的引用等等
)
,那么这些框架就不能在集群环境下使用。其次,对象序列化和反序列化的过程对性能的开销很大,尤其是采用数据库持久化方法的时候。在这种情况下,应该避免存储大对象和存储的对象个数较多。如果使用的是内存复制的办法,那么必须注意
HTTPSession
中不能存在交叉引用的属性。还有就是必须使用
setAttribute()
方法对
HTTPSession
中的属性进行修改。
缓存
(Cache)
几乎所有的
Java EE
项目都使用缓存来改善性能,但这些缓存都是针对非集群环境设计的,只能在一个
JVM
实例上工作。需要缓存的原因是有的对象频繁创建,有的对象在创建时需要消耗大量资源,所以我们需要在缓存池中保存这些对象避免后续创建。使用缓存的根本原因是维护管理缓存的开销比创建新的对象划算。在集群环境下,每个
JVM
实例需要维护自己的缓存,还需要维护从别的服务器上同步过来的缓存,以便保证所有服务器实例的状态一致。有时,这种同步机制会带来更低的性能。
静态变量
一些设计模式,比如单实例模式将使用静态变量来共享多个对象的状态。在集群环境下,每个服务器实例需要保存自己的静态变量,这就打破了该模式的机制。比如用静态变量对在线用户数进行统计的情况。在集群环境下,这种用法将失败,在集群环境下,最好的办法是将数据存入数据库。
外部资源
很多系统都使用了外部
I/O
操作,比如上传或动态创建
XML
配置文件。在集群应用服务器中,没有办法跨服务器进行文件复制,所以只能通过数据库或外部文件的方法来解决。
特殊服务
比如计时器
(
固定时间间隔触发任务
)
之类的特殊服务很难在集群环境下运行。之类的例子还有邮件通知服务、在整个系统启动时的初始化服务等。
这些服务都是由时间触发的,而不是由请求触发的,而且只能执行一次。对他们进行负载均衡和失败转移意义不大。
有一些产品在这方面也做了一些工作,例如
JBoss
的
“
集群下单模式工具
(clustered singleton facility)”
。
总结
集群与普通的环境不同,
Java EE
的厂商实施集群的方法也不同。必须要认真考虑是否需要采用集群环境,并且认真选择相应的产品来支持集群环境的正常工作。