实战Apache+Tomcat集群和负载均衡
目录
1. 什么是J2EE集群... 3
1.1. 序言... 3
1.2. 基本术语... 3
伸缩性(Scalability):... 4
高可用性(High availability):... 4
负载均衡(Load balancing):... 4
容错(Fault tolerance):... 5
失效转移(Failover):... 5
等幂方法(Idempotent methods):... 5
1.3. 什么是J2EE集群... 5
2. J2EE集群给我们带来了什么... 9
3. 实战准备... 9
4. 首战失败... 9
5. 详细配置... 9
6. 负载均衡... 9
7. 失败转移... 9
1. 什么是J2EE集群
1.1. 序言
越来越多的关键应用运行在J2EE(Java 2, Enterprise Edition)中,这些诸如银行系统和账单处理系统需要高的可用性(High Availability, HA),同时像Google和Yahoo这种大系统需要大的伸缩性。高可用性和伸缩性在今天高速增长的互连接的世界的重要性已经证实了。eBay于1999年6月停机22小时的事故,中断了约230万的拍卖,使eBay的股票下降了9.2个百分点。
J2EE集群是用来提供高可用性和伸缩性服务,同时支持容错处理的一种流行的技术。但是,由于J2EE规范缺乏对集群的支持,J2EE供应商实现集群的方法也各异。这给J2EE架构师和开发人员带来了很多困难。以下是几个常见的问题:
l 为什么带集群功能的商业J2EE服务器产品如此昂贵?(10倍于不带集群功能的产品)
l 为什么基于单服务器环境构建的应用不能在集群中运行?
l 为什么应用在集群环境中运行得很慢,但在非集群环境中却快得多?
l 为什么集群的应用移植到其他服务器中失败?
l 理解这些限制和要素的最佳方法是学习他们的实现方式。
1.2. 基本术语
在我们讨论不同的集群实现之前,先谈谈几个概念。这有助于理解不同的J2EE集群产品不同的设计结果和概念:
1.2.1. 伸缩性(Scalability):
在一些大的系统中,预测最终用户的数量和行为是非常困难的,伸缩性是指系统适应不断增长的用户数的能力。提高这种并发会话能力的一种最直观的方式就增加资源(CPU,内存,硬盘等),集群是解决这个问题的另一种方式,它允许一组服务器组在一起,像单个服务器一样分担处理一个繁重的任务。
1.2.2. 高可用性(High availability):
单一服务器的解决方案并不是一个健壮方式,因为容易出现单点失效。像银行、账单处理这样一些关键的应用程序是不能容忍哪怕是几分钟的死机。它们需要这样一些服务在任何时间都可以访问并在可预期的合理的时间周期内有响应。集群方案通过在集群中增加的冗余的服务器,使得在其中一台服务器失效后仍能提供服务,从而获得高的可用性。
1.2.3. 负载均衡(Load balancing):
负载均衡是集群的一项关键技术,通过把请求分发给不同的服务器,从而获得高可用性和较好的性能。一个负载均衡器可以是从一个简单的Servlet或Plug-Ins(例如一个Linux box利用ipchains来实现),到昂贵的内置SSL加速器的硬件。除此之外,负载均衡器还需执行一些其他的重要任务,如“会话胶粘”让一个用户会话始终存在一个服务器上,“健康检查”用于防止将请求分发到已失效的服务器上。有些负载均衡器也会参与我们下面将要谈到“失效转移”过程。
1.2.4. 容错(Fault tolerance):
高可用性意味着对数据正确性的要求不那么高。在J2EE集群中,当一个服务器实例失效后,服务仍然是有效的,这是因为新的请求将被冗余服务器处理。但是,当一个请求在一个正在失效的服务器中处理时,可能得到不正确的结果。不管有多少个错误,容错的服务应当能确保有严格的正确的行为。
1.2.5. 失效转移(Failover):
失效转移是集群中用来获取容错能力的另一项关键的技术。当一个结点失效后,通过选择集群中的另一个结点,处理将会继续而不会终止。转移到另一个结点可以被显式的编码,或是通过底层平台自动地透明地路由到另一个服务器。
1.2.6. 等幂方法(Idempotent methods):
等幂方法是指这样一些方法:重复用相同的参数调用都能得到相同的结果。这些方法不会影响系统状态,可以重复调用而不用担心改变系统。例如:getUsername()就是等幂的,而deleteFile就不是。当我们讨论HTTP Session失效转移和EJB失效转移时,它是一个重要的概念。
1.3. 什么是J2EE集群
一个天真的问题,不是吗?但我仍要用几句话和图来回答它。通常,J2EE集群技术包括"负载均衡"和"失效转移"。
如图1所示,负载均衡意味着有许多客户端向目标对象同时发出请求。负载均衡器在调用者和被调用者之间,分发请求到与原始对象相同的冗余对象中。伸缩性和高可用性就是这样得到的。
如图2所示,失效转移与负载均衡不同。有时客户端会连续发请求到目标对象,如果请求中间目标对象失效了,失效转移系统将检测到这次失败,并将请求重定向到另一个可用的对象。通过这种方式可以获得容错能力。
如果你想知道更多的有关J2EE集群的知识,你就会问到一个基本的问题,“什么对象可以集群?”和“在我的J2EE代码中哪里会发生负载均衡和失效转移呢?”。这些都是用来理解J2EE集群的非常好的问题。实际上,并不是所有的对象都能被集群的,并且负载均衡和失效转移并不是在J2EE代码所有地方都能发生。看看下面的例子代码:
在Class A的bussiness()方法中,instance1可以负载均衡吗?或是当其失效,可以失效转移到其他B的实例上吗?我想是不行的!对负载均衡和失效转移来说,必须要有个拦截器在调用者和被调用者之间分发或重定向请求到不同的对象上。Class A和Class B的实例是运行在一个JVM中紧密耦合的,在方法调用间加入分发逻辑非常困难。
什么类型对象可以被集群?——只有那些可以被部署到分布式拓朴结构中的组件。
在我的J2EE代码中,什么地方会有负载均衡和失效转移?——只在你调用分布式组件的方法时。
在如图4所示的分布式环境中,调用者和被调用者被分离在有明显边界的不同的运行容器中,这个边界可以是JVM,进程和机器。
当目标对象被客户端调用时,目标对象的功能是在容器中运行的(这就是为什么我们说它是分布式的原因)。客户端和目标对象通过标准的网络协议通信。这些特性就为一些机制提供了机会可以介入到方法调用之间实现负载均衡和失效转移。
如图4,浏览器通过HTTP协议调用JSP对象,JSP运行在WEB服务器中,浏览器只需要返回结果而不关心它是怎么运行的。在上述场景中,一些东西就可以在浏览器与WEB服务器之间实现负载均衡和失效转移的功能。在J2EE平台,分布式技术包括:JSP(Servlet),JDBC,EJB,JNDI,JMS,WEB Service等。负载均衡和失效转移就发生在这些分布式方法被调用时。在后续部分我们将详细讨论这些技术。
2. 实战
2.1. 软件环境
2.1.1. Apache
apache 2.0.55 (由http://httpd.apache.org/进入下载)
2.1.2. Tomcat
Tomcat 5.5.25 (由http://tomcat.apache.org/进入下载)
2.1.3. Mod_jk
在页面 http://tomcat.apache.org/ Download 标题下找到 Tomcat Connectors 链接进入( 点击下载mod_jk-apache-2.0.55.so),看起来像是个Unix/Linux下的动态库,实际应是个Win32 的 DLL 动态库,大概是为保持不同平台配置的一致性,才用了这个扩展名。
2.2. 负载均衡
用Apache进行分流,把请求按照权重以及当时负荷分tomcat1,tomcat2...去处理
2.2.1. 安装apache,tomcat
我把Apache安装在F:\ZQ\apache2.0.55\Apache2
解压两分Tomcat, 分别在 F:\ZQ\apache-tomcat-5.5.1,F:\ZQ\apache-tomcat-5.5.2
2.2.2. 修改Apache配置文件http.conf
在apache安装目录下conf目录中找到http.conf,在文件最后加上下面一句话就可以了
include conf/mod_jk.conf
2.2.3. http.conf 同目录下新建mod_jk.conf文件,内容如下
#加载mod_jk Module
LoadModule jk_module modules/mod_jk-apache-2.0.55.so
#指定 workers.properties文件路径
JkWorkersFile conf/workers.properties
#指定那些请求交给tomcat处理,"controller"为在workers.propertise里指定的负载分配控制器
JkMount /*.jsp controller
如果还要指定*.do也进行分流就再加一行
JkMount /*.do controller
如果你想对所有的请求进行分流只需要写成
JkMount /* controller
2.2.4. 在http.conf同目录下新建 workers.properties文件,
内容如下(可能要去除 # 不在行首的注释)
worker.list = controller,tomcat1,tomcat2 #server 列表
#========tomcat1========
worker.tomcat1.port=8009
#ajp13 端口号,在tomcat下server.xml配置,默认8009
worker.tomcat1.host=localhost
#tomcat的主机地址,如不为本机,请填写ip地址
worker.tomcat1.type=ajp13
worker.tomcat1.lbfactor = 1
#server的加权比重,值越高,分得的请求越多
#========tomcat2========
worker.tomcat2.port=8109
#ajp13 端口号,在tomcat下server.xml配置,默认8009
worker.tomcat2.host=localhost
#tomcat的主机地址,如不为本机,请填写ip地址
worker.tomcat2.type=ajp13
worker.tomcat2.lbfactor = 2
#server的加权比重,值越高,分得的请求越多
#========controller,负载均衡控制器========
worker.controller.type=lb
worker.controller.balanced_workers=tomcat1,tomcat2
#指定分担请求的tomcat
worker.controller.sticky_session=1
2.2.5. 修改tomcat配置文件server.xml
如果你是水平集群,即在不同电脑上安装tomcat,tomcat的安装数量为一个,可以不必修改tomcat配置文件.我这里是在同一台电脑上安装两个tomcat,实现的是垂直集群方式,所以必须修改其中一个的设置,以避免端口冲突,按照参考文章是把原来以9开头的端口号改为以9开头端口号,但是在我机器上如果以9开头的端口号,例如9080、9082会与我的WebSphere Application Server配置冲突,所以我这里采取的策略是把原来端口号的第三位改为1,如8080改为8180。
打开tomcat2/conf/server.xml文件
1) 将关闭Tomcat的监听端口改成由8005改为8105
即把
<Server port="8005" shutdown="SHUTDOWN">
改为
<Server port="8105" shutdown="SHUTDOWN">
2) 把http服务端口号由8080改为8180
找到
<!-- Define a non-SSL HTTP/1.1 Connector on port 8080 -->
<CONNECTOR port="8080"
把这里的8080改为8180
3) 把AJP端口号由8009改为8109
找到
<!-- Define an AJP 1.3 Connector on port 8009 -->
<CONNECTOR port="8009"
把这里的8009改为8109
4) 把 HTTP 代理端口从8082改为8182(这个配置默认是被注释掉的,可跳过这一步)
找到
<CONNECTOR port="8082"
把这里的8082改为8182
5) 编写一个测试 jsp
建立一个目录TestCluster,里面新建一个test.jsp,内容为
<% System.out.println("==========================="); %> |
把TestCluster放到tomcat1,tomcat2的webapps下
6) 启动apache,tomcat1,tomcat2,进行测试
通过 http://localhost/TestCluster/test.jsp 访问,多刷新几次页面,查看Tomcat1和Tomcat2的窗口,你将可以看到打印了一行行"===========================",并且从统计上来说,大约在tomcat2打印的数量是在Tomcat1中的两倍,可以看到请求会被tomcat1,tomcat2按照不同的权重分流处理,实现了负载均衡。
作下面的集群配置,请在workers.properties把tomcat1和tomcat2的权重改为一样的,使请求较平均分配,将有便于看到实验的效果。
2.3. 配置集群
只配置负载均衡还不行,还要session复制,也就是说其中任何一个tomcat的添加的session,是要同步复制到其它tomcat, 集群内的tomcat都有相同的session
2.3.1. 修改tomcat1, tomcat2的server.xml,将集群部分配置,
即对<Cluster>节点的在注释符删掉,并将tomcat2的4001端口改为4002,以避免与tomcat冲突,当然,如果是两台电脑,是不用改端口的,去掉注释符即可
即取消对如下处
<Cluster className="org.apache.catalina.cluster.tcp.SimpleTcpCluster"
managerClassName="org.apache.catalina.cluster.session.DeltaManager"
expireSessionsOnShutdown="false"
............
<ClusterListener className="org.apache.catalina.cluster.session.ClusterSessionListener"/>
</Cluster>
前后的注释标记<!-- -->,启用该项配置,实现服务器间的Session复制。
2.3.2. 为 Tomcat1和 Tomcat2 增加 jvmRoute
这里的作用大概是为了在集群的TOMCAT里面,生成全局HTTP会话标识,这样可以保证会话的唯一性,有利于session的复制
(先跳过这一步,有精力可以试验一下)
在 Tomcat1 和 Tomcat2 的 server.xml 文件,找到
<ENGINE name="Catalina" defaultHost="localhost">
分别改为
<ENGINE name="Catalina" defaultHost="localhost" jvmRoute="tomcat1">
和
<ENGINE name="Catalina" defaultHost="localhost" jvmRoute="tomcat2">
然而实际我配置的时候还不能加jvmRoute属性,配置了反而有问题。
刷新浏览器窗口总是在某一个tomcat控制台输出形如
SessionID:154678FA6D4D0ABD57658B750E7A3532.tomcat1 (在tomcat1窗口)
或者
SessionID:3800571A532AECEA7280F45361861AD4.tomcat2 (在tomcat2窗口)
由控制台打印的结果可以看出,SessionID在哪个tomcat上产生,那么后续该会话的请求将总是会这个tomcat来处理。
并且注意到SessionID的形式比通常情况多了一个后缀.tomcat1或.tomcat2,还搞不清楚是为什么。
配置时请视实际情况而取舍。
2.3.3. 修改测试项目 TestCluster
修改test.jsp,内容如下
<%@ page contentType="text/html; charset=GBK" %> <%@ page import="java.util.*" %> <html><head><title>Cluster App Test</title></head> <body> <% System.out.println("SessionID:" + session.getId()); %> Server Info: <% out.println(request.getServerName() + " : " + request.getServerPort()+"<br>");%> <% out.println("<br> ID " + session.getId()+"<br>"); // 如果有新的 Session 属性设置 String dataName = request.getParameter("dataName"); if (dataName != null && dataName.length() > 0) { String dataValue = request.getParameter("dataValue"); session.setAttribute(dataName, dataValue); } out.print("<b>Session 列表</b><br>"); Enumeration e = session.getAttributeNames(); while (e.hasMoreElements()) { String name = (String)e.nextElement(); String value = session.getAttribute(name).toString(); out.println( name + " = " + value+"<br>"); System.out.println( name + " = " + value); } %> <form action="test.jsp" method="POST"> 名称:<input type=text size=20 name="dataName"> <br> 数值:<input type=text size=20 name="dataValue"> <br> <input type=submit> </form> </body> </html> |
4. 配置Session复制
在TestCluster目录下新建WEB-INF目录,WEB-INF下新建web.xml,内容如下
<web-app xmlns="http://java.sun.com/xml/ns/j2ee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd" version="2.4">
<display-name>TomcatClusterDemo</display-name>
<distributable/>
<web-app>
也就是在需要集群的应用的web.xml中加上属性,表明该应用可多应用分流处理,能进行Session的复制
把TestCluster复制到Tomcat1、Tomcat2的webapps目录下,重启apache,tomcat1,tomcat2
5. 测试Session的复制
通过 http://localhost/TestCluster/test.jsp 访问,输入名称为 name, 值为 Unmi,提交查询,多刷新几次浏览器窗口,你将会看到在两个Tomcat窗口都打印出相同的SessionID及其中的值,并且每次刷新后打印的结果都一样的。
如果不为应用的web.xml加上 ,同样测试上面那个test.jsp页面,每次刷新分流到不同的tomcat上都会产生不一样的SessionID,在同一个tomcat上也是间隔出现不同的sessionID。
更切身的体验是一定要自己动手配置一遍,并仔细观察两个tomcat的控制上的输出。因本文是参考 轻松实现Apache,Tomcat集群和负载均衡 的实践经历,
2.3.4. 测试session复制
启动环境后,启动2个浏览器,得到的是2个不同的session标识,各自提交您输入的key-value后,把一个tomcat2服务关闭掉,再访问页面,发现tomcat2的session会复制到tomcat1中
2.3.5. 后记:
用 WebSphere Application Server ND 版配置过垂直和水平集群,但是自己试验集群环境下的应用却不想搬弄这个庞然大物。眼下急于想体验的就是 Quartz 如何适应集群环境,问题的焦点就是:Quartz 定时任务随 Web 应用启动,而 Web 应用部署在集群环境中,如何保证同一时刻只有一个同名的任务实例在跑。
所以会考虑用Apache+Tomcat配置一个轻量级的WEB应用集群,一般进行HTTP分流都是使用Apache,包括WAS集群也是,很少用IIS的。虽然单纯的用Tomcat的balancer应用也能配置进行负载分流,但那个性能应该好不到哪儿去。
用Apache+Tomcat配置的Web应用集群就是部署起来麻烦些,总是要保持双份的应用拷贝,WAS集群则不需要,不知道Jboss做WEB应用集群是怎么样一种情况。
好了,下面要进行该做的事情了,最后也希望能写个工具能完成从下载到安装配置,启动,停止,重启的全自动化,以及界面的人性化。
3. 实战总结
3.1. 失败总结
在准备软件环境的环节中:最开始的时候小弟下载了Apache2.2.21,mod_jk下载的是2.2.X版本,没有按照网上给出的版本,所有配置都弄好后,访问页面,总是提示404错误
后来换回2.0.55版本,这次实战才成功了。
4. 参考文献
4.1. 结合Apache和Tomcat实现集群和负载均衡
http://blog.csdn.net/kypfos/article/details/3081330
4.2. 揭开J2EE集群的神秘面纱
http://www.kuqin.com/java/20080418/6942.html