Tomcat架构篇1-Tomcat总体设计以及核心组件

457 阅读16分钟

欢迎大家关注 github.com/hsfxuebao ,希望对大家有所帮助,要是觉得可以的话麻烦给点一下Star哈

1. 总体架构

1.1 架构图

tomcat中有一个非常重要的xml,server.xml,我们看下server.xml对应的真实的tomcat架构是啥样的。如下:

<?xml version="1.0" encoding="UTF-8"?>
<Server port="8005" shutdown="SHUTDOWN">
  <Listener className="org.apache.catalina.startup.VersionLoggerListener" />
 
  <Listener className="org.apache.catalina.core.AprLifecycleListener" SSLEngine="on" />
  <!-- Prevent memory leaks due to use of particular java/javax APIs-->
  <Listener className="org.apache.catalina.core.JreMemoryLeakPreventionListener" />
  <Listener className="org.apache.catalina.mbeans.GlobalResourcesLifecycleListener" />
  <Listener className="org.apache.catalina.core.ThreadLocalLeakPreventionListener" />

  <!-- 实现JNDI   Global JNDI resources
       Documentation at /docs/jndi-resources-howto.html
  -->
  <GlobalNamingResources>
    <!-- Editable user database that can also be used by
         UserDatabaseRealm to authenticate users
    -->
    <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>


<!--  Catalina服务【Tomcat请求处理服务】这个服务使用Connector,可以接收数据,处理请求-->
  <Service name="Catalina">

    <!--The connectors can use a shared executor, you can define one or more named thread pools-->
   
<!--   代表我们需要监听的端口 -->
    <Connector port="8080" protocol="HTTP/1.1"
               connectionTimeout="20000"
               redirectPort="8443" />
    
    <!--    Catalina Service 是处理请求的服务,真正会交给引擎进行处理,引擎控制整个处理逻辑-->
    <Engine name="Catalina" defaultHost="localhost">

      <!--  认证信息-->
      <Realm className="org.apache.catalina.realm.LockOutRealm">
        <!-- This Realm uses the UserDatabase configured in the global JNDI
             resources under the key "UserDatabase".  Any edits
             that are performed against this UserDatabase are immediately
             available for use by the Realm.  -->
        <Realm className="org.apache.catalina.realm.UserDatabaseRealm"
               resourceName="UserDatabase"/>
      </Realm>

      <!--   主机  http://localhost:8080/ -->
      <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 &quot;%r&quot; %s %b" />

      </Host>
      <!--    http://hsf66.com:8080/hello_test/hello  -->
      <Host name="hsf66.com" appBase="hsf"
            unpackWARs="true" autoDeploy="true">

      </Host>
    </Engine>
  </Service>

<!-- 当前Tomcat服务器  可以完成很多服务Service,不只是处理请求 -->
<!--  这个服务,实现缓存数据的功能类似redis-->
  <Service name="cacheService">
    <Connector port="5555"></Connector>

  </Service>
</Server>

我们把核心的一个组件抽取出来:如下:

Server{
    Service[]{:服务
        Connector[]:监听端口
        Engine {:控制处理逻辑
            Host[]{:虚拟主机,映射域名
                Context[]{:代表web应用
                    Wrapper[]{:每一个Wrapper封装一个Servlet的配置详情
                    }
                }
            }
        }
    }
}
localhost:8080/docs/xxx
hsf66.com:8080/hello test/hello

进而,得出总体架构图如下: image.png 从上图我们看出,最核心的两个组件--连接器(Connector)和容器(Container)起到心脏的作用,他们至关重要!他们的作用如下:

1、Connector用于处理连接相关的事情,并提供Socket与Request和Response相关的转化;
2、Container用于封装和管理Servlet,以及具体处理Request请求;

一个Tomcat中只有一个Server,一个Server可以包含多个Service,一个Service只有一个Container,但是可以有多个Connectors,这是因为一个服务可以有多个连接,如同时提供Http和Https链接,也可以提供向相同协议不同端口的连接。其中(Engine、Host、Context属于容器(Container))。

1.2 请求流程图

我们访问链接 http://localhost:8080/hsf/hello 为例,看一下整体请求过程: image.png

2. Tomcat架构演进和核心组件

为了使读者能更深刻地理解Tomcat的相关组件概念,我们将采用一种启发式的讲解方式来介绍Tomcat的总体设计。从如何设计一个应用服务器开始,逐步完善,直至最终推导出Tomcat的整体架构。

2.1 Server

从最基本的功能来讲,我们可以将服务器描述为这样一个应用:

它接收其他计算机(客户端)发来的请求数据并进行解析,完成相关业务处理,然 后把处理结果作为响应返回给请求计算机(客户端)。

通常情况下,我们通过使用Socket监听服务器指定端口来实现该功能。按照该描述,一个最 简单的服务器设计如图所示。
image.png

我们通过start()方法启动服务器,打开Socket链接,监听服务器端口,并负责在接收到客户 端请求时进行处理并返回响应。同时提供一个stop()方法来停止服务器并释放网络资源。

缺点:请求监听和请求处理放一起扩展性很差(协议的切换 tomcat独立部署使用HTTP协议,与Apache集成时使用AJP协议)

2.2 Connector 和 Container

将请求监听与请求处理放到一起扩展性很差,比如当我们想适配多种网 络协议,但是请求处理却相同的时候。那么我们如何通过面向对象的方式来解决这个问题?自然的想法就是将网络协议与请求处 理从概念上分离。于是,我们做了如下改进:

一个Server可以包含多个Connector和Container。其中Connector负责开启Socke併监听客户端 请求、返回响应数据;Container负责具体的请求处理。Connector和Container分别拥有自己的start。 和stop()方法来加载和释放自己维护的资源。

但是,这个设计有个明显的缺陷。既然Server可以包含多个Connector和Container,那么如何 知晓来自某个Connector的请求由哪个Container处理呢?当然,我们可以维护一个复杂的映射规则 来解决这个问题,但是这并不是必需的,后续章节你会发现Container的设计已经足够灵活,并不 需要一个Connect。儲接到多个Container。更合理的方式如图所示。

一个Server包含多个Service (它们互相独立,只是共享一个JVM以及系统类库),一个Service 负责维护多个Connector和一个Container,这样来自Connector的请求只能由它所属Service维护的 Container 处理。

2.3 Container 设计

上一节的设计已经解决了网络协议和容器的解耦,但是应用服务器是用来部署并运行Web 应用的,是一个运行环境,而不是一个独立的业务处理系统。因此,我们需要在Engine容器中 支持管理Web应用,当接收到Connector的处理请求时,Engine容器能够找到一个合适的Web应用 来处理。

我们用Host表示虚拟主机的概念,Host可以包含多Context。在一个Web应用中,可包含多个Servlet实例以处理来 自不同链接的请求。因此,我们还需要一个组件概念来表示Servlet定义。在Tomcat中,Servlet定 义被称为Wrapper,基于此修改后的设计如图所示。

image.png

我们使用Container来表示容器,Container可以添加并维护子容器,因此Engine、Host、Context, Wrapper均继承自Container。我们将它们之间的组合关系改为虚线,以表示它们之间是弱依赖的 关系,即它们之间的关系是通过Container的父子容器的概念体现的。不过Service持有的是Engine 接口(8.5.6版本之前为Container接口,更加通用)。

注意: 既然Tomcat的Container可以表示不同的概念级别:Servlet引擎、虚拟主机、Web应用和 Servlet,那么我们就可以将不同级别的容器作为处理客户端请求的组件,这具体由我们 提供的服务器的复杂度决定。

image.png

此外,Tomcat的Container还有一个很重要的功能,就是后台处理。在很多情况下,我们的 Container需要执行一些异步处理,而且是定期执行,如每隔30秒执行一次,Tomcat对于Web应用 文件变更的扫描就是通过该机制实现的。Tomcat针对后台处理,在Container上定义了 backgroundProcess()方法,并且其基础抽象类(ContainerBase )确保在启动组件的同时,异步 启动后台处理。因此,在绝大多数情况下,各个容器组件仅需要实现Container的backgroundProcess()方法即可,不必考虑创建异步线程

2.4 Lifecycle

我们很容易发现,所有组件均存在启动、停止等生命周期方法,拥有生命周期管理的特性。 因此,我们可以基于生命周期管理进行一次接口抽象。

我们针对所有拥有生命周期管理特性的组件抽象了一个Lifecycle通用接口,该接口定义了生 命周期管理的核心方法。

  • init():初始化组件。
  • start():启动组件。
  • stop():停止组件。
  • destroy():销毁组件。

image.png

同时,该接口支持组件状态以及状态之间的转换,支持添加事件监听器(LifecycleListener) 用于监听组件的状态变化。如此,我们可以采用一致的机制来初始化、启动、停止以及销毁各个 组件。如Tomcat核心组件的默认实现均继承自LifecycleMBeanBase抽象类,该类不但负责组件各个 状态的转换和事件处理,还将组件自身注册为MBean,以便通过Tomcat的管理工具进行动态维护

并不是每个状态都会触发生命周期事件,也不是所有生命周期事件均存在对应状态。 状态与应用生命周期事件的对应如表所示:

方法状态生命周期事件
init()初始化中(INITIALIZING)初始化前(BEFORE_INIT_EVENT)
已初始化(NITIALIZED)初始化后(AFTER INIT_EVENT)
start()启动前(STARTING_PREP)启动前(BEFORE_START_EVENT)
启动中(STARTING)启动(START_EVENT)
已启动(STARTED)启动后(AFTER_START_EVENT)
stop()停止前(STOPPING_PREP)停止前(BEFORE_STOP_EVENT)
停止中(STOPPING)停止(STOP_EVENT)
已停止(STOPPED)停止后(AFTER_STOP_EVENT)
destroy()销段中(DESTROYING)销段前(BEFORE_DESTROY_EVENT)
已销段(DESTROYED)销段后(AFTER_DESTROY_EVENT)
周期事件(PERIODIC_EVENT)
配置启动(CONFIGURE_START_EVENT)
配置停止(CONFIGURE_STOP_EVENT)

每个生命周期方法影响的组件状态以及每个状态触发的事件。此外,我们还注意到,Tomcat默认提供了3个与状态无关的事件类型,其中PERIODIC_EVENT 主要用于Container的后台定时处理,每次调用后触发该事件。CONFIGURE_START_EVENT和 CONFIGURE_STOP_EVENT的使用在后续章节中将会讲到。

2.5 Pipeline 和 Valve

从架构设计的角度来考虑,至此的应用服务器设计主要完成了我们对核心概念的分解,确保了整体架构的可伸缩性和可扩展性,除此之外,我们还要考虑如何提高每个组件的灵活性,使其同样易于扩展。

在增强组件的灵活性和可扩展性方面,职责链模式是一种比较好的选择。Tomcat定义了Pipeline (管道)和Valve (阀)两个接口。前者用于构造职责链,后者代表职 责链上的每个处理器。当然,我们还可以从字面意思来理解这两个接口所扮演的角色一来自客户端的请求就像是流经管道的水一般,经过每个阀进行处理。其设计所示。

image.png

Pipeline中维护了一个基础的Valve,它始终位于Pipeline的末端(即最后执行),封装了具体 的请求处理和输出响应的过程。然后,通过addValve()方法,我们可以为Pipeline添加其他的Valve后添加的Valve位于基础Valve之前,并按照添加顺序执行。Pipeline通过获得首个Valve来启动整个链条的执行。

Tomcat容器组件的灵活之处在于,每个层级的容器(Engine, Host、Context, Wrapper )均有对应的基础Valve实现,同时维护了一个Pipeline实例。也就是说,我们可以在任何层级的容器 上针对请求处理进行扩展。

由于Tomcat每个层级的容器均通过Pipeline和Valve进行请求处理,那么,我们很容易将一些 通用的Valve实现根据需要添加到任何层级的容器上。修改后的应用服务器设计如图所示。

image.png

2.6 Connector 设计

前面我们重点讨论了容器组件的设计,集中于如何设计才能确保容器的灵活性和可扩展性, 并做到合理的解耦。接下来,我们再细化一下服务器设计中的另一个重要组件一Connector。要想与Container配合实现一个完整的服务器功能,Connector至少要完成如下几项功能。

  • 监听服务器端口,读取来自客户端的请求。
  • 将请求数据按照指定协议进行解析。
  • 根据请求地址匹配正确的容器进行处理。
  • 将响应返回客户端。

只有这样才能保证将接收到的客户端请求交由与请求地址匹配的容器处理。Tomcat的设计方案如图所示:

image.png

在Tomcat中,ProtocolHandler表示一个协议处理器,针对不同协议和I/O方式,提供了不同的 实现。ProtocolHandler包含一个Endpoint 用于启动Socket监听,该接口按照I/O方式进行分类实现,如Nio2Endpoint表示非阻塞式Socket I/O。 还包含一个Processor用于按照指定协议读取数据,并将请求交由容器处理,如Http11NioProcessor 表示在NIO的方式下HTTP请求的处理类。

注意: Tomcat并没有Endpoint接口,仅有AbstractEndpoint抽象类,此处仅作为概念讨论,故将其 视为 Endpoint 接口 。

在Connector启动时,Endpoint会启动线程来监听服务器端口,并在接收到请求后调用 Processor进行数据读取

当Processor读取客户端请求后,需要按照请求地址映射到具体的容器进行处理,这个过程即为请求映射。Tomcat通过Mapper和MapperListener两个类实现上述功能。前者用于维护容器映射信息,同 时按照映射规则(Servlet规范定义)查找容器。后者实现了ContainerListener和LifecycleListener, 用于在容器组件状态发生变更时,注册或者取消对应的容器映射信息。为了实现上述功能, MapperListener实现了Lifecycle接口,当其启动时(在Service启动时启动),会自动作为监听器注 册到各个容器组件上,同时将已创建的容器注册到Mapper。

Tomcat通过适配器模式(Adapter )实现了Connector与Mapper、Container的解耦。Tomcat默 认的Connector实现(Coyote )对应的适配器为CoyoteAdapter。也就是说,如果你希望使用Tomcat 的链接器方案,但是又想脱离Servlet容器(虽然这种情况几乎不可能出现,但是从架构可扩展性 的角度来讲,还是值得讨论一下),此时只需要实现我们自己的Adapter即可。当然,我们还需要 按照Container的定义开发我们自己的容器实现(不一定遵从Servlet规范)。Connector设计如图所示:

image.png

2.7 Executor

完成了Connector的设计之后,我们再进一步审视一下当前的应用服务器方案,很明显,我们 忽略了一个问题——并发

首先,Tomcat提供了一致的可插拔的组件环境,那么 我们自然也希望线程池作为一个组件进行统一管理。因此,Tomcat提供了Executor接口来表示一 个可以在组件间共享的线程池(默认使用了JDK5提供的线程池技术),该接口同样继承自 Lifecycle,可按照通用的组件进行管理。

其次,线程池的共享范围如何确定?在Tomcat中Executor由Service维护,因此同一个Service 中的组件可以共享一个线程池。

当然,如果没有定义任何线程池,相关组件(如Endpoint)会自动创建线程池,此时,线程 池不再共享。 在Tomcat中,Endpoint会启动一组线程来监听Socket端口,当接收到客户端请求后,会创建 请求处理对象,并交由线程池处理,由此支持并发处理客户端请求。添加Executor后,总体设计如图所示:

image.png

2.8 Bootstrap 和 Catalina

我们在前面几个小节中讲解了 Tomcat总体架构中的主要核心组件,它们代表了应用服务器程 序本身,这就如楼房的主体。但是,除了主体建筑外,楼房还需要外墙等装饰,Tomcat也一样, 我们还需要提供一套配置环境来支持系统的可配置性,便于我们通过修改相关配置来优化应用服务器。

当然,我们没有涉及集群、安全等组件,尽管它们也非常重要,但是,我们还是希望更多 地关注于一些通用概念。虽然集群、安全等作为一个完备的应用服务器必不可少,但是它们的 缺失并不会影响我们去理解应用服务器的基本概念和设计方式。

我们列举了几个重要配置文件,其中最核心的文件为server.xml。通过 这个文件,我们可以修改Tomcat组件的配置参数甚至添加相关组件,这也是后续性能调优阶段重 点涉及的文件。

Tomcat通过类Catalina提供了一个Shell程序,用于解析server.xml创建各个组件,同时,负责启动、停止应用服务器(只需要启动Tomcat顶层组件Server即可)。

Tomcat 使用Digester 解析 XML 文件,包括 server.xml 以及 web.xml 等,具体可参见 commons.apache.org/proper/comm…

最后,Tomcat提供了Bootstrap作为应用服务器启动入口。Bootstrap负责创建Catalina实例,根 据执行参数调用Catalina相关方法完成针对应用服务器的操作(启动、停止)。

也许你会有疑问,为什么Tomcat不直接通过Catalina启动,而是又提供了Bootstrap呢?你可 以查看一下Tomcat的发布包目录,Bootstrap并不位于Tomcat的依赖库目录下CATALINA_HOME/bin目录下。Bootstrap与Tomcat应用服务器完全松耦 合(通过反射调用Catalina实例),它可以直接依赖JRE运行并为Tomcat应用服务器创建共享类加 载器,用于构造Catalina实例以及整个Tomcat服务器。

Tomcat的启动方式可以作为非常好的示范来指导中间件产品设计。它实现了启动入口与 核心环境的解耦,这样不仅简化了启动(不必配置各种依赖库,因为只有独立的几个API), 而且便于我们更灵活地组织中间件产品的结构,尤其是类加载器的方案,否则,我们所 有的依赖库将统一放置到一个类加载器中,而无法做到灵活定制。

至此,我们应用服务器的完整设计如图所示: image.png

上述是Tomcat标准的启动方式。但是正如我们所说,既然Server及其子组件代表了应用服务 器本身,那么我们就可以不通过Bootstrap和Catalina来启动服务器。

Tomcat提供了一个同名类org.apache.catalina.startup.Tomcat,使用它我们可以将Tomcat 服务器嵌入到我们的应用系统中并进行启动。当然,你可以自己编写代码来启动Server,也可以 自定义其他配置方式启动,如YAML。这就是Tomcat灵活的架构设计带给我们的便利,也是我们 设计中间件产品的架构关注点之一。

2.9 总结

最后,我们再整体回顾一下上述讲解涉及的Tomcat服务器中的概念:

组件名称说明
Server表示整个Servlet容器,因此Tomcat运行环境中只有唯一一个Server实例
ServiceService表示一个或者多个Connectorl的集合,这些Connector共享同一个Container来处理其请求。在同一个Tomcat实例内可以包含任意多个Service实例,它们彼此独立
Connector即Tomcat链接器,用于监听并转化Socket请求,同时将读取的Socket请求交由Container处理,支持不同协议以及不同的I/O方式
Container表示能够执行客户端请求并返回响应的一类对象。在Tomeat中存在不同级别的容器:Engine、Host、Context、Wrapper
EngineEngine表示整个Servlet引擎。在Tomcat中,Engine为最高层级的容器对象。尽管Engine不是直接处理请求的容器,却是获取目标容器的入口
HostHost作为一类容器,表示Servlet引擎(即Engine)中的虚拟机,与一个服务器的网络名有关,如域名等。客户端可以使用这个网络名连接服务器,这个名称必须要在DNS服务器上注册
ContextContext作为一类容器,用于表示ServletContext,在Servlet规范中,一个ServletContext即表示一个独立的Web应用
WrapperWrapper作为一类容器,用于表示Web应用中定义的Servlet
Executor表示Tomcat组件间可以共享的线程池

总结图如下:

image.png

下面,我们分别重点两个核心内容Catalina和Coyote进行讲解,中间也会穿插源码阅读。

参考文章

Tomcat架构解析
Tomcat总体架构:Server+Container 设计+Lifecycle等
Tomcat总体架构:Pipeline 和 Valve+Connector 设计等