赞
踩
http请求包括:请求行、请求报头、请求正文
http响应包括:状态行、响应报头、报文主体
HTTP/1.0每次请求都会创建一个新的TCP连接,请求完成后连接就被关闭
HTTP/1.1引入HTTP长连接,会在响应头加入Connection:keep-alive,每次请求可以复用这个连接
tomcat工作流程
当客户请求资源时,http服务器会用一个ServletRequest对象把客户的请求信息封装起来,调用Servlet容器的service方法,Servlet容器拿到请求后,根据请求的url和servlet的映射关系,找到相应的servlet,如果servlet还没有被加载,就用反射机制创建这个servlet方法,并调用servlet的init方法来完成初始化,调用servlet的service的方法来处理请求,把ServletResponse对象返回给http服务器,http服务器会把响应发送给客户端。
1、编写一个类继承HttpServlet,重写doGet、doPost方法
2、将tomcat lib目录下的servlet-api.jar拷贝过去,servlet-api.jar中定义了Servlet接口,而我们的Servlet类实现了Servlet接口,编译时需要这个jar包
3、编译命令 javac -encoding utf-8 -cp ./servlet-api.jar ./MyServlet.java
4、建立Web应用的目录结构
5、新建web.xml配置servlet或注解方式部署servlet
6、把应用放到tomcat的webapps下并启动tomcat
EndPoint接收到Socket连接后,生成一个SocketProcessor任务提交到线程池去处理,SocketProcessor的Run方法会调用Processor组件去解析应用层协议,Processor通过解析生成Request对象后,会调用Adapter的service方法。
连接器的作用,比如:
1、监听网络端口
2、接收网络连接请求
3、读取请求网络字节流
4、根据具体应用层协议(HTTP/AJP)解析字节流,生成统一的Tomcat Request对象
5、将Tomcat Request对象转成标准的ServletRequest
6、调用Servlet容器,得到ServletResponse
7、将ServletResponse转成Tomcat Response对象
8、将Tomcat Response转成网络字节流
9、将响应字节流写回浏览器
请求的链式调用是基于Pipeline-Valve责任链来完成的。
LifeCycle接口与组件的生命周期有关,它定义了这么几个方法:init()、start()、stop()、destroy(),每个具体的组件去实现这些方法。在父组件的init()方法里需要创建子组件并调用子组件的init()方法。同样,在父组件的start方法里也需要调用子组件的start方法,这就是组合模式的使用。
如果将来需要增加新的逻辑,直接修改start方法,这样会违反开放封闭原则。我们注意到,组件的init和start方法调用是由它的父组件的状态变化触发的,上层组件的初始化会触发子组件的初始化。因此我们把组件的生命周期定义成一个个状态,把状态的转变看成一个事件。而事件是有监听器的,在监听器里可以实现一些逻辑,并且监听器也可以方便地添加和删除,这就是典型的观察者模式。
LifeCycleBase实现了LifeCycle接口中所有的方法,还定义了相应的抽象方法交给具体子类去实现,这是典型的模板设计模式。
下面是LifeCycleBase的init方法的实现
- @Override
- public final synchronized void init() throws LifecycleException {
- //1. 状态检查
- if (!state.equals(LifecycleState.NEW)) {
- invalidTransition(Lifecycle.BEFORE_INIT_EVENT);
- }
-
- try {
- //2. 触发 INITIALIZING 事件的监听器
- setStateInternal(LifecycleState.INITIALIZING, null, false);
-
- //3. 调用具体子类的初始化方法
- initInternal();
-
- //4. 触发 INITIALIZED 事件的监听器
- setStateInternal(LifecycleState.INITIALIZED, null, false);
- } catch (Throwable t) {
- ...
- }
- }
1、Tomcat本质上是一个Java程序,因此startup.sh脚本会启动一个JVM来运行Tomcat的启动类Bootstrap
2、Bootstrap的主要任务是初始化Tomcat的类加载器,并且创建Catalina。
3、Catalina是一个启动类,它通过解析server.xml,创建相应的组件,并调用Server的start方法
4、Server组件的职责就是管理Service组件,它会负责调用Service的start方法
5、Service组件的职责就是管理连接器和顶层容器Engine,因此它会调用连接器和Engine的start方法
Server在内部维护了若干Service组件,它是以数组来保存的,每添加一个组件就需要创建一个+1长度的数组,并由原来数组复制过去。
1、Acceptor监听连接请求,当有连接请求到达时就接受连接,一个连接对应一个Channel,Acceptor将Channel交给ManagedSelector来处理
2、ManagedSelector把Channel注册到Selector上,并创建一个EndPoint和Connection跟这个Channel绑定,接着就不断地检测I/O事件
3、I/O事件到了就调用EndPoint的方法拿到一个Runnable,并扔给线程池运行
4、线程池中调度某个线程执行Runnable
5、Runnable执行时,调用回调函数,这个回调函数是Connection注册到EndPoint中的。
6、回调函数内部实现,其实就是调用EndPoint的接口方法来读数据
7、Connection解析读到的数据,生成请求对象并交给Handler组件去处理
Jetty Connector设计中的特点是使用了回调函数来模拟异步I/O,比如Connection向EndPoint注册了一堆回调函数。它的本质将函数当作一个参数来传递,告诉对方,你准备好了就调用这个回调函数。
Jetty Server就是由多个Connector、多人Handler以及一个线程池组成。
Jetty本质就是一个Handler管理器,Jetty本身就提供了一些默认Handler来实现Servlet容器的功能。
Handler本质分成三种类型:
1、协调Handler,这种Handler负责将请求路由到一组Handler中去,如HandlerCollection,它内部持有一个Handler数组,当请求来时,它负责将请求转发到数组中的某一个Handler中去。
2、过滤器Handler,自己处理请求后再把请求转发到下一个Handler,如HandlerWrapper,它内部持有下一个Handler的引用。所有继承HandlerWrapper的Handler都具有过滤器Handler的特征,如ContextHandler、SessionHandler和WebAppContext等。
3、内容Handler,这些Handler会真正调用Servlet来处理请求生成响应内容,如ServletHandler,如果请求的是静态资源,也有相应的ResourceHandler来处理这个请求,返回静态页面。
Tomcat和Jetty的组件化设计,让我们可以通过搭积木的方式来定制化自己的Web容器。Web容器为了支持这种组件化设计,遵循了一些规范,如面向接口编程,用“管理者”去组装这些组件(Web容器提供一个载体Server把组件组装在一起工作,容器通过责任链模式把请求依次交给组件去处理),用反射的方式动态的创建组件、统一管理组件的生命周期,并且给组件生命状态的变化提供了扩展点,组件的具体实现一般遵循骨架抽象类和模板模式。
1、清理不必要的Web应用
2、清理xml配置文件,解析东西越少,速度越快
3、清理jar包,Web应用中的lib目录下不应该出现Servlet API或者Tomcat自身的jar,这些jar由Tomcat提供,如果用maven构建应用,可以对servlet api的依赖指定为<scope>provided</scope>
4、清理其他文件,如不需要的日志文件,同样还有work文件夹下的catalina文件夹,它其实是Tomcat把jsp转换为Class文件的工作目录
5、禁止Tomcat TLD扫描(不需要jsp则禁止,需要则指定扫描那些包含TLD文件的jar包),关闭WebSocket支持,关闭JSP支持,配置Web-Fragment扫描,随机数熵源优化,并行启动多个Web应用
当用户线程发起I/O操作后,网络数据读取操作会经历两个步骤
1、用户线程等待内核将数据从网卡拷贝到内核空间
2、内核将数据从内核空间拷贝到用户空间
阻塞和非阻塞是指应用程序在发起I/O操作时,是立即返回还是等待。而同步和异步,是指应用程序在与内核通信时,数据从内核空间到应用空间的拷贝,是由内核发起还是由应用程序来触发。
LimitLatch:连接控制器,负责控制最大连接数
Acceptor中在一个单独的线程里,它在一个死循环里调用accept方法来接收新连接,一旦有新的连接请求到来,accept方法返回一个Channel对象,接着Channel对象交给Poller去处理。
Poller本质是一个Selector,跑到单独线程里。Poller在内部维护一个Channel数组,它在一个死循环里不断检测Channel的数据状态,一旦有Channel可读,就生成一个SocketProcessor任务对象扔给Executor处理。
Executor是线程池,负责运行SocketProcessor任务类,SocketProcessor的run方法会调用Http11Processor来读取和解析请求数据。我们知道Http11Processor是应用层协议的封装,它会调用容器获得响应,再把响应通过Channel写出。
Nio2Endpoint中没有Poller组件,也就没有Selector。这是因为在异步I/O模式下,Selector的工作交给内核来做了。
Java在操作系统异步IO API的基础上进行了封装,提供了Java NIO.2 API,而Tomcat的异步I/O模型就是基于Java NIO.2实现的。
APR通过jni调用c语言函数,能显著提高性能。APR提升性能的秘密还有:通过DirectByteBuffer避免了JVM堆与本地内存之间的内存拷贝;通过sendfile特性避免了内核与应用之间的内存拷贝以及用户态和内核态的切换。
Tomcat扩展了Java线程池的核心类ThreadPoolExecutor,并重写了它的execute方法,定制自己的任务处理流程。同时Tomcat还实现了定制版的任务队列,重写了offer方法,使得在任务队列长度无限制的情况下,线程池仍然有机会创建新的线程。
Tomcat线程池与Java原生线程池的区别在于Tomcat线程总数达到最大数时,不是立即执行拒绝策略,而是再尝试向任务队列添加任务,添加失败后再执行拒绝策略。
WebSocket与HTTP协议一样,是一个应用层协议。为了跟现有的HTTP协议保持兼容,它通过HTTP协议进行一次握手,握手之后数据就直接从TCP层的Socket传输,就与HTTP协议无关了。浏览器发给服务端的请求会带上跟WebSocket有关的请求头。比如Connection: Upgrade和Upgrade: websocket。
jetty将I/O事件的侦测和处理放到同一个线程来处理,充分利用CPU缓存并减少了线程上下文切换的开销。根据Jetty的官方测试,这种名为“EatWhatYouKill"的线程策略将吞吐量提高了8倍。常规的NIO思路是事件的侦测和请求的处理分别用不同的线程处理,好处是它们互不干扰和阻塞对方。但是这种方式可能会使用不到cpu的缓存,它比内存快多了以及它带来了线程切换的开销。
Tomcat用SynchronizedStack类来实现对象池,它内部维护了一个对象数组,并用数组来实现栈的接口。用数组而不用类似ConcurrentLinkedQueue链表来维护对象,可以减少结点维护的内存开销,并且它本身只支持扩容不支持缩容,也就是数组对象在使用过程中不会被重新赋值,也就不会被gc。这样设计的目的是用最低的内存和gc的代价来实现无界容器。同时Tomcat的最大同时请求数是有限制的,因此不需要担心对象的数量会无限膨胀。
Jetty中的对象池ByteBufferPool,它本质是一个ByteBuffer对象池,只需在ByteBuffer对象池里拿到一块预先分配好的Buffer,这样避免了频繁的分配内存和释放内存。对象池需要尽量做到无锁化,比如Jetty就使用了ConcurrentLinkedDeque。如果你的内存足够大,可以考虑用线程本地(ThreadLocal)对象池。这样每个线程都有自己的对象池,线程之间互不干扰。
Tomcat和Jetty通过合理的I/O模型和线程模型减少了线程的阻塞。另外系统调用会导致用户态和内核态切换的过程,Tomcat和Jetty通过缓存和延迟解析尽量减少系统调用,另外还通过零拷贝技术避免多余的数据拷贝。高效的利用系统资源还包括另一层含义,就是用一种资源换取另一种资源。Tomcat和Jetty中使用对象池技术就是用内存换取CPU,将数据压缩后再传输就是用CPU换网络。高效的并发编程也很重要,多线程虽然可以提高并发度,也带来了锁的开销,应避免锁,可用原子变量和CAS操作来代替锁,如果实在避免不了用锁,那也要减少锁的范围和强度,比如用细粒度的对象锁或者低强度的读写锁。Tomcat和Jetty的代码也很好的实践了这一理念。
Socket read系统调用的过程:首先在CPU在用户态执行应用程序的代码,访问进程虚拟地址空间的用户空间;read系统调用时CPU从用户态切换到内核态,执行内核代码,内核检测到Socket上的数据未就绪时,将进程的task_struct结构体从运行队列中移到等待队列,并触发一次CPU调用,这时进程会让出CPU;当网卡数据到过时,内核将数据从内核空间拷贝到用户空间的Buffer,接着将进程的task_struct结构体重新移到运行队列,这样进程就有机会重新获得CPU时间片,系统调用返回,CPU又从内核态切换到用户态,访问用户空间的数据。
有了ContrainBase中的后台线程和backgroundProcess方法,各种子容器和通用组件不需要各自弄一个后台线程来处理周期性任务。有了ContrainerBase的周期性任务处理“框架”,作为具体容器子类,只需要实现自己的周期性任务就行。而Tomcat的热加载,就是在Context容器中实现的。
热部署会重新部署Web应用,原来的Context对象会整个被销毁,这样过程由Context的父容器Host实现的。Tomcat的热部署跟Context不一样,Host容器并没有在backgroundProcess方法中实现周期性检测任务,而是通过监听器HostConfig来实现的,它周期性检测Web应用目录变化。
双亲委托机制保证一个Java类在JVM中是唯一的,如果你不小心写了一个与JRE核心类同名的类,如Object类,双亲委托机制能保证加载的是JRE里的那个Object类,而不是你写的Object类。这是因为AppClassLoader在加载你的Object类时,会委托给ExtClassLoader去加载,而ExtClassLoader又会委托给BootstrapClassLoader,BootstrapClassLoader发现自己已经加载过了Object类,会直接返回,不会加载你写的Object类。
Tomcat自定义类加载器WebAppClassLoader打破了双亲委托机制,它首先自己尝试去加载某个类,如果找不到再代理给父类加载器,其目的就是优先加载Web应用自己定义的类。
Tomcat的loadClass方法过程如下:
1、在本地Cache查找类是否加载过,即查看Tomcat的类加载器是否已经加载过这个类
2、 如果Tomcat类加载器没有加载过这个类,再看系统类加载器是否加载过
3、如果都没有,就让ExtClassLoader去加载,目的是防止Web应用自己的为覆盖JRE的核心类。
4、如果ExtClassLoader加载失败,在本地Web应用目录查找并加载
5、如果本地目录没有这个类,由系统类加载器去加载
6、上述加载全部失败,就抛出ClassNotFound异常
Tomcat的Context组件为每个Web应用创建一个WebAppClassLoader类加载器,由于不同类加载器实例加载的类是互相隔离的,因此达到了隔离Web应用的目的,同时通过CommonClassLoader等父加载器来共享第三方JAR包。而共享的第三方JAR包怎么加载特定Web应用的类呢?可以通过设置线程上下文加载器来解决。
Tomcat通过Wrapper容器来管理Servlet,Wrapper包装了Servlet本身以及相应的参数,这体现了面向对象中”封装“的设计原则。
Tomcat会给每个请求生成一个Filter链,Filter链中的最后一个Filter会负责调用Servlet的service方法。
对于Listener来说,我们可以定制自己的监听器来监听Tomcat内部发生的各种事件:包括Web应用级别的、Session级别的和请求级别的。Tomcat中的Context容器统一维护了这些监听器,并负责触发。
异步Servlet将Tomcat线程和Web应用线程分开,体现了隔离的思想,也就是把不同的业务处理所使用了资源隔离,使得它们互不干扰,尤其是低优先级的业务不能影响高优先级的业务。
略
Tomcat和Jetty都实现了责任链模式,其中tomcat是通过pipeline-valve来实现的,而jetty是通过HandlerWrapper来实现的,将各Handler组成一个链表,如下:
WebAppContext->SessionHandler->SecuritHandler->ServletHandler
略
SLF4J门面模式接口规范,Log4j、Logback或Log4j2实现
session是会话的生命周期,每次请求都会重置超时时间,TCP连接超时,连接就被回收。tcp是系统网络层面,而session是应用层面的,应用层面完全由应用控制生命周期,所以tcp和session之间没有什么关系。
要保持会话信息在节点之间一致。
基本上有两种方式:
年轻代和年老代使用不同垃圾收集算法,比如年轻代采用复制-整理算法,年老代采用标记-清理算法
与CMS相比,G1收集器有两大特点:
具体收集过程是:
GC调优原则
CMS收集器:合理地设置年轻代和年老代的大小
G1收集器:不推荐直接设置年轻代大小,G1收集器会根据算法动态决定年轻和年老代的大小。因此对于G1收集器,我们需要关心Java堆的总大小。G1有参数-XX:MaxGCPauseMillis=n,用来限制最大gc暂停时间。G1根据先前收集的信息以及检测到的垃圾量,估计它可以立即收集的最大区域数量,避免GC时间超过限制。
Tomcat的关键指标有吞吐量、响应时间、错误数、线程池、CPU以及JVM内存
命令行查看Tomcat指标
ps -ef | grep tomcat
查看进程状态的大致信息
cat /proc/<pid>/status
查看tomcat的网络连接,比如Tomcat在8080端口上监听连接请求
netstat -na | grep 8080
通过ifstat来查看网络流量,大致可以看出tomcat当前的请求数和负载状况
对于一个Web应用来说,下游服务的延迟越大,Tomcat所需要的线程数越多,但是CPU保持稳定。所以如果你在实际工作碰到线程数飙升但是CPU没有增加的情况,这个时候你需要怀疑,你的Web应用所依赖的下游服务是不是出了问题,响应时间是否变长。
java.lang.OutOfMemoryError: Java heap space
JVM无法在堆中分配对象时,会抛出这个异常,导致这个异常的原因可能有三种
在Linux下执行ulimit -a,你会看到ulimit对各种资源的限制
cat /proc/sys/kernel/threads-max #这个参数限制操作系统全局的线程数,表明当前系统能创建的总的线程数
参数sys.kernel.pid_max限制系统全局的pid号数值
java.net.SocketTimeoutException
超时分为连接超时和读取超时,连接超时往往是由于网络不稳定造成的,但是读取超时不一定是网络延迟造成的,很有可能是下游服务的响应时间过长。
java.net.SocketException: Connection reset/Connect reset by peer: Socket write error
一方已将Socket关闭,可能主动关闭或异常退出,另一方还在读写数据。
为了避免异常发生,需要确保:
java.net.SocketException: Broken pipe
指通信管道已坏。发生这个异常的场景是,通信的一方在收到“Connect reset by peer: Socket write error”后,如果再继续写数据则会抛出 Broken pipe 异常,解决方法同上。
java.net.SocketException: Too many open files
指进程打开文件句柄数超过限制。可通过lsof -p pid查看进程打开了哪些文件,是不是有资源泄露。如无资源泄露,可增加最大文件句柄数
Tomcat两个比较关键的参数:maxConnections和acceptCount。TCP连接的建立过程:客户端向服务端发送SYN包,服务端回复SYN+ACK,同时将这个处理SYN_RECV状态的连接保存到半连接队列。客户端返回ACK包完成三次握手,服务端将ESTABLISHED状态的连接移入accept队列,等待应用程序(Tomcat)调用accept方法将连接取走。
acceptCount是backlog参数,默认是100,net.core.somaxconn默认是128。而Tomcat中的maxConnections是指Tomcat在任意时刻接收和处理的最大连接数。当Tomcat接收的连接数达到maxConnections时,Acceptor线程不会再从accept队列中取走连接,这时accept队列中的连接会越积越多。maxConnections的默认值与连接器类型有关:NIO的默认值是10000,APR默认是8192。所以你会发现tomcat的最大并发连接数等于maxConnections + acceptCount。如果acceptCount设置过大,请求等待时间会比较长;如果acceptCount设置过小,高并发情况下,客户端会立即触发Connection reset异常。
修改内核参数,在/etc/sysctl.conf中增加一行net.core.somaxconn=2048,然后执行命令sysctl -p
修改Tomcat参数acceptCount为2048,重启Tomcat,会发现jmeter 1000每秒并发出现的Connection reset异常消失了
在高并发中,netstat命令发现有大量tcp连接处于TIME_WAIT状态。这是因为当tcp的一端发起主动关闭,在发出最后一个ack包后,即第3次握手完成后发送了第四次握手的ack包后就进入TIME_WAIT状态,必须在此状态上停留两倍(MSL)报文最大生存时间。期间两端的端口不能使用,必须要超到2MSL时间结束才可继续使用。
top -H -p <pid> #查看java各线程情况
jstack <pid> > <pid>.log #生成线程快照到文件
vmstat 1 100 #查看线程上下文切换活动
操作系统层面调优,我们需要加大一些默认的限制值,这些参数主要可以在/etc/security/limits.conf中或通过sysctl命令进行配置,其实这些配置对于Tomcat来说也是适用的
TCP的发送和接收缓冲区最好加大到16MB,如下命令配置:
- sysctl -w net.core.rmem_max = 16777216
- sysctl -w net.core.wmem_max = 16777216
- sysctl -w net.ipv4.tcp_rmem =“4096 87380 16777216”
- sysctl -w net.ipv4.tcp_wmem =“4096 16384 16777216”
TCP连接队列大小默认为128,在高并发情况下明显不够用,会出现拒绝连接。但值调得过高,因为过多积压会造成请求处理的延迟,推荐设置为4096
sysctl -w net.core.somaxconn = 4096
net.core.netdev_max_backlog用来控制Java程序传入数据包队列的大小,可以适当调大
- sysctl -w net.core.netdev_max_backlog = 16384
- sysctl -w net.ipv4.tcp_max_syn_backlog = 8192
- sysctl -w net.ipv4.tcp_syncookies = 1
端口
如果Web应用程序作为客户端向远程服务端建立很多TCP连接,可能会出现TCP端口不足的情况。因此最好增加使用的端口范围,并允许在TIME_WAIT中重用套接字
- sysctl -w net.ipv4.ip_local_port_range =“1024 65535”
- sysctl -w net.ipv4.tcp_tw_recycle = 1
高负载服务器的文件句柄数很容易耗尽,这是因为系统默认值通过比较低,我们可以在/etc/security/limits.conf中为特定用户增加文件句柄数
- 用户名 hard nofile 40000
- 用户名 soft nofile 40000
Jetty本身的调优主要设置不同类型的线程数量,包括Acceptor和Thread Pool
Acceptor的个数accepts应该设置为大于等于1,并且小于等于CPU核数
Thread Pool默认队列是无限的,高负载下容易积压大量待处理请求,应该使用有界队列拒绝过多请求。比如,如果Web应用每秒可以处理100个请求,当负载高峰到业,我们允许一个请求在队列积压60s,那么可以设置队列长度为60*100=6000。一般Jetty的最大线程数应该在50到500之间。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。