Tomcat7中的集群/会话复制

原文参见http://tomcat.apache.org/tomcat-7.0-doc/cluster-howto.html。

简化的如下:
<Cluster className="org.apache.catalina.ha.tcp.SimpleTcpCluster"/>
也就是上文中的实例方案,该方案将会使用 DeltaManager启用所有集群节点(all-to-all)的回话复制,这种意味着在集群环境中的每个节点都拥有所有的session,这种方案对于小的集群环境非常适用,但是不推荐在多集群节点的环境中使用(有很多tomcat节点的环境),另外当使用了增量管理(delta manager)会复制到所有的节点,甚至该节点根本就没有部署应用程序。
为了解决这个问题,你需要使用 BackupManager,这种方式只会复制回话数据到一台备份节点上,并且是只针对于那些部署了应用的节点,BackupManager的缺点:没有经过像DeltaManager一样的“战斗测试”(引申为For the impatient“严格测试”?)。
下面是一些重要的默认值:
1、多路广播地址是228.0.0.4
2、多路广播端口是45564(地址和端口确定了集群成员)
3、IP广播是 java.net.InetAddress.getLocalHost().getHostAddress()(确保你不广播到127.0.0.1,这是一个常见的错误)
4、TCP端口监听复制消息是在4000-4100范围内的第一个可用的服务器套接字
5、两个监听器配置成 ClusterSessionListener
6、两个拦截器配置成 TcpFailureDetectorMessageDispatch15Interceptor
下面就是默认的集群节点的配置
<Cluster className="org.apache.catalina.ha.tcp.SimpleTcpCluster" channelSendOptions="8">
          <Manager className="org.apache.catalina.ha.session.DeltaManager"
                   expireSessionsOnShutdown="false" notifyListenersOnReplication="true"/>
          <Channel className="org.apache.catalina.tribes.group.GroupChannel">
            <Membership className="org.apache.catalina.tribes.membership.McastService"
                        address="228.0.0.4"
                        port="45564"
                        frequency="500"
                        dropTime="3000"/>
            <Receiver className="org.apache.catalina.tribes.transport.nio.NioReceiver"
                      address="auto"
                      port="4000"
                      autoBind="100"
                      selectorTimeout="5000"
                      maxThreads="6"/>
            <Sender className="org.apache.catalina.tribes.transport.ReplicationTransmitter">
              <Transport className="org.apache.catalina.tribes.transport.nio.PooledParallelSender"/>
            </Sender>
            <Interceptor className="org.apache.catalina.tribes.group.interceptors.TcpFailureDetector"/>
            <Interceptor className="org.apache.catalina.tribes.group.interceptors.MessageDispatch15Interceptor"/>
          </Channel>
          <Valve className="org.apache.catalina.ha.tcp.ReplicationValve" filter=""/>
          <Valve className="org.apache.catalina.ha.session.JvmRouteBinderValve"/>
          <Deployer className="org.apache.catalina.ha.deploy.FarmWarDeployer"
                    tempDir="/tmp/war-temp/"
                    deployDir="/tmp/war-deploy/"
                    watchDir="/tmp/war-listen/"
                    watchEnabled="false"/>
          <ClusterListener className="org.apache.catalina.ha.session.JvmRouteSessionIDBinderListener"/>
          <ClusterListener className="org.apache.catalina.ha.session.ClusterSessionListener"/>
        </Cluster>
  要在tomcat 7容器中运行会话复制,下面的步骤是必须的:
1、所有的session属性必须实现java.io.Serializable接口;
2、取消注释server.xml中的Cluster节点;
3、如果你定义了自定义的集群阀门,请确保在server.xml文件中的Cluster节点下面有关于ReplicationValve的定义;
4、如果tomcat实例是运行在相同的机器上的,请确保tcpListenPort的属性值对于每个实例是唯一的,大多数情况下,tomcat足够聪明会自己在4000-4100范围内自动侦听并找到可用的端口;
5、请确保你的web.xml文件中有<distributable/>元素;
6、如果使用mod_jk,请确保jvmRoute属性已经设置在<Engine name="Catalina" jvmRoute="node01">,并且jvmRoute属性值匹配workers.properties文件中的worker名称;
7、请确保所有的节点拥有相同的时间和NTP服务同步;
8、请确保负载均衡配置为粘性会话模式。
负载均衡可以通过多种技术实现。
注意:你的会话状态是由cookie跟踪的,所以你的URL必须从外面看是一样的,否则一个新的session就会被创建。
注意:集群支持目前需要JDK1.5或更高版本。
集群模块使用Tomcat JULI日志框架,所以可以通过正常的logging.properties文件的配置来进行配置日志记录,跟踪邮件的话你可以通过启用org.apache.catalina.tribes.MESSAGES开启日志。
 
要启用tomcat的会话复制,有三种不同的方式可以实现并且都能达到相同的目的:
1、使用会话复制,将会话保存到共享文件系统中(PersistenceManager + FileStore)
2、使用会话复制,将会话保存到共享数据库中(PersistenceManager + JDBCStore)
3、使用内存复制,使用tomcat附带的SimpleTcpCluster(lib/catalina-tribes.jar + lib/catalina-ha.jar)
在此版本的会话复制,Tomcat可以表现出对所有的会话状态复制使用 DeltaManager或者执行备份到仅有一个使用 BackupManager的节点上,这些所有的全部复制算法仅仅在集群数量比较小的时候是有效的,对于节点数比较多的集群,就需要使用 初级 - 次级(primary-secondary)会话复制将所有的会话保存到一台只需要设置BackupManager的备份机器上。现在你可以使用域的worker属性(mod_jk > 1.2.8)来构建具有与使用DeltaManager更具扩展性的集群解决方案(你需要配置域来进行拦截)的集群分区,为了保持在所有的环境中网络流量的下降,你可以将集群分成更小的组,这可以通过使用不同的多播地址在不同的组中很容易的实现(译注:即多IP+多端口),一个非常简单的设置是这么做的:

  值得一提的是,会话复制仅仅是集群的开始。另外一种流行的理念实现集群的方式是场(farming),例如,你仅部署你的应用到一台服务器上,集群就会把部署的程序分发部署到所有的集群中。这是一个可进入FarmWarDeployer的所有功能(在server.xml中的集群为例)。
接下来将会更深入的介绍会话复制是如何工作的以及如何去配置。(译注:详细介绍配置文件中各元素的含义)
Membership通过使用多播心跳建立。因此,如果你希望能够细分集群,你可以通过更改 <Membership>元素下的多播IP地址或端口来实现。心跳包含了Tomcat节点的IP地址和Tomcat监听的用于流量复制的TCP端口,所有的数据通信使用TCP协议。 ReplicationValve是用来找出如果有什么时间请求完成并且启动复制,数据只会在会话发生更改(通过调用setAttribute或者removeAttribute方法)的情况下进行复制。其中最重要的是在同步和异步复制上的性能方面的考虑,同步的情况下,请求直到复制的会话已经通过网络发送和重新实例化到其他所有的集群节点上才会返回,同步和异步是使用 channelSendOptions来进行标记的,并且是一个整数值,对于组合 SimpleTcpCluster/DeltaManager的默认值是8,这是异步的。在异步复制期间,请求在数据复制完成之前就会返回,异步复制产生更少的请求时间,同步复制保证了会话复制在请求返回之前完成。
如果你正在使用mod_jk并且没有使用粘性会话或者因为某些原因粘性会话无法工作,再或者是进行故障转移。会话ID会需要进行修改,因为以前的包含worker id的tomcat(在Engine节点下的jvmRoute中定义的)。为了解决这个问题,我们将要使用 JvmRouteBinderValve。JvmRouteBinderValve在故障转移后重写了会话id用以确保在下次请求中会依然保持粘性(而不是落回到随机节点上,因为worker已经不再可用)。该阀重写了cookie中具有相同名称的JSESSIONID。没有阀在合适的位置上,将很难确保出现故障的mod_jk模块保持粘性。默认情况下,如果没有配置阀,JvmRouteBinderValve将会被添加上。名为 JvmRouteSessionIDBinderListener的集群消息监听器也会被默认定义用于实际在集群环境中一旦出现一个故障转移的时候重写会话id。请记住,如果你增加了你自己的阀或者集群监听器在server.xml中,那么这些默认值将会失效,请确保你配置的是默认的合适的阀和监听器。
提示:在旧的会话id中你可以更改请求名为 sessionIdAttribute的属性,默认的属性名是 org.apache.catalina.ha.session.JvmRouteOrignalSessionID
技巧:你可以在删除一个节点到所有备份节点之前通过JMX启用mod_jk模式!设置所有的JvmRouteBinderValve备份enable为true,禁止worker在mod_jk,然后删除节点并重启!然后再次启用mod_jk Worker并且再次禁用JvmRouteBinderValve。这个用例意味着只有请求会话迁移。
下面对一个配置示例进行分析:
<Cluster className="org.apache.catalina.ha.tcp.SimpleTcpCluster"
 channelSendOptions="6">
 <Manager className="org.apache.catalina.ha.session.BackupManager"
  expireSessionsOnShutdown="false" notifyListenersOnReplication="true"
  mapSendOptions="6" />
 <!-- <Manager className="org.apache.catalina.ha.session.DeltaManager" expireSessionsOnShutdown="false"
  notifyListenersOnReplication="true"/> -->
 <Channel className="org.apache.catalina.tribes.group.GroupChannel">
  <Membership className="org.apache.catalina.tribes.membership.McastService"
   address="228.0.0.4" port="45564" frequency="500" dropTime="3000" />
  <Receiver className="org.apache.catalina.tribes.transport.nio.NioReceiver"
   address="auto" port="5000" selectorTimeout="100" maxThreads="6" />
  <Sender className="org.apache.catalina.tribes.transport.ReplicationTransmitter">
   <Transport
    className="org.apache.catalina.tribes.transport.nio.PooledParallelSender" />
  </Sender>
  <Interceptor
   className="org.apache.catalina.tribes.group.interceptors.TcpFailureDetector" />
  <Interceptor
   className="org.apache.catalina.tribes.group.interceptors.MessageDispatch15Interceptor" />
  <Interceptor
   className="org.apache.catalina.tribes.group.interceptors.ThroughputInterceptor" />
 </Channel>
 <Valve className="org.apache.catalina.ha.tcp.ReplicationValve"
  filter=".*\.gif|.*\.js|.*\.jpeg|.*\.jpg|.*\.png|.*\.htm|.*\.html|.*\.css|.*\.txt" />
 <Deployer className="org.apache.catalina.ha.deploy.FarmWarDeployer"
  tempDir="/tmp/war-temp/" deployDir="/tmp/war-deploy/" watchDir="/tmp/war-listen/"
  watchEnabled="false" />
 <ClusterListener
  className="org.apache.catalina.ha.session.ClusterSessionListener" />
</Cluster>
  拆分!
<Cluster className="org.apache.catalina.ha.tcp.SimpleTcpCluster" channelSendOptions="6"> 
  这个元素是主要的元素,在这个元素下面所有的集群信息可以进行配置。 channelSendOptions 属性是连接到每一个由 SimpleTcpCluster 类或者任意一个对象调用 SimpleTcpCluster.send 方法发出的每个消息的标记,该标记的描述参见API中关于 Channel 类的描述(http://tomcat.apache.org/tomcat-7.0-doc/api/org/apache/catalina/tribes/Channel.html), DeltaManager 发送信息使用 SimpleTcpCluster.send 方法,而备份管理器直接通过通道发送本身,详细信息请参见官方文档(http://tomcat.apache.org/tomcat-7.0-doc/config/cluster.html)。
<Manager className="org.apache.catalina.ha.session.BackupManager"
  expireSessionsOnShutdown="false" notifyListenersOnReplication="true"  mapSendOptions="6" /> 
 <!-- <Manager className="org.apache.catalina.ha.session.DeltaManager" expireSessionsOnShutdown="false"
  notifyListenersOnReplication="true"/> -->
  这是一个管理器的定义的模板如果没有在<Context>元素下面没有定义管理器。在Tomcat 5.x每一个被标记distributable的web应用必须去使用相同的管理器,现在已不再如此,因为在Tomcat中你可以为每一个web应用定义一个管理器类,所以你可以在集群中混合你的管理器。显然在一个节点应用上的管理器需要去对应那些在其他节点上相同应用使用相同管理器的节点。如果一个被标记为<distributable/>的web应用没有定义管理器,Tomcat将会使用这个管理器的配置并且使用一个管理器实例的克隆这个配置。
<Channel className="org.apache.catalina.tribes.group.GroupChannel">
  Channel元素是在Tomcat内部使用的部落和组通信框架。这个元素将会封装一切包含有通信和成员关系的逻辑。
<Membership className="org.apache.catalina.tribes.membership.McastService"
                        address="228.0.0.4"
                        port="45564"
                        frequency="500"
                        dropTime="3000"/>
  成员关系是使用多播建立的。请注意,部落也支持通过使用StaticMembershipInterceptor的静态成员关系,如果你想你的成员关系扩展并超过多播。address属性就是多播使用的地址,port就是多播使用的端口。这两个一起建立了集群隔离。如果你想有一个保证质量的集群和一个生产集群,最简单的配置就是质量保证集群和生产集群分别使用一个多播的地址/端口组合。
<Receiver className="org.apache.catalina.tribes.transport.nio.NioReceiver"
                      address="auto"
                      port="5000"
                      selectorTimeout="100"
                      maxThreads="6"/>
  在部落中,发送和接收数据的逻辑被拆分成两个功能模块。接收器,顾名思义就是负责接收消息的。由于部落栈上的线程是少的(现在已经通过其他框架以及一个流行的更新采用),有一个线程池在这个组件中并且拥有一个maxThreads和minThreads的设置。地址属性就是成员关系组件要广播到其他节点的主机地址。
<Sender className="org.apache.catalina.tribes.transport.ReplicationTransmitter">
              <Transport className="org.apache.catalina.tribes.transport.nio.PooledParallelSender"/>
            </Sender>
  发送者组件,顾名思义就是把负责将信息发送到其他节点上的组件。发送者有一个外壳组件ReplicationTransmitter,但是真正的东西是在子组件Transport上。部落支持一个发送者池,所以消息可以被NIO的发送者并行的发送,你也可以并发发送消息。并发意味着一个消息到多个发送者是同事的,并行意味着多个消息到在同一时间到达多个发送者。
<Interceptor className="org.apache.catalina.tribes.group.interceptors.TcpFailureDetector"/>
            <Interceptor className="org.apache.catalina.tribes.group.interceptors.MessageDispatch15Interceptor"/>
            <Interceptor className="org.apache.catalina.tribes.group.interceptors.ThroughputInterceptor"/>
          </Channel>
  部落使用栈发送消息。栈中的每一个元素都被成为称为一个拦截器,并且在Tomcat的servlet容器中工作的更像阀。使用了拦截器,逻辑可以被拆分成更容易管理的代码片段。上述的拦截器的配置是:
TcpFailureDetector:通过TCP验证崩溃的成员,如果多播数据包丢失的话,这个拦截器可以防止误报,即标记为崩溃,尽管它是运行着的存活着的节点。
MessageDispatch15Interceptor:发送消息到一个线程(线程池),使用异步的方式发送消息。
ThroughputInterceptor:打印出简单的消息流量的统计。
请注意,拦截器的顺序是很重要的。它们在server.xml中定义的方式就是它们在通道堆栈中的方式。把它当做一个链表,用头作为第一个拦截器,用尾作为最后一个拦截器。
<Valve className="org.apache.catalina.ha.tcp.ReplicationValve"
                 filter=".*\.gif|.*\.js|.*\.jpeg|.*\.jpg|.*\.png|.*\.htm|.*\.html|.*\.css|.*\.txt"/>
  集群使用阀来跟踪请求中的web应用,我们在前面提到了ReplicationValve和JvmRouteBinderValve。<Cluster>元素本身并不是Tomcat管道中的一部分,在集群中添加的是阀到它的父容器。如果<Cluster>元素在<Engine>元素中被配置,阀将会被添加到engine中。
<Deployer className="org.apache.catalina.ha.deploy.FarmWarDeployer"
                    tempDir="/tmp/war-temp/"
                    deployDir="/tmp/war-deploy/"
                    watchDir="/tmp/war-listen/"
                    watchEnabled="false"/>
  默认的Tomcat集群支持场部署,即集群可以部署活着取消部署在其他节点上。这个组件的状态目前是在不停的变化的,但是会很快得到解决。在这个地方,Tomcat 5.0和Tomcat 5.5有一个部署算法的变化,在这个点上,这个组件逻辑更改到具有相匹配的web应用目录下。
<ClusterListener className="org.apache.catalina.ha.session.ClusterSessionListener"/>
        </Cluster>
  由于SimpleTcpCluster本身就是一发送者和通道对象的接受者,组件可以注册自身作为SimpleTcpCluster的监听器。上述的ClusterSessionListener监听器监听DeltaManager复制消息并应用增量到那些反过来适用于会话的管理器。
集群结构:

  如何工作的?
为了更容易理解集群是如何工作的,我们将通过引入一系列的场景来介绍,在这个场景中我们仅仅计划使用两个tomcat实例 TomcatATomcatB。我们将介绍如下的事件序列:
1、 TomcatA启动
2、 TomcatB启动(等待 TomcatA启动完成)
3、 TomcatA接收一个请求,会话 S1创建
4、 TomcatA崩溃
5、 TomcatB为会话 S1接收一个请求
6、 TomcatA启动
7、 TomcatA接收一个请求, S1上的非法请求
8、 TomcatB接收一个请求,创建一个新的会话 S2
9、 TomcatA,会话 S2由于非活动而过期
现在,我们拥有一个良好的序列,我们将带你通过会话复制代码中探究在这个过程中到底发生了什么。
1、 TomcatA启动
TomcatA启动使用标准的启动序列,当主机对象被创建之后,一个集群对象将会被关联到该主机对象上,当进行上下文解析的时候,如果在web.xml中有distributable元素的定义的话,Tomcat就会要求集群类(在这个示例中是SimpleTcpCluster类)去创建一个对可复制上下文的管理者。所以当集群生效的时候,distributable元素在web.xml中被定义,Tomcat回创建一个DeltaManager为这个上下文而不是StandardManager。集群类会开启一个成员服务(membership service,即多播)和复制服务(tcp单播)。下文中有更多的关于这种架构的介绍。
 
2、 TomcatB启动(等待 TomcatA启动完成)
当TomcatB启动的时候,启动的序列和TomcatA启动的相同,仅有一处例外,集群将建立一个成员关系(TomcatA,TomcatB)。TomcatB将会从集群中的服务器上请求会话的状态,在这个示例中指的是TomcatA。TomcatA会响应这样的请求,并且在TomcatB开始监听HTTP请求的之前,会话的状态已经从TomcatA转移到TomcatB。假如TomcatA不响应的话,TomcatB将会在60秒之后超时,并且发出一个日志条目。所有在web.xml文件中定义了distributable属性的web应用将会传递会话的状态。注意:为了有效的使用会话复制,你所有的tomcat实例的配置必须一样。
 
3、 TomcatA接收一个请求,会话 S1创建
请求被TomcatA处理,完全以相同的方式,没有使用会话复制。当请求完成之后,ReplicationValve会在响应返回给用户之前拦截响应。在这个时候会发现会话被改变了,并且使用TCP来复制会话到TomcatB。一旦序列化了的数据已经被转交给操作系统的TCP逻辑,请求就会返回给用户,后面会通过阀管道。对于每个请求的会话复制过程,将允许代码修改会话的属性而不是通过调用setAttribute或者removeAttribute复制。配置useDirtyFlag参数的值可以优化一个会话被复制的次数。
 
4、 TomcatA崩溃
当TomcatA崩溃,TomcatB接收到一个通知被告知TomcatA已经被移除了集群。TomcatB从自己的成员关系列表中将TomcatA删除,并且TomcatA将不会受到任何来自TomcatB状态变化的通知。负载均衡器将会把所有到TomcatA的请求以及所有的当前会话状态重定向到TomcatB。
 
5、 TomcatB为会话 S1接收一个请求
没有奇怪的事情发生,TomcatB将会像处理其他请求一样处理请求。
 
6、 TomcatA启动
一旦启动,在TomcatA开始处理新的请求并将自己可用之前TomcatA将会按照之前1和2中描述到的启动序列进行。TomcatA会加入到集群中,和TomcatB对当前状态的所有会话进行通信。一旦接收到会话的状态,TomcatA完成加载就会打开自己的HTTP/mod_jk端口。所以在TomcatA把所有的会话状态从TomcatB接收完成,TomcatA是不会接收请求的。
 
7、 TomcatA接收一个请求, S1上的非法请求
这个无效的请求将会被拦截,并且该会话会被添加到失效会话的队列中,当请求完成之后,TomcatA会发出一个“过期”的消息到TomcatB,并且TomcatB会像TomcatA一样进行会话失效处理。
 
8、 TomcatB接收一个请求,创建一个新的会话 S2
跟在3中描述的步骤一致。
 
9、 TomcatA,会话 S2由于非活动而过期
当用户把一个会话失效的时候,同样无效的请求同样会被拦截,同时会话会被添加到失效队列中,在这一点上,试下的会话不会被复制过去直到另一个请求通过系统过来检查失效队列。
 
就酱。
成员关系(Membership):集群的成员关系通过非常简单的多播ping建立。每一个Tomcat实例会周期性的发出一个多播的ping,在这个ping消息中,这个实例会广播自己的IP和TCP监听端口用于复制。如果一个实例没有收到一个给定时间内类似的ping,该成员将会被认为已经死亡。非常简单,同时也非常有效。当然,你需要在你的系统上启用多播。
TCP复制(TCP Replication):一旦一个多播的ping被收到,这个成员就在下次复制请求的时候会被添加到集群中,发送实例会通过主机和端口信息建立一个TCP套接字。通过套接字发送序列化的数据。我选择TCP套接字的原因是它有内置的流量控制和保证交付机制。所以我明白,当我发送一些数据的时候,将会确保发送成功。
分布式锁和使用框架的页(Distributed locking and pages using frames):Tomcat并不保持会话实例在集群中进行同步,这种逻辑的实现将会是一个很大的开销并且会导致很多种的问题。如果你的客户端同时使用多个请求访问同一个会话,那么最后一个请求将会覆盖在集群中的其他会话。
 
可以使用JMX(Java Management Extensions)监控你的集群。当使用一个集群环境的时候,监控是一个非常重要的问题,一些集群对象是JMX MBeans的。将以下的参数添加到Java5的启动脚本中。
set CATALINA_OPTS=\
-Dcom.sun.management.jmxremote \
-Dcom.sun.management.jmxremote.port=%my.jmx.port% \
-Dcom.sun.management.jmxremote.ssl=false \
-Dcom.sun.management.jmxremote.authenticate=false
  有关集群中的MBeans的详细信息请参见 http://tomcat.apache.org/tomcat-7.0-doc/cluster-howto.html#Monitoring_your_Cluster_with_JMX
 
 
 
 

猜你喜欢

转载自yiyiboy2010.iteye.com/blog/2083116