1.Tomcat总体架构
Tomcat要实现2个核心功能:
-
处理Socket连接,负责网络字节流与Request和Response对象的转化。
-
加载和管理Servlet,以及具体处理Request请求。
Tomcat设计了两个核心组件连接器(Connector)和容器(Container)来分别做这两件事情。连接器负责对外交流,容器负责内部处理。
2.连接器有三个组件组成
Endpoint负责提供字节流给Processor,Processor负责提供Tomcat Request对象给Adapter,Adapter负责提供ServletRequest对象给容器。
Endpoint是通信端点,即通信监听的接口,是具体的Socket接收和发送处理器,是对传输层的抽象,因此Endpoint是用来实现TCP/IP协议的。(有两个重要的子组件:Acceptor和SocketProcessor。其中Acceptor用于监听Socket连接请求。SocketProcessor用于处理接收到的Socket请求,它实现Runnable接口,在run方法里调用协议处理组件Processor进行处理。为了)
Processor用来实现HTTP协议,Processor接收来自Endpoint的Socket,读取字节流解析成Tomcat Request和Response对象,并通过Adapter将其提交到容器处理,Processor是对应用层协议的抽象。
ProtocolHandler接口负责解析请求并生成Tomcat Request类。但是这个Request对象不是标准的ServletRequest,也就意味着,不能用Tomcat Request作为参数来调用容器。Tomcat设计者的解决方案是引入CoyoteAdapter,这是适配器模式的经典运用,连接器调用CoyoteAdapter的sevice方法,传入的是Tomcat Request对象,CoyoteAdapter负责将Tomcat Request转成ServletRequest,再调用容器的service方法。
6.容器
Tomcat设计了4种容器,分别是Engine、Host、Context和Wrapper。这4种容器不是平行关系,而是父子关系
连接器中的Adapter会调用容器的Service方法来执行Servlet,最先拿到请求的是Engine容器,Engine容器对请求做一些处理后,会把请求传给自己子容器Host继续处理,依次类推,最后这个请求会传给Wrapper容器,Wrapper会调用最终的Servlet来处理。那么这个调用过程具体是怎么实现的呢?答案是使用Pipeline-Valve管道。
7. 一键式启停:Lifecycle接口,使用观察者模式,
8. Tomcat的“高层们”都负责做什么
十一、Spring Bean的生命周期过程
十二 优化并提高Tomcat启动速度
清理tomcat (1. 清理不必要的Web应用;2. 清理XML配置文件;3. 清理JAR文件)
禁止Tomcat TLD扫描
关闭WebSocket支持
禁止Servlet注解扫描关闭JSP支持
随机数熵源优化(换非阻塞式)并行启动多个Web应用(artStopThreads的值表示你想用多少个线程来启动你的Web应用,如果设成0表示你要并行启动Web应用)
十四 IO模型
I/O模型是为了解决内存和外部设备速度差异的问题。我们平时说的阻塞或非阻塞是指应用程序在发起I/O操作时,是立即返回还是等待。而同步和异步,是指应用程序在与内核通信时,数据从内核空间到应用空间的拷贝,是由内核主动发起还是由应用程序来触发。
高并发思路。
在Tomcat中,Endpoint组件的主要工作就是处理I/O,而NioEndpoint利用Java NIO API实现了多路复用I/O模型。其中关键的一点是,读写数据的线程自己不会阻塞在I/O等待上,而是把这个工作交给Selector。同时Tomcat在这个过程中运用到了很多Java并发编程技术,比如AQS、原子类、并发容器,线程池等,都值得我们去细细品味。
Tomcat的NioEndpoint组件虽然实现比较复杂,但基本原理就是上面两步。我们先来看看它有哪些组件,它一共包含LimitLatch、Acceptor、Poller、SocketProcessor和Executor共5个组件,它们的工作过程如下图所示。
如何高并发
在弄清楚NioEndpoint的实现原理后,我们来考虑一个重要的问题,怎么把这个过程做到高并发呢?
高并发就是能快速地处理大量的请求,需要合理设计线程模型让CPU忙起来,尽量不要让线程阻塞,因为一阻塞,CPU就闲下来了。另外就是有多少任务,就用相应规模的线程数去处理。我们注意到NioEndpoint要完成三件事情:接收连接、检测I/O事件以及处理请求,那么最核心的就是把这三件事情分开,用不同规模的线程数去处理,比如用专门的线程组去跑Acceptor,并且Acceptor的个数可以配置;用专门的线程组去跑Poller,Poller的个数也可以配置;最后具体任务的执行也由专门的线程池来处理,也可以配置线程池的大小。
十六:Tomcat APR提高I_O性能的秘密
APR(Apache Portable Runtime Libraries)是Apache可移植运行时库,它是用C语言实现的,其目的是向上层应用程序提供一个跨平台的操作系统接口库。Tomcat可以用它来处理包括文件和网络I/O,从而提升性能。我在专栏前面提到过,Tomcat支持的连接器有NIO、NIO.2和APR。跟NioEndpoint一样,AprEndpoint也实现了非阻塞I/O,它们的区别是:NioEndpoint通过调用Java的NIO API来实现非阻塞I/O,而AprEndpoint是通过JNI调用APR本地库而实现非阻塞I/O的。
APR提升性能的秘密还有:通过DirectByteBuffer避免了JVM堆与本地内存之间的内存拷贝;通过sendfile特性避免了内核与应用之间的内存拷贝以及用户态和内核态的切换。
十七:Executor组件:Tomcat如何扩展Java线程池?
Tomcat扩展了Java线程池的核心类ThreadPoolExecutor,并重写了它的execute方法,定制了自己的任务处理流程。同时Tomcat还实现了定制版的任务队列,重写了offer方法,使得在任务队列长度无限制的情况下,线程池仍然有机会创建新的线程。
十八:Tomcat如何支持WebSocket?
Tomcat的WebSocket加载是通过SCI机制完成的。SCI全称ServletContainerInitializer,是Servlet 3.0规范中定义的用来接收Web应用启动事件的接口。那为什么要监听Servlet容器的启动事件呢?因为这样我们有机会在Web应用启动时做一些初始化工作,比如WebSocket需要扫描和加载Endpoint类。SCI的使用也比较简单,将实现ServletContainerInitializer接口的类增加HandlesTypes注解,并且在注解内指定的一系列类和接口集合。Tomcat启动时通过SCI技术来扫描和加载WebSocket的处理类ServerEndpoint,并且建立起了URL到ServerEndpoint的映射关系。
当第一个WebSocket请求到达时,Tomcat将HTTP协议升级成WebSocket协议,并将该Socket连接的Processor替换成UpgradeProcessor。这个Socket不会立即关闭,对接下来的请求,Tomcat通过UpgradeProcessor直接调用相应的ServerEndpoint来处理。
今天我讲了可以通过两种方式来开发WebSocket应用,一种是继承javax.websocket.Endpoint
,另一种通过WebSocket相关的注解。其实你还可以通过Spring来实现WebSocket应用,有兴趣的话你可以去研究一下Spring WebSocket的原理。
二十:Tomcat和Jetty中的对象池技术
Tomcat和Jetty都用到了对象池技术,这是因为处理一次HTTP请求的时间比较短,但是这个过程中又需要创建大量复杂对象。
对象池技术可以减少频繁创建和销毁对象带来的成本,实现对象的缓存和复用。如果你的系统需要频繁的创建和销毁对象,并且对象的创建代价比较大,这种情况下,一般来说你会观察到GC的压力比较大,占用CPU率比较高,这个时候你就可以考虑使用对象池了。
还有一种情况是你需要对资源的使用做限制,比如数据库连接,不能无限制地创建数据库连接,因此就有了数据库连接池,你也可以考虑把一些关键的资源池化,对它们进行统一管理,防止滥用。
二十一 总结:Tomcat和Jetty的高性能、高并发之道
I/O和线程模型
除了I/O模型,线程模型也是影响性能和并发的关键点。Tomcat和Jetty的总体处理原则是:
- 连接请求由专门的Acceptor线程组处理。
- I/O事件侦测也由专门的Selector线程组来处理。
- 具体的协议解析和业务处理可能交给线程池(Tomcat),或者交给Selector线程来处理(Jetty)
减少系统调用
池化、零拷贝
高效的并发编程
并发容器的使用
CopyOnWriteArrayList适用于读多写少的场景,比如Tomcat用它来“存放”事件监听器,这是因为监听器一般在初始化过程中确定后就基本不会改变,当事件触发时需要遍历这个监听器列表,所以这个场景符合读多写少的特征。
volatile关键字的使用
二十三 Host容器:Tomcat如何实现热部署和热加载?
- 热加载的实现方式是Web容器启动一个后台线程,定期检测类文件的变化,如果有变化,就重新加载类,在这个过程中不会清空Session ,一般用在开发环境。
- 热部署原理类似,也是由后台线程定时检测Web应用的变化,但它会重新加载整个Web应用。这种方式会清空Session,比热加载更加干净、彻底,一般用在生产环境。
- 热加载和热部署的实现都离不开后台线程的周期性检查,Tomcat在基类ContainerBase中统一实现了后台线程的处理逻辑,并在顶层容器Engine启动后台线程,这样子容器组件甚至各种通用组件都不需要自己去创建后台线程,这样的设计显得优雅整洁。
要说开启后台线程做周期性的任务,有经验的同学马上会想到线程池中的ScheduledThreadPoolExecutor,它除了具有线程池的功能,还能够执行周期性的任务。Tomcat就是通过它来开启后台线程的:
bgFuture = exec.scheduleWithFixedDelay(
new ContainerBackgroundProcessor(),//要执行的Runnable
backgroundProcessorDelay, //第一次执行延迟多久
backgroundProcessorDelay, //之后每次执行间隔多久
TimeUnit.SECONDS); //时间单位
上面的代码调用了scheduleWithFixedDelay方法,传入了四个参数,第一个参数就是要周期性执行的任务类ContainerBackgroundProcessor,它是一个Runnable,同时也是ContainerBase的内部类,ContainerBase是所有容器组件的基类,我们来回忆一下容器组件有哪些,有Engine、Host、Context和Wrapper等,它们具有父子关系。
ContainerBase的backgroundProcess方法实现如下:
public void backgroundProcess() {
//1.执行容器中Cluster组件的周期性任务
Cluster cluster = getClusterInternal();
if (cluster != null) {
cluster.backgroundProcess();
}
//2.执行容器中Realm组件的周期性任务
Realm realm = getRealmInternal();
if (realm != null) {
realm.backgroundProcess();
}
//3.执行容器中Valve组件的周期性任务
Valve current = pipeline.getFirst();
while (current != null) {
current.backgroundProcess();
current = current.getNext();
}
//4. 触发容器的"周期事件",Host容器的监听器HostConfig就靠它来调用
fireLifecycleEvent(Lifecycle.PERIODIC_EVENT, null);
}
24 Context容器(上):Tomcat如何打破双亲委托机制?
要打破双亲委托机制,需要继承ClassLoader抽象类,并且需要重写它的loadClass方法,因为ClassLoader的默认实现就是双亲委托。
Class.forName
调用交给系统类加载器的,因为Class.forName
的默认加载器就是系统类加载器(AppClassLoader)25 Context容器(中):Tomcat如何隔离Web应用?
- 每个Web应用自己的Java类文件和依赖的JAR包,分别放在
WEB-INF/classes
和WEB-INF/lib
目录下面。 - 多个应用共享的Java类文件和JAR包,分别放在Web容器指定的共享目录下。
- 当出现ClassNotFound错误时,应该检查你的类加载器是否正确。
线程上下文加载器不仅仅可以用在Tomcat和Spring类加载的场景里,核心框架类需要加载具体实现类时都可以用到它,比如我们熟悉的JDBC就是通过上下文类加载器来加载不同的数据库驱动的
26 Context容器(下):Tomcat如何实现Servlet规范?
Tomcat通过Wrapper容器来管理Servlet,Wrapper包装了Servlet本身以及相应的参数,这体现了面向对象中“封装”的设计原则。
Tomcat会给每个请求生成一个Filter链,Filter链中的最后一个Filter会负责调用Servlet的service方法。
对于Listener来说,我们可以定制自己的监听器来监听Tomcat内部发生的各种事件:包括Web应用级别的、Session级别的和请求级别的。Tomcat中的Context容器统一维护了这些监听器,并负责触发。
27 新特性:Tomcat如何支持异步Servlet?
非阻塞I/O模型可以利用很少的线程处理大量的连接,提高了并发度,本质就是通过一个Selector线程查询多个Socket的I/O事件,减少了线程的阻塞等待。
同样,异步Servlet机制也是减少了线程的阻塞等待,将Tomcat线程和业务线程分开,Tomcat线程不再等待业务代码的执行。
那什么样的场景适合异步Servlet呢?适合的场景有很多,最主要的还是根据你的实际情况,如果你拿不准是否适合异步Servlet,就看一条:如果你发现Tomcat的线程不够了,大量线程阻塞在等待Web应用的处理上,而Web应用又没有优化的空间了,确实需要长时间处理,这个时候你不妨尝试一下异步Servlet。
参考: Servlet- 技术专题 -Servlet3 异步原理与实践
28 新特性:Spring Boot如何使用内嵌式的Tomcat和Jetty?
在Spring Boot启动类上加上@ServletComponentScan注解后,使用@WebServlet、@WebFilter、@WebListener标记的Servlet、Filter、Listener就可以自动注册到Servlet容器中,无需其他代码,
30 Spring框架中的设计模式
Spring的两大核心功能IOC和AOP中用到的一些设计模式,主要有简单工厂模式、工厂方法模式、单例模式和代理模式。而代理模式又分为静态代理和动态代理。JDK提供实现动态代理的机制,除此之外,还可以通过CGLIB来实现。
JDK
的动态代理存在限制,那就是被代理的类必须是一个实现了接口的类,代理类需要实现相同的接口,代理接口中声明的方法。若需要代理的类没有实现接口,此时JDK
的动态代理将没有办法使用,于是Spring
会使用CGLib
的动态代理来生成代理对象。CGLib
直接操作字节码,生成类的子类,重写类的方法完成代理。
31 Logger组件:Tomcat的日志框架及实战
32 Manager组件:Tomcat的Session管理机制解析
Servlet规范中定义了HttpServletRequest和HttpSession接口,Tomcat实现了这些接口,但具体实现细节并没有暴露给开发者,因此定义了两个包装类,RequestFacade和StandardSessionFacade。
Tomcat是通过Manager来管理Session的,默认实现是StandardManager。StandardContext持有StandardManager的实例,并存放了HttpSessionListener集合,Session在创建和销毁时,会通知监听器。
33 Cluster组件:Tomcat的集群通信原理
server.xml
里的这一行的注释去掉就行:
Tomcat集群对Session的拷贝支持两种方式:DeltaManager和BackupManager。
当集群中节点比较少时,可以采用DeltaManager,因为Session数据在集群中各个节点都有备份,任何一个节点崩溃都不会对整体造成影响,可靠性比较高。
当集群中节点数比较多时,可以采用BackupManager,这是因为一个节点的Session只会拷贝到另一个节点,数据拷贝的开销比较少,同时只要这两个节点不同时崩溃,Session数据就不会丢失。
34 JVM GC原理及调优的基本思路
对于CMS来说,我们要合理设置年轻代和年老代的大小。你可能会问该如何确定它们的大小呢?这是一个迭代的过程,可以先采用JVM的默认值,然后通过压测分析GC日志。
如果我们看年轻代的内存使用率处在高位,导致频繁的Minor GC,而频繁GC的效率又不高,说明对象没那么快能被回收,这时年轻代可以适当调大一点。
如果我们看年老代的内存使用率处在高位,导致频繁的Full GC,这样分两种情况:如果每次Full GC后年老代的内存占用率没有下来,可以怀疑是内存泄漏;如果Full GC后年老代的内存占用率下来了,说明不是内存泄漏,我们要考虑调大年老代。
对于G1收集器来说,我们可以适当调大Java堆,因为G1收集器采用了局部区域收集策略,单次垃圾收集的时间可控,可以管理较大的Java堆。
35 如何监控Tomcat的性能?
Tomcat的关键指标有吞吐量、响应时间、错误数、线程池、CPU以及JVM内存。
如果我们监控到CPU上升,这时我们可以看看吞吐量是不是也上升了,如果是那说明正常;如果不是的话,可以看看GC的活动,如果GC活动频繁,并且内存居高不下,基本可以断定是内存泄漏。
对于一个Web应用来说,下游服务的延迟越大,Tomcat所需要的线程数越多,但是CPU保持稳定。所以如果你在实际工作碰到线程数飙升但是CPU没有增加的情况,这个时候你需要怀疑,你的Web应用所依赖的下游服务是不是出了问题,响应时间是否变长了。
36 Tomcat I_O和线程池的并发调优
后面还聊到Tomcat线程池的各种参数,其中最重要的参数是最大线程数maxThreads。理论上我们可以通过利特尔法则或者CPU时间与I/O时间的比率,计算出一个理想值,这个值只具有指导意义,因为它受到各种资源的限制,实际场景中,我们需要在理想值的基础上进行压测,来获得最佳线程数。
37 Tomcat内存溢出的原因分析及调优
38 Tomcat拒绝连接原因分析及网络优化
在这个基础上,我们还分析了Tomcat中两个比较重要的参数:acceptCount和maxConnections。acceptCount用来控制内核的TCP连接队列长度,maxConnections用于控制Tomcat层面的最大连接数。在实战环节,我们通过调整acceptCount和相关的内核参数somaxconn
,增加了系统的并发度。
39 Tomcat进程占用CPU过高怎么办?
top -H -p pid
命令定位到具体的线程。其次还要通jstack查看线程的状态,看看线程的个数或者线程的状态,如果线程数过多,可以怀疑是线程上下文切换的开销,我们可以通过vmstat和pidstat这两个工具进行确认