组件层级:
Server
|
Service
|
Engine
| \
| --- Cluster --*
|
Host
|
------
/ \
Cluster Context(1-N)
| \
| -- Manager
| \
| -- DeltaManager
| -- BackupManager
|
---------------------------
| \
Channel \
----------------------------- \
| \
Interceptor_1 .. \
| \
Interceptor_N \
----------------------------- \
| | | \
Receiver Sender Membership \
-- Valve
| \
| -- ReplicationValve
| -- JvmRouteBinderValve
|
-- LifecycleListener
|
-- ClusterListener
| \
| -- ClusterSessionListener
|
-- Deployer
\
-- FarmWarDeployer
为了便于理解集群的工作机制,下面将通过一些实际情境来加深一下你的理解,我们只打算采用 2 个 Tomcat 实例:Tomcat A
和 Tomcat B
。具体发生的事件流程为:
Tomcat A
启动。
Tomcat A
启动完毕后,Tomcat B
才启动。
Tomcat A
接收一个请求,创建了一个会话 S1
。
Tomcat A
崩溃。
Tomcat B
接收到对会话 S1
的请求。
Tomcat A
启动。
Tomcat A
接收到一个请求,调用会话 S1
上的 invalidate
方法。
Tomcat B
接收到一个对新会话 S2
的请求。
Tomcat A
会话 S2
由于不活跃而超时。
介绍完了事件序列,下面详细剖析一下在会话复制代码中到底发生了什么。
1.Tomcat A
启动
Tomcat 使用标准启动顺序来启动。Host 对象创建好之后,会关联一个 Cluster 对象。在解析上下文时,如果 web.xml 中包含 distributable 元素,Tomcat 就会让 Cluster 类(在该例中是 SimpleTcpCluster
)创建复制的上下文的管理器。启用了集群并在 web.xml 中设置了 distributable 元素后,Tomcat 会为该上下文创建一个 DeltaManager
(而不是 StandardManager
)。Cluster 类会启动一个成员服务(组播)和一个复制服务(TCP 单播)。下文将会介绍更多的架构细节。
2.Tomcat B
启动
Tomcat B 启动时,采取的顺序与 Tomcat A 基本一样。集群启动,建立成员(Tomcat A 与 Tomcat B)。Tomcat B 会请求集群中已有服务器(本例中是 Tomcat A)的会话状态。如果 Tomcat A 响应该请求,那么在 Tomcat B 开始侦听 HTTP 请求之前,Tomcat A 会将会话状态传到 Tomcat B那里;如果 Tomcat A 没有响应该请求,Tomcat 会等待 60 秒,超过这个时间之后,发出一个日志项。该会话状态会发送到每一个在 web.xml 中设置了 distributable 元素的应用。注意:为了有效地使用会话复制,所有的 Tomcat 实例都必须拥有相同的配置。
3.Tomcat A
接收一个请求,创建了一个会话 S1
Tomcat A 对发送给它的请求的处理方式,与没有会话复制时的处理方式完全相同。请求完成时会触发相应行为,ReplicationValve
会在响应返回用户之前拦截请求。如发现会话已经更改,则使用 TCP 将会话复制到 Tomcat B 上。一旦序列化的数据被转交给操作系统的 TCP 逻辑,请求就会重新通过 valve 管道返回给用户。对于每一个请求,都将复制所有的会话,这样做就有利于复制那些在会话中修改属性的代码,使其即使不必调用 setAttribute
或removeAttribute
,也能被复制。另外,使用 useDirtyFlag
配置参数也可以优化会话的复制次数。
4.Tomcat A
崩溃
当 Tomcat A 崩溃时,Tomcat B 会接到通知,得知 Tomcat A 已被移出集群,随即 Tomcat B 就在其成员列表中也将 Tomcat A 移除,Tomcat B 从而不再收到关于 Tomcat A 的任何通知。负载均衡器会把从 Tomcat A 发送给 Tomcat B 的请求重新定向,所有的会话都将保持现有的状态。
5.Tomcat B
接收到对会话 S1
的请求
毫无悬念,Tomcat B 会照处理其他请求的方式那样来处理该请求。
6.Tomcat A
启动
在 Tomcat A 开始接收新的请求之前,将会根据上面(1)(2)两条所所说明的启动序列来启动。Tomcat A 会加入集群,联系 Tomcat B 并获取所有的会话状态。一旦接收到会话状态,就会完成加载,并打开 HTTP/mod_jk 端口。所以,除非 Tomcat A 从 Tomcat B 那里接收到了会话变更,否则没有发给 Tomcat A 的请求。
7.Tomcat A
接收到一个请求,调用会话 S1
上的 invalidate
方法
会拦截对 invalidate
的调用, 并且 session
会被加入失效会话队列。 在请求完成时,不会发送会话改变消息,而是发送一个 “到期” 消息给 Tomcat B,Tomcat B 也会让此会话失效。
8.Tomcat B
接收到一个对新会话 S2
的请求
同步骤 3。
9.Tomcat A
会话 S2
由于不活跃而超时
invalidate 调用会被拦截,当一个会话被用户标记失效时,该会话就会加入到无效会话队列。此时,失效的会话不会被复制,直到另一个请求通过系统并检查无效会话队列。
Membership 集群成员是通过非常简单的组播 ping 命令来实现的。每个 Tomcat 实例都会定期发送一个组播 ping,ping 消息中包含 Tomcat 实例自身的 IP 和配置的 TCP 监听端口。如果实例在一个给定的时间内没有收到这样的 ping 信息,就会认为那个成员已经崩溃了。非常简洁高效!当然,您需要在系统上启用广播。
TCP 复制 一旦收到一个多播 ping 包,在下一个复制请求时成员被添加到集群,发送实例将使用的主机和端口信息,以及建立TCP套接字。使用该套接字发送序列化的数据。之选择TCP套接字,是因为它内建有流量控制和保证发送的功能。所以发送的数据肯定会到达那里。
分布式的锁定与使用架构的页面s Tomcat 在跨集群同步不保持会话实例。这种逻辑的实现将是多开销和导致各种各样的问题。如果你的客户用同一个会话同时发送多个请求,那么最后的请求将会覆盖集群中的其他会话。
使用集群时,如何监控是一个重要课题。有些集群对象是 JMX MBean。
添加下列属性到启动脚本上。
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
下面是 Cluster 的 MBean 列表:
名称 | 描述 | MBean 对象名-引擎 | MBean 对象名-主机 |
---|---|---|---|
Cluster
|
完整的 cluster 元素 |
type=Cluster
|
type=Cluster,host=${HOST}
|
DeltaManager
|
该管理器控制会话,并处理会话复制 |
type=Manager,context=${APP.CONTEXT.PATH}, host=${HOST}
|
type=Manager,context=${APP.CONTEXT.PATH}, host=${HOST}
|
FarmWarDeployer
|
将一个应用部署到该集群的所有节点上。 | 目前不支持 |
type=Cluster, host=${HOST}, component=deployer
|
Member
|
代表集群中的一个节点 |
type=Cluster, component=member, name=${NODE_NAME}
|
type=Cluster, host=${HOST}, component=memdber, name=${NODE_NAME}
|
ReplicationValve
|
该 valve 控制到备份节点的会话复制 |
type=Valve,name=ReplicationValve
|
type=Valve,name=ReplicationValve,host=${HOST}
|
JvmRouteBinderValve
|
将 Session ID 变为 tomcat 当前的 jvmroute 的集群回滚值 |
type=Valve,name=JvmRouteBinderValve, context=${APP.CONTEXT.PATH}
|
type=Valve,name=JvmRouteBinderValve,host=${HOST}, context=${APP.CONTEXT.PATH}
|