工作笔记 - Tomcat多实例部署研究和实现

概述

笔者在工作过程中,遇到了一个Web应用程序的部署和运维的需求。

业务应用程序是一个Java语言和平台开发的Web应用,程序通过War包提供。运行环境所使用的应用服务器是Tomcat,数据库系统使用MySQL。系统部署后,需要修改相关的应用程序配置,来连接应用系统和数据库系统。此外,在前端还使用了nginx系统作为Web应用的入口和反向代理程序,来提供更好的系统安全性和可扩展性。这也是一种典型的Web应用和部署的模式。

原有的部署方案中,为了更加充分的利用当前的硬件系统资源,在应用程序所在主机环境中,还使用了多实例的部署方式。基本方法就是复制多个Tomcat文件夹,在每个文件夹中,修改使用不同的服务器配置(主要的外部提现在不同的侦听端口),并且在每个Tomcat实例文件夹中的应用文件夹中部署应用程序。最后,修改Nginx配置,来使用这些多个tomcat的实例的端口,作为后端应用程序的端口,来达到负载均衡的目的。

但是,这一方案的主要问题是应用程序的维护不是很方便。除了需要维护多个Tomcat实例之外,我们还需要管理和维护多个在不同实例中运行的应用程序实例。但其实在绝大多数场景中,这些应用程序的执行方式和配置信息都是一致的。在理想的情况下,我们认为应该可以"共享"这个应用程序在多个Tomcat实例环境中运行,这样就只需要维护一份应用程序代码就可以了,更加简单方便,并且能够提高程序的一致性,避免由于代码的差异,造成用户在使用时可能遇到的不一致的问题。

本文想要研究和探讨的重点,就是基于这个思路,在实际应用环境中的实现和操作过程,以及在此过程中可能遇到和需要注意的问题。作为一个比较高阶的讨论,本文假设读者已经具备一定的基础,比如熟悉和了解一般的Java Web应用程序的运行部署方式、相关的软件和相关操作,不会在这些基础内容方面展开说明,而是重点讨论多实例的配置和部署方面的内容。

优化多实例部署

根据前面部署工作优化的基本思路,本文提出具体实施方案如下:

将整个程序系统分为主运行环境和配置环境,其中主运行环境包括了Tomcat主程序和业务应用程序文件,配置环境包括了各个实例的配置相关信息和工作环境,然后通过设置不同的启动方式,来让不同的Tomcat实例在启动的时候,使用不同的配置和运行环境。

下面是这一思路的具体操作:

1. 准备Tomcat主环境

我们假设操作系统中,已经安装好了JDK和Java环境。我们这里用的是JDK1.8。Tomcat版本使用8.5.99。

和普通的Tomcat安装方式一样,我们从Tomcat的官方网站下载对应操作系统的二进制压缩版本。可以将其解压到一个文件夹中,并将其命名为 "tomcat_main" 作为Tomcat主执行环境。

在这一阶段的操作,和普通的Tomcat环境没有太大的区别。配置完成后的基本情况如下:

js 复制代码
yanjh@d10-tw:/webapp/tomcat_main$ java -version
openjdk version "1.8.0_442"
OpenJDK Runtime Environment (Temurin)(build 1.8.0_442-b06)
OpenJDK 64-Bit Server VM (Temurin)(build 25.442-b06, mixed mode)

yanjh@d10-tw:/webapp/tomcat_main$ tree -L 1
.
├── bin
├── BUILDING.txt
├── conf
├── CONTRIBUTING.md
├── lib
├── LICENSE
├── logs
├── NOTICE
├── README.md
├── RELEASE-NOTES
├── RUNNING.txt
├── temp
├── webapps
└── work

在实际工作时,基于这个主执行环境,通过启动时不同的环境变量,来区分启动时,使用何种配置方式和相关的工作文件夹,达到单主机多配置多实例的目的。这是下一阶段的主要工作。

2. Tomcat配置环境

在配置阶段,笔者创建了一个"tomcat_conf"文件夹,作为实例的主配置文件夹。在这个文件夹中,为各个实例创建对应的配置信息和工作子文件夹。为了方便区分,我们可以使用的服务端口号来区分各个实例配置。这些实例配置文件夹的示例如下:

js 复制代码
yanjh@d10-tw:/webapp$ tree tomcat_conf -L 2
tomcat_conf
├── 6080
│   ├── conf
│   ├── logs
│   ├── temp
│   ├── webapps -> /webapp/tomcat_main/webapps/
│   └── work
└── 6090
    ├── conf
    ├── logs
    ├── temp
    ├── webapps -> /webapp/tomcat_main/webapps/
    └── work

12 directories, 4 files

在笔者的项目中,初步规划了两个Tomcat实例,分别侦听6080和6090两个端口。所以创建了两个端口命名的子文件夹。在这些子文件夹中,conf是复制的Tomcat程序文件夹中的内容,后面我们需要修改这其中的配置文件和信息。其他的如logs、temp和work都是创建的空的用于应用程序的工作文件夹。

3 服务器配置

在配置信息文件夹中,需要为每个tomcat的示例,进行适配性的修改。默认情况下,修改的内容,就是服务器的应用监听端口和管理端口。这其中最重要的内容,就是server.xml配置文件,在其中可以配置服务端口和连接器端口。这些配置信息的示例如下:

js 复制代码
## server.xml (in 6080 folder)

<Server port="6085" shutdown="SHUTDOWN">

<Connector port="6080" protocol="HTTP/1.1"
...

其他的配置信息,可以根据需要进行配置,只需要注意对于程序运行需要保持一致就可以了。

4 业务程序目录

为了共享业务程序,需要将业务程序,都按照在业务程序文件夹中,这里使用的Tomcat的原生程序文件夹:webapps,其实可以也可以配置成为任意文件夹,但由于需要额外的设置,笔者就没有修改。而且为了方便起见,在配置信息文件夹中,创建了一个软连接,指向了原生程序文件夹。对于这些Tomcat实例而言,它们都使用同一份应用程序文件夹。

创建这个链接的命令示例如下:

js 复制代码
cd tomcat_conf/6080
ln -s /webapp/tomcat_main/webapps/ webapps

5 实例启动脚本

这一步是多实例应用实现的关键。

对于Tomcat实例的启动,需要在启动时,设置启动的环境参数,这里使用一个参数化的启动脚本来实现,当执行脚本程序时,脚本程序根据调用的参数,来决定如何配置Tomcat启动的环境变量,达到无冲突的启动多个实例副本的目的。

我们编写的start.sh启动程序脚本的具体内容如下:

js 复制代码
## 脚本内容: tomcat_conf/start.sh 

#!/bin/sh

if [ -z "$1"]; then
    echo "Foler Not Set"
    exit 1
fi

dir="/webapp/tomcat_conf/$1"

echo "Base Folder: $dir"

if [ ! -d "$dir" ]; then
   echo "Folder Not Exist"
   exit
fi

export CATALINA_HOME=/webapp/tomcat_main
export CATALINA_BASE=$dir

$CATALINA_HOME/bin/startup.sh

ss -lpnt | grep $1

## 调用方式
./start.sh 6080

脚本的核心比较简单直接,就是根据参数,来选择和配置CATALINA_HOME和CATALINA_BASE两个文件夹,作为Tomcat实例工作的环境,然后调用Tomcat的启动脚本来启动实例。这里没有修改任何Tomcat的原本的配置和脚本,同时为了程序的健壮性适应性,在启动前需要检查参数的可用性。

6 实例的关闭

在技术上,准确的关闭Tomcat实例进程,应当是找到这个运行实例的pid,然后通过kill指令关闭。但考虑到作为网络服务程序,这样操作过于生硬,所以Tomcat一般提供了服务端口(在server.xml中设置)来实现比较平滑的关闭,包括处理完当前的请求等等。

为了简化配置和维护工作,我们这里做了一个简单的约定,就是程序端口号+5,就是这个服务端口。比如这个实例的工作端口是6080,那么其对应的服务端口就是6085。这样我们就可以方便的编写脚本,找到实例对应的关闭端口来实现平滑的关闭操作了。

根据上述的构想,结合tomcat的标准关闭脚本,我们编写的实例关闭脚本的示例代码如下:

js 复制代码
## 实例关闭程序 tomcat_conf/stop.sh

#!/bin/sh

if [ -z "$1"]; then
    echo "Foler Not Set"
    exit 1
fi

dir="/webapp/tomcat_conf/$1"

echo "BaseFolder:$dir"

if [ ! -d "$dir" ]; then
   echo "Folder Not Exist"
   exit
fi

export CATALINA_HOME=/webapp/tomcat_main
export CATALINA_BASE=$dir

$CATALINA_HOME/bin/shutdown.sh -port $(($1+5))

ss -lpnt | grep $1

## 执行调用方式
./stop.sh 6080

7 部署检查

实例启动后,可以简单的使用ps和ss检查当前运行的实例和侦听的端口。

js 复制代码
yanjh@d10-tw:/webapp$ ps aux | grep tomcat

yanjh    19362  0.2 12.8 6605288 1038632 pts/1 Sl   Mar18   3:04 /usr/bin/java -Djava.util.logging.config.file=/webapp/tomcat_conf/6080/conf/logging.properties -Djava.util.logging.manager=org.apache.juli.ClassLoaderLogManager -Djdk.tls.ephemeralDHKeySize=2048 -Djava.protocol.handler.pkgs=org.apache.catalina.webresources -Dsun.io.useCanonCaches=false -Dorg.apache.catalina.security.SecurityListener.UMASK=0027 -Dignore.endorsed.dirs= -classpath /webapp/tomcat_main/bin/bootstrap.jar:/webapp/tomcat_main/bin/tomcat-juli.jar -Dcatalina.base=/webapp/tomcat_conf/6080 -Dcatalina.home=/webapp/tomcat_main -Djava.io.tmpdir=/webapp/tomcat_conf/6080/temp org.apache.catalina.startup.Bootstrap start
yanjh    19385  0.2 12.3 6605288 998444 pts/1  Sl   Mar18   3:06 /usr/bin/java -Djava.util.logging.config.file=/webapp/tomcat_conf/6090/conf/logging.properties -Djava.util.logging.manager=org.apache.juli.ClassLoaderLogManager -Djdk.tls.ephemeralDHKeySize=2048 -Djava.protocol.handler.pkgs=org.apache.catalina.webresources -Dsun.io.useCanonCaches=false -Dorg.apache.catalina.security.SecurityListener.UMASK=0027 -Dignore.endorsed.dirs= -classpath /webapp/tomcat_main/bin/bootstrap.jar:/webapp/tomcat_main/bin/tomcat-juli.jar -Dcatalina.base=/webapp/tomcat_conf/6090 -Dcatalina.home=/webapp/tomcat_main -Djava.io.tmpdir=/webapp/tomcat_conf/6090/temp org.apache.catalina.startup.Bootstrap start
yanjh    21567  0.0  0.0   6072   816 pts/1    S+   10:29   0:00 grep tomcat

yanjh@d10-tw:/webapp$ ss -lpnt

State       Recv-Q      Send-Q                  Local Address:Port             Peer Address:Port
LISTEN      0           80                            0.0.0.0:3306                  0.0.0.0:*
LISTEN      0           128                         127.0.0.1:6379                  0.0.0.0:*
LISTEN      0           128                           0.0.0.0:80                    0.0.0.0:*
LISTEN      0           128                           0.0.0.0:22                    0.0.0.0:*
LISTEN      0           100                                 *:6080                        *:*          users:(("java",pid=19362,fd=57))
LISTEN      0           1                  [::ffff:127.0.0.1]:6085                        *:*          users:(("java",pid=19362,fd=65))
LISTEN      0           100                                 *:6090                        *:*          users:(("java",pid=19385,fd=57))
LISTEN      0           128                             [::1]:6379                     [::]:*
LISTEN      0           1                  [::ffff:127.0.0.1]:6095                        *:*          users:(("java",pid=19385,fd=65))
LISTEN      0           128                              [::]:80                       [::]:*
LISTEN      0           128                              [::]:22

8 部署扩展

使用上述的部署模式,再在主机中进行扩展就是非常方便的。只需要再复制一遍实例配置文件夹,修改新的配置信息即可。甚至不用关心启动关闭方式和应用程序部署的问题。

应用程序部署

前面,我们已经看到了如何配置、启动和关闭多个Tomcat实例的方法。剩下的问题,就是应用程序本身的部署和配置了。这里笔者不确定在多实例的环境中,Tomcat还支持应用程序的自动热部署方式,而是采用了常规基于war包的手动部署方式。

当然,为了配合这个多实例的执行方式,我们还可以对执行过程进行简单的优化,比如编写一个部署脚本,来更方便的执行部署过程,示例如下:

js 复制代码
yanjh@d10-tw:/webapp$ cat deploy2.sh
#!/bin/bash

## 1 Check Deploay File
[[ -f backup/demo.war ]] ||exit 1

## 2 Stop Tomcat Instance 
tomcat_conf/stop.sh 6080
tomcat_conf/stop.sh 6090

## 3 Backup Old Application
mv -f  tomcat_main/webapps/demo backup/demo_$(date +%Y%m%d)

## 4 Make App Folder
mkdir tomcat_main/webapps/demo

## 5. Deploy App
unzip backup/demo.war -d tomcat_main/webapps/demo

## 6. Copy Config File
cp -av backup/*.properties tomcat_main/webapps/demo/WEB-INF/classes/config/

## 7 Start Tomcat Instance
tomcat_conf/start.sh 6080
tomcat_conf/start.sh 6090

在部署之前,将新的应用程序的war包和相关的配置信息(通常不变),复制到backup文件夹中,然后执行这个部署脚本,就可以将新版本的应用程序部署到tomcat执行环境当中了。

Nginx配置

作为一个完整的部署和应用方案,在前端,需要使用一个反向代理负载均衡的机制,才能够真正的实现多实例应用的方式。这通常通过安装配置Nginx来实现。

我们不讨论常规的Nginx安装配置的过程,主要专注于负载均衡相关的配置操作。下面是典型的配置方式:

nginx.conf 复制代码
        // http 区段
        
        upstream appserver {
                ip_hash;
                health_check;
                server 192.168.10.106:6070;
                server 192.168.10.106:6080;
                server 192.168.10.106:6090;
        }
        
        // server 区段
        location /demo {
                proxy_set_header Host $host;
                proxy_set_header X-Forwarded-Scheme  $scheme;
                proxy_set_header X-Real-IP $remote_addr;
                proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
                proxy_pass http://appserver;
        }

这里的要点如下:

  • 首先在http区段,定义一个 upstream 代理服务器,内容包括了后端提供真实服务的端点地址
  • 此处选择iphas的负载均衡逻辑
  • 设置了主动监控检查,自动管理下线的服务器
  • 在server区段,配置某一个端点和路径,使用前面配置的代理服务器

小结

本文作为一个工作笔记,讨论了一种tomcat和其应用的多实例部署的技术方案。包括基本原理,技术特点,操作步骤和需要注意的问题等。还进一步讨论了相关问题如应用程序的脚本化部署和nginx配置等内容。

相关推荐
吴生43962 分钟前
数据库ALGORITHM = INSTANT 特性研究过程
后端
小麟有点小靈6 分钟前
VSCode写java时常用的快捷键
java·vscode·编辑器
程序猿chen17 分钟前
JVM考古现场(十九):量子封神·用鸿蒙编译器重铸天道法则
java·jvm·git·后端·程序人生·java-ee·restful
Chandler2434 分钟前
Go:接口
开发语言·后端·golang
&白帝&36 分钟前
java HttpServletRequest 和 HttpServletResponse
java·开发语言
ErizJ36 分钟前
Golang|Channel 相关用法理解
开发语言·后端·golang
automan0237 分钟前
golang 在windows 系统的交叉编译
开发语言·后端·golang
Pandaconda37 分钟前
【新人系列】Golang 入门(十三):结构体 - 下
后端·golang·go·方法·结构体·后端开发·值传递
我是谁的程序员1 小时前
Flutter iOS真机调试报错弹窗:不受信任的开发者
后端
蓝宝石Kaze1 小时前
使用 Viper 读取配置文件
后端