??Tomcat是一个免费的开放源代码的Web应用服务器 ,属于轻量级应用服务器,在中小型系统和并发访问用户不是很多的场合下被普遍使用。
一、Tomcat架构
1.1 Connector和Container
??Tomcat的顶层结构图: ??Tomcat中最顶层的容器是Server,代表着整个服务器,从上图中可以看出,一个Server可以包含至少一个Service,即可以包含多个Service,用于具体提供服务 。 ??Service主要包含两个部分:Connector和Container 。从上图中可以看出 Tomcat 的心脏就是这两个组件,他们的作用:
Connector用于处理连接相关的事情,并提供Socket与Request请求和Response响应相关的转化 ;Container用于封装和管理Servlet,以及具体处理Request请求 。
??一个Tomcat中只有一个Server,一个Server可以包含多个Service,一个Service只有一个Container,但是可以有多个Connectors,这是因为一个服务可以有多个连接,如同时提供Http和Https链接,也可以提供向相同协议不同端口的连接 ,示意图: ??多个 Connector 和一个 Container 就形成了一个 Service,有了 Service 就可以对外提供服务 了,但是 Service 还要一个生存的环境,必须要有人能够给她生命、掌握其生死大权,那就是Server。所以整个 Tomcat 的生命周期由 Server 控制。 ??上述的包含关系或者说是父子关系,都可以在tomcat的conf目录下的server.xml配置文件中看出,下图是删除了注释内容之后的一个完整的server.xml配置文件(Tomcat版本为8.0): ??上边的配置文件,还可以通过下边的一张结构图来帮助理解: ??Server标签设置的端口号为8005,shutdown=”SHUTDOWN” ,表示在8005端口监听“SHUTDOWN”命令,如果接收到了就会关闭Tomcat。一个Server有一个Service,当然还可以进行配置,一个Service有多个Connector,Service左边的内容都属于Container的,Service下边是Connector。
- Tomcat架构小结
?1、Tomcat中只有一个Server,一个Server可以有多个Service,一个Service可以有多个Connector和一个Container ; ?2、Server掌管着整个Tomcat的生死大权; ?3、Service 是对外提供服务的 ; ?4、Connector用于接受请求并将请求封装成Request和Response来具体处理 ; ?5、Container用于封装和管理Servlet,以及具体处理request请求 。
1.2 Container架构
1.2.1 Container结构
??Container用于封装和管理Servlet,以及具体处理Request请求,在Container内部包含了4个子容器,结构图: ??4个子容器的作用分别是: ???1、Engine :引擎,用来管理多个站点,一个Service最多只能有一个Engine; ???2、Host :代表一个站点,也可以叫虚拟主机,通过配置Host就可以添加站点; ???3、Context :代表一个应用程序,对应着平时开发的一套程序,或者一个WEB-INF目录以及下面的web.xml文件; ???4、Wrapper :每一Wrapper封装着一个Servlet; ??下面找一个Tomcat的文件目录对照一下,如下图: ??Context和Host的区别是Context表示一个应用,Tomcat中默认的配置下webapps下的每一个文件夹目录都是一个Context,其中ROOT目录中存放着主应用,其他目录存放着子应用,而整个webapps就是一个Host站点。 ??当访问应用Context的时候,如果是ROOT下的则直接使用域名就可以访问,例如:www.baidu.com,如果是Host(webapps)下的其他应用,则可以使用www.baidu.com/docs进行访问,当然默认指定的根应用(ROOT)是可以进行设定的,只不过Host站点下默认的主应用是ROOT目录下的。 ??看到这里我们知道Container是什么,但是还是不知道Container是如何进行请求处理的以及处理完之后是如何将处理完的结果返回给Connector的。
1.2.2 Container如何处理请求的
??Container处理请求是使用Pipeline-Valve管道来处理的。Pipeline-Valve是责任链模式,责任链模式是指在一个请求处理的过程中有很多处理者依次对请求进行处理,每个处理者负责做自己相应的处理,处理完之后将处理后的结果返回,再让下一个处理者继续处理。 ??Pipeline-Valve使用的责任链模式和普通的责任链模式有些不同!区别主要有以下两点: ???1、每个Pipeline都有特定的Valve,而且是在管道的最后一个执行,这个Valve叫做BaseValve,BaseValve是不可删除的; ???2、在上层容器的管道的BaseValve中会调用下层容器的管道。 ??Container包含四个子容器,而这四个子容器对应的BaseValve分别在:StandardEngineValve、StandardHostValve、StandardContextValve、StandardWrapperValve。 ??Pipeline的处理流程图:
- Connector在接收到请求后会首先调用最顶层容器的Pipeline来处理,这里的最顶层容器的Pipeline就是EnginePipeline(Engine的管道);
- 在Engine的管道中依次会执行EngineValve1、EngineValve2等等,最后会执行StandardEngineValve,在StandardEngineValve中会调用Host管道,然后再依次执行Host的HostValve1、HostValve2等,最后在执行StandardHostValve,然后再依次调用Context的管道和Wrapper的管道,最后执行到StandardWrapperValve。
- 当执行到StandardWrapperValve的时候,会在StandardWrapperValve中创建FilterChain,并调用其doFilter方法来处理请求,这个FilterChain包含着我们配置的与请求相匹配的Filter和Servlet,其doFilter方法会依次调用所有的Filter的doFilter方法和Servlet的service方法,这样请求就得到了处理!
- 当所有的Pipeline-Valve都执行完之后,并且处理完了具体的请求,这个时候就可以将返回的结果交给Connector了,Connector在通过Socket的方式将结果返回给客户端。
1.3 从代码层面理解Tomcat架构
Bootstrap :作为 Tomcat 对外界的启动类,在$CATALINA_BASE/bin 目录下,它通过反 射创建Catalina的实例并对其进行初始化及启动。Catalina : 解析$CATALINA_BASE/conf/server.xml 文件并创建 StandardServer 、 StandardService、StandardEngine、StandardHost 等。Server :代表整个Catalina Servlet容器,可以包含一个或多个Service。Service :包含一个或多个Connector,和一个Engine,Connector和Engine都是在 解析conf/server.xml 文件时创建的,Engine在Tomcat的标准实现是StandardEngine。Connector :实现某一协议的连接器,用来处理客户端发送来的协议,如默认的实现协议有HTTP、HTTPS、AJP。 ??Connector的主要作用有:
- 根据不同的协议解析客户端的请求;
- 将解析完的请求转发给 Connector 关联的 Engine 容器处理。
MapperListener :实现了LifecycleListener和ContainerListener接口,用于监听容器事件和生命周期事件。该监听器实例监听所有的容器,包括 StandardEngine、StandardHost、StandardContext、StandardWrapper,当容器有变动时,注册容器到Mapper。Engine :代表的是Servlet`引擎,接收来自不同Connector的请求,处理后将结果返回给Connector。Engine 是一个逻辑容器,包含一个或多个 Host。默认实现是StandardEngine, ??Engine主要有以下模块:
Cluster:实现 Tomcat 管理; Realm:实现用户权限管理模块; Pipeline和Valve(阀门):处理Pipeline上的各个Valve,是一种责任链模式。只是简单的将Connector传过来的变量传给Host容器。
Host :虚拟主机,即域名或网络名,用于部署该虚拟主机上的应用程序。通常包含多个Context (Context 在 Tomcat 中代表应用程序)。Context 在 Tomcat 中的标准实现是StandardContext。Context :部署的具体Web应用,每个请求都在是相应的上下文里处理,如一个war包。默认实现是StandardContext,通常包含多个Wrapper,主要有以下模块:
Realm:实现用户权限管理模块; Pipeline和Valve:处理Pipeline上的各个Valve,是一种责任链模式; Manager: 它主要是应用的 session 管理模块; Resources: 它是每个 web app 对应的部署结构的封装; Loader:它是对每个 web app 的自有的 classloader 的封装; Mapper:它封装了请求资源 URI 与每个相对应的处理 wrapper 容器的映射关系。
Wrapper :对应定义的 Servlet,一一对应。默认实现是 StandardWrapper,主要有以下模块:
Pipeline 和 Valve:处理 Pipeline 上的各个 Valve,是一种责任链模式 Servlet 和 Servlet Stack:保存 Wrapper 包装的 Servlet
StandardPipeline ,组件代表一个流水线,与 Valve(阀)结合,用于处理请求。 ??StandardPipeline 中含有多个 Valve, 当需要处理请求时,会逐一调用 Valve 的invoke 方法对 Request 和 Response 进行处理。特别的,其中有一个特殊的Valve,叫basicValve,每一个标准容器都有一个指定的 BasicValve,他们做的是最核心的工作。
StandardEngine指定的BasicValve是StandardEngineValve,他用来将 Request 映射到指定的Host; StandardHost指定的BasicValve是StandardHostValve, 他用来将Request映射到指定的Context; StandardContext指定的BasicValve是StandardContextValve,它用来将 Request 映射到指定的Wrapper; StandardWrapper指定的BasicValve是StandardWrapperValve,他用来加载 Rquest 所指定的Servlet,并调用 Servlet 的 Service 方法。
??由上可知,Catalina 中有两个主要的模块:连接器(Connector)和容器(Container) 。 ??以Tomcat 为例,它的主线流程大致可以分为 3 个:启动、部署、请求处理。入口点就是Bootstrap 类和 接受请求的 Acceptor 类。
二、Tomcat的生命周期
??在Tomcat启动时,会读取server.xml文件创建Server、Service、Connector、Engine、Host、Context、Wrapper等组件。
- 1、Lifestyle
??Tomcat 中的所有组件都继承了 Lifecycle 接口,Lifecycle 接口定义了一整套生命周期管理的函数,从组件的新建、初始化完成、启动、停止、失败到销毁,都遵守同样的规则,Lifecycle组件的状态转换图: ??正常的调用顺序是 init()->start()->destroy(),父组件的 init()和start()会触发子组件的 init()和 start(),所以 Tomcat 中只需调用 Server 组件的 init()和 start()即可。 ??每个实现组件都继承自 LifecycleBase,LifecycleBase 实现了 Lifecycle 接口,当容器状态发生变化时,都会调用 fireLifecycleEvent 方法,生成 LifecycleEvent,并且交由此容器的事件监听器处理。
2.1 Tomcat的启动流程
??tomcat/bin/startup.sh脚本,是启动了org.apache.catalina.startup.Bootstra 类的main方法,并传入start参数。 ??Tomcat启动的主要步骤:
- 新建Bootstrap对象 daemon,并调用其init()方法;
- 初始化Tomcat的类加载器(init);
- 用反射实例化org.apache.catalina.startup.Catalina对象catalinaDaemon(init);
- 调用daemon的load方法,实质上调用了catalinaDaemon的load方法(load);
- 加载和解析server.xml配置文件(load);
- 调用daemon的start方法,实质上调用了 catalinaDaemon 的 start 方法 (start);
- 启动Server组件,Server的启动会带动其他组件的启动,如Service, Container, Connector(start);
- 调用catalinaDaemon的await方法,循环等待接收Tomcat的shutdown命令。
2.2 BootStrap的main方法
2.2.1 BootStrap的init阶段
??init方法的主要内容:
- 初始化类加载器catalinaLoader并将其设为当前线程的父加载器。
- 预加载tomcat、javax包等自定义类。
- 一个org.apache.catalina.startup.Catalina对象(catalinaDaemon),该对象则负责了后续整个tomcat服务器的启动工作。
??源码:
public void init() throws Exception {
initClassLoaders();
Thread.currentThread().setContextClassLoader(catalinaLoader);
SecurityClassLoad.securityClassLoad(catalinaLoader);
if (log.isDebugEnabled())
log.debug("Loading startup class");
Class<?> startupClass = catalinaLoader.loadClass("org.apache.catalina.startup.Catalina");
Object startupInstance = startupClass.getConstructor().newInstance();
if (log.isDebugEnabled())
log.debug("Setting startup class properties");
String methodName = "setParentClassLoader";
Class<?> paramTypes[] = new Class[1];
paramTypes[0] = Class.forName("java.lang.ClassLoader");
Object paramValues[] = new Object[1];
paramValues[0] = sharedLoader;
Method method =
startupInstance.getClass().getMethod(methodName, paramTypes);
method.invoke(startupInstance, paramValues);
catalinaDaemon = startupInstance;
}
2.2.2 BootStrap的load阶段
??load方法的主要内容:
- 创建server.xml解析器digester实例。
- digester实例解析server.xml文件内容、并创建standardServer实例。
- standardServer实例绑定catalina容器、设置catalina所在路径。
- 初始化standardServer实例及所有组件Service、Connector、Engine等。
??源码:
private void load(String[] arguments) throws Exception {
String methodName = "load";
Object param[];
Class<?> paramTypes[];
if (arguments==null || arguments.length==0) {
paramTypes = null;
param = null;
} else {
paramTypes = new Class[1];
paramTypes[0] = arguments.getClass();
param = new Object[1];
param[0] = arguments;
}
Method method =
catalinaDaemon.getClass().getMethod(methodName, paramTypes);
if (log.isDebugEnabled())
log.debug("Calling startup class " + method);
method.invoke(catalinaDaemon, param);
}
2.2.3 BootStrap的start阶段
??start方法的主要内容:
- 判断catalinaDaemon后台实例是否完成初始化,若没有则重新实例化。
- 反射调用catalinaDaemon的start()方法,依序地启动server、service、engine容器、connector连接器等组件,若启动过程中出现异常则调用destory()方法销毁所有组件。
- catalinaDaemon注册钩子函数,保证standardServer、logManager实例在关闭时能被关闭、销毁。
??源码:
public void start() throws Exception {
if( catalinaDaemon==null ) init();
Method method = catalinaDaemon.getClass().getMethod("start", (Class[] )null);
method.invoke(catalinaDaemon, (Object [])null);
}
2.3 Tomcat的停止流程
??catalinaDaemon调用await等待停止命令,开发者一般是通过执行tomcat/bin/shutdown.sh来关闭 Tomcat,等价于执行org.apache.catalina.startup.Bootstra 类的main方法,并传入stop参数。 ??停止逻辑:
- 新建Bootstrap对象 daemon,并调用其 init()方法;
- 初始化Tomcat的类加载器;
- 用反射实例化org.apache.catalina.startupCatalina对象catalinaDaemon;
- 调用daemon的stopServer方法,实质上调用了catalinaDaemon的stopServer方法;
- 解析server.xml文件,构造出Server容器;
- 获取Server的socket监听端口和地址,创建Socket对象连接启动Tomcat时创建的
ServerSocket,最后向ServerSocket发送SHUTDOWN命令; - 运行中的Server调用stop方法停止。
??BootStrap的stopServer方法源码:
public void stopServer(String[] arguments) throws Exception {
Object param[];
Class<?> paramTypes[];
if (arguments==null || arguments.length==0) {
paramTypes = null;
param = null;
} else {
paramTypes = new Class[1];
paramTypes[0] = arguments.getClass();
param = new Object[1];
param[0] = arguments;
}
Method method =
catalinaDaemon.getClass().getMethod("stopServer", paramTypes);
method.invoke(catalinaDaemon, param);
}
2.4 请求处理
2.4.1 Connector
??在Tomcat9中,Connector支持的协议是HTTP和AJP,协议处理类分别对应org.apache.coyote.http11.Http11NioProtocol和org.apache.coyote.http11.Http11AprProtocol(已经取消 BIO 模式)。 ??Connector主要包含三个模块:Http11NioProtocol、Mapper、CoyoteAdapter。http请求在Connector中的流程:
- Acceptor为监听线程,调用serverSocketAccept()阻塞,本质上调用ServerSocketChannel.accept();
- Acceptor将接收到的Socket添加到Poller池中的一个Poller;
- Poller通过worker线程把socket包装成SocketProcessor;
- SocketProcessor调用getHandler()获取对应的ConnectionHandler;
- ConnectionHandler把socket交由Http11Processor处理,解析http的Header和Body;
- Http11Processor调用service()把包装好的request和response传给CoyoteAdapter;
- CoyoteAdapter会通过Mapper,把请求对应的session、servlet等关联好,准备传给
Container。
2.4.2 Container
??有4个Container,采用了责任链的设计模式。 ??Pipeline 就像是每个容器的逻辑总线,在Pipeline上按照配置的顺序,加载各个Valve。通过Pipeline完成各个Valve之间的调用,各个Valve实现具体的应用逻辑。每个请求在Pipeline上流动,经过每个Container(对应着一个或多个Valve阀门),各个Container按顺序处理请求,最终在Wrapper结束。 ??Connector中的CoyoteAdapter会调用invoke(),把request和response传给 Container,Container中依次调用各个Valve,每个Valve的作用:
- StandardEngineValve:StandardEngine中的唯一阀门,主要用于从request中选择其host映射的Host容器StandardHost;
- AccessLogValve:StandardHost中的第一个阀门,主要用于管道执行结束之后记录日志信息;
- ErrorReportValve:StandardHost中紧跟AccessLogValve的阀门,主要用于管道执行结束后,从request对象中获取异常信息,并封装到response中以便将问题展现给访问者;
- StandardHostValve: StandardHost中最后的阀门,主要用于从request中选择其
context 映射的Context容器StandardContext以及访问request中的Session以更新会话的最后访问时间; - StandardContextValve:StandardContext中的唯一阀门,主要作用是禁止任何对
WEB-INF或META-INF目录下资源的重定向访问,对应用程序热部署功能的实现,从request中获得StandardWrapper; - StandardWrapperValve : StandardWrapper中的唯一阀门,主要作用包括调用StandardWrapper的loadServlet方法生成Servlet实例和调用ApplicationFilterFactory 生成Filter链。
??最终将Response返回给Connector完成一次http的请求。
2.4.3 NioEndPoint
??在Tomcat中,Endpoint主要用来接收网络请求,处理则由ConnectionHandler来执行。 ??NioEndPoint包含了三个组件Acceptor、Poller和SocketProcessor: ??Acceptor :后台线程,负责监听请求,将接收到的Socket请求放到Poller队列中。 ??Poller :后台线程,当Socket就绪时,将Poller队列中的Socket交给Worker线程池处理。 ??SocketProcessor(Worker) :处理socket,本质上委托ConnectionHandler处理。 ??Connector启动以后会启动一组线程用于不同阶段的请求处理过程。Acceptor、Poller、worker所在的ThreadPoolExecutor都维护在NioEndpoint中。 ??Acceptor线程组 。用于接受新连接,并将新连接封装一下,选择一个Poller将新连接添加到Poller的事件队列中。 ??Poller线程组 。用于监听Socket事件,当Socket可读或可写等等时,将Socket封装一下添加到worker线程池的任务队列中。 ??worker线程组 。用于对请求进行处理,包括分析请求报文并创建Request对象,调用容器的pipeline进行处理。
- 1、 Acceptor的run方法
- Acceptor在启动后会阻塞在
ServerSocketChannel.accept(); 方法处,当有新连接到达时,该方法返回一个SocketChannel。 - 配置完Socket以后,将Socket封装到NioChannel中,并注册到Poller。注意:一开始就启动了多个Poller线程,注册的时候,连接是公平的分配到每个Poller的。NioEndpoint维护了一个Poller数组,当一个连接分配给pollers[index]时,下一个连接就会分配给
pollers[(index+1)%pollers.length] 。 - addEvent()方法会将Socket添加到该Poller的PollerEvent队列中。到此Acceptor的任务就完成了。
??Acceptor的run方法源码:
public void run() {
int errorDelay = 0;
while (endpoint.isRunning()) {
while (endpoint.isPaused() && endpoint.isRunning()) {
state = AcceptorState.PAUSED;
try {
Thread.sleep(50);
} catch (InterruptedException e) {
}
}
if (!endpoint.isRunning()) {
break;
}
state = AcceptorState.RUNNING;
try {
endpoint.countUpOrAwaitConnection();
if (endpoint.isPaused()) {
continue;
}
U socket = null;
try {
socket = endpoint.serverSocketAccept();
} catch (Exception ioe) {
endpoint.countDownConnection();
if (endpoint.isRunning()) {
errorDelay = handleExceptionWithDelay(errorDelay);
throw ioe;
} else {
break;
}
}
errorDelay = 0;
if (endpoint.isRunning() && !endpoint.isPaused()) {
if (!endpoint.setSocketOptions(socket)) {
endpoint.closeSocket(socket);
}
} else {
endpoint.destroySocket(socket);
}
} catch (Throwable t) {
ExceptionUtils.handleThrowable(t);
String msg = sm.getString("endpoint.accept.fail");
if (t instanceof Error) {
Error e = (Error) t;
if (e.getError() == 233) {
log.warn(msg, t);
} else {
log.error(msg, t);
}
} else {
log.error(msg, t);
}
}
}
state = AcceptorState.ENDED;
}
- 2、Poller的run方法
- 当Poller启动后,因为Selector中并没有已注册的Channel,所以当执行到该方法时只能阻塞。所有的Poller共用一个Selector,其实现类是sun.nio.ch.EPollSelectorImpl。
- events()方法会将通过addEvent()方法,添加到事件队列中的Socket注册到EPollSelectorImpl,当Socket可读时,Poller才对其进行处理。
- createSocketProcessor()方法,将Socket封装到SocketProcessor中,SocketProcessor实现了Runnable接口。worker线程通过调用其run()方法来对Socket进行处理。
- execute(SocketProcessor)方法,将SocketProcessor提交到线程池,放入线程池的workQueue中。workQueue是BlockingQueue的实例。到此Poller的任务就完成了。
??Poller的run方法源码:
public void run() {
while (true) {
boolean hasEvents = false;
try {
if (!close) {
hasEvents = events();
if (wakeupCounter.getAndSet(-1) > 0) {
keyCount = selector.selectNow();
} else {
keyCount = selector.select(selectorTimeout);
}
wakeupCounter.set(0);
}
if (close) {
events();
timeout(0, false);
try {
selector.close();
} catch (IOException ioe) {
log.error(sm.getString("endpoint.nio.selectorCloseFail"),ioe);
}
break;
}
} catch (Throwable x) {
ExceptionUtils.handleThrowable(x);
log.error("",x);
continue;
}
if ( keyCount == 0 ) hasEvents = (hasEvents | events());
Iterator<SelectionKey> iterator = keyCount > 0 ? selector.selectedKeys().iterator() : null;
while (iterator != null && iterator.hasNext()) {
SelectionKey sk = iterator.next();
NioSocketWrapper attachment = (NioSocketWrapper)sk.attachment();
if (attachment == null) {
iterator.remove();
} else {
iterator.remove();
processKey(sk, attachment);
}
}
timeout(keyCount,hasEvents);
}
getStopLatch().countDown();
}
- 3、Worker的run方法
- worker线程被创建以后就执行ThreadPoolExecutor的runWorker()方法,试图从workQueue中取待处理任务,但是一开始workQueue是空的,所以worker线程会阻塞在 workQueue.take()方法。
- 当新任务添加到workQueue后,workQueue.take()方法会返回一个Runnable,通常是SocketProcessor,然后worker线程调用SocketProcessor的run()方法对Socket进行处理。
- createProcessor()会创建一个Http11Processor,它用来解析Socket,将Socket中的内容封装到Request中。注意这个Request是临时使用的一个类,它的全类名是org.apache.coyote.Request,
- postParseRequest()方法封装一下Request,并处理一下映射关系(从URL映射到相应的Host、Context、Wrapper)。
- CoyoteAdapter将Rquest提交给Container处理之前,并将org.apache.coyote.Request 封装到org.apache.catalina.connector.Request,传递给Container处理的Request是org.apache.catalina.connector.Request。
- connector.getService().getMapper().map(),用来在Mapper中查询URL的映射关系。映射关系会保留到org.apache.catalina.connector.Request中,Container处理阶段,request.getHost()是使用的就是这个阶段查询到的映射主机,以此类推request.getContext()、request.getWrapper()都是。
- connector.getService().getContainer().getPipeline().getFirst().invoke(),会将请求传递到Container处理,当然了Container处理也是在Worker线程中执行的。
三、Tomcat类加载器
??Tomcat不能直接使用系统的类加载器,必须要实现自定义的类加载器。 Servlet应该只允许加载 WEB-INF/classes目录及其子目录下的类,和从部署的库到WEB-INF/lib目录加载类,实现不同的应用之间的隔离。另一个要实现自定义类加载器的原因是,为了提供热加载的功能。如果WEB-INF/classes或WEB-INF/lib目录下的类发生变化时,Tomcat应该会重新加载这些类。在Tomcat的类加载中,类加载使用一个额外的线程,不断检查Servlet类和其他类的文件的时间戳。Tomcat所有类加载器必须实现Loader接口,支持热加载的还需要实现Reloader接口。
- Tomcat类加载器结构
??commonLoader、catalinaLoader和sharedLoader是在Tomcat 容器初始化时创建的。catalinaLoader会被设置为Tomcat主线程的线程上下文类加载器,并且使用catalinaLoader加载Tomcat容器自身的class。 ??它们三个都是URLClassLoader类的一个实例,只是它们的类加载路径不一样,在tomcat/conf/catalina.properties 配置文件中配置(common.loader,server.loader,shared.loader)。
3.1 应用隔离
??对于每个webapp应用,都会对应唯一的StandContext,在StandContext中会引用WebappLoader,该类又会引用 WebappClassLoader,WebappClassLoader 就是真正加载webapp的classloader。 ??不同的StandardContext有不同的WebappClassLoader,那么不同的webapp 的类加载器就是不一致的。加载器的不一致带来了名称空间不一致,所以webapp之间是相互隔离的。 ??WebappClassLoader加载class的步骤:
- 先检查 webappclassloader 的缓存是否有该类。
- 为防止webapp覆盖java se类,尝试用application classloader(应用类加载器)加载。
- 尝试WebappClassLoader自己加载class。
- 最后无条件地委托给父加载器common classloader,加载CATALINA_HOME/lib下的类。
- 如果都没有加载成功,则抛出ClassNotFoundException异常。
3.2 热部署
??后台的定期检查,该定期检查是StandardContext的一个后台线程,会做reload的check,过期session清理等等,这里的modified实际上调用了WebappClassLoader中的方法以判断这个 class 是不是已经修改。注意到它调用了StandardContext的reload方法。
四、Tomcat常见问题
4.1 Tomcat的缺省端口是多少,怎么修改
??默认是8080端口 。以Tomcat7为例,其目录结构示例: ??修改方式:找到Tomcat目录下的conf文件夹,进入conf文件夹里面找到server.xml 文件,在server.xml文件里面找到下列信息, 把Connector标签的8080端口改成你想要的端口:
<Service name="Catalina">
<Connector port="8080" protocol="HTTP/1.1"
connectionTimeout="20000"
redirectPort="8443" />
4.2 Tomcat Connector(Tomcat连接器)有几种运行模式
??Tomcat Connector的三种运行模式,其配置项是在conf/server.xml文件中进行配置。
- 1、BIO:同步并阻塞
??传统的Java I/O操作,同步且阻塞IO。 ??一个线程处理一个请求 。缺点:并发量高时,线程数较多,浪费资源。Tomcat7或以下,在Linux系统中默认使用这种方式 。该模式性能最差,没有经过任何优化处理和支持。 ??配置项:protocol="HTTP/1.1" 。 ??maxThreads="150" 。Tomcat 使用线程来处理接收的每个请求。这个值表示Tomcat 可创建的最大的线程数。默认值 200。可以根据机器的时期性能和内存大小调整,一般可以在 400-500。最大可以在 800 左右。 ??minSpareThreads="25" 。Tomcat 初始化时创建的线程数。默认值 4。如果当前没有空闲线程,且没有超过 maxThreads,一次性创建的空闲线程数量。Tomcat 初始化时创建的线程数量也由此值设置。 ??maxSpareThreads="75" 。一旦创建的线程超过这个值,Tomcat 就会关闭不再需要的 socket 线程。默认值 50。一旦创建的线程超过此数值,Tomcat 会关闭不再需要的线程。线程数可以大致上用 “同时在线人数每秒用户操作次数系统平均操作时间” 来计算。 ??acceptCount="100" 。指定当所有可以使用的处理请求的线程数都被使用时,可以放到处理队列中的请求数,超过这个数的请求将不予处理。默认值10。如果当前可用线程数为 0,则将请求放入处理队列中。这个值限定了请求队列的大小,超过这个数值的请求将不予处理。 ??connectionTimeout="20000" 。网络连接超时,默认值 20000,单位:毫秒。设置为 0 表示永不超时,这样设置有隐患的。通常可设置为 30000 毫秒。 - 2、NIO:同步非阻塞IO
??利用Java的异步IO处理,可以通过少量的线程处理大量的请求,可以复用同一个线程处理多个connection(多路复用) 。 ??是Java SE 1.4及后续版本提供的一种新的I/O操作方式(即java.nio包及其子包)。Java nio是一个基于缓冲区、并能提供非阻塞I/O操作的Java API,因此nio也被看成是non-blocking I/O的缩写。它拥有比BIO更好的并发运行性能。 ??Tomcat8在Linux系统中默认使用这种方式。Tomcat7必须修改Connector配置来启动 。 ??配置项:protocol="org.apache.coyote.http11.Http11NioProtocol" ??备注:我们常用的Jetty,Mina,ZooKeeper等都是基于java nio实现。 - 3、APR(Apache Portable Runtime/Apache可移植运行时库)
??Tomcat将以JNI的形式调用Apache HTTP服务器的核心动态链接库来处理文件读取或网络传输操作,从而大大地提高Tomcat对静态文件的处理性能。从操作系统级别来解决异步的IO问题,大幅度的提高性能。 ??Tomcat apr也是在Tomcat上运行高并发应用的首选模式 。 ??配置项:protocol="org.apache.coyote.http11.Http11AprProtocol" ??备注:需在本地服务器安装APR库。Tomcat7或Tomcat8在Win7或以上的系统中启动默认使用这种方式。Linux如果安装了apr和native,Tomcat直接启动就支持apr。
4.3 Tomcat有几种部署方式
??在Tomcat中部署Web应用的方式:
- 1、利用Tomcat的自动部署
??直接将 web 项目文件(一般是复制生成的war包)复制到tomcat的webapps目录中,启动Tomcat ,即可访问。 - 2、修改conf/server.xml文件部署
??修改conf/server.xml文件,增加Context节点可以部署应用 。示例:
4.4 怎么在Linux上安装Tomcat
- 先去下载Tomcat的安装包;
- 上传到Linux上,解压;
- 修改端口号,也可以不修改;
- 修改好了之后,你就进入你这个tomcat下的bin目录, 执行
./startup.sh ,这样就启动成功。
4.5 怎么在Linux部署项目
??先使用eclipse或IDEA把项目打成.war包,然后上传到Linux服务器,然后把项目放在Tomcat的bin目录下的webapps,在重启Tomcat就行。
4.6 Tomcat的目录结构
??/bin:存放用于启动和暂停Tomcat的脚本 。 ??/conf:存放Tomcat的配置文件 。 ??/lib:存放Tomcat服务器需要的各种jar包。 ??/logs:存放Tomcat的日志文件。 ??/temp:Tomcat运行时用于存放临时文件。 ??/webapps:web应用的发布目录 。 ??/work:Tomcat把有jsp生成Servlet放于此目录下。
4.7 Connector和Container的关系
??Tomcat处理请求的流程:一个请求发送到Tomcat之后,首先经过Service然后会交给Connector,Connector用于接收请求并将接收的请求封装为Request和Response来具体处理,Request和Response封装完之后再交由Container进行处理,Container处理完请求之后再返回给Connector,最后在由Connector通过Socket将处理的结果返回给客户端。 ??Connector最底层使用的是Socket来进行连接的,Request和Response是按照HTTP协议来封装的,所以Connector同时需要实现TCP/IP协议和HTTP协议 。
??Connector用于接受请求并将请求封装成Request和Response,然后交给Container进行处理,Container处理完之后在交给Connector返回给客户端。因此,可以把Connector分为四个方面进行理解:
- Connector如何接受请求的?
- 如何将请求封装成Request和Response的?
- 封装完之后的Request和Response如何交给Container进行处理的?
- Container处理完之后如何交给Connector并返回给客户端的?
??Connector的结构图: ??Connector就是使用ProtocolHandler来处理请求的,不同的ProtocolHandler代表不同的连接类型,比如:Http11Protocol使用的是普通Socket来连接的,Http11NioProtocol使用的是NioSocket来连接的。 ??其中ProtocolHandler由包含了三个部件:Endpoint、Processor、Adapter: ???1、Endpoint用来处理底层Socket的网络连接,Processor用于将Endpoint接收到的Socket封装成Request,Adapter用于将Request交给Container进行具体的处理 。 ???2、Endpoint由于是处理底层的Socket网络连接,因此Endpoint是用来实现TCP/IP协议的,而Processor用来实现HTTP协议的,Adapter将请求适配到Servlet容器进行具体的处理。 ???3、Endpoint的抽象实现AbstractEndpoint里面定义的Acceptor和AsyncTimeout两个内部类和一个Handler接口。Acceptor用于监听请求,AsyncTimeout用于检查异步Request的超时,Handler用于处理接收到的Socket,在内部调用Processor进行处理。
五、Tomcat调优
- Connector的流程
??Tomcat有一个acceptor线程来accept socket连接,然后有工作线程来进行业务处理。对client端的一个请求进来,流程是这样的:tcp的三次握手建立连接,建立连接的过程中,OS维护了半连接队列(syn 队列)以及完全连接队列(accept 队列),在第三次握手之后,server收到了client的ack,则进入establish的状态,然后该连接由syn队列移动到accept队列。 ??Tomcat的acceptor线程则负责从accept队列中取出该connection,接受该connection,然后交给工作线程去处理(读取请求参数、处理逻辑、返回响应等等;如果该连接不是keep alived的话,则关闭该连接,然后该工作线程释放回线程池,如果是 keep alived 的话,则等待下一个数据包的到来直到keepAliveTimeout,然后关闭该连接释放回线程池),然后自己接着去accept队列取connection(当前socket连接超过maxConnections 的时候,acceptor线程自己会阻塞等待,等连接降下去之后,才去处理accept队列的下一个连接)。acceptCount指的就是这个accept队列的大小。
5.1 参数调优示例
??server.xml中部分参数默认值:
<Connector port="8080" protocol="HTTP/1.1"
connectionTimeout="20000"
redirectPort="8443" />
??调整后:
<Connector port="8080" protocol="org.apache.coyote.http11.Http11Nio2Protocol"
connectionTimeout="20000"
redirectPort="8443"
executor="TomcatThreadPool"
enableLookups="false"
acceptCount="100"
maxPostSize="10485760"
compression="on"
disableUploadTimeout="true"
compressionMinSize="2048"
noCompressionUserAgents="gozilla, traviata"
acceptorThreadCount="2"
compressableMimeType="text/html,text/xml,text/plain,text/css,text/javascript,application/jav
ascript"
URIEncoding="utf-8"/>
??线程池的相关设置:
<Executor name="TomcatThreadPool" namePrefix="catalina-exec-"
maxThreads="150" minSpareThreads="100"
prestartminSpareThreads="true" maxQueueSize="100"/>
- protocol
??Tomcat8设置nio2更好:org.apache.coyote.http11.Http11Nio2Protocol (如果这个用不了,就用下面那个)。 ??Tomcat6、7设置nio更好:org.apache.coyote.http11.Http11NioProtocol 。 ??apr:调用httpd核心链接库来读取或文件传输,从而提高 tomat 对静态文件的处理性能。Tomcat APR 模式也是Tomcat在高并发下的首选运行模式。 - URIEncoding
??设置为"UTF-8",可以使得Tomcat可以解析含有中文名的文件的url。 minSpareThreads ??类似于corePoolSize,最小备用线程数,Tomcat启动时的初始化的线程数。 - maxThreads
??Tomcat使用线程来处理接收的每个请求。这个值表示Tomcat可创建的最大的 线程数,即最大并发数。默认设置 200,一般建议在 500 ~ 800,根据硬件设施和业务来判断。 - maxQueueSize
??指定当所有可以使用的处理请求的线程数都被使用时,可以放到处理队列中的请求数,超过这个数的请求将不予处理,默认设置100。 - connectionTimeout
??网络连接超时时间毫秒数。 - enableLookups
??enableLookups=“false”,为了消除DNS查询对性能的影响,可以关闭 DNS 查询,方式是修改server.xml文件中的enableLookups参数值。 - maxConnections
??这个值表示最多可以有多少个socket连接到Tomcat上。NIO模式下默认是 10000。当连接数达到最大值后,系统会继续接收连接但不会超过acceptCount的值。 - acceptorThreadCount
??用于接收连接的线程的数量,默认值是1。一般这个指需要改动的时候是因为该服务器是一个多核CPU,如果是多核CPU一般配置为 2。 - HTTP压缩相关的配置
??示例:
compression=“on” compressionMinSize=“2048” compressableMimeType=“text/html,text/xml,text/javascript,text/css,text/plain”
??HTTP压缩可以大大提高浏览网站的速度,它的原理是,在客户端请求网页后,从服务器端将网页文件压缩,再下载到客户端,由客户端的浏览器负责解压缩并浏览。相对于普通的浏览过程 HTML,CSS,Javascript ,Text,它可以节省 40%左右的流量。更为重要的是,它可以对动态生成的,包括 CGI、PHP , JSP , ASP , Servlet,SHTML 等输出的网页也能进行压缩,压缩效率惊人。 ??优化配置示例:
1)compression=“on” 打开压缩功能 2)compressionMinSize=“2048” 启用压缩的输出内容大小,这里面默认为 2KB 3)noCompressionUserAgents=“gozilla, traviata” 对于以下的浏览器,不启用压缩 4)compressableMimeType=“text/html,text/xml” 压缩类型
5.2 Tomcat优化思路
5.2.1 优化连接配置
??以Tomcat7的参数配置为例,需要修改conf/server.xml文件,修改连接相关的参数: ??maxSpareThreads : 如果空闲状态的线程数多于设置的数目,则将这些线程中止,减少这个池中的线程总数。 ??minSpareThreads : 最小备用线程数,Tomcat启动时的初始化的线程数。 ??connectionTimeout : connectionTimeout为网络连接超时时间毫秒数。通常可设置为 30000 毫秒。 ??maxThreads : Tomcat使用线程来处理接收的每个请求。这个值表示Tomcat可创建的最大的线程数,即最大并发数。 ??acceptCount : 允许的最大连接数,acceptCount是当线程数达到maxThreads后,后续请求会被放入一个等待队列,这个acceptCount是这个队列的大小,如果这个队列也满了,就直接refuse connection。acceptCount应大于等于maxProcessors,默认值为100。 ??minProcessors:最小空闲连接线程数 ,用于提高系统处理性能,默认值为10。此属性表示服务器启动时创建的处理请求的线程数应该足够处理一个小量的负载。也就是说,如果一天内每秒仅发生5次单击事件,并且每个请求任务处理需要1秒钟,那么预先设置线程数为5就足够。 ??maxProcessors:最大连接线程数 ,即:并发处理的最大请求数,默认值为75。
??其中和最大连接数相关的参数为maxProcessors和acceptCount。如果要加大并发连接数,应同时加大这两个参数。 ??Web服务器允许的最大连接数还受制于操作系统的内核参数设置,通常Windows是2000个左右,Linux是1000个左右。
5.2.2 Tomcat内存优化
??内存方式的设置是在catalina.sh中,调整一下JAVA_OPTS 变量即可,因为后面的启动参数会把JAVA_OPTS作为JVM的启动参数来处理。 ??具体设置示例:
JAVA_OPTS="$JAVA_OPTS -Xmx3550m -Xms3550m -Xss128k -XX:NewRatio=4 -XX:SurvivorRatio=4"
??参数含义: ??-Xmx 3550m:设置JVM最大可用堆内存 为3550M。一般建议堆的最大值设置为可用内存的最大值的80%。 ??-Xms 3550m:设置JVM初始内存 为3550m。此值可以设置与-Xmx 相同,以避免每次垃圾回收完成后 JVM 重新分配内存。 ??-Xmn 2g:设置年轻代大小 为2G。整个堆大小=年轻代大小 + 年老代大小 + 持久代大小。持久代一般固定大小为 64m,所以增大年轻代后,将会减小年老代大小。此值对系统性能影响较大,Sun官方推荐年轻代配置为整个堆的 3/8 。 ??-Xss 128k:设置每个线程的堆栈大小 。JDK5.0 以后每个线程堆栈大小为1M,以前每个线程堆栈大小为 256K。更具应用的线程所需内存大小进行调整。在相同物理内存下,减小这个值能生成更多的线程。但是操作系统对一个进程内的线程数还是有限制的,不能无限生成,经验值在 3000~5000 左右。 ??-XX:NewRatio =4:设置年轻代(包括 Eden 和两个 Survivor 区)与年老代的比值 (除去持久代)。设置为 4,则年轻代与年老代所占比值为 1:4,年轻代占整个堆栈的 1/5。 ??-XX:SurvivorRatio =4:设置年轻代中Eden区与Survivor区的大小比值 。设置为 4,则两个 Survivor 区与一个 Eden 区的比值为 2:4,一个 Survivor 区占整个年轻代的 1/6。 ??-XX:MaxPermSize =16m:设置持久代大小 为 16m。 ??-XX:MaxTenuringThreshold =0:设置垃圾最大年龄 。如果设置为 0 的话,则年轻代对象不经过 Survivor 区,直接进入年老代。对于年老代比较多的应用,可以提高效率。如果将此值设置为一个较大值,则年轻代对象会在 Survivor 区进行多次复制,这样可以增加对象再年轻代的存活时间,增加在年轻代即被回收的概率。
5.2.3 禁用DNS查询
??当Web应用程序向要记录客户端的信息时,它也会记录客户端的IP地址或者通过域名服务器查找机器名转换为IP地址。DNS查询需要占用网络,并且包括可能从很多很远的服务器或者不起作用的服务器上去获取对应的 IP 的过程,这样会消耗一定的时间。为了消除DNS查询对性能的影响我们可以关闭DNS查询,方式是修改server.xml文件中的 enableLookups 参数值。
|