配置文件 server.xml
一个配置文件实例:<Server port="8005" shutdown="SHUTDOWN">
<Listener className="org.apache.catalina.startup.VersionLoggerListener" />
<Listener className="org.apache.catalina.core.AprLifecycleListener" SSLEngine="on" />
<Listener className="org.apache.catalina.core.JasperListener" />
<Listener className="org.apache.catalina.core.JreMemoryLeakPreventionListener" />
<Listener className="org.apache.catalina.mbeans.GlobalResourcesLifecycleListener" />
<Listener className="org.apache.catalina.core.ThreadLocalLeakPreventionListener" />
<GlobalNamingResources>
<Resource name="UserDatabase" auth="Container"
type="org.apache.catalina.UserDatabase"
description="User database that can be updated and saved"
factory="org.apache.catalina.users.MemoryUserDatabaseFactory"
pathname="conf/tomcat-users.xml" />
</GlobalNamingResources>
<Service name="Catalina">
<Connector port="8080" protocol="HTTP/1.1"
connectionTimeout="20000"
redirectPort="8443" />
<Connector port="8009" protocol="AJP/1.3" redirectPort="8443" />
<Engine name="Catalina" defaultHost="localhost">
<Realm className="org.apache.catalina.realm.LockOutRealm">
<Realm className="org.apache.catalina.realm.UserDatabaseRealm" resourceName="UserDatabase"/>
</Realm>
<Host name="localhost" appBase="webapps" unpackWARs="true" autoDeploy="true">
<Valve className="org.apache.catalina.valves.AccessLogValve" directory="logs"
prefix="localhost_access_log." suffix=".txt"
pattern="%h %l %u %t "%r" %s %b" />
</Host>
</Engine>
</Service>
</Server>
server.xml配置元素分类
整体结构
server.xml的整体结构如下:<Server>
<Service>
<Connector />
<Connector />
<Engine>
<Host>
<Context />
<Context />
<!-- 现在常常使用自动部署,不推荐配Context -->
<!-- Context表示一个War应用 -->
</Host>
</Engine>
</Service>
</Server>
该结构中只给出了Tomcat的核心组件,除了核心组件外,Tomcat还有一些其他组件,下面介绍一下组件的分类。
元素分类
server.xml文件中的元素可以分为以下4类:
- (1)顶层元素:
<Server>
和<Service>
<Server>
元素是整个配置文件的根元素,<Service>
元素则代表一个Engine元素以及一组与之相连的Connector元素。
- (2)连接器:
<Connector>
<Connector>
代表了外部客户端发送请求到特定Service的接口;同时也是外部客户端从特定Service接收响应的接口。
- (3)容器:
<Engine>
<Host>
<Context>
容器的功能是处理Connector接收进来的请求,并产生相应的响应。Engine、Host和Context都是容器,都实现了Container接口,但它们不是平行的关系,而是父子关系:Engine包含Host,Host包含Context。
- Engine 表示一个Servlet引擎,它可以包含一个或多个子容器,比如Host或者Context容器;
- Host 表示一台虚拟的主机,它可以包含一系列Context容器;
- Context 表示一个唯一的ServletContext,一个 Context 对应一个 Web 工程,它可以包含一个 或多个Wrapper容器;
- Wrapper 表示一个独立的Servlet定义,即Wrapper本质就是对Servlet进行了一层包装。
一个Engine组件可以处理Service中的所有请求,一个Host组件可以处理发向一个特定虚拟主机的所有请求,一个Context组件可以处理一个特定Web应用的所有请求。
- (4)内嵌组件:
可以内嵌到容器中的组件。实际上,Server、Service、Connector、Engine、Host和Context是最重要的最核心的Tomcat组件,其他组件都可以归为内嵌组件。
下面将详细介绍Tomcat中各个核心组件的作用,以及相互之间的关系。
核心组件
本部分将分别介绍各个核心组件的作用、特点以及配置方式等。
Server
Server元素在最顶层,代表整个Tomcat容器,因此它必须是server.xml中唯一一个最外层的元素。一个Server元素中可以有一个或多个Service元素。
在第一部分的例子中,在最外层有一个
Server的主要任务,就是提供一个接口让客户端能够访问到这个Service集合,同时维护它所包含的所有的Service的声明周期,包括如何初始化、如何结束服务、如何找到客户端要访问的Service。
Service
Service的作用,是在Connector和Engine外面包了一层,把它们组装在一起,对外提供服务。一个Service可以包含多个Connector,但是只能包含一个Engine;其中Connector的作用是从客户端接收请求,Engine的作用是处理接收进来的请求。
在第一部分的例子中,Server中包含一个名称为“Catalina”的Service。实际上,Tomcat可以提供多个Service,不同的Service监听不同的端口,后文会有介绍。
Connector
Connector的主要功能,是接收连接请求,创建Request和Response对象用于和请求端交换数据;然后分配线程让Engine来处理这个请求,并把产生的Request和Response对象传给Engine。
通过配置Connector,可以控制请求Service的协议及端口号。在第一部分的例子中,Service包含两个Connector:
<Connector port="8080" protocol="HTTP/1.1" connectionTimeout="20000" redirectPort="8443" /> |
- (1)通过配置第1个Connector,客户端可以通过8080端口号使用http协议访问Tomcat。其中,protocol属性规定了请求的协议,port规定了请求的端口号,redirectPort表示当强制要求https而请求是http时,重定向至端口号为8443的Connector,connectionTimeout表示连接的超时时间。
- (2)通过配置第2个Connector,客户端可以通过8009端口号使用AJP协议访问Tomcat。AJP协议负责和其他的HTTP服务器(如Apache)建立连接;在把Tomcat与其他HTTP服务器集成时,就需要用到这个连接器。之所以使用Tomcat和其他服务器集成,是因为Tomcat可以用作Servlet/JSP容器,但是对静态资源的处理速度较慢,不如Apache和IIS等HTTP服务器;因此常常将Tomcat与Apache等集成,前者作Servlet容器,后者处理静态资源,而AJP协议便负责Tomcat和Apache的连接。Tomcat与Apache等集成的原理如下图(图片来源):
Engine
Engine组件在Service组件中有且只有一个;Engine是Service组件中的请求处理组件。Engine组件从一个或多个Connector中接收请求并处理,并将完成的响应返回给Connector,最终传递给客户端。
在第一部分的例子中,Engine的配置语句如下:<Engine name="Catalina" defaultHost="localhost">
其中,name属性用于日志和错误信息,在整个Server中应该唯一。defaultHost属性指定了默认的host名称,当发往本机的请求指定的host名称不存在时,一律使用defaultHost指定的host进行处理;因此,defaultHost的值,必须与Engine中的一个Host组件的name属性值匹配。
Host
Engine与Host
Host是Engine的子容器。Engine组件中可以内嵌1个或多个Host组件,每个Host组件代表Engine中的一个虚拟主机。Host组件至少有一个,且其中一个的name必须与Engine组件的defaultHost属性相匹配。
Host的作用
Host虚拟主机的作用,是运行多个Web应用(一个Context代表一个Web应用),并负责安装、展开、启动和结束每个Web应用。
Host组件代表的虚拟主机,对应了服务器中一个网络名实体(如”www.test.com”,或IP地址”116.25.25.25”);为了使用户可以通过网络名连接Tomcat服务器,这个名字应该在DNS服务器上注册。
Host的配置
在第一部分的例子中,Host的配置如下:
<Host name="localhost" appBase="webapps" unpackWARs="true" autoDeploy="true"> |
下面对其中配置的属性进行说明:
name属性指定虚拟主机的主机名,一个Engine中有且仅有一个Host组件的name属性与Engine组件的defaultHost属性相匹配;一般情况下,主机名需要是在DNS服务器中注册的网络名,但是Engine指定的defaultHost不需要,原因在前面已经说明。
unpackWARs指定了是否将代表Web应用的WAR文件解压;如果为true,通过解压后的文件结构运行该Web应用,如果为false,直接使用WAR文件运行Web应用。
Host的autoDeploy和appBase属性,与Host内Web应用的自动部署有关;此外,本例中没有出现的xmlBase和deployOnStartup属性,也与Web应用的自动部署有关;将在下一节(Context)中介绍。
Context
Context的作用
Context元素代表在特定虚拟主机上运行的一个Web应用。每个Web应用基于WAR文件,或WAR文件解压后对应的目录(这里称为应用目录)。
Context是Host的子容器,每个Host中可以定义任意多的Context元素。
在第一部分的例子中,可以看到server.xml配置文件中并没有出现Context元素的配置。这是因为,Tomcat开启了自动部署,Web应用没有在server.xml中配置静态部署,而是由Tomcat通过特定的规则自动部署。下面介绍一下Tomcat自动部署Web应用的机制。
Web应用自动部署
如何开启自动部署War:
<Host name="localhost" appBase="webapps" |
此外Context组件还有一个reloadable属性, <Context docBase="xxx" path="/xxx" reloadable="true"/>
替换WEB-INF/lib目录中的jar文件或WEB-INF/classes目录中的class文件时,reloadable=”true”会让修改生效(但代价不小),该选项适合调试。
autoDeploy和reloadable的区别是, 前者是Host的属性后者是Context的属性,
前者监控的是webapps目录下War包的改动, 后者监控的是webapps下面文件夹内jar或者class文件的变化;
一般线上环境会关闭这两个参数, 开发阶段可以通过这两个参数无需重启tomcat预览改变;
自动部署的实现
Tomcat的Engine会启动一个线程,该线程每10s会发送一个发送一个事件,监听到该事件的部署配置类, 会自动去扫描webapp文件夹下的war包,将其加载成一个Context,即启动一个web服务。
Tomcat的StandardEngine
会在starInternal()
启动一个线程,该线程运行的是ContainerBackgroundProcessor.run()
方法,
这个run每隔10s唤醒调用一次processChildren()
, 继续跟踪该方法,会看到调用其子容器Engine、Host、Context、Wrapper各容器组件及与它们相关的其它组件的backgroundProcess方法。backgroundProcess()
发送一个事件Lifecycle.PERIODIC_EVENT
,
StandardHost通server.xml配置了HostConfig监听器,对该事件的响应方法是HostConfig.lifecycleEvent()
,lifecycleEvent()
会检查autoDeploy="true"
的配置, 如果开启了, 则调用deployApps()
扫描webapp文件夹下的war包,将其加载成一个Context,即启动一个web服务。
核心组件的关联
整体关系
核心组件之间的整体关系,在上一部分有所介绍,这里总结一下:
Server元素在最顶层,代表整个Tomcat容器;一个Server元素中可以有一个或多个Service元素。
Service在Connector和Engine外面包了一层,把它们组装在一起,对外提供服务。一个Service可以包含多个Connector,但是只能包含一个Engine; Connector接收请求,Engine处理请求。
Engine、Host和Context都是容器,且 Engine包含Host,Host包含Context。每个Host组件代表Engine中的一个虚拟主机;每个Context组件代表在特定Host上运行的一个Web应用。
如何确定请求由谁处理?
当请求被发送到Tomcat所在的主机时,如何确定最终哪个Web应用来处理该请求呢?
- (1)根据协议和端口号选定Service和Engine
Service中的Connector组件可以接收特定端口的请求,当请求进来时,Tomcat便可以根据协议和端口号选定处理请求的Service;Service一旦选定,Engine也就确定。
- (2)根据域名或IP地址选定Host
Service确定后,Tomcat在Service中寻找名称与域名/IP地址匹配的Host处理该请求。如果没有找到,则使用Engine中指定的defaultHost来处理该请求。
- (3)根据URI选定Context/Web应用
这一点在Context一节有详细的说明:Tomcat根据应用的 path属性与URI的匹配程度来选择Web应用处理相应请求,这里不再赘述。
- (4)举例
如何部署多个war包项目
@TODO
其他组件
Listener
Listener(即监听器)定义的组件,可以在特定事件发生时执行特定的操作;被监听的事件通常是Tomcat的启动和停止。
监听器可以在Server、Engine、Host或Context中,本例中的监听器都是在Server中。实际上,本例中定义的6个监听器,都只能存在于Server组件中。监听器不允许内嵌其他组件。
监听器需要配置的最重要的属性是className,该属性规定了监听器的具体实现类,该类必须实现了org.apache.catalina.LifecycleListener
接口。
GlobalNamingResources与Realm
@TODO
Valve
在第一部分的例子中,Host元素内定义了Valve组件:
<Valve className="org.apache.catalina.valves.AccessLogValve" directory="logs" prefix="localhost_access_log." suffix=".txt" pattern="%h %l %u %t "%r" %s %b" /> |
单词Valve的意思是“阀门”,在Tomcat中代表了请求处理流水线上的一个组件;Valve可以与Tomcat的容器(Engine、Host或Context)关联。
不同的Valve有不同的特性,下面介绍一下本例中出现的AccessLogValve。
AccessLogValve的作用是通过日志记录其所在的容器中处理的所有请求,在本例中,Valve放在Host下,便可以记录该Host处理的所有请求。AccessLogValve记录的日志就是访问日志,每天的请求会写到一个日志文件里。AccessLogValve可以与Engine、Host或Context关联;在本例中,只有一个Engine,Engine下只有一个Host,Host下只有一个Context,因此AccessLogValve放在三个容器下的作用其实是类似的。
性能优化
bin/catalina.sh
修改Xms, Xmx, PermSize
JAVA_OPTS="-Xms8g -Xmx8g -Xmn2g -server -DServer=mblog -XX:PermSize=128m -XX:MaxPermSize=128m -XX:MaxTenuringThreshold=4 -XX:+UseConcMarkSweepGC -XX:SurvivorRatio=8 -XX:CMSInitiatingOccupancyFraction=70 -XX:+ExplicitGCInvokesConcurrent -XX:+PrintFlagsFinal -XX:+PrintCommandLineFlags -XX:+PrintGCDateStamps -XX:+PrintTenuringDistribution -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintGCApplicationStoppedTime -XX:+PrintGCApplicationConcurrentTime -Xloggc:../gclogs/gc.log.$nowday" |
conf/server.xml
Tomcat 采用 Request Per Thread 策略, 每个用户请求由一个线程处理, <Executor>
部分定义了该线程池, 该线程池被 多个<Connector>
共享, server.xml里的优化主要在<Connector>
:
<Executor className="org.apache.catalina.core.StandardThreadExecutor" name="tomcatThreadPool" namePrefix="catalina-exec" |
- Executor参数
- maxThreads: 线程池最大线程数
- minSpareThreads: 线程池最小线程数(线程池初始化时), 默认25
Connector 参数:
- executor: 指明使用哪一个 Executor, 如果指定了,那么 Connector中所有关于线程的设定会被忽略, 如果没有指定 一个线程池, Connector将会创建一个私有的线程池.
- maxThreads: 用于处理客户端请求的最大线程数, 设置为多少视CPU处理能力而定, 一般单个应用不应该超过300, 如果超过300应考虑多个Tomcat组成集群方式
- enableLookups: 是否开启域名反查,一般设置为false来提高处理能力,它的取值还有true,一般很少使用。若设为true, 则支持域名解析,可把 ip 地址解析为主机名
- connectionTimeout: 网络连接超时,单位:毫秒。设置为 0 表示永不超时
- acceptorThreadCount: 默认为1,表示用于accept新socket连接的线程个数。建议设置为 cpu核数
- acceptCount: 当全部线程都在忙(意味着客户端并发数超过 maxThreads个线程), 新的请求会放入accept队列, 该值是队列的size, 默认100 (如果要增加Tomcat并发处理能力, 需要同时增加 acceptCount 和 maxThreads)
- maxConnections: Tomcat能 accept并 process的最大连接数, 超过这个数 tomcat仍然 能accept新的连接, 但不会process
- compressionMinSize: 大于这个数值讲开启压缩, 默认为2K
- compressableMimeType: 压缩哪些类型
protocol:协议类型,可选类型有四种,分别为 BIO(阻塞型IO),NIO,NIO2和APR。
- BIO:BIO(Blocking I/O),顾名思义,即阻塞式I/O操作,表示Tomcat使用的是传统的Java I/O操作(即java.io包及其子包)。Tomcat在默认情况下,是以bio模式运行的。遗憾的是,就一般而言,bio模式是三种运行模式中性能最低的一种。BIO配置采用默认即可。
- NIO:NIO(New I/O),是Java SE 1.4及后续版本提供的一种新的I/O操作方式(即java.nio包及其子包)。Java nio是一个基于缓冲区、并能提供非阻塞I/O操作的Java API,因此nio也被看成是non-blocking I/O的缩写。它拥有比传统I/O操作(bio)更好的并发运行性能。要让Tomcat以nio模式来运行也比较简单,我们只需要protocol类型修改为:
protocol="org.apache.coyote.http11.Http11NioProtocol"
对于互联网应用,我们应该在NIO、NIO2之间做选择,因为它能够有效的提升性能(主要是并发能力),其中NIO2即为AIO,需要JDK 1.7+、Linux 2.6+才能支持。- NIO:JDK 1.6+,tomcat 6.x+
- NIO2:JDK 1.7+,tomcat 7.x+
APR: 通过Native实现的I/O库, Tomcat通过JNI调用;
// Tomcat 支持的几种protocol:
//NIO
protocol="org.apache.coyote.http11.Http11NioProtocol"
//NIO2
protocol="org.apache.coyote.http11.Http11Nio2Protocol"
//ARP
protocol="org.apache.coyote.http11.Http11AprProtocol"
总结:
增加 acceptCount, maxConnections, maxThreads,
Tomcat 能 accept 的连接数量 = maxConnections + acceptCount;
Tomcat 能 accept 并process的连接数量 = maxConnections;
NIO线程模型
Tomcat使用 Connector 完成整个 HTTP Request的处理流程, 包括 accept socket → NIO Selector处理 socket读事件 → 把可读Socket分发给 Work线程 → 从socket读取数据并解析为Http请求 → Http请求交给CoyoteAdaper处理, CoyoteAdaper 通过Mapper找到对应的Servlet.
在NIO实现的Connector中,处理请求的主要实体是NIoEndpoint对象。NIoEndpoint中除了包含Acceptor和Worker外,还是用了Poller,处理流程如下图所示:
- Acceptor accept 客户端请求, 这里虽然是基于NIO的connector,但是在接收socket方面还是传统的
serverSocket.accept()
方式, Acceptor 获取到 客户端请求的socket, 封装进 tomcat的实现类org.apache.tomcat.util.net.NioChannel对象中, 然后将NioChannel对象封装在一个PollerEvent对象中,并将 PollerEvent对象压入events queue - Poller 读取 events queue 取出PollerEvent, Poller线程中维护了一个Selector对象, Poller 从Event中取出 客户端请求的socketChannel, 把这个channel的READ事件注册到 Selector上.
- Poller 通过
Selector.select()
遍历可读的 socketChannel, 从Worker线程池中拿到可用的Worker线程,将socket传递给Worker处理 - Worker 线程将socket封装在SocketProcessor对象中。然后从Http11ConnectionHandler中取出Http11NioProcessor对象,从Http11NioProcessor中调用CoyoteAdapter的逻辑
本节参考:
https://my.oschina.net/weiweiblog/blog/1830173
https://www.jianshu.com/p/f91f99610b9e
附: Connector和线程池参数解析
<Executor>
部分, 参数说明: https://tomcat.apache.org/tomcat-8.5-doc/config/executor.html- namePrefix: The name prefix for each thread created by the executor
- maxThreads: The max number of active threads in this pool, default is 200
- minSpareThreads: The minimum number of threads (idle and active) always kept alive, default is 25
- maxIdleTime: The number of milliseconds before an idle thread shutsdown, unless the number of active threads are less or equal to minSpareThreads. Default value is 60000(1 minute)
- maxQueueSize: The maximum number of runnable tasks that can queue up awaiting execution before we reject them. Default value is Integer.MAX_VALUE
<Connector>
部分, https://tomcat.apache.org/tomcat-8.5-doc/config/http.html- executor: A reference to the name in an Executor element. If this attribute is set, and the named executor exists, the connector will use the executor, and all the other thread attributes will be ignored. Note that if a shared executor is not specified for a connector then the connector will use a private, internal executor to provide the thread pool.
- 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.
- 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).
- 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.
- acceptorThreadCount: The number of threads to be used to accept connections. Increase this value on a multi CPU machine, although you would never really need more than 2. Also, with a lot of non keep alive connections, you might want to increase this value as well. Default value is 1.
- 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 varies by connector type. For NIO and NIO2 the default is 10000. For APR/native, the default is 8192.
Note that for APR/native on Windows, the configured value will be reduced to the highest multiple of 1024 that is less than or equal to maxConnections. This is done for performance reasons.
APR支持
APR(Apache Portable Runtime)可移植运行库,它是Apache HTTP Server 2.x的核心。APR有很多用途,包括访问高级IO功能(例如sendfile,
epoll和OpenSSL),OS级别功能(随机数生成,系统状态等等),本地进程管理(共享内存,NT管道和UNIX sockets)。
这些功能可以使Tomcat作为一个通常的前台WEB服务器,能更好地和其它本地web技术集成,总体上让Java更有效率作为一个高性能web服务器平台而不是简单作为后台容器。
可以简单地理解为: Tomcat将以JNI的形式调用 APR库中的Native Method处理文件读取或网络传输操作,提升Tomcat对静态文件的处理性能
安装步骤:
- 下载APR, 编译
make && make install
- 安装Tomcat Native到Tomcat的安装目录下
- 修改conf/server.xml, 修改
<Connector>
的protocol
日志
参考: 日志机制 - Tomcat 8 权威指南 - 极客学院Wiki
在 Apache Tomcat 上运行的 Web 应用可以使用以下日志:
- 任何自选的日志框架,如
log4j
; - 使用JDK提供的日志
java.util.logging
; - Java Servlets 规范所提供的日志 API,如
javax.servlet.ServletContext.log(...)
;
当tomcat启动时会为每个app分配了一个WebappClassLoader ,这样来避免多个app会加载相同jar包的问题,
不同Web应用程序下使用的Servlet日志(或者日志框架提供的日志,如log4j等)是相互独立的(这与Tomcat的class loader有关,参考Class Loader HOW-TO )。
如果Web应用程序使用的是java.util.logging日志,那么它们并不相互独立,这是因为java.util.logging是由JAVA系统中的Bootstrap ClassLoader来加载的,因此它在各Web应用程序之间是共享的。
JULI vs JUL
JUL API(java.util.logging
)的默认实现功能有限,因此tomcat的默认配置中,新增了另一种日志实现 JULI API(org.apache.juli
),
可以在Tomcat的 logging.properties 里看到定义了两种日志:
java.util.logging 的java.util.logging.ConsoleHandler
;
JULI的org.apache.juli.FileHandler
;
JULI 同样支持标准JDK java.util.logging的配置机制(都默认使用logging.properties作为配置文件),不同的是JULI的每一个类加载属性文件都可以被设置,并可以在其中定义处理器,这样就给了开发者更大的自由度。
JULI 的日志配置分为 全局配置 和 WebApp项目配置。
全局配置位于tomcat的配置目录${catalina.base}/conf/logging.properties
文件,
如果该文件未配置或不可读,那么tomcat将会使用JRE中的默认日志配置,可以在${java.home}/lib/logging.properties
查看配置文件的内容;
项目配置则是针对不同的项目,配置文件位于WEB-INFO/classes/logging.properties
.
JUL 和 JULI使用相同的日志级别:SEVERE (最高级别) > WARNING > INFO > CONFIG > FINE > FINER > FINEST (所有内容,最低级别)
JULI 所使用的配置与 java.util.logging 所支持的配置基本相同,只不过使用了一些扩展,以便更灵活地配置 logger 和 handler。主要的差别在于:
- JULI 的 handler 名称前可以加上前缀,所以同一类可以实例化出多个 handler。前缀是一个以数字开头的字符串,并以 . 结尾。比如 22foobar. 就是个有效的前缀。
- JULI 的 handler 支持额外的属性, 比如
bufferSize
Tomcat日志配置解析
以下是一个 $CATALINA_BASE/conf
中的默认 logging.properties 文件:
# 声明所有的handlers |
java.util.logging.ConsoleHandler
:
When running Tomcat on unixes, the console output is usually redirected to the file named catalina.out.
The name is configurable using an environment variable. (See the startup scripts).
Whatever is written to System.err/out will be caught into that file. That may include:- Uncaught exceptions printed by java.lang.ThreadGroup.uncaughtException(..)
- Thread dumps, if you requested them via a system signal
java.util.logging.ConsoleHandler 是java自带的日志处理系统(JUL)的控制台日志Handler,
Tomcat通过System.err.println()
/System.out.println()
打出的日志会通过 java.util.logging.ConsoleHandler 写入Tomcat进程的stdout/stderr,
并最终输出到文件”catalina.out”中, 这个文件名是在Tomcat启动脚本里定义的:
org.apache.juli.FileHandler
:
org.apache.juli.FileHandler supports buffering of the logs.
The buffering is not enabled by default. To configure it, use the bufferSize property of a handler.
The value of 0 uses system default buffering (typically an 8K buffer will be used).
A value of < 0 forces a writer flush upon each log write.
A value > 0 uses a BufferedOutputStream with the defined value but note that the system default buffering will also be applied.
org.apache.支持日志缓存。日志缓存默认是没有启用的。使用 handler 的 bufferSize 属性可以配置它:
属性值为 0 时,代表使用系统默认的缓存(通常使用 8k 缓存);
属性值小于 0 时,将在每个日志写入上强制使用 writer flush(将缓存区中的数据强制写出到系统输出)功能;
属性值大于 0 时,则使用带有定义值的 BufferedOutputStream 类——但要注意的是,这也将应用于系统默认的缓存。
WebApp的日志配置解析
下例是一个用于 servlet-examples 应用的 WEB-INF/classes 中的 logging.properties 文件
handlers = org.apache.juli.FileHandler |
Tomcat启动时报错 “SEVERE: Error listenerStart” 或者 “SEVERE: Error filterStart” 等, 但没有具体的错误日志:
这种一般是因为 Tomcat WebAppClassLoader 加载的org.springframework
类的日志没有关联一个Handler,
可以修改 webapps/xxx/WEB-INF/classes/logging.properties, Tomcat就会在打印 org.springframework 类的详细的报错信息了.
注意, 老的应用可能还在使用 System.out 或 System.err,这种情况下还需要在web应用的classes/logging.properties 里增加 java.util.logging.ConsoleHandler:
handlers = org.apache.juli.FileHandler, java.util.logging.ConsoleHandler |
对于还在使用 System.out 或 System.err的应用:
可以通过在 Context 元素上设置 swallowOutput 属性来调整。如该属性设为 true,那么在请求阶段对 System.out/err 的调用就会被拦截,它们的输出也会通过javax.servlet.ServletContext.log(...)
调用反馈给日志系统。
日志文件
- catalina.2017-08-29.log #Cataline引擎的日志文件
- localhost.2017-08-29.log #Tomcat下内部代码抛出的日志jsp页面内部错误的异常
- manager.2017-08-29.log #Tomcat下默认 manager应用日志
- host-manager.2017-08-29.log #Tomcat下默认 manager应用日志
- localhost_access_log.2017-08-29.txt #访问日志记录
- catalina.out #控制台输出的日志,Linux下默认重定向到catalina.out
查看catalina.sh, 最终启动tomcat执行的命令行是 java ${JAVA_OPTS} org.apache.catalina.startup.Bootstrap start
生产环境中的日志
可能需要注意以下方面:
- 将 ConsoleHandler 从配置中移除。默认( .handlers 设置)日志会使用 FileHandler 和 ConsoleHandler。
后者的输出经常会被捕获到一个文件中,比如 catalina.out。从而导致同一消息可能生成了两个副本。 - 对于不使用的应用(比如 host-manager),可以考虑将 FileHandlers 移除。
- handler 默认使用系统缺省编码来写入日志文件,通过 encoding 属性可以修改设置,详情查看相关的 javadoc 文档。
- 增加Access访问日志。
使用log4j
参考 使用 Log4j @Ref
- 如果只是想在自己的 Web 应用上使用 log4j 时
- 只需将 log4j.jar 和 log4j.properties 放到 Web 应用的 WEB-INF/lib 和 WEB-INF/classes 中即可
- 如果想配置 Tomcat 以便利用 log4j 来进行自身日志记录时,下面的步骤都是必需的:
- 创建一个包含下列配置的 log4j.properties 文件,将其保存到 $CATALINA_BASE/lib。Important!
- 下载 log4j
- 下载或构建 tomcat-juli.jar 和 tomcat-juli-adapters.jar,以便作为 Tomcat 的额外组件使用。
- 将 log4j.jar 和 tomcat-juli-adapters.jar 从 extras 中放入 $CATALINA_HOME/lib 中。
- 用 extras 中的 tomcat-juli.jar 替换 $CATALINA_HOME/bin/tomcat-juli.jar。
- 删除 $CATALINA_BASE/conf/logging.properties,以防止 java.util.logging 生成零长度的日志文件。
高级IO
推送: Comet 支持
施工中
发送大型静态文件: sendfile
施工中
How to deploy war
有3中方式部署war包:
- 在server.xml的
<Host>
标签中声明<Context>
标签 - 在server.xml的
<Host>
标签中开启autoDeploy, 将war包放入webapps中会自动部署 - context.xml配置方式
Using the UI manager
Tomcat提供了一个网页版的Manager App, 默认位置在webapps/manager, 也是一个web项目, 使用方式参考: Apache Tomcat 6.0 (6.0.53) - Manager App HOW-TO @Ref
Go to [<protocol>://]localhost:<port>/manager/html/
(usually localhost:8080/manager/html/
),
If you get:403 Access Denied
go to %CATALINA_HOME%\conf\tomcat-users.xml
and check that you have enabled a line like this:<user username="tomcat" password="tomcat" roles="tomcat,role1,manager-gui"/>
Using maven
待补充…
ClassLoader
与很多服务器应用一样,Tomcat 也安装了各种类加载器。借助类加载器,容器的不同部分以及运行在容器里的 Web Apps 就可以访问不同的仓库(保存着可使用的类和资源)。
// 这里英文原文是”different repositories of available classes and resources.” 不知道该怎么翻译这里的”repositories” ?
在 Java 环境中,类加载器的布局结构是一种父子树的形式。通常,类加载器被请求加载一个特定的类或资源时,它会先把这一请求委托给它的父类加载器,只有(一个或多个)父类加载器无法找到请求的类或资源时,它才开始查看自身的仓库。
注意,Web 应用的类加载器模式跟这个稍有不同,下文将详细介绍,但基本原理是一样。
当 Tomcat 启动后,它就会创建一组类加载器,这些类加载器被布局成如下图所示这种父子关系,父类加载器在子类加载器之上:
Bootstrap |
如上图所示,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 环境变量自身的内容,相反从下列仓库来构建系统类加载器:$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 日志库的包重命名副本。详情参看 Tomcat 日志文档。如果
*$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 文件。
默认,它包含以下这些内容:- annotations-api.jar JavaEE 注释类。
- catalina.jar Tomcat 的 Catalina servlet 容器部分的实现。
- jsp-api.jar JSP 2.3 API
- servlet-api.jar Servlet 3.1 API
- tomcat-api.jar Tomcat 定义的一些接口
- tomcat-dbcp.jar 数据库连接池实现,基于 Apache Commons Pool 的包重命名副本和 Apache Commons DBCP。
- tomcat-jdbc.jar 一个数据库连接池替代实现,又被称作 Tomcat JDBC 池。详情参看 JDBC 连接池文档。
- …
- WebappX 为每个部署在单个 Tomcat 实例中的 Web 应用创建的类加载器。你的 Web 应用的 /WEB-INF/classes 目录中所有的解包类及资源,以及 /WEB-INF/lib 目录下 JAR 文件中的所有类及资源,对于该应用而言都是可见的,但对于其他应用来说则不可见。
如上所述,Web 应用类加载器背离了默认的 Java 委托模式(根据 Servlet 规范 2.4 版的 9.7.2 Web Application Classloader一节中提供的建议)。
当某个请求想从 Web 应用的 WebappX 类加载器中加载类时,该类加载器会先查看自己的仓库,而不是预先进行委托处理。
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 类
源码导读 (Tomcat 8.5)
编译
ant clean |
启动
java -cp ./output/classes -Dcatalina.home=./output/build org.apache.catalina.startup.Bootstrap |
启动过程源码调用时序
Bootstrap.main // 入口方法 |
源码分析
参考 Tomcat实现:源码分析Tomcat实现细节 @Archived
* Connect.start()—创建并发线程模型: Work线程, Poller线程, Acceptor线程, AsyncTimeout线程 * 请求处理: Acceptor线程, Poller线程, Selector
如何解析server.xml
- 在Catalina.load() 创建digester:
Digester digester = createStartDigester()
createStartDigester
方法创建了digester对象, 并给digester对象添加多种Rule, 每种Rule都对应server.xml里的一个节点类型, 比如<Server>
,<Connector>
;- digester对server.xml设置的标签动作有5种调用:
- addObjectCreate:遇到起始标签的元素,初始化一个实例对象入栈
- addSetProperties:遇到某个属性名,使用setter来赋值
- addSetNext:遇到结束标签的元素,调用相应的方法
- addRule:调用rule的begin 、body、end、finish方法来解析xml,入栈和出栈给对象赋值
- addRuleSet:调用addRuleInstances来解析xml标签
- 从这些规则和xml中可以看到,Calatina的Server对象是StandardServer。 StandardService包含了多个Connector(xml中有2个connector)和一个StandardEngine Container。 StandardEngine包含了一个Host Container
初始化Connector
根据配置文件 protocol = “HTTP/1.1”,”AJP/1.3” 创建对应 protocol, 默认是 Http11NioProtocol,
再由Http11NioProtocol 创建 NioEndpoint:
代码流程
- 调用
Connector(String protocol)
, 构造函数Connector
中默认创建org.apache.coyote.http11.Http11NioProtocol
- 以
Http11NioProtocol
为例,Http11NioProtocol.init()
最终调用到NioEndpoint.bind()
=>NioEndpoint.initServerSocket()
=>serverSock.socket().bind(addr,getAcceptCount())
完成了对端口的绑定 - bind()的最后调用了
NioSelectorPool.open()
, 这是一个存放Selector的池子,
启动Connector
Connector 主要功能实现都是在 NioEndpoint, NioEndpoint 包括x个Acceptor线程, x个 Poller线程;
Acceptor线程(默认一个)用于 accept 客户端请求, 并把 客户端请求socket 封装进event, 放入 events queue;
Poller线程池用于消费 events queue, 每个Poller 都有自己的 Selector对象, 不断取出event, 并从中解析出sockt, 并把socket 的 READ事件注册到自己的 Selector.
代码调用流程:
- Connector的启动会调用
start
方法, =>Connector.startInternal
方法 =>Http11NioProtocol.start()
=>AbstractProtocol.start()
=>NioEndpoint.start()
=>NioEndpoint.startInternal()
- 在
NioEndpoint.startInternal()
中,- 如果Worker线程池是空, 则自己创建: 调用了父类
AbstractEndpoint#createExecutor()
, 创建work线程池, 名称前缀 “-exec-“; - 创建
NioEndpoint$Poller[]
数组, Poller是Runnable的实现, 然后所有的Poller线程都start起来, 线程名前缀是 “-ClientPoller-“, 数组的大小也就是Poller的数量是Math.min(2,Runtime.getRuntime().availableProcessors())
, 可见 Poller数量 是 min(2, cpu的process数量) - 调用
startAcceptorThreads()
, 创建Acceptor
线程, 默认一个(线程数是server.xml里的acceptCount
), , 线程名前缀是”-Acceptor-“
- 如果Worker线程池是空, 则自己创建: 调用了父类
accept请求
Acceptor 线程 accept , 并把 客户端请求socket 封装进event, 放入 events queue, 调用流程:
Acceptor.run()
:while (endpoint.isRunning()) {
socket = endpoint.serverSocketAccept();
endpoint.setSocketOptions(socket) // 调用了 NioEndpoint.setSocketOptions()
}再看
NioEndpoint.setSocketOptions()
里做了什么: 把客户端请求的socket 封装进 NioChannel,
调用 Poller.register(NioChannel): 把 NioChannel 封装进PollerEvent, 每个 Poller都有一个 PollerEvent队列(events queue), 把PollerEvent放入这个队列 // Poller 有多个, 这里会轮询的方式选择出其中一个,AtomicInteger.incrementAndGet()) % pollers.length
处理一次Req请求
Poller线程用于消费 events queue, 代码调用流程:
Poller.run()
while循环从 event queue取出 PollerEvent, 然后调用PollerEvent.run()
PollerEvent.run()
// 主要是 在 Poller自己的Selector上注册 READ事件Poller.processKey(SelectionKey , NioSocketWrapper)
调用->Poller.processSocket
// 处理 OPEN_READ/OPEN_WRITE等事件- 创建一个 SocketProcessorBase的实例, 把 socket 和 Event 封装进去, SocketProcessorBase 继承自Runnable
- executor.execute(SocketProcessorBase) // 用Worker线程池运行这个 SocketProcessorBase
SocketProcessorBase.run()
->SocketProcessor.doRun()
再看SocketProcessor 调用流程:
SocketProcessor.doRun()
: // 调用SocketChannel.keyFor()AbstractProtocol$ConnectionHandler.process()
NioEndpoint$SocketProcessor.doRun()
Http11Processor.service()
: 处理Socket IO流, 解析为Http RequestApplicationFilterChain.internalDoFilter()
: 调用Filter.doFilter()
,以及Servlet.service()
;
如何SHUTDOWN
- StandardServer.await() 创建一个在8005监听的ServerSocket, 是用来监听关闭Tomcat命令的, 当执行shutdown.sh关闭tomcat时就是连接8005端口执行“SHUTDOWN”命令;
- 关闭请求发给Tomcat, 由StandardServer.await处理, await方法验证关闭请求是否有效, 如果有效则退出await方法, 进入Catalina.stop(), 调用
StandardServer.stop, StandardServer.destroy, 然后关闭Connector, Service
从日志可以看到:
- WebappLoader.stopInternal -> WebappClassLoaderBase.stop -> WebappClassLoaderBase.clearReferences
- WebappClassLoaderBase.clearReferencesJdbc
- WebappClassLoaderBase.clearReferencesThreads
- AbstractProtocol.pause Pausing ProtocolHandler [“http-nio-8080”]
- AbstractProtocol.pause Pausing ProtocolHandler [“ajp-nio-8009”]
- StandardService.stopInternal
###
https://www.zhihu.com/question/53498767
Useful Java API usage
- AsyncChannelWrapperSecure:
- Executors.newFixedThreadPool , shutdownNow
- AsynchronousSocketChannel
- ByteBuffer, flip, hasRemaining,
- AtomicBoolean
- WsWebSocketContainer
- AsynchronousSocketChannel
并发的处理代码
用线程池启动容器内组件
// Start our child containers, if any
Container children[] = findChildren();
List<Future<Void>> results = new ArrayList<>();
for (int i = 0; i < children.length; i++) {
results.add(startStopExecutor.submit(new StartChild(children[i])));
}
boolean fail = false;
for (Future<Void> result : results) {
try {
result.get();
} catch (Exception e) {
}}通过Callable封装带返回值的任务
private static class StartChild implements Callable<Void> {
private Container child;
public StartChild(Container child) {
this.child = child;
}
public Void call() throws LifecycleException {
child.start();
return null;
} }