性能优化专题 01 - Tomcat性能优化之源码分析

前言

性能优化专题共计四个部分,分别是:

  • Tomcat 性能优化
  • MySql 性能优化
  • JVM 性能优化
  • 性能测试

本节是性能优化专题第一部分 —— Tomcat性能优化篇,共计三个小节,分别是:

What is Tomcat

Tomcat官网上,映入眼帘的就是开篇的一句话:

The Apache Tomcat® software is an open source implementation of the Java Servlet, JavaServer Pages, Java Expression Language and Java WebSocket technologies.

ApacheTomcat®软件是Java Servlet,JavaServer Pages,Java Expression Language和Java WebSocket技术的开源实现。

版本的选择与理由

环境选择

在本专题,Tomcat我们一律采用 8.0.11 版本。
JDK版本:大于等于1.7。

tomcat各个版本下载地址:各个版本产品和源码 【 Download/Archives 】

选择理由

扫描二维码关注公众号,回复: 12303615 查看本文章

在tomcat7.0中没有NIO2,在tomcat8.5中没有BIO,而在tomcat8.0中支持的比较丰富

可以在Tomcat8.0.11源码(没有的同学可以参考文末的下载链接)中验证一下: AbstractEndpoint.bind()—>implementation
在这里插入图片描述
AbstractEndpoint的bind方法,其实现类包括NIO与BIO,实现类丰富,便于知识点覆盖,所以采用此版本。

手写Tomcat源码

在从 Tomcat官网 上下载到Tomcat源码:

在这里插入图片描述
然后选择V8.0.11版本:
在这里插入图片描述
最后选择src目录:
在这里插入图片描述
下载到源码以后,为了方便查阅,我们需要将pom.xml文件集成到根目录,pom文件如下:

<?xml version="1.0" encoding="utf-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>org.apache.tomcat</groupId>
    <artifactId>Tomcat8.0.11</artifactId>
    <name>Tomcat8.0.11</name>
    <version>8.0.11</version>
    <build>
        <finalName>Tomcat8.0.11</finalName>
        <!-- 指定源文件为java 、test -->
        <sourceDirectory>java</sourceDirectory>
        <testSourceDirectory>test</testSourceDirectory>
        <resources>
            <resource>
                <directory>java</directory>
            </resource>
        </resources>
        <testResources>
            <testResource>
                <directory>test</directory>
            </testResource>
        </testResources>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>2.3.2</version>
                <configuration>
                    <encoding>UTF-8</encoding>
                    <!-- 指定jdk 编译 版本 ,没装jdk 1.7的可以变更为1.6 -->
                    <source>1.8</source>
                    <target>1.8</target>
                </configuration>
            </plugin>
        </plugins>
    </build>
    <!-- 添加tomcat8 所需jar包依赖 -->
    <dependencies>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.12</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>ant</groupId>
            <artifactId>ant</artifactId>
            <version>1.7.0</version>
        </dependency>
        <dependency>
            <groupId>wsdl4j</groupId>
            <artifactId>wsdl4j</artifactId>
            <version>1.6.2</version>
        </dependency>
        <dependency>
            <groupId>javax.xml</groupId>
            <artifactId>jaxrpc</artifactId>
            <version>1.1</version>
        </dependency>
        <dependency>
            <groupId>org.easymock</groupId>
            <artifactId>easymock</artifactId>
            <version>3.3</version>
        </dependency>
        <dependency>
            <groupId>org.eclipse.jdt.core.compiler</groupId>
            <artifactId>ecj</artifactId>
            <version>4.6.1</version>
        </dependency>
    </dependencies>
</project>

如果上面的过程你觉得没有必要,可以直接找到文末我已经整理好的源码,直接下载即可。
接下来我们使用IDE工具打开源码:在这里插入图片描述
(1)bin:主要用来存放命令,.bat是windows下,.sh是Linux下

(2)conf:主要用来存放tomcat的一些配置文件

(3)lib:存放tomcat依赖的一些jar包

(4)logs:存放tomcat在运行时产生的日志文件

(5)temp:存放运行时产生的临时文件

(6)webapps:存放应用程序

(7)work:存放tomcat运行时编译后的文件,比如JSP编译后的文件

这块咱们就不详细去说了,因为在Javaweb中都学过,即使忘了一些文件或者文件夹的作用,网上介绍的一大堆~

Tomcat 8.0 Mini

One More Thing,分析Tomcat源码之前,请允许我现在站在上帝视角,以极其简短的代码概括Tomcat8.0的核心功能,我将其称之为Tomcat 8.0 Mini!

这里有人要问了,Tomcat到底是做什么的?核心功能是什么?
实际上Tomcat是一个web服务器,说白了就是能够让客户端和服务端进行交互。比如客户端想要获取服务端某些资源,服务端可以通过tomcat去进行一些处理并且返回。

为什么要手写?既然上述提到了tomcat是java语言写的,又和servlet相关,那就自己设计一个试试,先不管作者的想法如何 。

基于Socket进行网络通信

//基于网络编程socket套接字来做
class MyTomcat{
    
    
	ServerSocket server = new ServerSocket(8080);
	Socket socket = server.accept();
	InputStream in = socket.getInputStream();
	OutputStream out = socket.getOutputStream();
}

好了,这就是mini版tomcat,实际上就是通过serversocket在服务端监听一个端口,等待客户端的连接,然后能够获取到对应的输入输出流。

是不是没过瘾,接下来我们对其做一个升级

Tomcat 8.0 Pro

//优化1:将输入输出流封装到对象
class MyTomcat{
    
    
	ServerSocket server=new ServerSocket(8080);
	Socket socket=server.accept();
	InputStream in=socket.getInputStream();
	new Request(in);
	OutputStream out=socket.getOutputStream();
	new Response(out);
}

class Request{
    
    
	private String host;
	private String accept-language;
} 
class Response{
    
    }

发现一个比较靠谱的tomcat已经被我们写出来了,问题是这个tomcat如果使用起来方便吗?你会发现不方便,因为对应的request和response都放到了tomcat源码的内部,业务人员想要进行开发时,很难获得request对象,从而获得客户端传来的数据,也不能进行很好的返回,怎么办呢?

Tomcat 8.0 Pro Max

我们发现在JavaEE中有servlet这项技术,比如我们进行登录功能业务代码开发时,写过如下这段代码和配置
servlet:

package com.test.web.servlet.SimpleServlet;

//优化2前奏:
class LoginServlet extends HttpServlet{
    
    
	doGet(request,response){
    
    }
	doPost(request,response){
    
    }
}

web.xml配置:

<servlet>
	<servlet-name>LoginServlet</servlet-name>
	<servlet-class>com.test.web.servlet.SimpleServlet</servlet-class> 
</servlet>

<servlet-mapping>
	<servlet-name>LoginServlet</servlet-name>
	<url-pattern>/login</url-pattern>
</servlet-mapping>

所以我们现在通过集成HttpServlet获取到request与response对象后再做调整:

//优化2:
class MyTomcat{
    
    
	List list = new ArrayList();
	ServerSocket server = new ServerSocket(8080);
	Socket socket = server.accept();
	//也就是这个地方不是直接处理request和response
	//而是处理一个个servlets
	list.add(servlets);
}

手写版tomcat[servlets]真的可行吗?

换句话说:tomcat官方开发者对于用list集合保存项目中的servlets也是这样想的吗?我们可以从几个维度进行一下推测

servlet之业务代码

业务代码中关于servlet想必大家都配置过,或者用注解的方式,原本开发web应用就采用的是这样的方式。你的controller中有很多自己写的servlet,都继承了HttpServlet类,然后web.xml文件中配置过所有的servlets,也就是mapping映射,这个很简单。

servlet之产品角度

如果apache提供的tomcat也这么做了,势必也要跟servlet规范有关系,也就是要依赖servlet的jar包,我们来看一下在tomcat产品的文件夹之下有没有servlet.jar,发现有。

servlet之源码角度

最后我们如果能够在tomcat源码中找到载入servlets的依据,就更加能说明问题了

于是我们在idea中的tomcat8.0源码,关键是到哪里找呢?总得有个入口吧?源码中除了能够看到各种Java类型的文件之外,一脸懵逼,怎么办?

不妨先跳出来想想,如果我们是tomcat源码的设计者,也就是上述手写的代码,我们怎么将业务代码中的servlets加载到源码中?我觉得可以分为两步

(1)加载web项目中的web.xml文件,解析这个文件中的servlet标签,将其变成java中的对象
(2)在源码中用集合保存

注意第(1)步,为什么是加载web.xml文件呢?因为要想加载servlets,一定是以web项目为单位的,而一个web项目中有多少个servlet类,是会配置在web.xml文件中的。

寻找和验证,加载和解析web.xml文件

加载 :ContextConfig.webConfig()—>getContextWebXmlSource()—>Constants.ApplicationWebXml
在这里插入图片描述
解析 :org.apache.catalina.startup.ContextConfig.webConfig()—>configureContext(webXml)—>context.createWrapper()
在这里插入图片描述
将servlets加载到list集合中

org.apache.catalina.core.StandardContext.loadOnStartup(Container children[])—>list.add(wrapper)

在这里插入图片描述

加载servlets的疑惑

怎么知道上面找的过程的?

我们会发现上面加载web.xml文件和添加servlets都和Context有点关系,因为都有这个单词,那这个Context大家眼熟吗?其实我们见过,比如你把web项目想要供外界访问时,你会添加web项目到webapps目录,这是tomcat的规定,除此之外,还可以在conf/server.xml文件中配置Context标签。

按照经验之谈,一般框架的设计者都会提供一两个核心配置文件给我们,比如server.xml就是tomcat提供给我们的,而这些文件中的标签属性最终会对应到源码的类和属性。

手写版tomcat[监听端口]可行吗?

换句话说:tomcat官方开发者对于监听端口也是这么设计的吗

其实我们手写的tomcat这块有两个核心:第一是监听端口,第二是添加servlets,上面解决了添加servlets。

接下来显然我们有必要验证一下监听端口tomcat也是这么做的吗?

监听端口之源码角度

org.apache.catalina.connector.Connector.initInternal()->protocolHandler.init()
在这里插入图片描述
->AbstractProtocol.init()
在这里插入图片描述
->endpoint.init()->bind()

这里的bind()的实现方式,就是我们开篇提到的Socket实现方式,包括Apr,JIo,NIO,NIO2这四种方式,正因为实现类丰富,我们才使用了Tomcat8.0.11版本。

监听端口的疑惑

为什么知道找Connector?

再次回到conf/web.xml文件,发现有一个Connector标签,而且还可以配置port端口,我们能够联想到监听端口,按照配置文件到源码类的经验,源码中一定会有这样一个类用于端口的监听。

Tomcat架构图

在tomcat这块左边一定会监听在某个端口,等待客户端的连接,不然所有的操作都没办法进行交互
在这里插入图片描述
里面的模块,Engine,Host等模块可依据配置文件web.xml抽丝剥茧,不难分析出来。
而我们通过官网查到的Tomcat架构图也印证了我们的猜想:
在这里插入图片描述

Tomcat源码分析

认识强化主要组件的含义

从上面的Tomcat架构图中我们可以知道,不同的组件分工不同,用以实现客户端与服务端之间的交互。那么接下来,我们通过官方文档进一步加深对于不同组件的理解,毕竟,学习一项开源技术,官方网站相对比较权威。

官网访问步骤:Documentation/Tomcat8.0/Development/Architecture/Overview

最终打开页面:http://tomcat.apache.org/tomcat-8.0-doc/architecture/overview.html

我们发现:
在这里插入图片描述

组件之间的关联关系

换句话说:之前找了两个点,监听端口,加载servlets的调用过程是如何的?

比如bind(), loadOnstartup()到底谁来调用?

此时大家还是要回归到最初的流程,客户端发起请求到得到响应来看。

  • 客户端角度:发起请求,最终得到响应

  • tomcat代码角度 :虽然是要监听端口和添加servlets进来,但是肯定有一个主函数,从主函数开始调用

说白了,如果我是源码设计者,既然架构图我都了解了,肯定是要把这些组件初始化出来,然后让它们一起工作,也就是:

  • 初始化一个个组件

  • 利用这些组件进行相应的操作

寻找源码开始的地方

我们总说看别人的代码是一种痛苦,尤其是源码。很大的一个原因就在于我们不了解整体架构,不知道作者这样表达的含义在哪里?
更何况一款优秀的开源框架往往都是一个团队花费大量时间与精力,精雕细琢出来的。在我们不了解逻辑的情况下,盲目查看,不自觉之间就会掉入设计模式的汪洋大海里。久之,则失去了对于源码探索的勇气,多了一份对于其的恐惧感,而往往对于源码的理解则是程序员的分水岭!

那么我们如何去学习源码以提高认知呢?

  • 第一手资料:查阅官网,看别的人转发的文章,往往都是第二手资料,我们每个人的理解是不同的,消息的传递有可能造成对于你的误解。
  • 根据前人总结:结合自己的理解,一步一步地将其融化。我即将在下文总结Tomcat的源码解析。
  • 熟能生巧:源码不是一成不变的东西,也需要更新与维护,在掌握了整体脉络以后,我们自己是否可以对其升华?

好的,闲聊结束,开始正文,建议同学们下载好源码,与我一起跟着步骤依次翻阅,这样理解更深!

一定有一个类,这个类中有main函数开始,这样才能有一款java源码到产品,一贯的作风。感性的认知: org.apache.catalina.startup.BootStrap ->main()->根据脚本命令->startd
在这里插入图片描述
果然被我们找到了,先加载再启动,那就继续看咯

类比推理,不难得出,daemon.load() 为加载过程,daemon.start()启动过程…

加载:daemon.load()的过程

Bootstrap.main()->Bootstrap.load()
在这里插入图片描述
紧接着进入Bootstrap.load()方法:
在这里插入图片描述

这里的method.invoke 执行的方法是什么呢?实际上在Catalina.init()方法里就对method做了赋值,即method为org.apache.catalina.startup.Catalina,而在load()方法里,则指定了Catalina类里具体的方法以及构造参数,所以Catalina.load()这里最终会带着指定参数执行Catalina.load()方法。

现在看->Catalina.load(),有一段关键内容,即 start the new server:
在这里插入图片描述

初始化的依据是什么?考虑coder的设计server.xml,即加载server.xml文件,初始化一些组件。

于是我们看getServer().init(),发现会跳转到Lifecycle.init()
->LifecycleBase.init()->LifecycleBase.initInternal()
在这里插入图片描述
LifecycleBase.init()有三个实现类,这里我们选择默认实现,即LifecycleBase.initInternal()
在这里插入图片描述
->StandardServer.initInternal()
在我们前面提到的Tomcat架构图里我们说,Server组件是最外层的部分,所以这里选择StandardServer。记得这里是一个十字路口,因为会初始化很多东西,后面还对多次提到不同的初始化内容。
在这里插入图片描述
->services[i].init()
参考Tomcat架构图,不难发现,在Server层里面,Service组件是可以存在多个的,所以这里进行循环遍历,依次初始化。

在这里插入图片描述
->StandardService.initInternal()

回过头来到十字路口,再看其他组件初始化过程,现在我们看Service初始化:
在这里插入图片描述
->executor.init()/ connector.init()

executor.init()这里初始化以后,发现又回回到初始化默认实现LifecyleBase.initInternal()方法

->LifecyleBase.initInternal()

既然又回到十字路口,我们依据Tomcat架构图看看Connector的初始化
->Connector.initInternal()
在这里插入图片描述
->protocolHandler.init()
在这里插入图片描述
这里我们选择AbstractProtocol
->AbstractProtocol.init()
在这里插入图片描述
->endpoint.init()->bind()
又回到最初的起点,前面已提过这里的Socket实现。
在这里插入图片描述

->Apr,JIo,NIO,NIO2

在这里插入图片描述

之所以采用多种IO操作,其目的就在于适用于不同的场景,IO的不同,其性能是不同的。

conclusion:请求目前没有来,只是内部的初始化工作

启动:daemon.start()的过程

前面我们通过daemon.load()方法,主要对Tomcat中Server、Service、Connector组件进行了初始化,那么关于Engine,Host,Context,Wrapper等这些组件却没有提及,回过头来我们看Bootstrap启动startd的过程中除了daemon.load(),剩下的就是daemon.start()。

Bootstrap.start()->Catalina.start()
同load()过程,这里使用method.invoke,直接跳转到Catalina.start()

->getServer.start()->LifecycleBase.start()
同load()过程,这里选择LifecycleBase默认初始化方法

->LifecycleBase.startInternal()
在这里插入图片描述

->StandardServer.startInternal()
在这里插入图片描述

->services[i].start()
同load()过程,循环遍历service,分别初始化
在这里插入图片描述
->StandardService.startInternal()

同load()过程,选择LifecycleBase,然后继续初始化其他组件
在这里插入图片描述

这里我们进入

container.start();

在这里插入图片描述

-> container.start()/executors.init()/connectors.start()

查看一下Container接口,发现Engine,Host,Context,Wrapper等都是他的实现类。那么 container.start(),实际上就是对于这些子类进行初始化。
在这里插入图片描述
回过头来我们进入container.start()方法:
在这里插入图片描述
同上,这里依旧选择默认:

在这里插入图片描述
再次进入初始化startInternal()方法。
在这里插入图片描述
->engine.start()

现在进入最外层的组件,选择StandardEngine

在这里插入图片描述
->StandardEngine.startInternal()

查看一下StandardEngine类关系结构图,发现ContainerBase是它的爸爸,而这个爸爸有多少孩子呢?
Engine,Host,Context,Wrapper都是它的孩子

->super[ContainerBase].startInternal()->代码呈现

//调用Engine子容器的start方法
results.add(startStopExecutor.submit(new StartChild(children[i])))

那子容器是什么呢?可能就是Engine,Host,Context,Wrapper等。

我们继续看线程池提交任务的过程都做了什么事情?

首先看StartChild方法:
在这里插入图片描述

注意child.start,那么这里的child是谁呢?可能就是Engine,Host,Context,Wrapper等。
似曾相识吧,这里我们再次轮询上面的过程。

->LifecycleBase.start()
->startInternal()
->StandardHost.startInternal()

以Host为例,这个过程就完成了Host组件初始化, Host将一个个web项目加载进来:

StandardHost.startInternal()
->ContainerBase.startInternal()

在这里插入图片描述
通过不断的循环子类放入到线程池的过程(责任链模式),就完成了对于Engine,最后threadStart()。

->threadStart()

protected void threadStart() {
    
    

        if (thread != null)
            return;
        if (backgroundProcessorDelay <= 0)
            return;

        threadDone = false;
        String threadName = "ContainerBackgroundProcessor[" + toString() + "]";
        //核心实现
        thread = new Thread(new ContainerBackgroundProcessor(), threadName);
        thread.setDaemon(true);
        thread.start();

    }

我们看new Thread后重写run方法,都做了什么事情,所以我们看初始化ContainerBackgroundProcessor的过程

->container.backgroundProcess()
在这里插入图片描述
老套路,进入默认实现ContainerBase
->ContainerBase.backgroundProcess()
在这里插入图片描述
->fireLifecycleEvent(Lifecycle.PERIODIC_EVENT, null)

在这里插入图片描述
->listener.lifecycleEvent(event)->interested[i].lifecycleEvent(event)

在这里插入图片描述

->监听器HostConfig->HostConfig.lifecycleEvent(LifecycleEvent event)
在这里插入图片描述

->check()-> deployApps()
deployApps顾名思义,发布APP

在这里插入图片描述

protected void deployApps() {
    
    

        File appBase = host.getAppBaseFile();
        File configBase = host.getConfigBaseFile();
        String[] filteredAppPaths = filterAppPaths(appBase.list());
        // Deploy XML descriptors from configBase
        deployDescriptors(configBase, configBase.list());
        // Deploy WARs
        deployWARs(appBase, filteredAppPaths);
        // Deploy expanded folders
        deployDirectories(appBase, filteredAppPaths);

    }

回到 StandardHost.startInternal(),回到最初的起点,我们看看Context组件初始化过程:

-> super.startInternal()
在这里插入图片描述

-> results.add(startStopExecutor.submit(new StartChild(children[i])));
在这里插入图片描述

然后又会调用它的子容器
-> super.startInternal()

在这里插入图片描述

-> StandardContext.initInternal()

在这里插入图片描述
解析每个web项目
-> StandardContext.startInternal()
-> fireLifecycleEvent(Lifecycle.CONFIGURE_START_EVENT, null)
在这里插入图片描述
-> listener.lifecycleEvent(event)
-> interested[i].lifecycleEvent(event)

在这里插入图片描述

->[找实现]ContextConfig.lifecycleEvent(LifecycleEvent event)

在这里插入图片描述

-> configureStart()

在这里插入图片描述

-> webConfig()
在这里插入图片描述
解析每个web项目的xml文件了
-> getContextWebXmlSource()
-> Constants.ApplicationWebXml
在这里插入图片描述
这就回到了刚才我们提过的

public static final String ApplicationWebXml = "/WEB-INF/web.xml";

通过ContextConfig.webConfig()加载配置后,解析元素到servlets包装成wrapper对象。

何时调用loadOnstartup()?

在这里插入图片描述

官网验证上述流程

在前面的Tomcat初始化组件的过程,我们也可以从官网的UML时序图加以认证。

Server Startup

关于Startup官网访问步骤:
Documentation/Tomcat8.0/Apache Tomcat Development/Architecture/Server Startup

Server Startup

Request Process

关于Process官网访问步骤:
Documentation/Tomcat8.0/Apache Tomcat Development/Architecture/Request Process

UML sequence diagram

写在最后

Tomcat 8.0.11 源码地址:

https://github.com/harrypottry/apache-tomcat-8.0.11-src

更多架构知识,欢迎关注本套系列文章Java架构师成长之路

猜你喜欢

转载自blog.csdn.net/qq_34361283/article/details/109744651