tomcat入门指北
本文最后更新于:March 3, 2021 pm
本文主要是偏向运维角度来对tomcat进行入门介绍,重点讲解了tomcat的基本概念、基本配置和tomcat的I/O模型。是之前的tomcat篇的汇总整理之作。
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的目录结构
我们先来看一下tomcat8.5和tomcat9中的home目录中的文件:
可以看到除掉一些说明文件之后,还有7个目录:
目录名 | 用途 |
---|---|
bin | 存放用于启动及关闭的文件,以及其他一些脚本。其中,UNIX 系统专用的 *.sh 文件在功能上等同于 windows 系统专用的 *.bat 文件。因为 Win32 的命令行缺乏某些功能,所以又额外地加入了一些文件 |
conf | 配置文件及相关的 DTD(document type definition 文档类型定义,DTD文件一般和XML文件配合使用,主要是为了约束XML文件)。其中最重要的文件是 server.xml,这是容器的主配置文件 |
lib | 存放tomcat服务器自身和所有的web应用都可以访问的JAR文件 |
logs | 日志文件的默认目录 |
temp | 存放临时文件的默认目录 |
webapps | 在tomcat上发布Java web应用的时候,默认把web应用的文件存放在这个目录 |
work | tomcat的工作目录,tomcat把运行时生成的一些工作文件存放在这个目录,如默认情况下tomcat会把编译JSP生成的Servlet类文件存放在这里 |
实际上除了主目录里有lib目录,在webapps目录下的web应用中的WEB-INF目录下也存在一个lib目录:
两者的区别在于:
● Tomcat主目录下的lib目录:存放的JAR文件不仅能被Tomcat访问,还能被所有在Tomcat中发布的Java Web应用访问
● webapps目录下的Java Web应用的lib目录:存放的JAR文件只能被当前Java Web应用访问
既然有多个lib目录,那么肯定就有使用的优先顺序,Tomcat类加载器的目录加载优先顺序如下:
Tomcat的类加载器负责为Tomcat本身以及Java Web应用加载相关的类。假如Tomcat的类加载器要为一个Java Web应用加载一个类,类加载器会按照以下优先顺序到各个目录中去查找该类的.class文件,直到找到为止,如果所有目录中都不存在该类的.class文件,则会抛出异常:
- 在Java Web应用的
WEB-INF/classes
目录下查找该类的.class文件 - 在Java Web应用的
WEB-INF/lib
目录下的JAR文件中查找该类的.class文件 - 在Tomcat的
lib
子目录下直接查找该类的.class文件 - 在Tomcat的
lib
子目录下的JAR文件中查找该类的.class文件
3、Tomcat安装配置
tomcat的配置安装需要先在系统上配置好jdk环境,这里我们使用centos7.7
版本的Linux系统和jdk8
版本。
3.1 配置jdk8
我们首先到官网下载JDK8的安装包,这里我们选择tar.gz
格式的压缩包下载,需要注意建议先使用浏览器下载再使用工具传输到Linux上,因为下载需要登录注册账号。
接着我们解压将安装包解压到自己想要配置的jdk安装目录下,这里我们使用/home/目录
1 |
|
在/etc/profile
中添加以下三个参数并导入
1 |
|
重新载入配置文件
1 |
|
检查配置是否生效,如不生效可以重启终端试试:
1 |
|
3.2 配置tomcat
tomcat的安装配置和上面几乎一样,由于我们已经在/etc/profile
中设定了全局的java环境变量,因此在tomcat中就不用再特殊配置,直接就会使用默认的全局变量。
这里我们还是使用官网
提供的tar.gz
压缩包来安装。
1 |
|
首先我们来看一下tomcat中的主要目录:
- /bin 存放用于启动及关闭的文件,以及其他一些脚本。其中,UNIX 系统专用的
*.sh
文件在功能上等同于 Windows 系统专用的*.bat
文件。因为 Win32 的命令行缺乏某些功能,所以又额外地加入了一些文件。 - /conf 配置文件及相关的 DTD。其中最重要的文件是 server.xml,这是容器的主配置文件。
- /log 日志文件的默认目录。
- /webapps 存放 Web 应用的相关文件。
接着我们进入tomcat目录下的bin
目录就可以看到各种各样的脚本文件,主要分为bat
和sh
两类,其中bat主要是在windows系统上使用的,我们可以把它们删掉,接着我们执行一些version.sh
这个脚本就可以看到版本信息。
接下来我们来看一下和tomcat相关的几个变量:
JRE_HOME
这里我们可以看到
JRE_HOME
这个变量是之前设置了的JAVA_HOME
环境变量。- 如果同时定义了
JRE_HOME
和JAVA_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_HOME
和CATALINA_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。
3.3 manager应用
tomcat本身内置了两个web应用,专门用来管理tomcat,它们分别是host-manager(管理virtual host)和manager(管理web应用)。
1 |
|
在启动tomcat之后,我们访问上面的这两个网址可以发现被403了。因为我们还没有在配置文件中增加相关的用户,为了保证安全,这里的用户默认都是禁用的,我们需要自己创建。
我们编辑tomcat目录下的conf子目录中的tomcat-users.xml
,添加对应的配置即可:
1 |
|
Users with the
admin-gui
role should not be granted theadmin-script
role.注意被授予
admin-gui
权限的用户不应该授予admin-script
权限Users with the
manager-gui
role should not be granted either themanager-script
ormanager-jmx
roles.注意被授予
manager-gui
权限的用户不应该授予manager-script
或manager-jmx
权限
tomcat9中默认是只允许部署tomcat的机器访问manger和host-manager的页面的,因此我们需要修改tomcat目录下对应的web应用的配置文件:
1 |
|
然后修改里面限制的IP地址为全部或者自己的IP地址即可。
1 |
|
然后我们就可以访问web界面来查看tomcat服务器的运行状态了。
- manager的web界面
- host-manager的web界面
3.4 Tomcat的守护进程(jsvc)
在Windows上,tomcat会默认注册成系统服务,这样设置启动和运行都方便很多,而在Linux上,我们需要借助jsvc来实现这一效果。
3.4.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.4.2 jsvc工作原理
jsvc使用了三个进程来工作:一个启动进程、一个控制进程、一个被控制进程。其中被控制进程一般来说就是java主线程(我们这里就是tomcat),如果JVM虚拟机崩溃了,那么控制进程会在下一分钟重启。因为jsvc是守护进程,所以它应该使用root用户来启动,同时我们可以使用-user参数来进行用户的降级(downgrade),即先使用root用户来创建进程,然后再降级到指定的非root用户而不丢失root用户的特殊权限,如监听1024以下的端口。
3.4.3 jsvc配置tomcat守护进程(daemon)
tomcat的二进制安装包中的bin目录下就有jsvc的安装包,我们需要使用GCC编译器对其进行编译安装。同时在编译的时候我们需要指定jdk的路径,由于我们前面已经手动指定了,这里不需要再指定。如果没有,可以使用./configure --with-java=$JAVA_HOME
来进行操作。
1 |
|
使用jsvc来启动tomcat,我们使用下面的参数来进行启动
1 |
|
注意看这时的用户和PID,上面的12839的用户为root,也就是我们前面说的控制进程,后面被12839进程控制的12840进程才是我们主要运行的tomcat进程,而这里的用户也符合我们使用-user参数指定的tomcat用户。如果我们不指定进程的PID文件位置,那么默认就会在/var/run目录下生成PID文件,我们可以看到这个jsvc.pid对应的正好是jsvc运行的三个进程中的被控制进程。
如果需要关闭,我们可以使用下面的命令:
1 |
|
这个时候可能就会有同学发现,前面不是说jsvc主要有三个进程来工作的吗,怎么这里只有两个进程呢?
我们在上面的启动命令的选项里面加入一个-wait 10
的参数,然后启动之后迅速查看一下进程。
一般情况下,启动进程在启动了控制进程之后就会结束,而当我们使用了
-wait
参数之后,启动进程会等待被控制进程启动好了之后向其发送一个”I am ready”信号,启动进程在收到信号之后就会结束。-wait 10
表示等待时间为10秒,需要注意等待时间要是10的倍数。
这时候可以看到存在三个jsvc相关的进程,等tomcat启动完之后再查看的时候我们就会发现最上面的19347号进程,也就是jsvc启动进程消失了。并且控制进程19350的父进程变成了1号进程。
我们再进一步查看以下进程的关系:
接着我们再来查看一下1号进程。可以发现,在centos7中的1号进程是systemd
。
接着我们可以总结以上的整个过程为下列步骤:
- 系统启动,0号进程启动,0号通过fork()生成1号进程systemd;
- 1号进程systemd通过fork()创建进程sshd,这就是我们使用的ssh服务的进程;
- 用户使用ssh远程登录系统,sshd进程创建了对应的终端进程pts;
- 用户在终端输入指令,pts根据系统中指定的该用户使用的shell(此处为bash shell)来执行对应的操作,这里具体表现为根据我们输入的指令来创建jsvc的启动进程;
- jsvc启动进程创建jsvc控制进程,并根据启动参数决定是否在等待jsvc控制进程的”I am ready”信号再结束,同时jsvc启动进程在结束之前会把jsvc控制进程交给1号进程systemd来管理控制;
- jsvc控制进程创建jsvc被控制进程,也就是我们的主要进程tomcat,同时jsvc控制进程会监视jsvc被控制进程,如果它崩溃了,jsvc控制进程则会重启,确保其正常运行;
这里使用jsvc来启动tomcat的好处就是启动完成了之后即使我们的shell终端关闭了也不会影响它的运行,当然如果我们直接使用tomcat的bin目录下的启动脚本来进行启动然后再送入后台运行也是可以达到这样的效果。实际上我们还可以通过编写systemd的unit单元配置文件,将tomcat注册成系统服务。
3.4.4 daemon.sh
同样的,在tomcat的bin目录下,集成了一个daemon.sh的脚本,用来调用jsvc从而实现tomcat的守护进程。daemon.sh的实现原理还是jsvc,只不过在脚本中加入了大量的变量判断和环境配置文件读取等操作
在官网上会建议我们直接把daemon.sh脚本复制到 /etc/init.d
目录下,就可以实现开机自动启动了。不过在CentOS7等使用了systemd的系统上,我个人更推荐使用systemd来管理。
3.5 Tomcat的守护进程(systemd+jsvc)
这里先放上archwiki和fedoraproject官网上面的链接作为参考资料:
https://wiki.archlinux.org/index.php/Systemd
https://docs.fedoraproject.org/en-US/quick-docs/understanding-and-administering-systemd/index.html
3.5.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/
:系统管理员安装的单元
3.5.2 systemd原理
这里我们重点分析一下systemd的并行操作性能以及service服务的配置单元。
和前任的sysvinit的完全串行相比,systemd为了加速整个系统启动,实现了几乎所有的进程都并行启动(包括需要上下进程依赖的进程也并行启动)。想要实现这一点,主要需要解决三个方面的依赖问题:socket、D-Bus和文件系统。
3.5.2.1 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 的时候将该套接字传递给新的服务进程,而新进程直接使用该套接字进行服务即可。
3.5.2.2 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 的服务就可以实现并行启动。
3.5.2.3 文件系统依赖(automounter)
系统启动过程中,文件系统相关的活动是最耗时的,比如挂载文件系统,对文件系统进行磁盘检查(fsck),磁盘配额检查等都是非常耗时的操作。在等待这些工作完成的同时,系统处于空闲状态。那些想使用文件系统的服务似乎必须等待文件系统初始化完成才可以启动。但是 systemd 发现这种依赖也是可以避免的。
systemd 参考了 autofs 的设计思路,使得依赖文件系统的服务和文件系统本身初始化两者可以并行工作。autofs 可以监测到某个文件系统挂载点真正被访问到的时候才触发挂载操作,这是通过内核 automounter
模块的支持而实现的。systemd 集成了autofs的实现,对于系统中的挂载点,比如/home
,当系统启动的时候,systemd 为其创建一个临时的自动挂载点。在这个时刻/home
真正的挂载设备尚未启动好,真正的挂载操作还没有执行,文件系统检测也还没有完成。可是那些依赖该目录的进程已经可以并发启动,他们的 open()
操作被内建在 systemd 中的 autofs
捕获,将该 open()
调用挂起(可中断睡眠状态)。然后等待真正的挂载操作完成,文件系统检测也完成后,systemd 将该自动挂载点替换为真正的挂载点,并让 open()
调用返回。由此,实现了那些依赖于文件系统的服务和文件系统本身同时并发启动。
对于/
根目录的依赖实际上一定还是要串行执行,因为 systemd 自己也存放在/
根目录之下,必须等待系统根目录挂载检查好。
不过对于类似
/home
等挂载点,这种并发可以提高系统的启动速度,尤其是当/home
是远程的 NFS 节点,或者是加密盘等,需要耗费较长的时间才可以准备就绪的情况下,因为并发启动,这段时间内,系统并不是完全无事可做,而是可以利用这段空余时间做更多的启动进程的事情,总的来说就缩短了系统启动时间。
3.5.2.4 总结
从上面的三个办法我们可以看出,systemd让多个程序并行启动的解决思路就是先创建一个虚拟点,让各类需要依赖的服务先运行起来,最后再把虚拟点换成实际的服务使得能够正常运行。
3.5.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
。
3.5.3.1 使用daemon.sh
首先我们尝试在systemd中使用自带的脚本进行启动和关闭tomcat,这里我们先把startup.sh
和shutdown.sh
两个脚本给排除掉,虽然它们无法启动守护进程的缺陷可以使用systemd来进行弥补,但是还是无法使用jsvc,无法在特权端口和运行用户之间取得两全,我们直接使用daemon.sh
来运行。
需要注意的是,systemd并不会去读取我们先前在/etc/profile中设定的变量,因此我们直接把变量写进unit配置文件中。
1 |
|
添加了新的unit单元之后我们先systemctl daemon-reload
重启一下daemon进程,再使用systemctl start tomcat9.service
来启动服务,接着查看状态,发现无法正常运行,一启动进程就failed掉了,查看daemon脚本默认的日志文件(位于tomcat目录下的logs/catalina-daemon.out
)我们发现返回了143错误。
1 |
|
网上搜索了一下,有个解决方案是把daemon.sh脚本中的wait参数时间从10调成240,在125行左右的位置:
1 |
|
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 |
|
3.5.3.2 直接使用jsvc
既然搞清楚了运行原理,我们也就可以跳过脚本直接在unit文件中定义各种参数:
1 |
|
注意:ExecStart和ExecStop两个命令中的执行文件路径需要使用绝对路径
4、Tomcat的工作模式
Tomcat不仅可以单独运行,还可以与其他的Web服务器集成,作为其他Web服务器的进程内或进程外的servlet容器。集成的意义在于:对于不支持运行Java Servlet的其他Web服务器,可通过集成Tomcat来提供运行Servlet的功能。
Tomcat有三种工作模式:
第一种:Tomcat在一个Java虚拟机进程中独立运行,此时客户端直接和tomcat通信。Tomcat可看作是能运行Servlet的独立Web服务器。Servlet容器组件作为Web服务器中的一部分而存在。这是Tomcat的默认工作模式。
第二种:Tomcat运行在其他Web服务器的进程中,Tomcat不直接和客户端通信,仅仅为其他Web服务器处理客户端访问Servlet的请求。进程内的Servlet容器对于单进程、多线程的Web服务器非常合适,可以提供较高的运行速度,但缺乏伸缩性。
在这种模式下,Tomcat分为Web服务器插件和Servlet容器组件两部分。如下图所示,Web服务器插件在其他Web服务器进程的内部地址空间启动一个Java虚拟机,Servlet容器组件在此Java虚拟机中运行。如有客户端发出调用Servlet的请求,Web服务器插件获得对此请求的控制并将它转发(使用JNI通信机制)给Servlet容器组件。
JNI(Java Native Interface)指的是Java本地调用接口,通过这一接口,Java程序可以和采用其他语言编写的本地程序进行通信。
第三种:Tomcat在一个Java虚拟机进程中独立运行,但是它不直接和客户端通信,仅仅为与它集成的其他Web服务器处理客户端访问Servlet的请求。
在这种模式下,Tomcat分为Web服务器插件和Servlet容器组件两部分。如下图所示,Web服务器插件在其他Web服务器的外部地址空间启动一个JVM进程,Servlet容器组件在此JVM中运行。如有客户端发出调用Servlet的请求,Web服务器插件获得对此请求的控制并将它转发(采用IPC通信机制)给Servlet容器。
进程外Servlet容器对客户请求的响应速度不如进程内Servlet容器,但进程外容器具有更好的伸缩性和稳定性。
IPC(Inter-Process Communication,进程间通信)是两个进程之间进行通信的一种机制。
5、Tomcat的整体架构
我们先从tomcat的源码目录来分析一下tomcat的整体架构,前面我们配置jsvc运行tomcat的时候,我们知道tomcat中启动运行的最主要的类是org.apache.catalina.startup.Bootstrap
,那么我们在tomcat的源码中的java目录下的org目录的apache目录可以找到主要的源码的相对应的类。
图中的目录如果画成架构图,可以这样表示:
Tomcat 本质上就是一款Servlet 容器,因此catalina
才是Tomcat的核心 ,其他模块都是为catalina
提供支撑的。
coyote
模块主要负责链接通信,Tomcat作为http服务器,需要从socket中获得HTTP数据流;而Tomcat作为容器,只能处理封装好的org.apache.coyote.Request
,因此从socket到Request之间的转换就交给coyote
来负责了。因此,连接socket和容器之间的重任就交给了Coyote。简单说就是coyote来处理底层的socket,并将http请求、响应等字节流层面的东西,包装成Request和Response两个类(这两个类是tomcat定义的,而非servlet中的ServletRequest和ServletResponse),供容器使用;同时,为了能让我们编写的servlet能够得到ServletRequest,tomcat使用了facade模式,将比较底层、低级的Request包装成为ServletRequest(这一过程通常发生在Wrapper容器一级)
jasper
模块提供JSP引擎,在jsp文件被初次访问的时候做出响应,将jsp页面翻译成servlet请求,然后调用java编译器对servlet进行编译得到class文件,再调用jvm来执行class文件生成应答,最后把应答发送回客户端。el
全名为Expression Language,也叫JUEL,主要在Java Web应用中用于将表达式嵌入到web页面naming
提供JNDI 服务(Java Naming and Directory Interface,Java命名和目录接口),为开发人员提供了查找和访问各种命名和目录服务的通用、统一的接口,由管理者将JNDI API映射为特定的命名服务和目录系统,使得Java应用程序可以和这些命名服务和目录服务之间进行交互。juli
提供日志服务,JDK 所提供的默认java.util.logging
实现功能太过局限,不能实现针对每一应用进行日志记录,因为配置是针对VM的。而juli
通过自定义的LogManager
能分辨运行在 Tomcat 上的不同 Web 应用(以及它们所用的不同的类加载器),还能针对每一应用进行私有的日志配置。
6、Tomcat的容器架构
Tomcat设计了4种容器,分别是Engine、Host、Context和Wrapper。这4种容器是父子关系, Tomcat通过一种分层的架构,使得Servlet容器具有很好的灵活性。
如上图所示,我们可以看到:
- 一台机器上可以通过设置不同的CATALINA_BASE来运行多个tomcat实例,即可以运行多个server
- 一个server中只有一个Engine,而Engine就是实现了servlet规范的引擎,这里就是Catalina
- 一个engine中可以包含多个host,即和apache、nginx等服务器相同,可以配置多个virtual host站点
- 一个host中可以包含多个context,即可以包含多个web应用
- 一个warpper表示一个Servlet,wrapper 作为容器中的最底层,不能包含子容器
Tomcat使用组合模式来管理这些容器,所有容器组件都实现了Container接口,因此组合模式可以使得用户对单容器对象(最底层的Wrapper)和组合容器对象(Context、Host或者Engine)的使用具有一致性。
Tomcat 服务器的配置主要集中于 tomcat/conf
下的 catalina.policy
、 catalina.properties
、context.xml
、server.xml
、tomcat-users.xml
、web.xml
文件。
Tomcat的这一设计思想在其配置文件server.xml
中得到了很好的诠释,server.xml
是tomcat 服务器的核心配置文件,包含了Tomcat的 Servlet 容器 (Catalina)的所有配置。下面我们先来了解一下server.xml
文件中的一些主要配置。
7、Tomcat的connector简介
7.1 connector的工作原理
这里我们说的Tomcat中三种不同的I/O模型主要指的是其连接器(connector)的工作模型,对于tomcat而言,连接器一般指的是coyote,其工作原理大致如下图所示:
连接器中的各个组件的作用如下:
7.1.1 EndPoint
EndPoint
即Coyote通信端点,是通信监听的接口,是具体Socket接收和发送处理器,是对传输层(四层)的抽象,因此EndPoint
用来实现TCP/IP协议的。Tomcat 并没有EndPoint
接口,而是提供了一个抽象类AbstractEndpoint
, 里面定义了两个内部类:Acceptor
和SocketProcessor
。Acceptor
用于监听Socket连接请求。 SocketProcessor
用于处理接收到的Socket请求,它实现Runnable
接口,在Run
方法里 调用协议处理组件Processor
进行处理。为了提高处理能力,SocketProcessor
被提交到线程池来执行,而这个线程池叫作执行器(Executor)。
7.1.2 Processor
Processor
是coyote的协议处理接口 。如果说EndPoint是用来实现TCP/IP协议的,那么 Processor
用来实现HTTP协议,Processor
接收来自EndPoint的Socket,读取字节流解析成Tomcat的Request
和Response
对象,并通过Adapter
将其提交到容器处理, Processor
是对应用层(七层)协议的抽象。
7.1.3 ProtocolHandler
ProtocolHandler
是Coyote的协议接口,通过Endpoint和Processor ,实现对具体协议(HTTP或AJP)的处理。Tomcat 按照协议和I/O 提供了6个实现类 : AjpNioProtocol
, AjpAprProtocol
, AjpNio2Protocol
, Http11NioProtocol
,Http11Nio2Protocol
, Http11AprProtocol
。我们在配置tomcat/conf/server.xml
中的connecter
块时 , 至少要指定具体的ProtocolHandler
, 当然也可以指定协议名称(如HTTP/1.1)。
7.1.4 Adapter
由于协议不同,客户端发过来的请求信息也不尽相同,Tomcat定义了自己的Request类来存放这些请求信息。ProtocolHandler
接口负责解析请求并生成Tomcat的Request
类。 但是这个Request对象不是标准的ServletRequest,不能用来作为参数来调用容器。因此需要引入CoyoteAdapter
,连接器调用CoyoteAdapter
的Sevice
方法,传入Tomcat的Request
对象,CoyoteAdapter将Request
转成ServletRequest
,再调用容器的Service方法。
7.2 connector的几个重要参数
7.2.1 connectionTimeout
The number of milliseconds this Connector will wait, after accepting a connection, for the request URI line to be presented. Use a value of -1 to indicate no (i.e. infinite) timeout. The default value is 60000 (i.e. 60 seconds) but note that the standard server.xml that ships with Tomcat sets this to 20000 (i.e. 20 seconds). Unless disableUploadTimeout is set to
false
, this timeout will also be used when reading the request body (if any).
在connector和请求的客户端建立连接之后开始计时,当超过该值的时候就会超时,然后断开连接。使用值-1表示无超时,默认值为60000(即60秒),但Tomcat中的server.xml将此值设置为20000(即20秒)。
除非disableUploadTimeout设置为false,否则在读取请求正文(如果有)时也会使用此超时。
7.2.2 maxThreads
The maximum number of request processing threads to be created by this Connector, which therefore determines the maximum number of simultaneous requests that can be handled. If not specified, this attribute is set to 200. If an executor is associated with this connector, this attribute is ignored as the connector will execute tasks using the executor rather than an internal thread pool. Note that if an executor is configured any value set for this attribute will be recorded correctly but it will be reported (e.g. via JMX) as
-1
to make clear that it is not used.
最大线程数,大并发请求时,tomcat能创建来处理请求的最大线程数,超过则放入请求队列中进行排队,默认值为200。
7.2.3 acceptCount
The maximum queue length for incoming connection requests when all possible request processing threads are in use. Any requests received when the queue is full will be refused. The default value is 100.
当最大线程数(maxThreads)被使用完时,可以放入请求队列排队个数,超过这个数返回connection refused(请求被拒绝),默认值为100;
7.2.4 maxConnections
The maximum number of connections that the server will accept and process at any given time. When this number has been reached, the server will accept, but not process, one further connection. This additional connection be blocked until the number of connections being processed falls below maxConnections at which point the server will start accepting and processing new connections again. Note that once the limit has been reached, the operating system may still accept connections based on the
acceptCount
setting. The default value is8192
.For NIO/NIO2 only, setting the value to -1, will disable the maxConnections feature and connections will not be counted.
Tomcat在任意时刻接收和处理的最大连接数。当Tomcat接收的连接数达到maxConnections时,Acceptor线程不会读取accept队列中的连接;这时accept队列中的线程会一直阻塞着,直到Tomcat接收的连接数小于maxConnections。默认值为8192。
对于NIO / NIO2,将该值设置为-1将禁用maxConnections功能,并且不计算连接数。
7.2.5 图解
按照被处理的先后顺序我们可以把tomcat中的线程队列和以上四个参数使用该图进行表示
- 当
maxThreads + acceptCount < maxConnections
的时候将不会有线程被阻塞 - 当阻塞的线程时间超过connectionTimeout还没得到返回值将返回连接超时
8、常见的服务器I/O模型
在开始了解Tomcat的I/O模型之前,我们需要先对服务器中常见的I/O模型进行简单介绍。
8.1 阻塞I/O处理模型
8.1.1 单线程阻塞I/O模型
单线程阻塞I/O模型是最简单的一种服务器I/O模型,单线程即同时只能处理一个客户端的请求,阻塞即该线程会一直等待,直到处理完成为止。对于多个客户端访问,必须要等到前一个客户端访问结束才能进行下一个访问的处理,请求一个一个排队,只提供一问一答服务。
如上图所示:这是一个同步阻塞服务器响应客户端访问的时间节点图。
- 首先,服务器必须初始化一个套接字服务器,并绑定某个端口号并使之监听客户端的访问
- 接着,客户端1调用服务器的服务,服务器接收到请求后对其进行处理,处理完后写数据回客户端1,整个过程都是在一个线程里面完成的
- 最后,处理客户端2的请求并写数据回客户端2,期间就算客户端2在服务器处理完客户端1之前就进行请求,也要等服务器对客户端1响应完后才会对客户端2进行响应处理
这种模型的特点在于单线程和阻塞I/O。单线程即服务器端只有一个线程处理客户端的所有请求,客户端连接与服务器端的处理线程比是n:1
,它无法同时处理多个连接,只能串行处理连接。而阻塞I/O是指服务器在读写数据时是阻塞的,读取客户端数据时要等待客户端发送数据并且把操作系统内核复制到用户进程中,这时才解除阻塞状态。写数据回客户端时要等待用户进程将数据写入内核并发送到客户端后才解除阻塞状态。这种阻塞带来了一个问题,服务器必须要等到客户端成功接收才能继续往下处理另外一个客户端的请求,在此期间线程将无法响应任何客户端请求。
该模型的特点:它是最简单的服务器模型,整个运行过程都只有一个线程,只能支持同时处理一个客户端的请求(如果有多个客户端访问,就必须排队等待),服务器系统资源消耗较小,但并发能力低,容错能力差。
8.1.2 多线程阻塞I/O模型
多线程阻塞I/O模型在单线程阻塞I/O模型的基础上对其进行改进,加入多线程,提高并发能力,使其能够同时对多个客户端进行响应,多线程的核心就是利用多线程机制为每个客户端分配一个线程。
如上图所示,服务器端开始监听客户端的访问,假如有两个客户端同时发送请求过来,服务器端在接收到客户端请求后分别创建两个线程对它们进行处理,每条线程负责一个客户端连接,直到响应完成。期间两个线程并发地为各自对应的客户端处理请求,包括读取客户端数据、处理客户端数据、写数据回客户端等操作。
这种模型的I/O操作也是阻塞的,因为每个线程执行到读取或写入操作时都将进入阻塞状态,直到读取到客户端的数据或数据成功写入客户端后才解除阻塞状态。尽管I/O操作阻塞,但这种模式比单线程处理的性能明显高了,它不用等到第一个请求处理完才处理第二个,而是并发地处理客户端请求,客户端连接与服务器端处理线程的比例是1:1
。
多线程阻塞I/O模型的特点:支持对多个客户端并发响应,处理能力得到大幅提高,有较大的并发量,但服务器系统资源消耗量较大,而且如果线程数过多,多线程之间会产生较大的线程切换成本,同时拥有较复杂的结构。
8.2 非阻塞I/O模型
8.2.1 非阻塞情况下的事件检测
在探讨单线程非阻塞I/O模型前必须要先了解非阻塞情况下套接字事件的检测机制,因为对于单线程非阻塞模型最重要的事情是检测哪些连接有感兴趣的事件发生。一般会有如下三种检测方式。
此处“有感兴趣的事件发生”指的是需要进行读写数据等操作。
(1)应用程序遍历套接字的事件检测
当多个客户端向服务器请求时,服务器端会保存一个套接字连接列表中,应用层线程对套接字列表轮询尝试读取或写入。如果成功则进行处理,如果失败则下次继续。这样不管有多少个套接字连接,它们都可以被一个线程管理,这很好地利用了阻塞的时间,处理能力得到提升。
但这种模型需要在应用程序中遍历所有的套接字列表,同时需要处理数据的拼接,连接空闲时可能也会占用较多CPU资源,不适合实际使用。
(2)内核遍历套接字的事件检测
这种方式将套接字的遍历工作交给了操作系统内核,把对套接字遍历的结果组织成一系列的事件列表并返回应用层处理。对于应用层,它们需要处理的对象就是这些事件,这是一种事件驱动的非阻塞方式。
服务器端有多个客户端连接,应用层向内核请求读写事件列表。内核遍历所有套接字并生成对应的可读列表readList和可写列表writeList。readList和writeList则标明了每个套接字是否可读/可写。应用层遍历读写事件列表readList和writeList,做相应的读写操作。
内核遍历套接字时已经不用在应用层对所有套接字进行遍历,将遍历工作下移到内核层,这种方式有助于提高检测效率。然而,它需要将所有连接的可读事件列表和可写事件列表传到应用层,假如套接字连接数量变大,列表从内核复制到应用层也是不小的开销。另外,当活跃连接较少时,内核与应用层之间存在很多无效的数据副本,因为它将活跃和不活跃的连接状态都复制到应用层中。
(3)内核基于回调的事件检测
通过遍历的方式检测套接字是否可读可写是一种效率比较低的方式,不管是在应用层中遍历还是在内核中遍历。所以需要另外一种机制来优化遍历的方式,那就是回调函数。内核中的套接字都对应一个回调函数,当客户端往套接字发送数据时,内核从网卡接收数据后就会调用回调函数,在回调函数中维护事件列表,应用层获取此事件列表即可得到所有感兴趣的事件。
内核基于回调的事件检测方式有两种
方式一:
第一种是用可读列表readList
和可写列表writeList
标记读写事件,套接字的数量与readList
和writeList
两个列表的长度一样。
服务器端有多个客户端套接字连接
当客户端发送数据过来时,内核从网卡复制数据成功后调用回调函数将
readList/writeList
对应的元素标记为可读/可写应用层发送请求读、写事件列表,内核返回包含了事件标识的
readList
和writeList
事件列表,此时返回的两个列表内容大致如下套接字 readList 1 1 2 0 3 1 …… …… n …… 套接字 writeList 1 0 2 1 3 0 …… …… n …… 应用程序接着分表遍历读事件列表readList和写事件列表writeList,对置为1的元素对应的套接字进行读或写操作
这样就避免了遍历套接字的操作,但仍然有大量无用的数据(状态为0的元素)从内核复制到应用层中。从上面的表格中我们可以看到实际上有用的数据只是在List中被标记为1的数据(意味着可读或可写),其他的数据并没有传送回去的必要。
方式二:
服务器端有多个客户端套接字连接。
应用层告诉内核每个套接字感兴趣的事件,这时候直接发送一个列表给内核
套接字 操作 1 read 2 write 3 read …… …… n …… 接着,当客户端发送数据过来时,对应会有一个回调函数,内核从网卡复制数据成功后即调回调函数将套接字1作为可读事件event1加入到事件列表,同样地,内核发现网卡可写时就将套接字2作为可写事件event2添加到事件列表中
应用层向内核请求读、写事件列表,内核将包含了event1和event2的事件列表返回应用层,此时的列表内容大致如下:
套接字 可以进行的操作 1 read 2 write 注意这时不能进行读写操作的套接字是不会被记录到列表中返回给应用层的,这就大大地减少了数据的传输量。
应用层通过遍历事件列表得知哪些套接字可以进行哪些操作,然后执行对应的操作。
上面两种方式由操作系统内核维护客户端的所有连接并通过回调函数不断更新事件列表,而应用层线程只要遍历这些事件列表即可知道可读取或可写入的连接,进而对这些连接进行读写操作,极大提高了检测效率,自然处理能力也更强。
8.2.2 单线程非阻塞I/O模型
单线程非阻塞I/O模型最重要的一个特点是,在调用读取或写入接口后立即返回,而不会进入阻塞状态。虽然只有一个线程,但是它通过把非阻塞读写操作与上面几种检测机制配合就可以实现对多个连接的及时处理,而不会因为某个连接的阻塞操作导致其他连接无法处理。在客户端连接大多数都保持活跃的情况下,这个线程会一直循环处理这些连接,它很好地利用了阻塞的时间,大大提高了这个线程的执行效率。
单线程非阻塞I/O模型的主要优势体现在对多个连接的管理,一般在同时需要处理多个连接的发场景中会使用非阻塞NIO模式,此模型下只通过一个线程去维护和处理连接,这样大大提高了机器的效率。一般服务器端才会使用NIO模式,而对于客户端,出于方便及习惯,可使用阻塞模式的套接字进行通信。
8.2.3 多线程非阻塞I/O模型
在多核的机器上可以通过多线程继续提高机器效率。最朴实、最自然的做法就是将客户端连接按组分配给若干线程,每个线程负责处理对应组内的连接。比如有4个客户端访问服务器,服务器将套接字1和套接字2交由线程1管理,而线程2则管理套接字3和套接字4,通过事件检测及非阻塞读写就可以让每个线程都能高效处理。
多线程非阻塞I/O模式让服务器端处理能力得到很大提高,它充分利用机器的CPU,适合用于处理高并发的场景,但它也让程序更复杂,更容易出现问题(死锁、数据不一致等经典并发问题)。
8.2.4 Reactor模式
最经典的多线程非阻塞I/O模型方式是Reactor模式。首先看单线程下的Reactor,Reactor将服务器端的整个处理过程分成若干个事件,例如分为接收事件、读事件、写事件、执行事件等。Reactor通过事件检测机制将这些事件分发给不同处理器去处理。在整个过程中只要有待处理的事件存在,即可以让Reactor线程不断往下执行,而不会阻塞在某处,所以处理效率很高。
基于单线程Reactor模型,根据实际使用场景,把它改进成多线程模式。常见的有两种方式:一种是在耗时的process处理器中引入多线程,如使用线程池;另一种是直接使用多个Reactor实例,每个Reactor实例对应一个线程。
Reactor模式的一种改进方式如下图所示。其整体结构基本上与单线程的Reactor类似,只是引入了一个线程池。由于对连接的接收、对数据的读取和对数据的写入等操作基本上都耗时较少,因此把它们都放到Reactor线程中处理。然而,对于逻辑处理可能比较耗时的工作,可以在process处理器中引入线程池,process处理器自己不执行任务,而是交给线程池,从而在Reactor线程中避免了耗时的操作。将耗时的操作转移到线程池中后,尽管Reactor只有一个线程,它也能保证Reactor的高效。
Reactor模式的另一种改进方式如下图所示。其中有多个Reactor实例,每个Reactor实例对应一个线程。因为接收事件是相对于服务器端而言的,所以客户端的连接接收工作统一由一个accept处理器负责,accept处理器会将接收的客户端连接均匀分配给所有Reactor实例,每个Reactor实例负责处理分配到该Reactor上的客户端连接,包括连接的读数据、写数据和逻辑处理。这就是多Reactor实例的原理。
9、Tomcat的三种主要I/O模型
Tomcat支持的I/O模型如下表(自8.5/9.0 版本起,Tomcat移除了对BIO的支持),在 8.0 之前 , Tomcat 默认采用的I/O方式为 BIO , 之后改为 NIO。 无论 NIO、NIO2 还是 APR, 在性能方面均优于以往的BIO。
IO模型 | 描述 |
---|---|
NIO | 同步非阻塞I/O,采用Java NIO类库实现 |
NIO2 | 异步非阻塞I/O,采用JDK 7最新的NIO2类库实现 |
APR | 采用Apache可移植运行库实现,是C/C++编写的本地库,需要单独安装APR库 |
在开始之前,我们先看一下tomcat官网给出的这三种I/O模型的工作参数的一个对比图:
这里我们可以看到一般说的NIO、NIO2和APR使用的是非阻塞方式指的就是在读取请求报头和等待下一个请求的时候是使用的非阻塞方式。
Tomcat的NIO是基于I/O复用(同步I/O)来实现的,而NIO2是使用的异步I/O。参考经典书籍《UNIX网络编程 卷1 套接字联网API》,两者的主要原理如下:
I/O复用
I/O复用(I/O multiplexing)可以调用
select
或poll
,阻塞在这两个系统调用中的某一个之上,而不是阻塞在真正的I/O系统调用上。进程阻塞于select
调用,等待数据报套接字变为可读。当select
返回套接字可读这一条件时,进程调用recvfrom
把所读数据报复制到应用进程缓冲区,尽管这里需要使用select
和recvfrom
两个系统调用,但是使用select
的可以等待多个描述符就绪,即可以等待多个请求。异步IO
异步I/O(asynchronous I/O)的工作机制是:告知内核启动某个操作,并让内核在整个操作(包括将数据从内核复制到应用程序的缓冲区)完成后通知应用程序。需要注意的是:异步I/O模型是由内核通知应用进程I/O操作何时完成。
最后我们可以把上面的过程结合剩下没有提到的三种UNIX系统中的IO模型进行对比得到下图:
9.1 NIO(New I/O APIs、同步非阻塞)
Tomcat中的NIO模型是使用的JAVA的NIO类库,其内部的IO实现是同步的(也就是在用户态和内核态之间的数据交换上是同步机制),采用基于selector实现的异步事件驱动机制(这里的异步指的是selector这个实现模型是使用的异步机制)。而对于Java来说,非阻塞I/O的实现完全是基于操作系统内核的非阻塞I/O,它将操作系统的非阻塞I/O的差异屏蔽并提供统一的API,让我们不必关心操作系统。JDK会帮我们选择非阻塞I/O的实现方式。
这里需要提一下同步异步和阻塞非阻塞的概念:
同步和异步关注的是消息通信机制,同步异步指的是应用程序发起的调用请求和获得的返回值是否一起返回,如果一起返回就是同步,否则就是异步,异步可以通过回调函数等方式实现。
阻塞和非阻塞关注的是程序在等待调用结果时的状态,应用程序发起调用请求之后不能干别的事情直到请求处理完成了就是阻塞,否则就是非阻塞。
所以我个人认为,对于阻塞I/O谈同步异步是没有太大意义的,因为此时进程已经阻塞,想要去干别的事情必须得等请求处理完,而请求处理完必然会得到返回值。
上面我们提到得内核基于回调得事件检测方式二就是典型的异步非阻塞I/O模型。
9.2 NIO2(New I/O APIs 2、异步非阻塞、AIO)
NIO2和前者相比的最大不同就在于引入了异步通道来实现异步IO操作,因此也叫AIO(Asynchronous I/O)。NIO.2 的异步通道 APIs 提供方便的、平台独立的执行异步操作的标准方法。这使得应用程序开发人员能够以更清晰的方式来编写程序,而不必定义自己的 Java 线程,此外,还可通过使用底层 OS 所支持的异步功能来提高性能。如同其他 Java API 一样,API 可利用的 OS 自有异步功能的数量取决于其对该平台的支持程度。
异步通道提供支持连接、读取、以及写入之类非锁定操作的连接,并提供对已启动操作的控制机制。Java 7 中用于 Java Platform(NIO.2)的 More New I/O APIs,通过在 java.nio.channels
包中增加四个异步通道类,从而增强了 Java 1.4 中的 New I/O APIs(NIO),这些类在风格上与 NIO 通道 API 很相似。他们共享相同的方法与参数结构体,并且大多数对于 NIO 通道类可用的参数,对于新的异步版本仍然可用。主要区别在于新通道可使一些操作异步执行。
异步通道 API 提供两种对已启动异步操作的监测与控制机制。第一种是通过返回一个 java.util.concurrent.Future
对象来实现,它将会建模一个挂起操作,并可用于查询其状态以及获取结果。第二种是通过传递给操作一个新类的对象,java.nio.channels.CompletionHandler
,来完成,它会定义在操作完毕后所执行的处理程序方法。每个异步通道类为每个操作定义 API 副本,这样可采用任一机制。
9.3 APR
Apache可移植运行时(Apache Portable Runtime,APR)是Apache HTTP服务器的支持库,最初,APR是作为Apache HTTP服务器的一部分而存在的,后来成为一个单独的项目。其他的应用程序可以使用APR来实现平台无关性(跨平台)。APR提供了一组映射到下层操作系统的API,如果操作系统不支持某个特定的功能,APR将提供一个模拟的实现。这样程序员使用APR编写真正可在不同平台上移植的程序。
9.4 Tomcat配置APR
1 |
|
顺利安装完成后会显示apr的lib库路径,一般都是/usr/local/apr/lib
安装完成之后我们还需要修改环境变量和配置参数
这里我们使用的是systemd调用jsvc来启动tomcat,所以我们直接在systemd对应的tomcat的unit文件中的ExecStart
中添加一个路径参数-Djava.library.path=/usr/local/apr/lib
指向apr库的路径:
1 |
|
然后我们在tomcat的home目录下的conf子目录中对server.xml文件进行修改
把8080端口对应的配置修改成apr:(其他端口配置也类似)
1 |
|
重启tomcat服务我们从tomcat的日志中就可以看到协议已经从默认的nio变成了apr。
9.5 三者之间的区别:
NIO | NIO2 | APR | |
---|---|---|---|
实现 | JAVA NIO库 | JDK1.7 NIO2库 | C |
IO模型 | 同步非阻塞 | 异步非阻塞 | 取决于系统 |
APR的重点在于使用C语言实现并且能够跨平台使用,它相当于将UNIX系统中的IO操作进行了一层封装使得编程开发更容易
NIO性能是最差的这是毋庸置疑的,如果是考虑到高并发的情况,显然异步非阻塞I/O模式的NIO2和APR库在性能上更有优势,实际上NIO2的性能表现也和APR不相上下,但是NIO2要求Tomcat的版本要在8.0以上,而APR只需要5.5以上即可,但是APR需要额外配置库环境,相对于内置集成的NIO2来说APR这个操作比较麻烦,两者各有优劣。具体使用哪个还是需要结合实际业务需求和环境进行测试才能决定。
10、server.xml
Tomcat中的大多数配置都会在server.xml
文件中,server.xml
的地位就好像nginx中的nginx.conf
文件,因此我们想要学习配置tomcat的各类参数,最先开始学习的配置就是server.xml
文件。
10.1 server.xml整体架构
首先我们需要知道server.xml
中的xml代码块分类,tomcat官网将其主要分为四类:
- Top Level Elements:
server
块是整个配置文件的根元素,而service
块代表与引擎关联的一组连接器(connector)。 - Connectors :表示外部客户端向特定服务发送请求和接收响应的接口(比如我们之前提到的coyote连接器以及对应的NIO等IO模式都是整个范畴内的概念)。
- Containers:容器(
Container
)负责处理传入的请求并创建相应的响应。Engine
处理对Service的所有请求,Host
处理对特定virtual host
的所有请求,而Context
处理对特定Web应用程序的所有请求。 - Nested Components:表示可以嵌套在
Container
元素内的元素。 注意一些元素可以嵌套在任何Container中,而另一些元素只能嵌套在Context
中。
10.2 Top Level Elements
3.2.1 Server块
Server块代表的是整个catalina servlet容器。因此,它必须是conf/server.xml
配置文件中最外面的单个元素。它的属性代表了整个servlet容器的特征。Tomcat9中默认的配置文件中Server
块内嵌的子元素为 Listener
、GlobalNamingResources
、Service
(可以嵌套多个)。具体的每个属性参数我们可以查询官网,下面解释默认的参数配置。
1 |
|
3.2.2 Service块
Service元素用于创建 Service 实例,默认使用 org.apache.catalina.core.StandardService
。 默认情况下,Tomcat9中默认仅指定了Service的名称为Catalina
。
1 |
|
Service
可以内嵌的元素为 : Listener
、Executor
、Connector
、Engine
,详细的参数可以点击这里查看官网
Listener
用于为Service
添加生命周期监听器Executor
用于配置Service
共享线程池Connector
用于配置Service
包含的链接器Engine
用于配置Service
中连接器(connector
)对应的Servlet 容器引擎
10.3 Executor
executor
表示可组件之间Tomcat中共享的线程池。默认情况下,Service
并未添加共享线程池配置。executor
实现了tomcat中的org.apache.catalina.Executor
接口。 如果不配置共享线程池,那么Catalina 各组件在用到线程池时会独立创建。由于executor
是Service
元素的嵌套元素。为了使它能够被Connector
使用,Executor
元素必须出现在server.xml
中的Connector
元素之前。下面展示的是一个简单的executor的配置,具体的配置参数可以点这里查看官网:
1 |
|
属性 | 含义 |
---|---|
name | 线程池名称,用于Connector 中指定。 |
namePrefix | 所创建的每个线程的名称前缀,一个单独的线程名称为 namePrefix +threadNumber 。 |
daemon | 是否作为守护线程(类似于守护进程),默认为true |
maxThreads | 线程池中最大线程数。 |
minSpareThreads | 活跃线程数,也就是核心池线程数,这些线程不会被销毁,会一直存在。 |
maxIdleTime | 线程空闲时间,超过该时间后,空闲线程会被销毁,默 认值为6000(1分钟),单位毫秒。 |
maxQueueSize | 在被执行前最大线程排队数目,默认为int 的最大值,也就是广义的无限。除非特殊情况,这个值不需要更改, 否则会有请求不会被处理的情况发生。 |
prestartminSpareThreads | 启动线程池时是否启动 minSpareThreads 部分线程。 默认值为false ,即不启动。 |
threadPriority | 线程池中线程优先级,默认值为5,值从1到10。 |
className | 线程池实现类,未指定情况下,默认实现类为 org.apache.catalina.core.StandardThreadExecutor 。 如果想使用自定义线程池首先需要实现 org.apache.catalina.Executor 接口。 |
10.4 Connector
Connector 用于创建链接器实例。默认情况下,server.xml 配置了两个链接器,一个支 持HTTP协议,一个支持AJP协议。因此大多数情况下,我们并不需要新增链接器配置, 只是根据需要对已有链接器进行优化。
1 |
|
port
为监听的端口,如果设置为0,Tomcat将会随机选择一个可用的端口号给当前Connector 使用protocol
为Connector的协议,这里默认的是HTTP和AJP两种协议,后面可以指定对应协议的不同版本,默认情况下会检测本机是否配置了APR库,如果有并且useAprConnector
设置为true则会默认使用APR模式的IO协议,如果无则会使用NIO模式connectionTimeOut
:Connector 接收链接后的等待超时时间,单位为毫秒。 -1表示永不超时redirectPort
:当前Connector 不支持SSL请求, 接收到了一个请求, 并且也符合 security-constraint 约束, 需要SSL传输,Catalina自动将请求重定向到指定的端口executor
: 指定前面提到的共享线程池的名称,也可以通过maxThreads、minSpareThreads 等属性对该connector进行单独配置对应的内部线程池URIEncoding
: 用于指定编码URI的字符编码, Tomcat8.x和Tomcat9.x版本默认的编码为 UTF-8 , Tomcat7.x版本默认为ISO-8859-1
10.5 engine
Engine 作为Servlet 引擎的顶级元素,内部可以嵌入: Cluster、Listener、Realm、 Valve和Host。
1 |
|
name
:用于指定Engine 的名称, 默认为CatalinadefaultHost
:默认使用的虚拟主机名称,当客户端请求访问的host无效时,会跳转到默认的host来处理请求
10.6 Host
Host 元素用于配置一个虚拟主机,它支持以下嵌入元素:Alias、Cluster、Listener、 Valve、Realm、Context
如果在Engine下配置Realm,那么此配置将在当前Engine下的所有Host中共享。 同样,如果在Host中配置Realm ,则在当前Host下的所有Context 中共享
Context中的Realm优先级 > Host的Realm优先级 > Engine中的Realm优先级
1 |
|
上面这一段Host的配置文件中还额外添加了Valve配置来实现自定义的日志记录。其中一些参数的详细信息和配置方式可以查看官网的说明
。The shorthand pattern
pattern="common"
corresponds to the Common Log Format defined by ‘%h %l %u %t “%r” %s %b’.
- name: 当前Host通用的网络名称,也就是常用的域名,如果有多个域名对应同一个Host的应用,我们可以设置一个或多个Alias来实现访问
- appBase:当前Host应用对应的目录,当前Host上部署的Web应用均在该目录下(相对路径和绝对路径均可),默认为webapps
- unpackWARs:设置为true,Host在启动时会将appBase目录下war包解压为目 录。设置为false,Host将直接从war文件启动
- autoDeploy: 控制tomcat是否在运行时定期检测并自动部署新增或变更的web应用
10.7 Context
Context的完整配置官网文档,Context 用于配置一个Web应用,默认的配置如下。它支持的内嵌元素为:CookieProcessor,Loader,Manager,Realm,Resources,WatchedResource,JarScanner,Valve。
1 |
|
- docBase:Web应用目录或者War包的部署路径。可以是绝对路径,也可以是相对于该Context所属的Host中的
appBase
的相对路径。 - path:Web应用的Context的访问路径。
假设tomcat的安装目录为/home/tomcat9
,Host为默认的localhost, 则该web应用访问的根路径为: http://localhost:8080/myApp
,对应的部署文件所存放的路径为:/home/tomcat9/webapps/myAppDeploy
。