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
的厂商实施集群的方法也不同。必须要认真考虑是否需要采用集群环境,并且认真选择相应的产品来支持集群环境的正常工作。