J2EE clustering part 2
使用容易的方法把你的应用程序从单机移植到集群
By Abraham Kang
翻译 TianYu 2002-12-16
http://www.javaworld.com/javaworld/jw-08-2001/jw-0803-extremescale2.html
摘要
因为大多数开发者在非群集的环境构造他们的web应用,那些应用程序在移植到集群里时经常会崩溃,停止工作.在这篇文章的第二部分, Abraham Kang解释了与群集相关的编程,设置和管理的问题.有了这些知识,你将能够从一开始就写clusterable的代码,从而以后避免隐患.
在J2EE框架里为集群提供了一个高可用性(HA)和伸缩性的基础结构.集群是指在一组应用服务器上显式运行你的J2EE应用,就象是单独的实体一样.然而,当Web应用被群集时,这些应用的行为表现是不同的,因为他们必须通过系列化和其它的集群成员共享应用对象.此外,你将不得不应付额外的结构配置和设置时间.
为了避免主要的Web应用程序返工和重新设计,你应该从你的开发过程一开始就考虑与群集相关的编程问题.为了支持智能负载均衡和失败转移,也需要考虑关键性的设置和结构配置决策.最后你还需要有个处理失败的管理策略.
阅读”J2EE clustering”系列:
· 群集对于好的网站设计是至关重要的;你了解clustering的基础知识么?
以第一部分的知识为基础,我将透露我对群集实际应用的理解.更进一步地,还将研究与群集相关的要点和可能的解决方案.还有每种选择的优缺点.我也将示范指导群集的编程.最后,我对如何为故障作准备做了说明.(注意:由于许可权的约束,这篇文章不适用基准.)
建立你的集群
在集群设置期间,你需要作出重要的决策.首先,你必须选择一种负载均衡方法.第二,你必须决定如何支持服务器亲合力(server affinity).最后,还需要决定如何在群集节点之间部署服务器实例.
负载均衡
你通常可以在两种公认的选项间来选择一个负载均衡的集群: DNS round robin 或者 hardware load balancers.
DNS round robin
DNS是把逻辑名(例如www.javaworld.com)转换成IP地址的过程.在DNS round-robin负载均衡里,单一的逻辑名能返回集群里任何机器的IP地址.
DNS round-robin负载均衡的优点包括:
·便宜,容易设置
·简单
缺点包括:
·不支持服务器亲合力.当用户收到一个IP地址时,它会被缓存在浏览器里.一旦缓存过期,用户发送另一个与逻辑名相关联的IP地址的请求.第二个请求能返回集群里其它任何一台机器的IP地址,这会导致session丢失.
· 不支持HA.设想一个有n台服务器的集群.如果一台服务器down掉了, DNS服务器上的每个第n个请求仍然继续访问这个死掉的机器.
· 集群变更传播到Internet需要花费时间.许多公司和ISPs的DNS服务器缓存了来自于客户端的DNS查找.即使你的集群里服务器的DNS清单能够动态改变,但在其他DNS服务器上缓存的入口,距离过期还需要一段时间.例如,AOL有台已经down掉的服务器,把它从集群的DNS列表清单移出去之后,如果AOL的DNS服务器缓存了这台down掉的服务器入口的话,AOL的客户端仍然会尝试连接到这台down掉的服务器上.结果,即使集群里的其它机器是可用的,AOL用户还是不能连接到这个站点.
· 不保证相同的客户端分布通过集群里的所有服务器.如果你没配置DNS服务器共同协作支持DNS负载均衡,他们可能只把从最初查找到的第一个IP地址返回,使用这个IP来处理客户端的请求.设想一个有数千职员的合伙人公司,所有的请求都被固定到集群里的单一服务器上,结果会怎样!
Hardware load balancers
相反,硬件负载均衡器(like F5's Big IP)通过虚拟IP地址解决了大部分问题.硬件负载均衡器表示只有单个IP地址的集群世界.负载均衡器收到每个请求,重写指向集群里其它机器的头.如果你移出集群里的一些机器,这些变化立即见效.
硬件负载均衡器优点包括:
·服务器亲合力(Server affinity) ---在不使用SSL时
·HA服务--- 失败转移,监控,等等
·规格(Metrics)--- 活动session,响应时间,等等
·保证相同客户端分布通过集群.
可是,硬件负载均衡器也显示了缺点:
·花费高--根据特点不同,$10,000 to $40,000不等
·设置和结构配置复杂.
一旦你已经选出了负载均衡方案,就必须决定集群如何支持服务器亲合力.
服务器亲合力(Server affinity)
在没用Web服务器代理使用SSL时,服务器亲合力会有个问题.(由于session的持续性,服务器亲合力把用户指向集群里一个具体的服务器) .硬件负载均衡器依靠cookies或者URL readings确定请求被指向哪里.如果请求是SSL加密的,硬件负载均衡器不能读取头,cookies或者URL信息.为了解决这个问题,你有两个选择: Web服务器代理(Web server proxies)或者SSL加速器(SSL accelerators).
Web服务器代理(Web server proxies)
在这种情形里,对于Web服务器代理,硬件负载均衡器表现的像DNS负载均衡器,除了这之外,它也有个单独的IP地址.Web服务器解密SSL请求,把它们传到web服务器插件(Web服务器代理).一旦插件收到一个解密的请求,它就可以解析cookies和URL信息,接着把请求重定向到用户session状态所驻留的应用服务器上.
Web服务器代理主要的优点包括:
·Server affinity with SSL
·不需要额外的硬件(只需要硬件负载均衡器)
缺点:
·硬件负载均衡器不能利用metrics定向请求.
·在web服务器上依靠additional strain扩展SSL的用途.
·Web服务器代理需要支持服务器亲合力
如果你的站点所处理的大部分事务必须要求是安全的,当支持服务器亲合力时,SSL加速器可以增加集群拓扑的柔性.
SSL加速器(SSL accelerators)
SSL加速器网络硬件处理到集群的SSL请求.它位于硬件负载均衡器的前面,允许硬件负载均衡器读取在cookies,头和URL里的加密信息.接着硬件负载均衡器用自己的规格定向请求.如果选择通过SSL获得服务器亲合力,使用这种设置可以避免Web代理.
使用SSL加速器的好处:
·支持服务器亲合力和SSL的柔性拓扑(有Web代理或者没有)
·Off-loaded SSL处理到SSL加速器,增加了伸缩性
·中央集中的SSL证书管理
缺点包括:
·为了获得HA而买两个加速器需要高的费用.
·增加了设置和结构配置的复杂性.
一旦你已经决定了服务器亲合力的设置,就需要你把你的应用服务器实例巧妙地布置到集群节点.
应用服务器分布
在把应用服务器实例分布到集群时,你必须决定你是否需要在集群里的一个节点上部署多个应用服务器实例,也需要决定集群里节点的总数.
在一个节点上的应用服务器实例的数量,依靠CPUs数,CPU利用率和可用的内存.
在下面三个条件下,在一个单元考虑多个实例:
·有三个或更多的负荷没有达到饱和的CPUs
·实例堆的尺寸设置的过大,引起垃圾收集时间增加
·应用程序没有I/O边界
确定集群里最佳的节点数是个迭代的过程.首先,profile和优化应用.第二,用负载测试软件模拟你期望的最高利用率.最后,当失败发生时,增加剩余的节点处理负载.
理想的做法,最好把开发版本推到实施的集群里,当有群集问题出现时, 就可以捕获它们.不幸的是,开发者在单机上构建大部分应用,然后把它们移植到群集环境,这种情形会导致应用程序崩溃,停止工作.
Session存储指导
为了破坏最小化,下面列出了应用服务器的基本指导,它们使用了内存或者数据库session持久性.
·确定所有的对象和它们递归引用的那些对象
在HttpSession里是序列化的, 根据经验, 就其标准的一部分来说,所有对象都应该实现java.io.Serializable.
·在HttpSession里,无论你什么时候改变对象的状态,调用session.setAttribute(...)标识改变的对象并且把这些变更保存到一个备份服务器或者数据库.
AccountModel am = (AccountModel)session.getAttribute("account");
am.setCreditCard(cc);
//You need this so the AccountModel object on the backup receives the
//Credit card
session.setAttribute("account",am);
·ServletContext不是序列化的,因此不用它作为一个实例变量.(除非它被标识为暂态的),因为任何直接或间接的对象都存储在HttpSession里. 在Servlet 2.3容器里,当HttpSessionBindingEvent保持有ServletContext的引用时,更容易得到ServletContext的一个引用,这已经被证实了.
·EJB remotes可能不是序列化的.当它们不是序列化时,你需要过载隐含的serialization机制,如下所示(这个类没实现java.io.Serializable,因为AccountModel它的父类实现了序列化.):
...
public class AccountWebImpl extends AccountModel
implements ModelUpdateListener, HttpSessionBindingListener {
transient private Account acctEjb;
...
private void writeObject(ObjectOutputStream s) {
try {
s.defaultWriteObject();
Handle acctHandle = acctEjb.getHandle();
s.writeObject(acctHandle);
} catch (IOException ioe) {
Debug.print(ioe);
throw new GeneralFailureException(ioe);
} catch (RemoteException re) {
throw new GeneralFailureException(re);
}
}
private void readObject(ObjectInputStream s) {
try {
s.defaultReadObject();
Handle acctHandle = (Handle)s.readObject()
Object ref = acctHandle.getEJBObject();
acctEjb = (Account)
PortableRemoteObject.narrow(ref,Account.class);
} catch (ClassNotFoundException cnfe) {
throw new GeneralFailureException(cnfe);
} catch (RemoteException re) {
throw new GeneralFailureException(re);
} catch (IOException ioe) {
Debug.print(ioe);
throw new GeneralFailureException(ioe);
}
}
· 从磁盘恢复session和在每次调用HttpSession's setAttribute(...)方法后, 调用HttpSessionBindingListener的valueBound(HttpSessionBindingEvent event)方法.在失败转移期间不调用valueBound(HttpSessionBindingEvent event)方法.
内存session状态复制
内存session状态复制被看做是比数据库持久性更复杂的,因为在HttpSession里单个对象发生变化时,是被序列化到一个备份服务器.使用数据库session持久性,当sessions里的对象中的一个发生变化,session里的对象就被一起序列化. 内存session状态复制的一个副作用就是克隆了所有的HttpSession对象,这些对象都带有一个session key.如果每个带有一个session key的存储对象都独立于带有不同session key的其他对象,这实际上没有什么效果.可是,如果带有session key的存储对象在HttpSession里对其他的对象有很高的依赖性, 拷贝将在备份机器上获利. 在失败转移之后,你的应用程序将继续运行,但是一些特性可能不工作---例如,购物车可能拒绝接受更多的数据项.这个问题滋生自你的应用程序的不同部分(更新购物车和显示购物车),它们引用了购物车对象自己的拷贝.当JSPs尝试显示购物车的拷贝时,这时承担起更新购物车的类正在改变它的购物车的拷贝.在单一服务器环境里,这个问题将不会出现,因为你应用的这两部分都指向相同的购物车.
这有个间接拷贝对象的例子:
import java.io.*;
public class Aaa implements Serializable {
String name;
pubic void setName (String name) {
this.name = name;
}
public String getName( ) {
return name;
}
}
import java.io.*;
public class Bbb implements Serializable {
Aaa a;
public Bbb (Aaa a) {
this.a = a;
}
pubic void setName (String name) {
a.setName(name);
}
public String getName( ) {
return a.getName();
}
}
server1上的first.jsp:
<%
Aaa a = new Aaa();
a.setName("Abe");
Bbb b = new Bbb(a);
// a is copied to backup machine under key "a"
session.setAttribute("a",a);
// b is copied to backup machine under key "b"
session.setAttribute("b",b);
%>
server1上的second.jsp:
<%
Bbb b = (Bbb)session.getAttribute("b");
b.setName("Bob");
// b is copied to backup machine under key "b"
// but object Aaa under key "a" has the name "Abe"
// and "b"'s Aaa has the name "Bob"
session.setAttribute("b",b);
%>
---> 失败,尝试接触到server1上的third.jsp
----->失败转移到server2(备份机器)的third.jsp
server2上的third.jsp:
和对象Aaa联系的名字是:
<%=((Aaa)session.getAttribute("a")).getName()%>
通过对象Bbb,和对象Aaa联系的名字是:
<%=((Bbb)session.getAttribute("b")).getName()%>
...
//End of third.jsp
第一个标签里的表达式输出"Abe",而第二个标签里的表达式输出"Bob".在单个的服务器上,两个标签里的表达式都输出"Bob".
为了了解无效的session状态拷贝,构建了一个集群对象关系图(CORD),如下图.
JavaPetStore CORD.
在这个图里,椭圆表示HttpSession里带有键-值的直接存储的对象.矩形表示被带有HttpSession key的存储对象所直接引用的对象. 黑箭头指向的对象是原对象实例变量.空箭头指出继承关系.
通常,统计指向对象的箭头数,会告诉你这个对象生成的拷贝数.如果箭头指向一个椭圆,你只好给箭头总数再加一个,因为这个带有session key的椭圆被存储在HttpSession里.例如,看一下ContactInformation这个椭圆,有个箭头指向它,这表示有两个拷贝:带有session key的一个是直接存储,另一个作为AccountWebImpl的一个实例变量存储.
我要强调的是由于复杂的关系往往导致你被经验所左右.看一下ShoppingClientControllerWebImpl;有一个箭头指向它,因此在那有两个拷贝,对么? 错! 那里有三个拷贝:一个通过ModelManager,一个通过AccountWebImpl, 一个通过RequestProcessor.你不必计算ShoppingClientControllerWebImpl它自己,因为它不直接带着HttpSession key存储.
试试另一个例子.看图,在那有多少个ModelManager的拷贝?如果回答是四个,你对了.在那有来自ModelManager自身的一个拷贝,因为它是带有HttpSession key被存储的, 还有AccountWebImpl, ShoppingCartWebImpl和RequestProcessor.我没有计算来自RequestToEventTranslator的箭头,因为它并没直接带有HttpSession key存储,它对ModelManager的引用和来自RequestProcessor的引用是相同的.
注意: 关于内存复制,永远要记住:存储带有HttpSession key的任何对象都将被克隆并且被发送到备份机器上.
为了保持对象拷贝数量最小,只存储原始对象类型,简单对象,或者在HttpSession里带有session keys的独立对象.如果你在HttpSesion里不能避免有复杂关系的对象,一旦用户已经失败转移,就使用session.setAttribute(...)移出拷贝.例如,用实际的ModelManager,代替在AccountWebImpl里的ModelManager的克隆,你需要调用session.setAttribute("account",accountWebImplWithRealModelManager).大多数情况下,你需要集中分析共享状态的拷贝对象.如果对象还没有任何的共享实例变量,你就可以不管它.唯一的负面影响:对于失败转移的客户端,增加了内存的使用率.
在测试检验完成后,如果你的集群里每个都能失败转移的话,还需要开发一个集群管理策略.
集群管理策略文档定义什么会发生故障,如何解决这些问题.大体上,这些问题有四个分类:
1 硬件
2 软件
3 网络
4 数据库
既然这是javaworld,我将集中在软件上.你或许应该分别和你周围的系统管理员,网络管理员和DBA一起论述其他的问题.
如果集群里机器上的软件失败了,集群里的其他成员通过失败转移的处理,必须承担起已经down掉的服务器的责任.在JSP应用程序里,失败转移出现在Web代理或者硬件负载均衡器.例如,进入到web代理的一个请求, web代理注意到这个请求要到达的服务器down掉了,因此它把这个请求发送到另外一个应用服务器.这个应用服务器激活用户的备份session状态,完成这次请求.在EJB客户端应用程序里, remote引用尝试连接集群里的不同服务器,直到它们收到一个应答.
这些进行失败转移的描述好象是微不足道的,但是我们只提到了在请求之间发生失败转移的最好情形.当在请求期间出现失败时,失败转移被证明是很难的.让我们看一个涉及JSPs和EJBs的例子.
对于我们的JSP例子,设想一个用户在你的站点上正在下一个订单,当用户点击提交按钮时,这个页面似乎中止了, 结果他收到一个消息,”没有数据响应”.这种情形下,可能已经发生了两种情况中的一种.订单已经完成,但是在发送响应页面之前服务器失败.另外一种情况是在保存订单和发送响应之前服务器可能已经失败了.代理在另外一台服务器上自动调用同样的URL,并且重新初始化这个事务,可能引发副本么? 或者它返回一个错误,迫使用户同你们的支持小组的某个成员联系么??
在我们的EJB例子里,设想一个用户正在一个java应用程序上修改inventory.一旦用户点击保存按钮,一个请求就到达一个EJB,但是这个应用返回RemoteException. 同样,可能发生了两种情况中的任意一种情况.. inventory可能已经被保存了,但是服务器可能在发送一个应答之前就已经失败了.或者,服务器在做inventory变更之前已经失败了.EJB remote自动在集群里另外一台服务器上重新尝试远程调用并且冒副本或者不一致数据的风险么?
在尝试不同的失败转移技术后,我归纳出显式的失败转移的责任全在于应用服务器.你可以在cookies里尝试使用事务tables或者事务标识符,但是一分辛劳,一分收获也不见的有什么道理.最简单的解决方案:在服务器失败后,在数据库里运行一组查找不一致性的存储过程,通知给感兴趣的组织.
结束语
这篇文章使你已经理解了群集, 它是很实用的.你已经掌握了如何设置,编程和维护J2EE集群.你也明白了与群集相关的问题,和可能的解决方案.有了这些实际的知识,你可以设置一个工作中的J2EE集群.
但是你的工作还没有做完:你不得不开发网络,硬件和数据库基础设施来保证HA.开发这些服务的最容易的方法就是通过企业托管服务(enterprise hosting service).大多数企业托管服务设置网络冗余,数据管理和硬件服务.你只提供clusterable的代码就可以了.
Good luck and happy clustering.
关于作者
Abraham Kang是在Infogain的企业整合解决方案组里的整合架构师. 他有一年半的J2EE应用服务器群集经验.此外,他还是Sun认证的程序员和开发者, Oracle 8i专业认证DBA,CCNA,和CCDA.. Abraham想要在这里对Jessie Spencer-Cooke, Aleksey Shcherbakov, Raju Chiluvuri, 和 Roger Plichta他们的支持表示感谢.
Resources
"J2EE Clustering," Abraham Kang (JavaWorld):
Part 1: Clustering technology is Crucial to good Website design; do you know the basics? (February 2001)
Part 2: Migrate your application from a single machine to a cluster, the easy way (August 2001)
Enterprise hosting services:
Hardware load balancers:
Enterprise JavaBeans, Richard Monson-Haefel (O'Reilly & Associates, 2000; ISBN: 1565928695):
http://www.amazon.com/exec/obidos/ASIN/1565928695/javaworld
Java Pitfalls: Time-Saving Solutions and Workarounds to Improve Programs, Michael C. Daconta (Editor), Eric Monk, J. Paul Keller, Keith Bohnenberger (John Wiley & Sons, 2000; ISBN: 0471361747):
http://www.amazon.com/exec/obidos/ASIN/0471361747/javaworld
"Flatten Your Objects," Todd M. Greanier (JavaWorld, July 2000):
http://www.javaworld.com/javaworld/jw-07-2000/jw-0714-flatten.html
For a good look at the canonical form, read Bill Venners' "The Canonical Object Idiom" (JavaWorld, October 1998):
http://www.javaworld.com/javaworld/jw-10-1998/jw-10-techniques.html
For more information on high availability, see the High-Availability Linux Project:
"Get the App Out," Chang Sau Sheong (JavaWorld, January 2001) describes the ins and outs of J2EE assembly and deployment:
http://www.javaworld.com/javaworld/jw-01-2001/jw-0119-j2eeassembly.html