- Published on
Tomcat 架构设计解析
1、tomcat整体架构设计概览
Tomat里有Server的概念,一个Server里包含多个Service,然后一个Service包含多个Connector和一个Engine,每个Connector负责开启Socket监听指定端口的连接和请求,以及返回响应,Engine就负责具体的请求处理工作了
Connector获取到的请求只能由他所属Service的那唯一一个Engine来进行处理
不同的Connector有什么意义呢?答案是监听不同的端口号以及解析 不同的协议,比如说Tomcat的话,最常见支持的是http协议,但是他也可以跟apache整合,去接收apache的AJP协议的请求,所以可以用不同的Connector来支持
但是不同的Connector对应的仅仅是不同的协议,解析完毕后,请求处理都是同一个Engine来处理的
接着来看,这个Engine虽然是负责处理请求的,但是我们都知道,这个请求tomcat自己又处理不了,对吧,必须要依靠tomcat里的不同的web应用来进行处理,就比如我们写的一个一个的web应用,就是一个一个的web系统
一个tomcat里是可以包含多个不同的web应用/系统的,我们都是可以用war包的形式把我们写好的web系统放到tomcat指定目录下去,tomcat就去里面加载我们的类就可以了,就是这么简单对吧
完了tomcat收到请求以后怎么知道请求交给哪个web应用处理呢?所以一般都有web.xml,你需要在里面来配置请求url路径跟web应用的servlet/filter之间的对应关系,交给web应用里我们自己写的servlet/filter来处理
那如果我们web应用用的是spring mvc一类的框架,那相当于就是让tomcat把请求交给我们的web应用里集成进来的spring mvc框架的DispatcherServlet就可以了。
所以说,tomcat里的engine,是可以包含多个context的,每个context代表一个web应用,也就是我们写的一个web系统,也就是一个war包吧,完了engine可以根据web.xml里的映射配置,把请求交给一个web应用里的servlet来处理
然后这里又要引申出一个概念来了,就是虚拟主机这个概念,就是我们可以让不同的域名解析到我们这一台服务器,都由这个tomcat来处理,那么tomcat就等于针对不同的域名提供不同的虚拟主机了
所谓虚拟,就是虽然咱们就tomcat一台服务器,但是对外开起来似乎是有多个域名,有多个虚拟出来的主机,人家以为每个域名都有一个主机,其实不是,都是一个tomcat,那么tomcat就是有多个虚拟主机
所以其实一个Engine还得包含多个Host,每个Host就是一个虚拟主机的概念,每个Host包含多个Context,等于一个虚拟主机可以包含多个Web应用,然后每个Web应用其实都是有很多Servlet去处理请求的, 在Tomcat里把servlet叫做Wrapper,所以每个Context又有多个Servlet
这样就清晰了吧,Connector负责监听连接和请求,按照指定协议解析请求,接着交给Engine处理,Engine会找到一个Host虚拟主机,找里面的某个Context应用的Wrapper,也就是一个Servlet来处理这个请求
然后呢,这里又要引入一个Container的概念了,这个Container呢,其实是类似于一个父接口,然后Engine、Host、Context、Wrapper都是继承自这个Container父接口的,所以他们都属于Container的范畴, 因为这样可以非常灵活的去组装一个web server,一个server里有一个service,service里可以有支持不同协议的connector对吧,然后呢关键是请求处理这块,如果你没有虚拟主机,就一个web应用
你完全可以直接让service里集成一个context就可以了,跳过engine和host的概念
因为都是继承自Container父接口,所以这些概念都是可以替换的
另外就是Container里还有backgroundProcess()这么一个接口,他会确保请求处理组件可以在启动的时候开启后台线程定时调度执行一些逻辑
2、Tomcat 多层级组件统一生命周期机制
Bootstrap(加载JDK核心类)、Extension、System(负责加载Tomcat的Bootstrap类)
Common(加载tomcat home目录下的lib中的类,包括了tomcat内部类和web应用可见的类,比如servlet规范的类和工具类)
Common下有Catalina(可以通过配置,去加载tomcat内部可见的类,比如tomcat的具体实现类什么的,但是默认情况下其实都会委托Common去加载)
Common下有Shared(可以通过配置,去负责加载web应用共享的类,tomcat是不依赖这些类的,但是默认情况其实都会委托Common去加载)
Shared下有多个WebApp类加载器(负责加载每个web应用的/WEB-INF/classes下的类,就是你自己写的类,还有WEB-INF/lib下的jar包,就是你依赖的第三方jar包,而且tomcat这里打破了双亲委派机制,就是先自己加载,加载不到的再委派)
通过这种机制,基本可以实现各个web应用的类都是自己的类加载器,但是如果是共享的一些类,比如servlet规范一类的,可以走双亲委派,去找到common去加载
tomcat的几个亮点:
(1) 以面向对象的思想,对web服务器的组件体系设计,非常的优秀,抽象的非常好
(2) 网络IO和多线程并发的模型,设计的成熟和稳定
(3) web服务器的技术上的机制,设计的非常成熟和完善,包括但是不限于web容器的规范、servlet规范的支持、web应用的部署和加载、JSP模板解析引擎、安全机制、共享会话机制、集群机制、JNDI
3、Tomcat servlet同步模式下并发量不高
tomcat->servlet,同步模式,并发量不高,4核cpu的机器,tomcat工作线程数量,一般在200~400之间
假设tomcat里有200个工作线程,每个线程同一时间只能处理一个请求,处理请求的时候调用我们自 己写的一系列的代码逻辑,完成之后我们返回响应,tomcat的工作线程会拿到这个响应,再通过网络连接输出给浏览器/APP就可以了
servlet同步模式
假设每个请求处理需要200ms,一个线程每秒可以处理5个请求,200个线程,每秒可以处理1000个请求,此时对你来说,系统的TPS,讲究的是TPS,就是对于执行复杂事务的接口,一般叫做TPS,TPS是1000/s
servlet同步模式下,你的并发能力也就这么多了,一两百到一两千不等,要看每个请求处理的时候有多复杂,如果秒杀系统,每个请求过来,一系列复杂的操作,会导致每个请求都会执行大量的外部redis的操作,导致你的cpu负载会飙升
完成一个秒杀请求,可能要请求外部的redis几十次,4核cpu,每秒也就处理100200秒杀请求,200300个秒杀请求,此时cpu负载已经非常非常高了,如果你的请求处理就是几个mysql的sql而已,500800,10002000个请求都有可能
网关系统来说,对于每个请求,处理其实很简单,只不过是做一个透传和转发而已,只不过是把这个请求转发给你的后端的某个系统而已,你的cpu负载不会太高,你的并发量不会受到cpu负载而限制住了
你每秒能处理多少请求,也就是有多少并发,主要核心在于,你每次转发请求,要等待多长时间才能从后端系统拿到响应,每个请求转发要耗时多久,每个线程每秒可以转发多少个请求,200个线程每秒可以路由多少个请求
一秒处理1000请求的时候,可能cpu负载还好,还可以支撑,此时问题不大,制约你的并发能力的是谁?转发请求时候的耗时
假设你一个线程处理一个请求平均只要20ms,你一个线程每秒可以处理50个请求,200个线程每秒可以处理10000个请求,网关系统,单机QPS可以达到上万了,也不是不能实现的,redis基于内存的缓存系统,外部系统大量的往redis打过去请求,redis对每个请求都基于纯内存来实现,每个请求处理速度极快,在几毫秒到几十毫秒不等
普通4核,8核的机器,部署一个redis,每秒并发量达到上万,很轻松的,机器的cpu负载也刚好差不多打满
4、tomcat生产调优经验
Java网关系统,基于tomcat部署,提升网关的并发能力,servlet同步模式下的问题,java网关必然是servlet 3.x异步模式,大幅度提升并发能力
哪怕是你用tomcat+servlet同步模式(默认情况下都是同步模式),我们要做tomcat的生产调优,怎么来做,tomcat的工作线程数量,线程数量越多,并发能力越高,并不是如此,1000,2000,3000,绝对废掉
儒猿云平台秒杀系统的tomcat调优经验,尽可能的把tomcat线程数量搞多一些,800,直接开始进行压测,压测系统会对你的tomcat拼命发起请求,每个线程都在拼命的执行秒杀的请求,每个秒杀请求就是一个秒杀事务,实现,完成,对redis发起几十次访问,真实的秒杀业务场景下,搞的是比较复杂的
几百毫秒之内对redis发起几十次访问,tomcat的线程拼命的进行网络通信,就是通过网络跟redis建立连接,发送网络请求,800个线程拼命的疯狂的执行网络通信和请求,服务器的cpu负载直接打到100多%,130%
废了,机器废了,cpu负载太高,执行不过来了,线程就没法继续运转,直接影响,线程跑不动,没法处理请求,压测系统发送过来的请求大量的都是超时->失败,请求成功率很低很低,大量请求都是失败的
4核cpu
每个线程每次请求处理,仅仅少数一两次网络通信,访问mysql/redis一两次,压测了,先设置一个比较大的线程数,然后直接压测,压测到极致,看看他的cpu负载,如果cpu负载还好,50%,60%,4核cpu,每个请求仅仅是内存了做了一大堆的业务逻辑,然后直接写kafka,你有几百个线程的时候,每秒处理的请求量达到了一两千,此时cpu负载也就50%~60%,不同的系统,压测跑起来了,cpu负载是不一样的
线程数量继续加,开始减少
200个线程,ok了,每秒大致就处理200左右的秒杀请求,让每个线程每秒就处理一个秒杀请求,每个请求大致要几百毫秒完成,不要立即处理下一个请求,让线程休息一会儿吧,cpu负载大致是在70~90%之间,此时就差不多了
tomcat的线程数量大致在合理范围,调优结束
4核cpu,200~400之间,cpu核越多,线程数量肯定越多,tomcat并发能力就越强
调优一般两块,tomcat自己本身是一个jvm进程,jvm调优,垃圾回收,full gc一定要少,减少stop the world的次数和时间,就可以了,jvm调优也就这么回事儿