接上篇《Tomcat新手成长之路:安装部署优化全解析(上)》: link
7.应用部署
部署这个术语描述的就是,将 Web 应用(第三方的 WAR 文件,或是你自己定制的 Web 应用)安装到 Tomcat 服务器上的整个过程。
在 Tomcat 服务器上,可以通过多种方法部署 Web 应用:
- 静态部署。在启动 Tomcat 之前安装 Web 应用。
- 动态部署。使用 Tomcat 的 Manager 应用直接操控已经部署好的 Web 应用(依赖 auto-deployment 特性)
7.1.上下文
为了在 Tomcat 中配置上下文,需要用到上下文描述符文件( Context Descriptor)。上下文描述符文件其实就是一个 XML 文件,含有 Tomcat 与上下文相关的配置信息,例如命名资源或会话管理器配置信息。在 Tomcat 的早期版本中,上下文描述符文件配置的内容经常保存在 Tomcat 的主要配置文件 server.xml 中,但现在不再推荐采用这一方式(虽然目前它依然有效)。
上下文描述符文件位于:
- $CATALINA_BASE/conf/[enginename]/[hostname]/[webappname].xml
- $CATALINA_BASE/webapps/[webappname]/META-INF/context.xml
在目录 1 中的文件名为 [webappname].xml,但在目录 2 中,文件名为 context.xml。如果某个 Web 应用没有相应的上下文描述符文件,Tomcat 就会使用默认值配置该应用。
7.2.启动时进行部署
先把 Web 应用静态地部署到 Tomcat 中,然后再启动 Tomcat。 这种情况下应用部署的位置由 appBase 目录属性来决定,每台主机都指定有这样一个位置。该位置既可以放入未经压缩的 Web 应用资源文件(通常被称为 exploded web application,“膨胀 Web 应用”),也可以放置已压缩过的 Web 应用资源文件(.WAR 文件)。
再次解释一下,应用部署位置由主机(默认主机为 localhost)的 appBase 属性来指定。默认的 appBase 属性所指定的目录为 $CATALINA_BASE/webapps。只有当主机的 deployOnStartup 属性为 true, 应用才会在 Tomcat 启动时进行自动部署。
在以上情况下,当 Tomcat 启动时,部署的具体步骤如下:
- 先部署上下文描述符文件。
- 然后再对没被任何上下文描述符文件引用过的膨胀 Web 应用进行部署。 如果在 appBase 中已存在与这种应用有关的 .WAR 文件,而且要比膨胀应用文件更新,那么就会将膨胀应用的文件夹清除,转而从 .WAR 文件中部署 Web 应用。
部署 .WAR 文件。
7.3.动态应用部署
除了静态部署之外,也可以在运行中的 Tomcat 服务器上进行应用部署。
如果主机的 autoDeploy 属性为 true,主机就会在必要时尝试着动态部署并更新 Web 应用。 例如,当把一个新 .WAR 文件放入 appBase 所指定的名录时。为了实现这种操作,主机就需要启用后台处理,当然这也是默认的配置。
当 autoDeploy 设置为 true 时,运行中的 Tomcat 服务器能够允许实现以下行为:
- 对放入主机 appBase指定目录下的 .WAR 文件进行配置。
- 对放入主机的膨胀 Web 应用进行配置。
- 对于已通过 .WAR 文件配置好的应用,如果又提供了更新的 .WAR 文件,则使用新 .WAR 文件对该应用重新进行配置。在这种情况下,会先移除原有的膨胀 Web 应用,然后再次对 .WAR 文件进行扩展(膨胀)。注意,如果在主机配置中,没有把 unpackWARs 属性设为 false,则 WAR 文件将不会膨胀,这时 Web 应用将部署为一个压缩文档。
- 如果 /WEB-INF/web.xml 文件(或者任何其他被定义为 WatchedResource 的资源)更新,则重新加载 Web 应用。
- 如果用来部署 Web 应用的上下文描述符更新,则重新部署该 Web 应用。
- 如果 Web 应用所使用的全局或者每台主机中的上下文描述符已更新,则重新部署与该应用有依赖关系的 Web 应用。
- 如果一个上下文描述符被添加到 $CATALINA_BASE/conf/[enginename]/[hostname]/ 目录中,并且该描述文件带有与之前部署的 Web 应用的上下文路径相对应的文件名,则重新部署该 Web 应用。
- 如果某个 Web 应用的文档库(docBase)被删除,则取消对该应用的部署。注意,在 Windows 系统下,要想实现这样的行为,必须开启防锁死功能,否则无法删除运行中的 Web 应用的资源文件。
8.Tomcat 类加载机制
8.1.简介
与很多服务器应用一样,Tomcat 也安装了各种类加载器(那就是实现了 java.lang.ClassLoader 的类)。借助类加载器,容器的不同部分以及运行在容器上的 Web 应用就可以访问不同的仓库(保存着可使用的类和资源)。这个机制实现了 Servlet 规范 2.4 版(尤其是 9.4 节和 9.6 节)里所定义的功能。
在 Java 环境中,类加载器的布局结构是一种父子树的形式。通常,类加载器被请求加载一个特定的类或资源时,它会先把这一请求委托给它的父类加载器,只有(一个或多个)父类加载器无法找到请求的类或资源时,它才开始查看自身的仓库。注意,Web 应用的类加载器模式跟这个稍有不同,下文将详细介绍,但基本原理是一样。
当 Tomcat 启动后,它就会创建一组类加载器,这些类加载器被布局成如下图所示这种父子关系,父类加载器在子类加载器之上:
接下来,通过几节内容来详细说明每一个类加载器的特点,其中还将讲解这些加载器可使用的类和资源的来源。
8.2.类加载器定义
如上图所示,Tomcat 在初始化时会创建如下这些类加载器:
- Bootstrap 这种类加载器包含 JVM 所提供的基本的运行时类,以及来自系统扩展目录($JAVA_HOME/jre/lib/ext)里 JAR 文件中的类。注意:在有些 JVM 的实现中,它的作用不仅仅是类加载器,或者它可能根本不可见(作为类加载器)。
- System 这种类加载器通常是根据 CLASSPATH 环境变量内容进行初始化的。所有的这些类对于 Tomcat 内部类以及 Web 应用来说都是可见的。不过,标准的 Tomcat 启动脚本
($CATALINA_HOME/bin/catalina.sh 或 %CATALINA_HOME%\bin\catalina.bat)
完全忽略了 CLASSPATH 环境变量自身的内容,相反从下列仓库来构建系统类加载器:o$CATALINA_HOME/bin/bootstrap.jar
包含用来初始化 Tomcat 服务器的 main() 方法,以及它所依赖的类加载器实现类。$CATALINA_BASE/bin/tomcat-juli.jar 或 $CATALINA_HOME/bin/tomcat-juli.jar
日志实现类。其中包括了对 java.util.logging API 的功能增强类(Tomcat JULI),以及对 Tomcat 内部使用的 Apache Commons 日志库的包重命名副本。- 如果
*$CATALINA_BASE/bin*
中存在tomcat-juli.jar
,就不会使用*$CATALINA_HOME/bin*
中的那一个。它有助于日志的特定配置。 $CATALINA_HOME/bin/commons-daemon.jar Apache Commons Daemon
项目的类。该 JAR 文件并不存在于由 catalina.bat 或 catalina.sh 脚本所创建的 CLASSPATH 中,而是引用自 bootstrap.jar 的清单文件。
- Common 这种类加载器包含更多的额外类,它们对于Tomcat 内部类以及所有 Web 应用都是可见的。
通常,应用类不会放在这里。该类加载器所搜索的位置定义在$CATALINA_BASE/conf/catalina.properties
的 common.loader 属性中。默认的设置会搜索下列位置(按照列表中的上下顺序)。-
$CATALINA_BASE/lib
中的解包的类和资源。 -
$CATALINA_BASE/lib
中的 JAR 文件。 -
$CATALINA_HOME/lib
中的解包类和资源。 -
$CATALINA_HOME/lib
中的 JAR 文件。默认,它包含以下这些内容:
-
oannotations-api.jar JavaEE 注释类。
-
ocatalina.jar Tomcat 的 Catalina servlet 容器部分的实现。
-
ocatalina-ant.jar Tomcat Catalina Ant 任务。
-
ocatalina-ha.jar 高可用性包。
-
ocatalina-storeconfig.jar
-
ocatalina-tribes.jar 组通信包
-
oecj-*.jar Eclipse JDT Java 编译器
-
oel-api.jar EL 3.0 API
-
ojasper.jar Tomcat Jasper JSP 编译器与运行时
-
ojasper-el.jar Tomcat Jasper EL 实现
-
ojsp-api.jar JSP 2.3 API
-
oservlet-api.jar Servlet 3.1 API
-
otomcat-api.jar Tomcat 定义的一些接口
-
otomcat-coyote.jar Tomcat 连接器与工具类。
-
otomcat-dbcp.jar 数据库连接池实现,基于 Apache Commons Pool 的包重命名副本和 Apache Commons DBCP。
-
otomcat-i18n-**.jar 包含其他语言资源束的可选 JAR。因为默认的资源束也可以包含在每个单独的 JAR 文件中,所以如果不需要国际化信息,可以将其安全地移除。
-
otomcat-jdbc.jar 一个数据库连接池替代实现,又被称作 Tomcat JDBC 池。详情参看 JDBC 连接池文档。
-
otomcat-util.jar Apache Tomcat 多种组件所使用的常用类。
-
otomcat-websocket.jar WebSocket 1.1 实现
-
owebsocket-api.jar WebSocket 1.1 API
-
- WebappX 为每个部署在单个 Tomcat 实例中的 Web 应用创建的类加载器。你的 Web 应用的 /WEB-INF/classes 目录中所有的解包类及资源,以及 /WEB-INF/lib 目录下 JAR 文件中的所有类及资源,对于该应用而言都是可见的,但对于其他应用来说则不可见。
如上所述,Web 应用类加载器背离了默认的 Java 委托模式(根据 Servlet 规范 2.4 版的 9.7.2 Web Application Classloader一节中提供的建议)。当某个请求想从 Web 应用的 WebappX 类加载器中加载类时,该类加载器会先查看自己的仓库,而不是预先进行委托处理。There are exceptions。JRE 基类的部分类不能被重写。对于一些类(比如 J2SE 1.4+ 的 XML 解析器组件),可以使用 J2SE 1.4 支持的特性。最后,类加载器会显式地忽略所有包含 Servlet API 类的 JAR 文件,所以不要在 Web 应用包含任何这样的 JAR 文件。Tomcat 其他的类加载器则遵循常用的委托模式。
因此,从 Web 应用的角度来看,加载类或资源时,要查看的仓库及其顺序如下:
- JVM 的 Bootstrap 类
- Web 应用的
/WEB-INF/classes
类 - Web 应用的
/WEB-INF/lib/*.jar
类 - System 类加载器的类(如上所述)
- Common 类加载器的类(如上所述)
如果 Web 应用类加载器配置有 <Loader delegate="true"/>
,则顺序变为:
- JVM 的 Bootstrap 类
- System 类加载器的类(如上所述)
- Common 类加载器的类(如上所述)
- Web 应用的
/WEB-INF/classes
类 - Web 应用的
/WEB-INF/lib/*.jar
类
8.3.XML解析器和 Java
从 Java 1.4 版起,JRE 就包含了一个 JAXP API 的副本和一个 XML 解析器。这对希望使用自己的 XML 解析器的应用产生了一定的影响。
在过去的 Tomcat 中,你只需在 Tomcat 库中简单地换掉 XML 解析器,就能改变所有 Web 应用使用的解析器。但对于现在版本的 Java 而言,这一技术并没有效果,因为通常的类加载器委托进程往往会优先选择 JDK 内部的实现。
Java 支持一种叫做“授权标准覆盖机制”,从而能够替换在 JCP 之外创建的 API(例如 W3C 的 DOM 和 SAX)。
为了利用该机制,Tomcat 在启动容器的命令行中包含了系统属性设置 -Djava.endorsed.dirs=$JAVA_ENDORSED_DIRS
。该选项的默认值为 $CATALINA_HOME/endorsed
。但要注意,这个 endorsed 目录并非默认创建的。
9.JMS监控
9.1.简介
监控是系统管理中的重要环节。系统管理员的日常工作就包括:观察服务器的运行细节,获取统计数据,或者重新配置应用的某些内容。
9.2.启用 JMX 远程监控
注意:该配置只适用于需用远程监控 Tomcat 的情况,使用同样的用户在本地监控 Tomcat 则不需要这么配置。
将下列参数添加到 Tomcat 的 setenv.sh
脚本
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
- 如果需要授权,则添加并修改下列命令:
-Dcom.sun.management.jmxremote.authenticate=true
-Dcom.sun.management.jmxremote.password.file=../conf/jmxremote.password
-Dcom.sun.management.jmxremote.access.file=../conf/jmxremote.access
- 编辑访问权限文件 $CATALINA_BASE/conf/jmxremote.access:
monitorRole readonly
controlRole readwrite
- 编辑密码文件
$CATALINA_BASE/conf/jmxremote.password
:
monitorRole tomcat
controlRole tomcat
密码文件应该是只读的,并且只能被运行 Tomcat 的操作系统用户所访问。
9.3.JMXProxyServlet
Tomcat 为使用远程(或者甚至本地的)JMX 连接提供了一个替代方案:Tomcat 的 JMXProxyServlet,但它仍能让你访问 JMX 所提供的任何内容。
JMXProxyServlet 允许客户端通过 HTTP 接口来发送 JMX 查询。相比直接从客户端程序使用 JMX 来说,该技术具有以下优势:
- 无需加载完整的 JVM 并执行远程 JMX 连接,只需从服务器上请求一小块数据即可。
- 无需了解处理 JMX 连接的方式。
- 无需任何复杂的配置。
- 无需用 Java 来编写客户端程序。
常见的服务器监控软件中都存在过度使用 JMX 的问题:如果想通过 JMX 监控 10 项,就必须启动 10 个 JVM,保持 10 个 JMX 连接,每过几分钟就要将它们全部关闭。有了 JMXProxyServlet,利用 10 个 HTTP 连接就能搞定了。
10.集群和会话保持
10.1.集群概述
要想在 Tomcat 上运行会话复制,需要执行以下步骤:
- 所有的会话属性必须实现 java.io.Serializable。
- 在 server.xml 中取消注释 Cluster 元素。
- 如果你已经定义了自定义集群值,确保在 server.xml 中的 Cluster 元素下面也定义了 ReplicationValve。
- 如果你的多个 Tomcat 实例都运行在同一台机器上,则要确保每个实例都具有唯一的 tcpListenPort。通常 Tomcat 会自行解决这个问题,会在 4000 - 4100 上自动侦测可用的端口。
- 确保 web.xml 含有 属性。
- 如果使用 mod_jk,则要确保在 上设定 jvmRoute 属性。jvmRoute 属性值必须匹配 workers.properties 中的 worker 名称。
- 所有的节点必须具有相同的时间,并且与 NTP 服务器同步。
- 确保负载均衡配置了会话模式。
注意:会话状态是通过 cookie 来记录的,所以你的 URL 必须保持一致,否则就会创建一个新会话。
集群模块使用 Tomcat 的 JULI 日志框架,所以可以通过 logging.properties 文件来配置日志。为了跟踪消息,你可以启用 org.apache.catalina.tribes.MESSAGES 键上的日志。
在 Tomcat 中,可以使用以下方法中的一种启用会话复制:
- 使用会话持久性,将会话保存到共享文件系统中(PersistenceManager + FileStore)。
- 使用会话持久性,将会话保存到共享数据库中(PersistenceManager + JDBCStore)。
- 使用内存复制,使用 Tomcat 自带的 SimpleTcpCluster(lib/catalina-tribes.jar + lib/catalina-ha.jar)。
10.2.集群信息
通过组播心跳包(heartbeat)建立起成员(Membership)关系,因此,如果希望细分集群,可以改变 元素中的组播 IP 地址或端口。
心跳包中含有 Tomcat 节点的 IP 地址,以及 Tomcat 用来侦听会话复制流量的 TCP 端口。所有的数据通信都使用了 TCP 协议。
ReplicationValve 用于查找请求结束的时间,如果存在会话复制,就对该复制进行初始化。只复制会话变更的数据(通过在会话上调用 setAttribute 或 removeAttribute 来完成)。
复制的异步与同步模式应该是最值得我们注意的一个特点了。在同步复制模式下,复制的会话通过线缆传送,重新在所有集群节点上实例化,这样才会返回请求。同步和异步是通过 channelSendOptions 标志(整型值)来配置的。
SimpleTcpCluster/DeltaManager 组合的默认值是 8,从而是异步。详情可以参考一下 send flag(overview) 或 send flag(javadoc)。在异步复制过程中,请求不必等到数据被复制完毕即可返回。异步复制缩短了请求时间,而同步复制则保证了能在请求返回之前复制完会话。
10.3.故障转移
当发生崩溃时,将会话绑定到故障转移节点。
如果你使用了 mod_jk 而没有使用粘性会话(sticky session),或者粘性会话由于某种原因而不起作用,或者仅是故障转移,会话 id 需要修改,因为它之前含有之前 Tomcat 的 worker id(通过 Engine 元素中的 jvmRoute 定义)。为了解决这个问题,就要用到 JvmRouteBinderValve。
JvmRouteBinderValve 将重写会话 id,以便确保下一个请求在故障转移后依然能保持粘性(不会因为 worker 不再可用而回滚到某个随机的节点中)。利用同样的名字,该值重写了 cookie 中的 JSESSIONID 值。假如没有正确地设置 valve,将使 mod_jk 模块在失败后很难保持会话的粘性。
记住,如果在 server.xml 中自定义值,那么默认值将不再有效,所以一定要确保添加了默认所定义的值。
提示:
利用属性 sessionIdAttribute 可以改变包含旧会话 id 的请求属性名。默认的请求属性名是:org.apache.catalina.ha.session.JvmRouteOrignalSessionID。
技巧:
可以启用 mod_jk 翻转模式在删除一个节点, 然后启用了 mod_jk Worker 禁用 JvmRouteBinderValves 。这种用例意味着只有请求的会话才能得到迁移。
10.4.配置范例
<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">
Cluster 是主要元素,可在该元素内配置所有的集群相关细节。 对于 SimpleTcpCluster 类或者调用 SimpleTcpCluster.send 方法的对象,它们所发出的每一个消息上都附加着一个 channelSendOptions 标志。DeltaManager 使用 SimpleTcpCluster.send
方法发送信息,而备份管理器则直接通过 channel 来发送自身。
<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>
元素中没有定义 manager,则以上可当做 manager 的配置模板。我们可以为每个应用定义一个 manager 类,从而在集群中混合多个 manager。显然,A 节点上的某个应用的所有 manager 必须与 B 节点上的同样应用的 manager 相同。如果没有为应用指定 manager,而且该应用被标识为 <distributable/>
,Tomcat 就会采取这种 manager 配置,创建一个克隆该配置的 manager 实例。
<Channel className="org.apache.catalina.tribes.group.GroupChannel">
Channel 元素是 Tribes 架构的一个重要组成部分,Tribes 是 Tomcat 内部所使用的分组通信架构。Channel 元素封装了所有通信相关事项以及成员逻辑。
<Membership className="org.apache.catalina.tribes.membership.McastService"
address="228.0.0.4"
port="45564"
frequency="500"
dropTime="3000"/>
成员关系(Membership)是通过组播来实现的。注意,如果你想将成员扩展到组播范围之外的某个点时,Tribes 现在已经能够支持使用 StaticMembershipInterceptor 的静态成员。address 属性是所用的组播地址,port 是所用的组播端口号。这两项组合起来将集群隔离开。如果你希望一个 QA 集群和一个生产集群,最简单的方法就是将 QA 集群的组播地址和端口号不同于生产集群的组播地址和端口号组合。
成员组件将其自身的 TCP 地址和端口广播到其他节点处,从而使节点间的通信都可以通过 TCP 协议来完成。请注意被广播的 TCP 地址正是 Receiver.address
属性值。
<Receiver className="org.apache.catalina.tribes.transport.nio.NioReceiver"
address="auto"
port="5000"
selectorTimeout="100"
maxThreads="6"/>
在 Tribes 架构中,数据的发送与接收以及被拆分为两种功能性组件了。正如其名所示,Receiver 负责接收信息。由于 Tribes 与线程无关(其他架构也开始采用这一种常见改进了),该组件内部包含一个线程池,设定有 maxThreads 和 minThreads 两种参数。
address 参数值是主机地址,由成员组件广播到其他节点中。
<Sender className="org.apache.catalina.tribes.transport.ReplicationTransmitter">
<Transport className="org.apache.catalina.tribes.transport.nio.PooledParallelSender"/>
</Sender>
Sender 组件负责将消息发送给其他节点。Sender 含有一个 shell 组件 ReplicationTransmitter
,但真正所要完成的任务则是通过子组件 Transport
来完成的。由于 Tribes 支持一个 Sender 池,所以消息可以做到同步;如果使用的是 NIO Sender,你也可以并发地发送消息。
并发(Concurrently)意味着将同时有多个发送者对应着一条消息,并行(Parallel)则意味着同时有多个消息对应着多个发送者。
<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>
Tribes 利用了一个堆栈传送消息。每个堆栈内的元素都被称为拦截器,跟 Tomcat 容器中的 valve 的作用差不多。使用拦截器,逻辑可被分成更容易管理的代码段。上面配置中的拦截器:
- TcpFailureDetector 通过 TCP 核实崩溃的节点。如果组播包丢失,该拦截器就会防止误报的情况出现,比如,某个正在运行的节点虽然活跃,但也被标示为已崩溃。
- MessageDispatch15Interceptor 分派消息到线程(线程池),异步发送消息。
- ThroughputInterceptor 输出对信息流量的简单统计。
请注意,拦截器的顺序很重要。在 server.xml 中定义的顺序正是它们出现在 channel 堆栈中的顺序。这种机制就像是链表,最前面的是第一个拦截器,末尾的是最后一个拦截器。
<Valve className="org.apache.catalina.ha.tcp.ReplicationValve"
filter=".*\.gif|.*\.js|.*\.jpeg|.*\.jpg|.*\.png|.*\.htm|.*\.html|.*\.css|.*\.txt"/>
集群使用 valve 来跟踪针对 Web 应用的请求。我们之前已经提到过 ReplicationValve 和 JvmRouteBinderValve。<Cluster>
元素本身并不是 Tomcat 管道的一部分,集群将 valve 添加到了它的父容器上,比如说 <Cluster>
元素被配置到 <Engine>
元素中,那么 valve 就会被加到 <Engine>
元素中。
<Deployer className="org.apache.catalina.ha.deploy.FarmWarDeployer"
tempDir="/tmp/war-temp/"
deployDir="/tmp/war-deploy/"
watchDir="/tmp/war-listen/"
watchEnabled="false"/>
默认的 Tomcat 集群支持耕种部署(farmed deployment),比如说集群可以在其他的节点上部署和取消部署应用。该组件的状态目前还不稳定,但我们很快就会解决这个问题。组件的逻辑改变到部署目录必须与应用目录相匹配。
<ClusterListener className="org.apache.catalina.ha.session.ClusterSessionListener"/>
</Cluster>
因为 SimpleTcpCluster
本身既是 Channel 对象的发送者,又是接受者,所以组件可以将它们自身注册成SimpleTcpCluster 的侦听器。 上面这个侦听器 ClusterSessionListener
将侦听 DeltaManager
复制的消息,并将会话变更应用到 manager 上,反过来应用到会话上。
10.5.流程说明
为了便于理解集群的工作机制,下面将通过一些实际情境来验证,我们采用 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套接字,是因为它内建有流量控制和保证发送的功能。所以发送的数据肯定会到达那里。
分布式的锁定与使用架构的页面 Tomcat 在跨集群同步不保持会话实例。这种逻辑的实现将是多开销和导致各种各样的问题。如果你的客户用同一个会话同时发送多个请求,那么最后的请求将会覆盖集群中的其他会话。
11.高可用集群
11.1.简介
此方案使用:Keepalived + Nginx+tomcat 实现。
- Keepalived + Nginx,实现Web服务的高可用。
- Nginx 是一个高性能的 HTTP反向代理服务器
- Keepalived 是一个基于VRRP协议来实现的服务高可用方案,可以利用其来避免服务的单点故障
11.2.架构图
12.优化策略
12.1.服务器资源
服务器所能提供CPU、内存、硬盘的性能对处理能力有决定性影响。
- 对于高并发情况下会有大量的运算,那么CPU的速度会直接影响到处理速度。
- 内存在大量数据处理的情况下,将会有较大的内存容量需求,可以用-Xmx -Xms -XX:MaxPermSize等参数对内存不同功能块进行划分。我们之前就遇到过内存分配不足,导致虚拟机一直处于full GC,从而导致处理能力严重下降。
- 硬盘主要问题就是读写性能,也就是多少次IO,当大量文件进行读写时,磁盘极容易成为性能瓶颈。最好的办法还是利用下面提到的缓存。
12.2.利用缓存和压缩
对于静态页面最好是能够进行缓存。通常采用Nginx作为缓存服务器,将图片、css、js文件都进行了缓存,有效的减少了后端tomcat的访问。
另外,为了能加快网络传输速度,开启gzip压缩也是必不可少的。但考虑到tomcat已经需要处理很多东西了,所以把这个压缩的工作就交给前端的Nginx来完成。这里实际上就是nginx+tomcat动静分离的初衷。
除了文本可以用gzip压缩,其实很多图片也可以用图像处理工具预先进行压缩,找到一个平衡点可以让画质损失很小而文件可以减小很多。
一般在生产环境缓存和压缩是交给web服务器nginx来做,相关配置会在后面文章分享。
12.3.采用集群
单个服务器性能总是有限的,最好的办法自然是实现横向扩展,那么组建tomcat集群是有效提升性能的手段。我们还是采用了Nginx来作为请求分流的服务器,后端多个tomcat共享session来协同工作。
12.4.优化tomcat参数
需要修改conf/server.xml文件,主要是优化连接配置,关闭客户端dns查询,下面为示例,供参考。
<connector p="" <="" port="8080">
protocol="org.apache.coyote.http11.Http11NioProtocol"
connectionTimeout="20000"
redirectPort="8443"
maxThreads="500"
minSpareThreads="20"
acceptCount="100"
disableUploadTimeout="true"
enableLookups="false"
URIEncoding="UTF-8" />
12.5.改用APR库
tomcat默认采用的BIO模型,在几百并发下性能会有很严重的下降。tomcat自带还有NIO的模型,另外也可以调用APR的库来实现操作系统级别控制。
NIO模型是内置的,调用很方便,只需要将上面配置文件中protocol修改成org.apache.coyote.http11.Http11NioProtocol,重启即可生效。上面配置我已经改过了,默认的是HTTP/1.1。
APR则需要安装第三方库,在高并发下会让性能有明显提升。安装完成后重启即可生效。如使用默认protocal就是apr,但最好把将protocol修改成org.apache.coyote.http11.Http11AprProtocol,会更加明确。
如果tomcat实在是没法优化了,可以试下tomcat的APR模式,这个对性能是有明显的提升的,之前做实验测试过,大家有时间也可以配置试一下,实际上在启动的时候看最后的日志就可以看出tomcat用的是什么模式了。
12.6.优化网络
Joel也明确提出了优化网卡驱动可以有效提升性能,这个对于集群环境工作的时候尤为重要。由于我们采用了Linux服务器,所以优化内核参数也是一个非常重要的工作。给一个参考的优化参数:
修改/etc/sysctl.cnf文件,在最后追加如下内容:【示例,可以根据实际情况调整】
net.core.netdev_max_backlog = 32768
net.core.somaxconn = 32768
net.core.wmem_default = 8388608
net.core.rmem_default = 8388608
net.core.rmem_max = 16777216
net.core.wmem_max = 16777216
net.ipv4.ip_local_port_range = 1024 65000
net.ipv4.route.gc_timeout = 100
net.ipv4.tcp_fin_timeout = 30
net.ipv4.tcp_keepalive_time = 1200
net.ipv4.tcp_timestamps = 0
net.ipv4.tcp_synack_retries = 2
net.ipv4.tcp_syn_retries = 2
net.ipv4.tcp_tw_recycle = 1
net.ipv4.tcp_tw_reuse = 1
net.ipv4.tcp_mem = 94500000 915000000 927000000
net.ipv4.tcp_max_orphans = 3276800
net.ipv4.tcp_max_syn_backlog = 65536
保存退出,执行sysctl -p生效
13.常见故障及解决
13.1.正常启动应用无法访问
解决方法:
- 检查tomcat服务是否运行
- 检查tomcat是否正常监听端口
- 服务器防火墙是否放行访问端口
- 检查系统是否开启了selinux
- 检查日志,是否有服务或者项目配置问题
13.2.JAVA环境变量问题
Tomcat 启动的时候 有类似:
Neither the JAVA_HOME nor the JRE_HOME environment variable is defined At least one of these environment variable is needed to run this program
这样的信息,则说明java环境变量需要配置。
解决方法:
- 确定需要安装的jdk版本及是否已经安装
- 增加环境变量配置
13.3.端口冲突
解决方法:
- 确定端口冲突的应用,重新规划端口
- 若是需要修改tomcat端口,则需要修改server.xml文件中冲突的具体端口配置及重启tomcat服务。
如 8080端口冲突,则找到类似如下:
<Connector port="8080" protocol="HTTP/1.1"
connectionTimeout="20000"
redirectPort="8443" />
将port="8080"改为其它的不冲突的就可以了。
13.4.日志乱码问题
若在tomcat的应用日志中,打印中文日志有乱码,则需要在启动脚本($CATALINA_BASE /bin/catalina.sh
中)中增加如下配置,重启服务。
JAVA_OPTS="$JAVA_OPTS -Djavax.servlet.request.encoding=UTF-8 -Dfile.encoding=UTF-8 -Duser.language=zh_CN -Dsun.jnu.encoding=UTF-8"