Tomcat篇01-概念简介和守护进程配置

本文最后更新于:March 27, 2020 am

本文主要包括tomcat服务器的主要概念介绍、在systemd上的tomcat守护进程的配置、jsvc的原理介绍和systemd的并发实现原理介绍。

1、Tomcat简介

在了解tomcat之前我们需要了解一些基本的概念。

1.1 web应用

所谓Web应用,就是指需要通过编程来创建的Web站点。Web应用中不仅包括普通的静态HTML文档,还包含大量可被Web服务器动态执行的程序。用户在Internet上看到的能开展业务的各种Web站点都可看作Web应用,例如,网上商店和网上银行都是Web应用。此外,公司内部基于Web的Intranet工作平台也是Web应用。

Web应用与传统的桌面应用程序相比,具有以下特点:

  • 以浏览器作为展示客户端界面的窗口。
  • 客户端界面一律表现为网页形式,网页由HTML语言写成。
  • 客户端与服务器端能进行和业务相关的动态交互
  • 能完成与桌面应用程序类似的功能。
  • 使用浏览器—服务器架构(B/S),浏览器与服务器之间采用HTTP协议通信。
  • Web应用通过Web服务器来发布。

web应用的一大好处就是可以轻易地跨平台运行,不论是windows、mac、ios、android还是linux,只要安装了浏览器,一般都可以使用web应用,而浏览器在各个平台都是标配的软件,因此给web应用的普及提供了非常良好的条件。同样的,web应用使用的是B/S架构,即Browser/Server架构,主要的计算任务都交给Server端进行,因此都客户端的性能要求较低,同时也推动了服务端的负载均衡、高可用等技术的发展。

Context:在tomcat中一般指web应用

1.2 Servlet

Servlet(Server Applet),全称Java Servlet。是用Java编写的服务器端程序。其主要功能在于交互式地浏览和修改数据,生成动态Web内容。狭义的Servlet是指Java语言实现的一个接口,广义的Servlet是指任何实现了这个Servlet接口的类别,一般情况下,我们说的Servlet为后者。

Servlet运行于支持Java的应用服务器中。从实现上讲,Servlet可以响应任何类型的请求,但绝大多数情况下Servlet只用来扩展基于HTTP协议的Web服务器。也就是说Web服务器可以访问任意一个Web应用中所有实现Servlet接口的类。而Web应用中用于被Web服务器动态调用的程序代码位于Servlet接口的实现类中。既然servlet和java关系密切,那么servlet接口的标准制定毫无疑问也是由甲骨文公司来主导。

Servlet规范把能够发布和运行Java Web应用的Web服务器称为Servlet容器。Servlet容器最主要的特征是动态执行Java Web应用中Servlet实现类的程序代码。由Apache开源软件组织创建的Tomcat是一个符合Servlet规范的优秀Servlet容器。

1.3 jsp

JSP(全称JavaServer Pages)是由Sun Microsystems公司主导建立的一种动态网页技术标准。JSP是HttpServlet的扩展。JSP将Java代码和特定变动内容嵌入到静态的页面中,实现以静态页面为模板,动态生成其中的部分内容。JSP在首次被访问的时候被应用服务器转换为servlet,在以后的运行中,容器直接调用这个servlet,而不再访问JSP页面。JSP的实质仍然是servlet。

1.4 Tomcat

Tomcat
是在Oracle公司的JSWDK(JavaServer Web DevelopmentKit,是Oracle公司推出的小型Servlet/JSP调试工具)的基础上发展起来的一个优秀的Servlet容器,Tomcat本身完全用Java语言编写。作为一个开源软件,Tomcat除了运行稳定、可靠,并且效率高之外,还可以和目前大部分的主流Web服务器(如IIS、Apache、Nginx等)一起工作。

tomcat的版本实际上比较复杂,目前有7、8、9、10四个版本并行发布,具体的各个版本的兼容信息我们可以通过官网查询。

2、Tomcat安装配置

tomcat的配置安装需要先在系统上配置好jdk环境,这里我们使用centos7.7版本的Linux系统和jdk8版本。

2.1 配置jdk8

我们首先到官网下载JDK8的安装包,这里我们选择tar.gz格式的压缩包下载,需要注意建议先使用浏览器下载再使用工具传输到Linux上,因为下载需要登录注册账号。

接着我们解压将安装包解压到自己想要配置的jdk安装目录下,这里我们使用/home/目录

1
tar -zxvf jdk-8u241-linux-x64.tar.gz -C /home/

/etc/profile中添加以下三个参数并导入

1
2
3
4
JAVA_HOME=/home/jdk_1.8.0_241
CLASSPATH=%JAVA_HOME%/lib:%JAVA_HOME%/jre/lib
PATH=$PATH:$JAVA_HOME/bin:$JAVA_HOME/jre/bin
export JAVA_HOME CLASSPATH PATH

重新载入配置文件

1
source /etc/profile

检查配置是否生效,如不生效可以重启终端试试:

1
2
3
4
[root@tiny-yun ~]# java -version
java version "1.8.0_241"
Java(TM) SE Runtime Environment (build 1.8.0_241-b07)
Java HotSpot(TM) 64-Bit Server VM (build 25.241-b07, mixed mode)

2.2 配置tomcat

tomcat的安装配置和上面几乎一样,由于我们已经在/etc/profile中设定了全局的java环境变量,因此在tomcat中就不用再特殊配置,直接就会使用默认的全局变量。

这里我们还是使用官网
提供的tar.gz压缩包来安装。

1
2
3
4
5
6
# tomcat可以直接使用wget下载
wget https://downloads.apache.org/tomcat/tomcat-8/v8.5.53/bin/apache-tomcat-8.5.53.tar.gz
# 解压到安装目录并重命名
tar -zxvf apache-tomcat-8.5.53.tar.gz /home/
cd /home
mv apache-tomcat-8.5.53 tomcat-8.5.53

tomcat目录

首先我们来看一下tomcat中的主要目录:

  • /bin 存放用于启动及关闭的文件,以及其他一些脚本。其中,UNIX 系统专用的 *.sh 文件在功能上等同于 Windows 系统专用的 *.bat 文件。因为 Win32 的命令行缺乏某些功能,所以又额外地加入了一些文件。
  • /conf 配置文件及相关的 DTD。其中最重要的文件是 server.xml,这是容器的主配置文件。
  • /log 日志文件的默认目录。
  • /webapps 存放 Web 应用的相关文件。

接着我们进入tomcat目录下的bin目录就可以看到各种各样的脚本文件,主要分为batsh两类,其中bat主要是在windows系统上使用的,我们可以把它们删掉,接着我们执行一些version.sh这个脚本就可以看到版本信息。

接下来我们来看一下和tomcat相关的几个变量:

JRE_HOME

这里我们可以看到JRE_HOME这个变量是之前设置了的JAVA_HOME环境变量。

  • 如果同时定义了JRE_HOMEJAVA_HOME这两个变量,那么使用的是JRE_HOME
  • 如果只定义了JAVA_HOME,那么JRE_HOME变量值就是JAVA_HOME的变量值
  • 如果两个变量都没定义,那么tomcat无法运行

前面我们提到过tomcat是使用Java编写的,这也就意味着它在运行的时候需要创建一个JVM虚拟机,所以如果没定义JAVA环境变量,tomcat是无法运行的

CATALINA_HOME

tomcat安装目录的根目录

CATALINA_BASE

tomcat实例运行的目录,默认情况下等于CATALINA_HOME,如果我们需要在一台机器上运行多个tomcat实例,可以设置多个CATALINA_BASE

setenv.sh

这个脚本默认是不存在的,需要我们自己手动创建在bin目录下,在windows系统则应该是setenv.bat,我们在里面指定了JRE_HOME环境变量以及PID文件的位置,这样在运行的时候就能比较方便的定位到运行进程

注意前面提到的CATALINA_HOMECATALINA_BASE两个变量不能在这里设定,因为tomcat就是根据这两个变量来找到 setenv.sh的。

1
2
3
$ cat setenv.sh 
JRE_HOME=/home/jdk1.8.0_241/jre
CATALINA_PID="$CATALINA_BASE/tomcat.pid"

这时候运行./catalina.sh start或者是./startup.sh文件就可以启动tomcat,注意要在防火墙中放行默认的8080端口。如果没有指定PID文件的位置,在关闭tomcat的时候可能会出现错误。此外,一般不建议使用root用户来运行tomcat。

个人感觉使用catalina.sh加参数的方式来控制tomcat进程要更加灵活强大一些。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
$ ./catalina.sh -h
Using CATALINA_BASE: /home/tomcat-8.5.53
Using CATALINA_HOME: /home/tomcat-8.5.53
Using CATALINA_TMPDIR: /home/tomcat-8.5.53/temp
Using JRE_HOME: /home/jdk1.8.0_241/jre
Using CLASSPATH: /home/tomcat-8.5.53/bin/bootstrap.jar:/home/tomcat-8.5.53/bin/tomcat-juli.jar
Using CATALINA_PID: /home/tomcat-8.5.53/tomcat.pid
Usage: catalina.sh ( commands ... )
commands:
debug Start Catalina in a debugger
debug -security Debug Catalina with a security manager
jpda start Start Catalina under JPDA debugger
run Start Catalina in the current window
run -security Start in the current window with security manager
start Start Catalina in a separate window
start -security Start in a separate window with security manager
stop Stop Catalina, waiting up to 5 seconds for the process to end
stop n Stop Catalina, waiting up to n seconds for the process to end
stop -force Stop Catalina, wait up to 5 seconds and then use kill -KILL if still running
stop n -force Stop Catalina, wait up to n seconds and then use kill -KILL if still running
configtest Run a basic syntax check on server.xml - check exit code for result
version What version of tomcat are you running?
Note: Waiting for the process to end and use of the -force option require that $CATALINA_PID is defined

3、 jsvc配置daemon(守护进程)

在Windows上,tomcat会默认注册成系统服务,这样设置启动和运行都方便很多,而在Linux上,我们需要借助jsvc来实现这一效果。

3.1 什么是jsvc

Commons Daemon(共享守护进程),原名JSVC,是一个属于Apache的Commons项目的Java库。守护程序提供了一种启动和停止正在运行服务器端应用程序的Java虚拟机(JVM)的便携式方法。守护程序包括两部分:用C编写的操作系统接口的原生库 ,以及提供用Java编写的Daemon API的库。

有两种使用Commons守护程序的方法:直接调用实现守护程序接口(interface)或调用为守护程序提供所需方法(method)的类(class)。例如,Tomcat-4.1.x使用守护程序接口,而Tomcat-5.0.x提供了一个类,该类的方法直接由JSVC调用。

3.2 jsvc工作原理

jsvc使用了三个进程来工作:一个启动进程、一个控制进程、一个被控制进程。其中被控制进程一般来说就是java主线程(我们这里就是tomcat),如果JVM虚拟机崩溃了,那么控制进程会在下一分钟重启。因为jsvc是守护进程,所以它应该使用root用户来启动,同时我们可以使用-user参数来进行用户的降级(downgrade),即先使用root用户来创建进程,然后再降级到指定的非root用户而不丢失root用户的特殊权限,如监听1024以下的端口。

3.3 jsvc配置tomcat守护进程(daemon)

tomcat的二进制安装包中的bin目录下就有jsvc的安装包,我们需要使用GCC编译器对其进行编译安装。同时在编译的时候我们需要指定jdk的路径,由于我们前面已经手动指定了,这里不需要再指定。如果没有,可以使用./configure --with-java=$JAVA_HOME来进行操作。

1
2
3
4
5
6
7
8
9
10
11
# 首先我们进入tomcat的bin目录进行编译
cd $CATALINA_HOME/bin
tar xvfz commons-daemon-native.tar.gz
cd commons-daemon-1.2.2-native-src/unix
./configure
make
# 编译完成后,会在当前文件夹生成一个jsvc的文件,将它拷贝到tomcat的/bin/目录下
cp jsvc ../..
cd ../..
# 接着我们可以这样查看jsvc的帮助文档
./jsvc -help

使用jsvc来启动tomcat,我们使用下面的参数来进行启动

1
2
3
4
5
6
7
8
9
10
./jsvc \
-user tomcat \
-classpath $CATALINA_HOME/bin/bootstrap.jar:$CATALINA_HOME/bin/tomcat-juli.jar \
-outfile $CATALINA_BASE/logs/catalina.out \
-errfile $CATALINA_BASE/logs/catalina.err \
-Dcatalina.home=$CATALINA_HOME \
-Dcatalina.base=$CATALINA_BASE \
-Djava.util.logging.manager=org.apache.juli.ClassLoaderLogManager \
-Djava.util.logging.config.file=$CATALINA_BASE/conf/logging.properties \
org.apache.catalina.startup.Bootstrap

注意看这时的用户和PID,上面的12839的用户为root,也就是我们前面说的控制进程,后面被12839进程控制的12840进程才是我们主要运行的tomcat进程,而这里的用户也符合我们使用-user参数指定的tomcat用户。如果我们不指定进程的PID文件位置,那么默认就会在/var/run目录下生成PID文件,我们可以看到这个jsvc.pid对应的正好是jsvc运行的三个进程中的被控制进程。

如果需要关闭,我们可以使用下面的命令:

1
2
3
4
./jsvc -stop org.apache.catalina.startup.Bootstrap stop

# 还可以指定pid文件位置,如果前面没有使用默认的pid文件目录的话
./jsvc -stop -pidfile /var/run/jsvc.pid org.apache.catalina.startup.Bootstrap stop

这个时候可能就会有同学发现,前面不是说jsvc主要有三个进程来工作的吗,怎么这里只有两个进程呢?

我们在上面的启动命令的选项里面加入一个-wait 10的参数,然后启动之后迅速查看一下进程。

一般情况下,启动进程在启动了控制进程之后就会结束,而当我们使用了-wait参数之后,启动进程会等待被控制进程启动好了之后向其发送一个”I am ready”信号,启动进程在收到信号之后就会结束。-wait 10表示等待时间为10秒,需要注意等待时间要是10的倍数。

这时候可以看到存在三个jsvc相关的进程,等tomcat启动完之后再查看的时候我们就会发现最上面的19347号进程,也就是jsvc启动进程消失了。并且控制进程19350的父进程变成了1号进程。

我们再进一步查看以下进程的关系:

接着我们再来查看一下1号进程。可以发现,在centos7中的1号进程是systemd

接着我们可以总结以上的整个过程为下列步骤:

  1. 系统启动,0号进程启动,0号通过fork()生成1号进程systemd;
  2. 1号进程systemd通过fork()创建进程sshd,这就是我们使用的ssh服务的进程;
  3. 用户使用ssh远程登录系统,sshd进程创建了对应的终端进程pts;
  4. 用户在终端输入指令,pts根据系统中指定的该用户使用的shell(此处为bash shell)来执行对应的操作,这里具体表现为根据我们输入的指令来创建jsvc的启动进程;
  5. jsvc启动进程创建jsvc控制进程,并根据启动参数决定是否在等待jsvc控制进程的”I am ready”信号再结束,同时jsvc启动进程在结束之前会把jsvc控制进程交给1号进程systemd来管理控制;
  6. jsvc控制进程创建jsvc被控制进程,也就是我们的主要进程tomcat,同时jsvc控制进程会监视jsvc被控制进程,如果它崩溃了,jsvc控制进程则会重启,确保其正常运行;

这里使用jsvc来启动tomcat的好处就是启动完成了之后即使我们的shell终端关闭了也不会影响它的运行,当然如果我们直接使用tomcat的bin目录下的启动脚本来进行启动然后再送入后台运行也是可以达到这样的效果。实际上我们还可以通过编写systemd的unit单元配置文件,将tomcat注册成系统服务。

3.4 daemon.sh

同样的,在tomcat的bin目录下,集成了一个daemon.sh的脚本,用来调用jsvc从而实现tomcat的守护进程。daemon.sh的实现原理还是jsvc,只不过在脚本中加入了大量的变量判断和环境配置文件读取等操作

在官网上会建议我们直接把daemon.sh脚本复制到 /etc/init.d 目录下,就可以实现开机自动启动了。不过在CentOS7等使用了systemd的系统上,我个人更推荐使用systemd来管理。

4、systemd配置

这里先放上archwiki和fedoraproject官网上面的链接作为参考资料:

https://wiki.archlinux.org/index.php/Systemd

https://docs.fedoraproject.org/en-US/quick-docs/understanding-and-administering-systemd/index.html

4.1 systemd简介

systemd 是 Linux 下一个与 SysV 和 LSB 初始化脚本兼容的系统和服务管理器,是 Linux 系统中最新的初始化系统(init),它主要的设计目标是克服 sysvinit 固有的缺点,提高系统的启动速度。systemd 和 ubuntu 的 upstart 是竞争对手,不过现在ubuntu也使用了systemd。

systemd 使用 socket 和 D-Bus 来开启服务,提供基于守护进程(daemon)的按需启动策略,保留了 Linux cgroups 的进程追踪功能,支持快照和系统状态恢复,维护挂载和自挂载点,实现了各服务间基于从属关系的一个更为精细的逻辑控制,拥有前卫的并行性能。systemd 无需经过任何修改便可以替代 sysvinit 。

systemd 开启和监督整个系统是基于 unit 的概念。unit 是由一个与配置文件对应的名字和类型组成的(例如:avahi.service unit 有一个具有相同名字的配置文件,是守护进程 Avahi 的一个封装单元)。一个unit单元配置文件可以描述的内容有:系统服务(.service)、挂载点(.mount)、sockets(.sockets) 、系统设备(.device)、交换分区(.swap)、文件路径(.path)、启动目标(.target)、由 systemd 管理的计时器(.timer)。

  • service :守护进程的启动、停止、重启和重载是此类 unit 中最为明显的几个类型。
  • socket :此类 unit 封装系统和互联网中的一个 socket 。当下,systemd 支持流式、数据报和连续包的 AF_INET、AF_INET6、AF_UNIX socket 。也支持传统的 FIFO(先进先出) 传输模式。每一个 socket unit 都有一个相应的服务 unit 。相应的服务在第一个连接(connection)进入 socket 或 FIFO 时就会启动(例如:nscd.socket 在有新连接后便启动 nscd.service)。
  • device :此类 unit 封装一个存在于 Linux 设备树中的设备。每一个使用 udev 规则标记的设备都将会在 systemd 中作为一个设备 unit 出现。udev 的属性设置可以作为配置设备 unit 依赖关系的配置源。
  • mount :此类 unit 封装系统结构层次中的一个挂载点。
  • automount :此类 unit 封装系统结构层次中的一个自挂载点。每一个自挂载 unit 对应一个已挂载的挂载 unit (需要在自挂载目录可以存取的情况下尽早挂载)。
  • target :此类 unit 为其他 unit 进行逻辑分组。它们本身实际上并不做什么,只是引用其他 unit 而已。这样便可以对 unit 做一个统一的控制。(例如:multi-user.target 相当于在传统使用 SysV 的系统中运行级别5,即GUI图形化界面);bluetooth.target 只有在蓝牙适配器可用的情况下才调用与蓝牙相关的服务,如:bluetooth 守护进程、obex 守护进程等)
  • snapshot :与 target unit 相似,快照本身不做什么,唯一的目的就是引用其他 unit 。

systemd的unit文件可以从多个地方加载,使用systemctl show --property=UnitPath 可以按优先级从低到高显示加载目录。

主要的unit文件在下面的两个目录中:

  • /usr/lib/systemd/system/ :软件包安装的单元
  • /etc/systemd/system/ :系统管理员安装的单元

4.2 systemd原理

这里我们重点分析一下systemd的并行操作性能以及service服务的配置单元。

和前任的sysvinit的完全串行相比,systemd为了加速整个系统启动,实现了几乎所有的进程都并行启动(包括需要上下进程依赖的进程也并行启动)。想要实现这一点,主要需要解决三个方面的依赖问题:socket、D-Bus和文件系统。

socket 依赖(inetd)

绝大多数的服务依赖是套接字依赖。比如服务 A 通过一个套接字端口 S1 提供自己的服务,其他的服务如果需要服务 A,则需要连接 S1。因此如果服务 A 尚未启动,S1 就不存在,其他的服务就会得到启动错误。

所以传统地,人们需要先启动服务 A,等待它进入就绪状态,再启动其他需要它的服务。

systemd 认为,只要我们预先把套接字端口S1建立好,那么其他所有的服务就可以同时启动而无需等待服务 A来创建套接字端口S1了。如果服务 A 尚未启动,那么其他进程向套接字端口S1发送的服务请求实际上会被 Linux 操作系统缓存,其他进程会在这个请求的地方等待(这里使用FIFO方式)。一旦服务A启动就绪,就可以立即处理缓存的请求,一切都开始正常运行。

那么服务如何使用由 init 进程创建的套接字呢?

Linux 操作系统有一个特性,当进程调用fork或者exec创建子进程之后,所有在父进程中被打开的文件句柄 (file descriptor) 都被子进程所继承。套接字也是一种文件句柄,进程A可以创建一个套接字,此后当进程 A调用 exec 启动一个新的子进程时,只要确保该套接字的close_on_exec标志位被清空,那么新的子进程就可以继承这个套接字。子进程看到的套接字和父进程创建的套接字是同一个系统套接字,就仿佛这个套接字是子进程自己创建的一样,没有任何区别。

这个特性以前被一个叫做inetd的系统服务所利用。Inetd进程会负责监控一些常用套接字端口,比如 ssh,当该端口有连接请求时,inetd才启动telnetd进程,并把有连接的套接字传递给新的telnetd进程进行处理。这样,当系统没有 ssh 客户端连接时,就不需要启动 sshd 进程。Inetd 可以代理很多的网络服务,这样就可以节约很多的系统负载和内存资源,只有当有真正的连接请求时才启动相应服务,并把套接字传递给相应的服务进程。

和 inetd 类似,systemd(1号进程)是所有其他进程的父进程,它可以先建立所有需要的套接字,然后在调用 exec 的时候将该套接字传递给新的服务进程,而新进程直接使用该套接字进行服务即可。

D-Bus 依赖(bus activation)

D-Bus 是 desktop-bus 的简称,是一个低延迟、低开销、高可用性的进程间通信机制。它越来越多地用于应用程序之间通信,也用于应用程序和操作系统内核之间的通信。很多现代的服务进程都使用D-Bus 取代套接字作为进程间通信机制,对外提供服务。

Linux的 NetworkManager 服务就使用 D-Bus 和其他的应用程序或者服务进行交互:Linux上常见的邮件客户端软件 evolution 可以通过 D-Bus 从 NetworkManager 服务获取网络状态的改变,以便做出相应的处理。

D-Bus 支持所谓"bus activation"功能。如果服务 A 需要使用服务 B 的 D-Bus 服务,而服务 B 并没有运行,则 D-Bus 可以在服务 A 请求服务 B 的 D-Bus 时自动启动服务 B。而服务 A 发出的请求会被 D-Bus 缓存,服务 A 会等待服务 B 启动就绪。利用这个特性,依赖 D-Bus 的服务就可以实现并行启动。

文件系统依赖(automounter)

系统启动过程中,文件系统相关的活动是最耗时的,比如挂载文件系统,对文件系统进行磁盘检查(fsck),磁盘配额检查等都是非常耗时的操作。在等待这些工作完成的同时,系统处于空闲状态。那些想使用文件系统的服务似乎必须等待文件系统初始化完成才可以启动。但是 systemd 发现这种依赖也是可以避免的。

systemd 参考了 autofs 的设计思路,使得依赖文件系统的服务和文件系统本身初始化两者可以并行工作。autofs 可以监测到某个文件系统挂载点真正被访问到的时候才触发挂载操作,这是通过内核 automounter 模块的支持而实现的。systemd 集成了autofs的实现,对于系统中的挂载点,比如/home,当系统启动的时候,systemd 为其创建一个临时的自动挂载点。在这个时刻/home 真正的挂载设备尚未启动好,真正的挂载操作还没有执行,文件系统检测也还没有完成。可是那些依赖该目录的进程已经可以并发启动,他们的 open()操作被内建在 systemd 中的 autofs 捕获,将该 open()调用挂起(可中断睡眠状态)。然后等待真正的挂载操作完成,文件系统检测也完成后,systemd 将该自动挂载点替换为真正的挂载点,并让 open()调用返回。由此,实现了那些依赖于文件系统的服务和文件系统本身同时并发启动。

对于/根目录的依赖实际上一定还是要串行执行,因为 systemd 自己也存放在/根目录之下,必须等待系统根目录挂载检查好。

不过对于类似/home等挂载点,这种并发可以提高系统的启动速度,尤其是当/home是远程的 NFS 节点,或者是加密盘等,需要耗费较长的时间才可以准备就绪的情况下,因为并发启动,这段时间内,系统并不是完全无事可做,而是可以利用这段空余时间做更多的启动进程的事情,总的来说就缩短了系统启动时间。

总结

从上面的三个办法我们可以看出,systemd让多个程序并行启动的解决思路就是先创建一个虚拟点,让各类需要依赖的服务先运行起来,最后再把虚拟点换成实际的服务使得能够正常运行。

4.3 systemd实现tomcat的daemon进程

我们在/usr/lib/systemd/system/目录下新建一个tomcat9.service文件,接下来我们可以使用systemctl命令来进行控制:

  • 使用 systemctl 控制单元时,通常需要使用unit文件的全名,包括扩展名(例如 sshd.service )。但是有些unit可以在 systemctl 中使用简写方式。

  • 如果无扩展名,systemctl 默认把扩展名当作 .service 。例如 tomcat 和 tomcat.service 是等价的。

  • 挂载点会自动转化为相应的 .mount 单元。例如 /home 等价于 home.mount

  • 设备会自动转化为相应的 .device 单元,所以 /dev/sda1 等价于 dev-sda1.device

使用daemon.sh

首先我们尝试在systemd中使用自带的脚本进行启动和关闭tomcat,这里我们先把startup.shshutdown.sh两个脚本给排除掉,虽然它们无法启动守护进程的缺陷可以使用systemd来进行弥补,但是还是无法使用jsvc,无法在特权端口和运行用户之间取得两全,我们直接使用daemon.sh来运行。

需要注意的是,systemd并不会去读取我们先前在/etc/profile中设定的变量,因此我们直接把变量写进unit配置文件中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
[Unit]
Description=Apache Tomcat 9

[Service]
User=tomcat
Group=tomcat
PIDFile=/var/run/tomcat.pid
Environment=JAVA_HOME=/home/jdk8/
Environment=JRE_HOME=/home/jdk8/jre
Environment=CLASSPATH=%JAVA_HOME%/lib:%JAVA_HOME%/jre/lib
Environment=CATALINA_HOME=/home/tomcat9
Environment=CATALINA_BASE=/home/tomcat9
Environment=CATALINA_TMPDIR=/home/tomcat9/temp
ExecStart=/home/tomcat9/bin/daemon.sh start
ExecStop=/home/tomcat9/bin/daemon.sh stop

[Install]
WantedBy=multi-user.target

添加了新的unit单元之后我们先systemctl daemon-reload重启一下daemon进程,再使用systemctl start tomcat9.service来启动服务,接着查看状态,发现无法正常运行,一启动进程就failed掉了,查看daemon脚本默认的日志文件(位于tomcat目录下的logs/catalina-daemon.out)我们发现返回了143错误。

1
Service exit with a return value of 143

网上搜索了一下,有个解决方案是把daemon.sh脚本中的wait参数时间从10调成240,在125行左右的位置:

1
2
# Set the default service-start wait time if necessary
test ".$SERVICE_START_WAIT_TIME" = . && SERVICE_START_WAIT_TIME=10

wait参数调大之后,等待启动成功之后(这里用的主机配置很低,启动比较耗时)就可以正常访问了

但是在四分钟(240s)之后我们再查看tomcat9.service就会发现,进程已经结束了,再次访问默认的8080端口也无法访问,查找进程也没有找到相关的进程。

试图分析一波

我们来根据上面的情况结合原理来试图分析一下:

首先我们可以看到-wait参数时长调到240之后,bash shell进程的生命周期延长了,根据之前的jsvc工作原理部分我们可以知道-wait参数会影响jsvc的启动进程的生命周期,而从systemd输出的信息来看,有包括jsvc三个进程和bash shell进程在内共计四个进程,这和之前我们直接运行daemon.sh之后最终只有jsvc的两个进程(控制进程和被控制进程不同),且Main PID参数指向的是bash shell进程。

于是乎我们大胆猜测一下:使用daemon.sh start命令启动tomcat,systemd会把启动daemon.sh的bash的PID作为整个service的PID来监控,而这个bash进程在启动了jsvc之后是会自行退出的,这也就导致了systemd认为service已经运行失败,从而清理掉了关联的进程,进而使得jsvc相关的tomcat进程也被清理掉了。而-wait参数时长调到240之后,bash shell进程的存活时间变长,我们就能在tomcat启动完成之后且bash shell进程结束之前访问到tomcat服务器。

考虑到这种情况,我们可以试一下使用daemon.sh run来启动tomcat,因为在终端中使用run参数的时候会一直把log信息输出到终端,我猜测这个运行方式是和start不太一样的。

把systemd的unit文件的启动参数改为run,同时将-wait参数时长调回默认的10,再次启动服务。

这次我们可以看到systemd的Main PID对应为jsvc的主进程,tomcat服务也能一直正常的在后台运行。应该算是成功的使用systemd来管理jsvc启动的tomcat进程了。

那么这两者的区别在哪里呢?接着我们打开daemon.sh这个脚本来查看一下两者的不同:

从图中我们可以看到两者最大的不同就是使用run命令的时候是exec调用jsvc来启动tomcat并且使用了-nodetach参数。

shell中的exec命令和直接调用不同,命令exec将并不启动新的shell,而是用要被执行命令替换当前的shell进程,并且将老进程的环境清理掉,而且exec命令后的其它命令将不再执行。

也就是说,run命令使用exec调用了jsvc,是直接替代原来启动daemon.sh的bash shell进程,并且在这个exec命令执行完之后才会执行后面的exit命令。这样就可以让systemd的Main PID从bash shell进程顺理成章地变为jsvc的启动进程。

那么我们知道,jsvc的启动进程在启动完jsvc控制进程之后还是会退出的,这个时候systemd还是会监听失败。而-nodetach参数的作用就是不脱离父进程而成为守护进程( don’t detach from parent process and become a daemon),这样就能顺利地使得jsvc控制进程从它的父进程jsvc启动进程那里“得到”systemd的Main PID的位置,成为该service的主要进程。

我们直接在终端中运行jsvc并加上-nodetach参数,可以看到即使是运行成功了之后也不会退出(控制进程继承了启动进程成为守护进程一直运行),而没加的情况下则是jsvc启动进程退出后就会退出。

这里再放上systemd使用daemon.sh启动tomcat的整个unit文件的配置及注释:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
[Unit]
Description=Apache Tomcat 9
# 对整个serive的描述,相当于备注,会出现在systemd的log中
After=network.target
# 在network服务启动之后再启动

[Service]
User=tomcat
Group=tomcat
# 运行该service的用户及用户组

PIDFile=/var/run/tomcat.pid
# 该service的PID文件

Environment=JAVA_HOME=/home/jdk8/
Environment=JRE_HOME=/home/jdk8/jre
Environment=CLASSPATH=%JAVA_HOME%/lib:%JAVA_HOME%/jre/lib
Environment=CATALINA_HOME=/home/tomcat9
Environment=CATALINA_BASE=/home/tomcat9
Environment=CATALINA_TMPDIR=/home/tomcat9/temp
# 定义了运行时需要的变量

ExecStart=/home/tomcat9/bin/daemon.sh start
ExecStop=/home/tomcat9/bin/daemon.sh stop
# 对应systemd控制的start和stop命令

[Install]
WantedBy=multi-user.target
# 运行级别为第三级(带有网络的多用户模式)

直接使用jsvc

既然搞清楚了运行原理,我们也就可以跳过脚本直接在unit文件中定义各种参数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
[Unit]
Description=Apache Tomcat 9
After=network.target

[Service]
User=root
Group=root
# 这里使用root用户启动方便jsvc监听特权端口
# 后面可以在jsvc参数中使用-user降权到tomcat用户

PIDFile=/var/run/tomcat.pid

Environment=JAVA_HOME=/home/jdk8/
Environment=JRE_HOME=/home/jdk8/jre
Environment=CLASSPATH=%JAVA_HOME%/lib:%JAVA_HOME%/jre/lib
Environment=CATALINA_HOME=/home/tomcat9
Environment=CATALINA_BASE=/home/tomcat9
Environment=CATALINA_TMPDIR=/home/tomcat9/temp

ExecStart=/home/tomcat9/bin/jsvc \
-user tomcat \
-nodetach \
-java-home ${JAVA_HOME} \
-pidfile ${CATALINA_BASE}/tomcat.pid \
-classpath ${CATALINA_HOME}/bin/bootstrap.jar:${CATALINA_HOME}/bin/tomcat-juli.jar \
-outfile ${CATALINA_BASE}/logs/catalina.out \
-errfile ${CATALINA_BASE}/logs/catalina.err \
-Dcatalina.home=${CATALINA_HOME} \
-Dcatalina.base=${CATALINA_BASE} \
-Djava.io.tmpdir=${CATALINA_TMPDIR} \
-Djava.util.logging.manager=org.apache.juli.ClassLoaderLogManager \
-Djava.util.logging.config.file=${CATALINA_BASE}/conf/logging.properties \
org.apache.catalina.startup.Bootstrap

ExecStop=/home/tomcat9/bin/jsvc \
-stop \
-classpath ${CLASSPATH} \
-Dcatalina.base=${CATALINA_BASE} \
-Dcatalina.home=${CATALINA_HOME} \
-pidfile ${CATALINA_BASE}/tomcat.pid \
-Djava.io.tmpdir=${CATALINA_TMPDIR} \
-Djava.util.logging.config.file=${CATALINA_BASE}/conf/logging.properties \
-Djava.util.logging.manager=org.apache.juli.ClassLoaderLogManager \
org.apache.catalina.startup.Bootstrap

[Install]
WantedBy=multi-user.target

注意:ExecStart和ExecStop两个命令中的执行文件路径需要使用绝对路径