Hi,我是阿昌,今天学习记录的是关于Cluster组件:Tomcat的集群通信原理的内容。
为了支持水平扩展和高可用,Tomcat 提供了集群部署的能力,但与此同时也带来了分布式系统的一个通用问题,那就是如何在集群中的多个节点之间保持数据的一致性,比如会话(Session)信息。
要实现这一点,基本上有两种方式,一种是把所有 Session 数据放到一台服务器或者一个数据库中,集群中的所有节点通过访问这台 Session 服务器来获取数据。
另一种方式就是在集群中的节点间进行 Session 数据的同步拷贝,这里又分为两种策略:第一种是将一个节点的 Session 拷贝到集群中其他所有节点;
第二种是只将一个节点上的 Session 数据拷贝到另一个备份节点。
对于 Tomcat 的 Session 管理来说,这两种方式都支持。
要实现集群通信,首先要知道集群中都有哪些成员。
Tomcat 是通过组播(Multicast)来实现的。那什么是组播呢?
为了理解组播,先来说说什么是“单播”。网络节点之间的通信就好像是人们之间的对话一样,一个人对另外一个人说话,此时信息的接收和传递只在两个节点之间进行,比如你在收发电子邮件、浏览网页时,使用的就是单播,也就是我们熟悉的“点对点通信”。
如果一台主机需要将同一个消息发送多个主机逐个传输,效率就会比较低,于是就出现组播技术。
组播是一台主机向指定的一组主机发送数据报包,组播通信的过程是这样的:
每一个 Tomcat 节点在启动时和运行时都会周期性(默认 500 毫秒)发送组播心跳包,同一个集群内的节点都在相同的组播地址和端口监听这些信息;在一定的时间内(默认 3 秒)不发送组播报文的节点就会被认为已经崩溃了,会从集群中删去。
因此通过组播,集群中每个成员都能维护一个集群成员列表。
有了集群成员的列表,集群中的节点就能通过 TCP 连接向其他节点传输 Session 数据。
Tomcat 通过 SimpleTcpCluster 类来进行会话复制(In-Memory Replication)。
要开启集群功能,只需要将server.xml里的这一行的注释去掉就行:

变成这样:

虽然只是简单的一行配置,但这一行配置等同于下面这样的配置,也就是说 Tomcat 给我们设置了很多默认参数,这些参数都跟集群通信有关。
<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.MessageDispatchInterceptor"/>
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.
ClusterSessionListener"/>
Cluster>
从上面的的参数列表可以看到,默认情况下 Session 管理组件 DeltaManager 会在节点之间拷贝 Session,DeltaManager 采用的一种 all-to-all 的工作方式,即集群中的节点会把 Session 数据向所有其他节点拷贝,而不管其他节点是否部署了当前应用。
当集群节点数比较少时,比如少于 4 个,这种 all-to-all 的方式是不错的选择;
但是当集群中的节点数量比较多时,数据拷贝的开销成指数级增长,这种情况下可以考虑 BackupManager,BackupManager 只向一个备份节点拷贝数据。
在大体了解了 Tomcat 集群实现模型后,就可以对集群作出更优化的配置了。
Tomcat 推荐了一套配置,使用了比 DeltaManager 更高效的 BackupManager,并且通过 ReplicationValve 设置了请求过滤。
这里还请注意在一台服务器部署多个节点时需要修改 Receiver 的侦听端口,另外为了在节点间高效地拷贝数据,所有 Tomcat 节点最好采用相同的配置,具体配置如下:
<Cluster className="org.apache.catalina.ha.tcp.SimpleTcpCluster"
channelSendOptions="6">
<Manager className="org.apache.catalina.ha.session.BackupManager"
expireSessionsOnShutdown="false"
notifyListenersOnReplication="true"
mapSendOptions="6"/>
<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.MessageDispatchInterceptor"/>
<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>
Tomcat 的官网给出了一个例子,来说明 Tomcat 集群模式下是如何工作的,以及 Tomcat 集群是如何实现高可用的。比如集群由 Tomcat A 和 Tomcat B 两个 Tomcat 实例组成,按照时间先后顺序发生了如下事件:
Tomcat A 启动过程中,当 Host 对象被创建时,一个 Cluster 组件(默认是 SimpleTcpCluster)被关联到这个 Host 对象。当某个应用在web.xml中设置了 Distributable 时,Tomcat 将为此应用的上下文环境创建一个 DeltaManager。SimpleTcpCluster 启动 Membership 服务和 Replication 服务。
首先 Tomcat B 会执行和 Tomcat A 一样的操作,然后 SimpleTcpCluster 会建立一个由 Tomcat A 和 Tomcat B 组成的 Membership。接着 Tomcat B 向集群中的 Tomcat A 请求 Session 数据,如果 Tomcat A 没有响应 Tomcat B 的拷贝请求,Tomcat B 会在 60 秒后 time out。在 Session 数据拷贝完成之前 Tomcat B 不会接收浏览器的请求。
Tomcat A 响应客户请求,在把结果发送回客户端之前,ReplicationValve 会拦截当前请求(如果 Filter 中配置了不需拦截的请求类型,这一步就不会进行,默认配置下拦截所有请求),如果发现当前请求更新了 Session,就调用 Replication 服务建立 TCP 连接将 Session 拷贝到 Membership 列表中的其他节点即 Tomcat B。在拷贝时,所有保存在当前 Session 中的可序列化的对象都会被拷贝,而不仅仅是发生更新的部分。
当 Tomcat A 崩溃时,Tomcat B 会被告知 Tomcat A 已从集群中退出,然后 Tomcat B 就会把 Tomcat A 从自己的 Membership 列表中删除。并且 Tomcat B 的 Session 更新时不再往 Tomcat A 拷贝,同时负载均衡器会把后续的 HTTP 请求全部转发给 Tomcat B。在此过程中所有的 Session 数据不会丢失。
Tomcat B 正常响应本应该发往 Tomcat A 的请求,因为 Tomcat B 保存了 Tomcat A 的所有 Session 数据。
Tomcat A 按步骤 1、2 操作启动,加入集群,并从 Tomcat B 拷贝所有 Session 数据,拷贝完成后开始接收请求。
Tomcat 继续接收发往 Tomcat A 的请求,Session 1 设置为失效。请注意这里的失效并非因为 Tomcat A 处于非活动状态超过设置的时间,而是应用程序执行了注销的操作(比如用户登出)而引起的 Session 失效。这时 Tomcat A 向 Tomcat B 发送一个 Session 1 Expired 的消息,Tomcat B 收到消息后也会把 Session 1 设置为失效。
同理这个新的 Session 也会被拷贝到 Tomcat A。
因超时原因引起的 Session 失效 Tomcat A 无需通知 Tomcat B,Tomcat B 同样知道 Session 2 已经超时。因此对于 Tomcat 集群有一点非常重要,所有节点的操作系统时间必须一致。不然会出现某个节点 Session 已过期而在另一节点此 Session 仍处于活动状态的现象。
Tomcat 集群对 Session 的拷贝支持两种方式:DeltaManager 和 BackupManager。
当集群中节点比较少时,可以采用 DeltaManager,因为 Session 数据在集群中各个节点都有备份,任何一个节点崩溃都不会对整体造成影响,可靠性比较高。
当集群中节点数比较多时,可以采用 BackupManager,这是因为一个节点的 Session 只会拷贝到另一个节点,数据拷贝的开销比较少,同时只要这两个节点不同时崩溃,Session 数据就不会丢失。
在 Tomcat 官方推荐的配置里,ReplicationValve 被配置成下面这样:
<Valve className="org.apache.catalina.ha.tcp.ReplicationValve"
filter=".*\.gif|.*\.js|.*\.jpeg|.*\.jpg|.*\.png|.*\
.htm|.*\.html|.*\.css|.*\.txt"/>
你是否注意到,filter 的值是一些 JS 文件或者图片等,这是为什么呢?
这些静态资源不涉及session,直接过滤就好
小的集群可以用Tomcat原生方案,大集群还是用Redis