精华内容
下载资源
问答
  • 一、如何排查系统的性能瓶颈点? 梳理系统的性能瓶颈点这件事应该不是一件简单的...这里我结合常用的nginx+tomcat+redis+mysql这类常见架构进行分析: 请求入口 所有的请求打入到后台的服务当中,首先需要考虑的

    一、如何排查系统的性能瓶颈点?

    梳理系统的性能瓶颈点这件事应该不是一件简单的事情,需要针对不同设计的系统来进行单独分析。

    首先一套完整可用的系统应该是有ui界面的(这里强调的是一套完整的,可用的系统,而并不是指单独的一个中台系统),系统分为了前端模块和后端模块。

    这里由于我个人的擅长领域更多是处于后端模块,所以对于系统的瓶颈点梳理我会从后端进行分析。

    这里我结合常用的nginx+tomcat+redis+mysql这类常见架构进行分析:

    请求入口 所有的请求打入到后台的服务当中,首先需要考虑的一个点就是:

    1、带宽因素

    假设有200m的流量同时请求进入服务器,但是带宽只有1m,这么来算光是接收这批数据量信息也要消耗大约200s的时间。带宽可以理解为在指定时间内从一端请求到另一端的流量总量。而且局域网和广域网的带宽计算其实也是不一样的

    2、服务器的ulimit

    通常我们使用的线上服务器都是centos系列,这里我列举centos7相关的系统配置:ulimit配置 查看服务器允许的最大打开文件数目(linux系统中设计概念为一切皆文件) 通常如果我们的java程序需要增大一些socket的链接数目,可以通过调整ulimit 里面的open参数进行配置。

    [root@izwz9ic9ggky8kub9x1ptuz ~]# ulimit -a | grep open
    open files                      (-n) 1000
    

    查看用户的最大进程数目

    [root@izwz9ic9ggky8kub9x1ptuz ~]# ulimit -a | grep user
    max user processes              (-u) 7284
    

    相关的配置存放在了/etc/security/limits.conf文件中。

    3、系统的一些内核参数配置

    如果是在一些压力测试场景中,我们通常会预见到这种报错:

    apr_socket_recv: Connection reset by peer (54)
    

    通常这种情况是因为系统内部的一些防范参数设置导致的,需要调整/etc/sysctl.conf 文件中的相关参数:

    net.ipv4.tcp_syncookies = 0
    #当并发请求数目超过了1000之后,服务器自身可能会认为是收到了syn泛洪攻击,但对于高并发系统,要禁用此设置
    
    net.ipv4.tcp_max_syn_backlog
    #参数决定了SYN_RECV状态队列的数量,一般默认值为512或者1024,即超过这个数量,系统将不再接受新的TCP连接请求,一定程度上可以防止系统资源耗尽。可根据情况增加该值以接受更多的连接请求。
    
    net.ipv4.tcp_tw_recycle
    #参数决定是否加速TIME_WAIT的sockets的回收,默认为0。
    
    net.ipv4.tcp_tw_reuse
    #参数决定是否可将TIME_WAIT状态的sockets用于新的TCP连接,默认为0。
     
    net.ipv4.tcp_max_tw_buckets
    #参数决定TIME_WAIT状态的sockets总数量,可根据连接数和系统资源需要进行设置。 

    对于防范参数还可以如下修改查看:

    cd /proc/sys/net/ipv4
    echo "0" > tcp_syncookies

    通常企业中使用的都是nginx进行接收请求,然后进行负载均衡转发。在nginx层里面会有几个核心参数配置:最大连接数,最大并发访问数。

    #指定同一个ip的每次请求数量都限制为10次
    limit_conn_zone $binary_remote_addr zone=perip:10m;
    limit_conn perip 10;

    二、Tomcat部分分析

    Tomcat支持三种接收请求的处理方式:BIO、NIO、APR 。

    1、Bio方式,阻塞式I/O操作即使用的是传统Java I/O操作,Tomcat7以下版本默认情况下是以bio模式运行的,由于每个请求都要创建一个线程来处理,线程开销较大,不能处理高并发的场景,在三种模式中性能也最低

    2、Nio方式,是Java SE 1.4及后续版本提供的一种新的I/O操作方式(即java.nio包及其子包),是一个基于缓冲区、并能提供非阻塞I/O操作的Java API,它拥有比传统I/O操作(bio)更好的并发运行性能。tomcat 8版本及以上默认nio模式

    3、apr模式,简单理解,就是从操作系统级别解决异步IO问题,大幅度的提高服务器的处理和响应性能, 也是Tomcat运行高并发应用的首选模式。启用这种模式稍微麻烦一些,需要安装一些依赖库, 而apr的本质就是使用jni技术调用操作系统底层的IO接口,所以需要提前安装所需要的依赖,首先是需要安装openssl和apr。

    tomcat连接参数调整

    在tomcat中有这么一段经典的配置参数:

    <maxHttpHeaderSize="8192"
        maxThreads="4000" minSpareThreads="1000" maxSpareThreads="2000"
        enableLookups="false" redirectPort="8443" acceptCount="2000"
        connectionTimeout="20000" disableUploadTimeout="true" />

    maxThreads表示tomcat最多可以创建多少个线程来处理请求。

    minSpareThread表示tomcat一开始启动的时候会创建多少个线程,即使是闲着也会创建。

    maxSpareThread表示tomcat创建的最大闲置线程数目。一旦tomcat创建的线程数目达到这个瓶颈,那么就需要进行线程的回收了。

    connectionTimeout表示连接的超时时长。

    假设我们同时有1000个请求并发访问,但是一台tomcat的maxThreads只设置为了500,那么此时就会出现请求拥塞的情况,也就是瓶颈点之一。

    三、Redis部分性能瓶颈分析

    一些大key的查询,导致网络出现拥塞情况

    例如说往一个list集合中存储了50m的数据,一旦发生list全量查询,同时又有其他指令在进行访问的时候,就容易会导致网络堵塞。因为redis的设计为单线程处理请求,所以其他指令发送到redis服务端的时候,都需要等待redis将之前的任务处理完毕之后才能继续执行。

    线上环境出现了一些”违规操作“

    比较常见的违规操作:批量执行keys指令

    在redis处于高qps的状态下,随意一个keys指令都可能是致命的。keys指令的时间复杂度是O(n)级别,容易导致一时间系统的卡顿。

    内存空间不足

    当redis处于内存空间不足的时候,基本就是整个系统处于瘫痪作用。因此我们在对每个存储在redis中的数值都需要设置一个合理的过期时间,以及需要思考存储数据的体积大小。

    四、MySQL部分性能瓶颈分析

    通常我们在分析sql查询方面都容易出现一个误区,就是上来直接进行explian分析,但是却忽略了系统的运作上下文环境。

    假设有一张t_user表,已经存储了几千万的数据,并且也对用户的id进行了索引建立,但是sql执行速度依旧是超过1s时长,这个时候就需要换一种思路进行分析了。

    例如从表的拆分方面进行思考,是否该对表进行横向拆分,拆解为t_user_01,t_user_02......

    以下是我总结的一些对于数据库层面可能出现性能瓶颈的几点总结:

    1、锁

    排查是否会存在锁表的情况导致数据库响应缓慢。

    2、sql查询还有优化空间,有待完善

    通常我们对于sql的执行分析都会使用explain命令进行查看:

    这里我贴出了一张关于explain的常用参数含义表供大家参考:

    idSELECT识别符。这是SELECT的查询序列号
    select_typeSIMPLE:简单SELECT(不使用UNION或子查询)PRIMARY:最外面的SELECTUNION:UNION中的第二个或后面的SELECT语句DEPENDENT UNION:UNION中的第二个或后面的SELECT语句,取决于外面的查询UNION RESULT:UNION 的结果SUBQUERY:子查询中的第一个SELECTDEPENDENT SUBQUERY:子查询中的第一个SELECT,取决于外面的查询DERIVED:导出表的SELECT(FROM子句的子查询)
    table查询sql过程中关联的表名称
    type连接类型。下面给出各种联接类型,按照从最佳类型到最坏类型进行排序:system:表仅有一行(=系统表)。这是const联接类型的一个特例。const:表最多有一个匹配行,它将在查询开始时被读取。因为仅有一行,在这行的列值可被优化器剩余部分认为是常数。const表很快,因为它们只读取一次!eq_ref:对于每个来自于前面的表的行组合,从该表中读取一行,性能仅次于const。ref:对于每个来自于前面的表的行组合,所有有匹配索引值的行将从这张表中读取。ref_or_null:该联接类型如同ref,但是添加了MySQL可以专门搜索包含NULL值的行。index_merge:该联接类型表示使用了索引合并优化方法。unique_subquery:该类型替换了下面形式的IN子查询的ref: value IN (SELECT primary_key FROM single_table WHERE some_expr) unique_subquery是一个索引查找函数,可以完全替换子查询,效率更高。index_subquery:该联接类型类似于unique_subquery。可以替换IN子查询,但只适合下列形式的子查询中的非唯一索引: value IN (SELECT key_column FROM single_table WHERE some_expr)range:只检索给定范围的行,使用一个索引来选择行。index:该联接类型与ALL相同,除了只有索引树被扫描。这通常比ALL快,因为索引文件通常比数据文件小。ALL:对于每个来自于先前的表的行组合,进行完整的表扫描。
    possible_keys这个参数更像是mysql的一次预测,预测指定sql可能执行过程中哪些索引会生效
    keysql在执行过程中实际生效的索引列
    key_len显示MySQL决定使用的键长度。
    ref显示使用哪个列或常数与key一起从表中选择行。
    rows显示MySQL认为它执行查询时必须检查的行数。多行之间的数据相乘可以估算要处理的行数。
    filtered显示了通过条件过滤出的行数的百分比估计值。
    Extra

    Distinct:MySQL发现第1个匹配行后,停止为当前的行组合搜索更多的行。Not exists:MySQL能够对查询进行LEFT JOIN优化,发现1个匹配LEFT JOIN标准的行后,不再为前面的的行组合在该表内检查更多的行。range checked for each record (index map: #):MySQL没有发现好的可以使用的索引,但发现如果来自前面的表的列值已知,可能部分索引可以使用。Using filesort:MySQL需要额外的一次传递,以找出如何按排序顺序检索行。Using index:从只使用索引树中的信息而不需要进一步搜索读取实际的行来检索表中的列信息。Using temporary:为了解决查询,MySQL需要创建一个临时表来容纳结果。Using where:WHERE 子句用于限制哪一个行匹配下一个表或发送到客户。Using sort_union(...), Using union(...), Using intersect(...):这些函数说明如何为index_merge联接类型合并索引扫描。Using index for group-by:类似于访问表的Using index方式,Using index for group-by表示MySQL发现了一个索引,可以用来查 询GROUP BY或DISTINCT查询的所有列,而不要额外搜索硬盘访问实际的表。 

     

     

    3、查询出的数据量过大

    例如说一条sql直接查询了全表的数据信息量,直接占满了网络带宽,因此访问时候出现了网络拥塞。

    4、硬件设备不足

    例如在面对一些高qps的查询时候,数据库本身的机器硬件配置较低,自然处理速度会比较慢。

    5、自适应hash出现锁冲突

    AHI是innodb存储引擎特有的属性,innodb存储引擎会针对索引数据的查询结果做自适应的优化,当某些特定的索引查询频率特别高的时候会自动为其建立hash索引,从而提升查询的效率。相比于B+Tree索引来说,hash索引能够大大减少对于io的访问次数,“一击命中” 查询数据,具备更加高效的性能,而且hash索引是由mysql内部自动适配的,无需dba在外部做过多的干预。

    早期版本的hash索引是采用了单锁模式来防范并发访问问题,这对于程序自身的一个运作高效性有一定的”折扣“,后期通过对hash索引进行了分区,不同页的数据用不同的hashtable,每个分区有对应的锁来做并发访问的预防。

    如果某天你发现了有很多线程都被堵塞在了RW-latches的时候,有可能就是因为hash索引的并发访问负载过高导致的堵塞,这个时候可以通过增大hash索引的分区参数,或者关闭自适应hash索引特性来进行处理。

     

    往期精彩内容:

    Java知识体系总结(2021版)

    Java多线程基础知识总结(绝对经典)

    超详细的springBoot学习笔记

    常见数据结构与算法整理总结

    Java设计模式:23种设计模式全面解析(超级详细)

    Java面试题总结(附答案)

     

     原创不易,转载自:https://blog.csdn.net/weixin_39602569/article/details/112291362

     

     

    展开全文
  • 一、Tomcat是什么? Tomcat是一个web应用服务器,是一个Servlet/Jsp容器,主要负责将客户端请求传递给对应的Servlet,并且将Servlet的响应数据返回给客户端。 Tomcat是基于组件的服务器。 二、Tomcat体系结构 Tomcat...

    Tomcat处理HTTP请求过程分析
    一、Tomcat是什么?
    Tomcat是一个web应用服务器,是一个Servlet/Jsp容器,主要负责将客户端请求传递给对应的Servlet,并且将Servlet的响应数据返回给客户端。
    Tomcat是基于组件的服务器。
    二、Tomcat体系结构
    Tomcat是一个基于组件的服务器,它的构成组件都是可配置的。其各个组件都在Tomcat安装目录下的…/conf/server.xml文件中配置。

    <?xml version="1.0" encoding="UTF-8"?>
     
    <!--顶层类元素,可以包含多个Service-->
     
    <Server port="8005" shutdown="SHUTDOWN">  
     
      <Listener className="org.apache.catalina.startup.VersionLoggerListener" />
     
      <Listener className="org.apache.catalina.core.AprLifecycleListener" SSLEngine="on" />
     
      <Listener className="org.apache.catalina.core.JreMemoryLeakPreventionListener" />
     
      <Listener className="org.apache.catalina.mbeans.GlobalResourcesLifecycleListener" />
     
      <Listener className="org.apache.catalina.core.ThreadLocalLeakPreventionListener" />
     
      <GlobalNamingResources>
     
        <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>
     
      <!--顶层类元素,可包含一个Engine(container),多个connector-->
     
      <Service name="Catalina">
     
      <!--连接器类元素,代表通信接口-->
     
        <Connector port="8080" protocol="HTTP/1.1"
     
                   connectionTimeout="20000"
     
                   redirectPort="8443" />
     
        <Connector port="8009" protocol="AJP/1.3" redirectPort="8443" />
     
        <!--容器类元素,为特定的service组件处理客户请求-->
     
        <Engine name="Catalina" defaultHost="localhost">
     
            <Realm className="org.apache.catalina.realm.UserDatabaseRealm"
     
                   resourceName="UserDatabase"/>
     
            </Realm>
     
            <!--容器类元素,为特定的虚拟主机组件处理客户请求-->
     
          <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 "%r" %s %b" />
     
          </Host>
     
        </Engine>
     
      </Service>
     
    </Server>
    

    在这里插入图片描述
    由上图可以看出Tomcat的心脏是两个核心组件:Connector(连接器)和Container(容器)。其中一个Container可以选择多个Connector。
    扩展:Tomcat默认提供两个Connector连接器,一个默认监听8080端口,一个默认监听8009端口,这两种连接器有什么区别呢?redirectPort有什么作用?
    (1)8080端口监听的是通过HTTP/1.1协议访问的连接,而8009端口主要负责和其他HTTP服务器(如Apache、IIS)建立连接,使用AJP/1.3协议,当Tomcat和其他服务器集成时就会使用到这个连接器。如下图。

    在这里插入图片描述

    Web1和Web2都是访问服务器的index.jsp页面。Web1直接访问Tomcat服务器,访问地址是http://localhost:8080/index.jsp。Web2访问HTTP服务器,HTTP服务器再通过访问Tomcat的8009端口找到index.jsp。假设HTTP服务器的端口为80端口,则访问地址为http://localhost:80/index.jsp 或者 http://localhost/index.jsp。
    Apache、IIS服务器一般只支持静态页面,如HTML,不支持JSP动态页面。Tomcat对HTML的解析速度不如Apache、IIS服务器。因此一般将两者整合使用。
    (2)redirectPort字面意思是重定向端口。当用户用http请求某个资源,而该资源本身又被设置了必须要https方式访问,此时Tomcat会自动重定向到这个redirectPort设置的https端口。
    三、组件
    1、Connector组件
    在这里插入图片描述

    Connector 最重要的功能就是接收连接请求然后分配线程让 Container来处理这个请求,所以Connector必然是多线程的,多线程的处理是 Connector 设计的核心。Connector监听指定端口上请求,当请求到来时创建一个request和response对象交换数据,然后新建一个线程来处理请求并把request和response传递给Engine组件,最后从Engine获取一个响应并返回给客户端。
    Connector组件常用属性说明:
    (1) address:指定连接器监听的地址,默认为所有地址,即0.0.0.0,可以自己指定地。(2) maxThreads:支持的最大并发连接数,默认为200;
    (3) port:监听的端口;
    (4) protocol:连接器使用的协议,默认为HTTP/1.1,定义AJP协议时通常为AJP/1.3;
    (5) redirectPort:如果某连接器支持的协议是HTTP,当接收客户端发来的HTTPS请求时,则转发至此属性定义的端口;
    (6) connectionTimeout:等待客户端发送请求的超时时间,单位为毫秒,默认为60000,即1分钟;
    (7) enableLookups:是否通过request.getRemoteHost()进行DNS查询以获取客户端的主机名;默认为true; 进行反解的,可以设置为false。
    (8) acceptCount:设置等待队列的最大长度;通常在tomcat所有处理线程均处于繁忙状态时,新发来的请求将被放置于等待队列中;

    2.container组件

    在这里插入图片描述

    Container是容器的父接口,该容器的设计用的是典型的责任链的设计模式,它由四个子容器组件构成,分别是Engine、Host、Context、Wrapper。这四个组件是负责关系,存在包含关系。其中Engine是最顶层,每个service 最多只能有一个Engine, Engine 里面可以有多个Host ,每个Host 下可以有多个Context ,每个Context 下可以有多个Wrapper。通常一个Servlet class对应一个Wrapper,如果有多个Servlet则定义多个Wrapper,如果有多个Wrapper就要定义一个更高的Container,如Context。 Context定义在父容器 Host 中,其中Host 不是必须的,但是要运行 war 程序,就必须要 Host,因为 war 中必有 web.xml 文件,这个文件的解析就需要 Host 了,如果要有多个 Host 就要定义一个 top 容器 Engine 了。而 Engine 没有父容器了,一个 Engine 代表一个完整的 Servlet 引擎。
    2.1、Engine
    Engine是Servlet处理器的一个实例,即servlet引擎, 一个Service 最多只能有一个Engine。默认为定义在server.xml中的Catalina。Engine需要defaultHost属性来为其定义一个接收所有请求的虚拟主机host组件。
    2.2、Host
    Host是Engine的子容器。一个 Host 在 Engine 中代表一个站点,也叫虚拟主机,这个虚拟主机的作用就是运行多个应用、接收并处理请求、保存一个主机应该有的信息。
    常用属性说明:
    (1)appBase:此Host的webapps目录,项目存放路径,可以使用绝对路径;
    (2)autoDeploy:在Tomcat处于运行状态时放置于appBase目录中的应用程序文件是否自动进行deploy;默认为true;
    (3)unpackWars:在启用此webapps时是否对WAR格式的归档文件先进行展开;默认为true;
    2.3、Context
    Context :代表一个应用程序,对应着平时开发的一套程序,或者一个WEB-INF 目录以及下面的web.xml 文件。它具备了 Servlet 运行的基本环境,理论上只要有 Context 就能运行 Servlet 了。简单的 Tomcat 可以没有 Engine 和 Host。Context 最重要的功能就是管理它里面的 Servlet 实例,Servlet 实例在 Context 中是以 Wrapper 出现的,还有一点就是 Context 如何才能找到正确的 Servlet 来执行它呢? Tomcat5 以前是通过一个 Mapper 类来管理的,Tomcat5 以后这个功能被移到了 request 中,获取子容器都是通过 request 来分配的。
    常用属性定义:
    (1) docBase:相应的Web应用程序的存放位置;也可以使用相对路径,起始路径为此Context所属Host中appBase定义的路径;切记,docBase的路径名不能与相应的Host中appBase中定义的路径名有包含关系,比如,如果appBase为deploy,而docBase绝不能为deploy-bbs类的名字;
    (2)path:相对于Web服务器根路径而言的URI;如果为空“”,则表示为此webapp的根路径;如果context定义在一个单独的xml文件中,此属性不需要定义,有可能是别名;
    (3) reloadable:是否允许重新加载此context相关的Web应用程序的类;默认为false;
    2.4、Wrapper
    Wrapper :每个Wrapper 封装着一个servlet,也代表一个 Servlet,它负责管理一个 Servlet,包括Servlet 的装载、初始化、执行以及资源回收。Wrapper 是最底层的容器,它没有子容器了,所以调用它的 addChild 将会报错。 Wrapper 的实现类是 StandardWrapper,StandardWrapper 还实现了 ServletConfig,由此看出 StandardWrapper 将直接和 Servlet 的各种信息打交道。
    2.5、Value
    Valve类似于过滤器,它可以工作于Engine和Host/Context之间、Host和Context之间以及Context和Web应用程序的某资源之间。一个容器内可以建立多个Valve,而且Valve定义的次序也决定了它们生效的次序。

    四、Tomcat处理一个HTTP请求的过程
    在这里插入图片描述

    1.用户在浏览器中输入网址localhost:8080/test/index.jsp,请求被发送到本机端口8080,被在那里监听的Coyote HTTP/1.1 Connector获得;
    2.Connector把该请求交给它所在的Service的Engine(Container)来处理,并等待Engine的回应;
    3.Engine获得请求localhost/test/index.jsp,匹配所有的虚拟主机Host;
    4.Engine匹配到名为localhost的Host(即使匹配不到也把请求交给该Host处理,因为该Host被定义为该Engine的默认主机)。名为localhost的Host获得请求/test/index.jsp,匹配它所拥有的所有Context。Host匹配到路径为/test的Context(如果匹配不到就把该请求交给路径名为“ ”的Context去处理);
    5.path=“/test”的Context获得请求/index.jsp,在它的mapping table中寻找出对应的Servlet。Context匹配到URL Pattern为*.jsp的Servlet,对应于JspServlet类;
    6.构造HttpServletRequest对象和HttpServletResponse对象,作为参数调用JspServlet的doGet()或doPost(),执行业务逻辑、数据存储等;
    7.Context把执行完之后的HttpServletResponse对象返回给Host;
    8.Host把HttpServletResponse对象返回给Engine;
    9.Engine把HttpServletResponse对象返回Connector;
    10.Connector把HttpServletResponse对象返回给客户Browser。
    11.扩展:下图是struts使用tomcat处理请求的过程
    在这里插入图片描述

    五、线程池的原理和Tomcat的Connector及线程池配置
    Tomcat处理请求流程:
    先启动若干数量的线程,并让这些线程都处于睡眠 状态,当客户端有一个新请求时,就会唤醒线程池中的某一个睡眠线程,让它来处理客户端的这个请求,当处理完这个请求后,线程又处于睡眠状态。可能你也许会 问:为什么要搞得这么麻烦,如果每当客户端有新的请求时,我就创建一个新的线程不就完了?这也许是个不错的方法,因为它能使得你编写代码相对容易一些,但 你却忽略了一个重要的问题??性能!例如:一个省级数据大集中的银行网络中心,高峰期每秒的客户端请求并发数超过100,如果 为每个客户端请求创建一个新线程的话,那耗费的CPU时间和内存将是惊人的,如果采用一个拥有200个线程的线程池,那将会节约大量的的系统资源,使得更 多的CPU时间和内存用来处理实际的商业应用,而不是频繁的线程创建与销毁。
    配置executor属性(各项参数值根据自身情况配置)
    1.1)打开/conf/server.xml文件,在Connector之前配置一个线程池:

    参数详解:
    name:共享线程池的名字。这是Connector为了共享线程池要引用的名字,该名字必须唯一。默认值:None;
    namePrefix:在JVM上,每个运行线程都可以有一个name 字符串。这一属性为线程池中每个线程的name字符串设置了一个前缀,Tomcat将把线程号追加到这一前缀的后面。默认值:tomcat-exec-;
    maxThreads:该线程池可以容纳的最大线程数。默认值:200;
    maxIdleTime:在Tomcat关闭一个空闲线程之前,允许空闲线程持续的时间(以毫秒为单位)。只有当前活跃的线程数大于minSpareThread的值,才会关闭空闲线程。默认值:60000(一分钟)。
    minSpareThreads:Tomcat应该始终打开的最小不活跃线程数。默认值:25。
    配置Connector

    参数详解:
    executor:表示使用该参数值对应的线程池;
    minProcessors:服务器启动时创建的处理请求的线程数;
    maxProcessors:最大可以创建的处理请求的线程数;
    acceptCount:指定当所有可以使用的处理请求的线程数都被使用时,可以放到处理队列中的请求数,超过这个数的请求将不予处理。
    问题:
    1.为什么要多线程?
    答:因为如果不采用多线程的策略,那么所有的请求放在一起处理,会大大降低CPU的使用效率,系统处理业务的效率也会因此大打折扣。采用多线程,可以提高CPU的使用效率,进而提高系统处理业务的效率。
    2.为什么多线程的情景下,系统只需要准备一份程序代码就够了?
    答:因为Tomcat从本质上来讲,是一个JVM进程。一个JVM可以派生出多个线程,每个线程都有自己的程序计数器、虚拟机栈、本地方法栈、所有线程共用堆区和方法区(元数据区)。学习ucosii操作系统时,可以知道真正在CPU上执行的单元是任务,并且任务之间可以切换。每个任务都有自己的任务控制块和任务堆栈,并且任务控制块中有一个指针SP指向任务堆栈,而任务堆栈中又有一个指针PC指向任务程序代码。ucosii中任务切换时,只要在代码中替换SP就可以实现任务切换。同理,在JVM中,线程的切换也是类似的道理。存在方法区或元数据区的代码是静态的,线程真正关心的是代码执行进度。形象的比喻一下,线程好比参加赛跑的运动员,存在方法区或元数据区的代码就好比一个用来制作跑到的模块,每个运动员都要依照模板制作一条属于自己的跑道,然后在上面跑起来。而自然的,系统只需要准备一份程序代码就够了。
    六、tomcat线程池和jdk线程池的关系
    前言
    Tomcat/Jetty 是目前比较流行的 Web 容器,两者接受请求之后都会转交给线程池处理,这样可以有效提高处理的能力与并发度。JDK 提高完整线程池实现,但是 Tomcat/Jetty 都没有直接使用。Jetty 采用自研方案,内部实现 QueuedThreadPool 线程池组件,而 Tomcat 采用扩展方案,踩在 JDK 线程池的肩膀上,扩展 JDK 原生线程池。
    JDK 原生线程池可以说功能比较完善,使用也比较简单,那为何 Tomcat/Jetty 却不选择这个方案,反而自己去动手实现那?
    JDK 线程池
    通常我们可以将执行的任务分为两类:
    cpu 密集型任务
    io 密集型任务
    cpu 密集型任务,需要线程长时间进行的复杂的运算,这种类型的任务需要少创建线程,过多的线程将会频繁引起上文切换,降低任务处理处理速度。
    而 io 密集型任务,由于线程并不是一直在运行,可能大部分时间在等待 IO 读取/写入数据,增加线程数量可以提高并发度,尽可能多处理任务。
    JDK 原生线程池工作流程如下:
    在这里插入图片描述
    上图假设使用 LinkedBlockingQueue 。
    灵魂拷问:上述流程是否记错过?在很长一段时间内,我都认为线程数量到达最大线程数,才放入队列中。 ̄□ ̄||
    上图中可以发现只要线程池线程数量大于核心线程数,就会先将任务加入到任务队列中,只有任务队列加入失败,才会再新建线程。也就是说原生线程池队列未满之前,最多只有核心线程数量线程。
    这种策略显然比较适合处理 cpu 密集型任务,但是对于 io 密集型任务,如数据库查询,rpc 请求调用等,就不是很友好了。
    由于 Tomcat/Jetty 需要处理大量客户端请求任务,如果采用原生线程池,一旦接受请求数量大于线程池核心线程数,这些请求就会被放入到队列中,等待核心线程处理。这样做显然降低这些请求总体处理速度,所以两者都没采用 JDK 原生线程池。
    解决上面的办法可以像 Jetty 自己实现线程池组件,这样就可以更加适配内部逻辑,不过开发难度比较大,另一种就像 Tomcat 一样,扩展原生 JDK 线程池,实现比较简单。
    下面主要以 Tomcat 扩展线程池,讲讲其实现原理。
    扩展线程池
    首先我们从 JDK 线程池源码出发,查看如何这个基础上扩展。
    在这里插入图片描述
    可以看到线程池流程主要分为三步,第二步根据 queue#offer 方法返回结果,判断是否需要新建线程。
    JDK 原生队列类型 LinkedBlockingQueue , SynchronousQueue ,两者实现逻辑不尽相同。
    LinkedBlockingQueue
    offer 方法内部将会根据队列是否已满作为判断条件。若队列已满,返回 false ,若队列未满,则将任务加入队列中,且返回 true 。
    SynchronousQueue
    这个队列比较特殊,内部不会储存任何数据。若有线程将任务放入其中将会被阻塞,直到其他线程将任务取出。反之,若无其他线程将任务放入其中,该队列取任务的方法也将会被阻塞,直到其他线程将任务放入。
    对于 offer 方法来说,若有其他线程正在被取方法阻塞,该方法将会返回 true 。反之,offer 方法将会返回 false。
    所以若想实现适合 io 密集型任务线程池,即优先新建线程处理任务,关键在于 queue#offer 方法。可以重写该方法内部逻辑,只要当前线程池数量小于最大线程数,该方法返回 false ,线程池新建线程处理。
    当然上述实现逻辑比较糙,下面我们就从 Tomcat 源码查看其实现逻辑。
    Tomcat 扩展线程池
    Tomcat 扩展线程池直接继承 JDK 线程池 java.util.concurrent.ThreadPoolExecutor ,重写部分方法的逻辑。另外还实现了 TaskQueue ,直接继承 LinkedBlockingQueue ,重写 offer 方法。
    首先查看 Tomcat 线程池的使用方法。
    在这里插入图片描述

    可以看到 Tomcat 线程池使用方法与普通的线程池差不太多。
    接着我们查看一下 Tomcat 线程池核心方法 execute 的逻辑。
    在这里插入图片描述

    execute 方法逻辑比较简单,任务核心还是交给 Java 原生线程池处理。这里主要增加一个重试策略,如果原生线程池执行拒绝策略的情况,抛出 RejectedExecutionException 异常。这里将会捕获,然后重新再次尝试将任务加入到 TaskQueue ,尽最大可能执行任务。
    这里需要注意 submittedCount 变量。这是 Tomcat 线程池内部一个重要的参数,它是一个 AtomicInteger 变量,将会实时统计已经提交到线程池中,但还没有执行结束的任务。也就是说 submittedCount 等于线程池队列中的任务数加上线程池工作线程正在执行的任务。 TaskQueue#offer 将会使用该参数实现相应的逻辑。
    接着我们主要查看 TaskQueue#offer 方法逻辑。
    在这里插入图片描述

    核心逻辑在于第三步,这里如果 submittedCount 小于当前线程池线程数量,将会返回 false。上面我们讲到 offer 方法返回 false ,线程池将会直接创建新线程。
    Dubbo 2.6.X 版本增加 EagerThreadPool ,其实现原理与 Tomcat 线程池差不多,感兴趣的小伙伴可以自行翻阅。
    折衷方法
    上述扩展方法虽然看起不是很难,但是自己实现代价可能就比较大。若不想扩展线程池运行 io 密集型任务,可以采用下面这种折衷方法。
    new ThreadPoolExecutor(10, 10, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue(100));
    不过使用这种方式将会使 keepAliveTime 失效,线程一旦被创建,将会一直存在,比较浪费系统资源。
    总结
    JDK 实现线程池功能比较完善,但是比较适合运行 CPU 密集型任务,不适合 IO 密集型的任务。对于 IO 密集型任务可以间接通过设置线程池参数方式做到。

    https://www.cnblogs.com/GooPolaris/p/8111837.html
    https://www.cnblogs.com/GooPolaris/p/8115784.html
    https://blog.csdn.net/gchd19921992/article/details/79071288
    https://blog.csdn.net/gchd19921992/article/details/79076926
    https://blog.csdn.net/LeiXiaoTao_Java/article/details/85003421
    https://www.cnblogs.com/pingxin/p/p00063.html
    https://blog.csdn.net/ljxljxljx747/article/details/80142293
    https://www.cnblogs.com/weiyiming007/p/12600027.html

    展开全文
  • java的阻塞式BIO:默认值是maxthreads的值 在BIO使用定制的Executor执行器:默认值是执行器中的maxthreads值 java新的NIO模式:默认值10000 注意: 如果设置为-1,则禁用maxConnections,代表不限制tomcat的连接...

    一、tomcat的三个重要配置

    1、maxConnections:最大连接数

    解释: 在同一时间下,tomcat能够接收的最大连接数。

    默认值:

    java的阻塞式BIO:默认值是maxthreads的值

    在BIO使用定制的Executor执行器:默认值是执行器中的maxthreads值

    java新的NIO模式:默认值10000

    注意: 如果设置为-1,则禁用maxConnections,代表不限制tomcat的连接数

    2、accept-count:最大等待数

    解释: 当所有的请求处理线程都被使用时,所能接收的请求队列长度,超过长度,则请求拒绝

    默认值: 100

    3、maxThreads:最大线程数

    解释: 每一次http请求,tomcat都会创建一个线程来处理请求,最大线程数,即决定了Web服务能同时处理多少个请求

    默认值: 200(开的线程越多,cpu消耗也越多)

    二、其他

    线程可以开很多,一般一个线程需要1MB,4G内存就可以开2000个线程(系统会预留一半)

    但是问题来了,如果2000个线程同时跑起来,那你服务器的cpu估计就废掉了,所以一般单机单cpu线程开100~200个已经足够处理普通高并发,如果真的还能解决问题,那么就得换成集群分布式来解决了
     

    展开全文
  • 上帝视角拆解 Tomcat 架构设计,在了解整个组件设计思路之后。我们需要下凡深入了解每个组件的细节实现。从远到近,架构给人以宏观思维,细节展现饱满的美。关注「码哥字节」获取更多硬核,你,准备好了么?上回「码...

    上帝视角拆解 Tomcat 架构设计,在了解整个组件设计思路之后。我们需要下凡深入了解每个组件的细节实现。从远到近,架构给人以宏观思维,细节展现饱满的美。关注「码哥字节」获取更多硬核,你,准备好了么?

    上回「码哥字节」站在上帝视角给大家拆解了 Tomcat 架构设计,分析 Tomcat 如何实现启动、停止,通过设计连接池与容器两大组件完成了一个请求的接受与响应。连接器负责对外交流,处理 socket 连接,容器对内负责,加载 Servlet 以及处理具体 Request 请求与响应。详情点我进入传输门:Tomcat 架构解析到工作借鉴。

    高并发拆解核心准备

    这回,再次拆解,专注 Tomcat 高并发设计之道与性能调优,让大家对整个架构有更高层次的了解与感悟。其中设计的每个组件思路都是将 Java 面向对象、面向接口、如何封装变与不变,如何根据实际需求抽象不同组件分工合作,如何设计类实现单一职责,怎么做到将相似功能高内聚低耦合,设计模式运用到极致的学习借鉴。

    这次主要涉及到的是 I/O 模型,以及线程池的基础内容。

    在学习之前,希望大家积累以下一些技术内容,很多内容「码哥字节」也在历史文章中分享过。大家可爬楼回顾……。希望大家重视如下几个知识点,在掌握以下知识点再来拆解 Tomcat,就会事半功倍,否则很容易迷失方向不得其法。

    一起来看 Tomcat 如何实现并发连接处理以及任务处理,性能的优化是每一个组件都起到对应的作用,如何使用最少的内存,最快的速度执行是我们的目标。

    设计模式

    模板方法模式: 抽象算法流程在抽象类中,封装流程中的变化与不变点。将变化点延迟到子类实现,达到代码复用,开闭原则。

    观察者模式:针对事件不同组件有不同响应机制的需求场景,达到解耦灵活通知下游。

    责任链模式:将对象连接成一条链,将沿着这条链传递请求。在 Tomcat 中的 Valve 就是该设计模式的运用。

    更多设计模式可查看「码哥字节」之前的设计模式专辑,这里是传送门。

    I/O 模型

    Tomcat 实现高并发接收连接,必然涉及到 I/O 模型的运用,了解同步阻塞、异步阻塞、I/O 多路复用,异步非阻塞相关概念以及 Java NIO 包的运用很有必要。本文也会带大家着重说明 I/O 是如何在 Tomcat 运用实现高并发连接。大家通过本文我相信对 I/O 模型也会有一个深刻认识。

    Java 并发编程

    实现高并发,除了整体每个组件的优雅设计、设计模式的合理、I/O 的运用,还需要线程模型,如何高效的并发编程技巧。在高并发过程中,不可避免的会出现多个线程对共享变量的访问,需要加锁实现,如何高效的降低锁冲突。因此作为程序员,要有意识的尽量避免锁的使用,比如可以使用原子类 CAS 或者并发集合来代替。如果万不得已需要用到锁,也要尽量缩小锁的范围和锁的强度。

    对于并发相关的基础知识,如果读者感兴趣「码哥字节」后面也给大家安排上,目前也写了部分并发专辑,大家可移步到历史文章或者专辑翻阅,这里是传送门,主要讲解了并发实现的原理、什么是内存可见性,JMM 内存模模型、读写锁等并发知识点。

    Tomcat 总体架构

    再次回顾下 Tomcat 整体架构设计,主要设计了 connector 连接器处理 TCP/IP 连接,container 容器作为 Servlet 容器,处理具体的业务请求。对外对内分别抽象两个组件实现拓展。一个 Tomcat 实例默认会有一个 Service,而一个 Service 可以包含多个连接器。连接器主要有 ProtocalHandler 和 Adapter 两个组件共同完成连接器核心功能。

    ProtocolHandler 主要由 Acceptor 以及 SocketProcessor 构成,实现了 TCP/IP 层 的 Socket 读取并转换成 TomcatRequest 和 TomcatResponse,最后根据 http 或者 ajp 协议获取合适的 Processor 解析为应用层协议,并通过 Adapter 将 TomcatRequest、TomcatResponse 转化成 标准的 ServletRequest、ServletResponse。通过 getAdapter().service(request, response);将请求传递到 Container 容器。

    adapter.service()实现将请求转发到容器 org.apache.catalina.connector.CoyoteAdapter// Calling the container

    connector.getService().getContainer().getPipeline().getFirst().invoke(

    request, response);

    这个调用会触发 getPipeline 构成的责任链模式将请求一步步走入容器内部,每个容器都有一条 Pipeline,通过 First 开始到 Basic 结束并进入容器内部持有的子类容器,最后到 Servlet,这里就是责任链模式的经典运用。具体的源码组件是 Pipeline 构成一条请求链,每一个链点由 Valve 组成。「码哥字节」在上一篇Tomcat 架构解析到工作借鉴 已经详细讲解。如下图所示,整个 Tomcat 的架构设计重要组件清晰可见,希望大家将这个全局架构图深深印在脑海里,掌握全局思路才能更好地分析细节之美。

    3e079a22ca6307268bfbffd05ff656b8.png

    启动流程:startup.sh 脚本到底发生了什么

    8c8436a6031ce5b2ebbb853ed0dc6b57.pngTomcat 本生就是一个 Java 程序,所以 startup.sh 脚本就是启动一个 JVM 来运行 Tomcat 的启动类 Bootstrap。

    Bootstrap 主要就是实例化 Catalina 和初始化 Tomcat 自定义的类加载器。热加载与热部署就是靠他实现。

    Catalina: 解析 server.xml 创建 Server 组件,并且调用 Server.start() 方法。

    Server:管理 Service 组件,调用 Server 的 start() 方法。

    Service:主要职责就是管理简介器的顶层容器 Engine,分别调用 Connector 和 Engine 的 start 方法。

    Engine 容器主要就是组合模式将各个容器根据父子关系关联,并且 Container 容器继承了 Lifecycle 实现各个容器的初始化与启动。Lifecycle 定义了 init()、start()、stop() 控制整个容器组件的生命周期实现一键启停。

    这里就是一个面向接口、单一职责的设计思想 ,Container 利用组合模式管理容器,LifecycleBase 抽象类继承 Lifecycle 将各大容器生命周期统一管理这里便是,而实现初始化与启动的过程又 LifecycleBase 运用了模板方法设计模式抽象出组件变化与不变的点,将不同组件的初始化延迟到具体子类实现。并且利用观察者模式发布启动事件解耦。

    具体的 init 与 start 流程如下泳道图所示:这是我在阅读源码 debug 所做的笔记,读者朋友们不要怕笔记花费时间长,自己跟着 debug 慢慢记录,相信会有更深的感悟。

    init 流程

    0219a4c09525c3f3c4a92a9c94d47a08.png

    start 流程

    de19cb27823856b50db13c27305095b1.png

    读者朋友根据我的两篇内容,抓住主线组件去 debug,然后跟着该泳道图阅读源码,我相信都会有所收获,并且事半功倍。在读源码的过程中,切勿进入某个细节,一定要先把各个组件抽象出来,了解每个组件的职责即可。最后在了解每个组件的职责与设计哲学之后再深入理解每个组件的实现细节,千万不要一开始就想着深入理解具体一篇叶子。

    每个核心类我在架构设计图以及泳道图都标识出来了,「码哥字节」给大家分享下如何高效阅读源码,以及保持学习兴趣的心得体会。

    如何正确阅读源码

    切勿陷入细节,不看全局:我还没弄清楚森林长啥样,就盯着叶子看 ,看不到全貌和整体设计思路。所以阅读源码学习的时候不要一开始就进入细节,而是宏观看待整体架构设计思想,模块之间的关系。

    1.阅读源码之前,需要有一定的技术储备

    比如常用的设计模式,这个必须掌握,尤其是:模板方法、策略模式、单例、工厂、观察者、动态代理、适配器、责任链、装饰器。大家可以看 「码哥字节」关于设计模式的历史文章,打造好的基础。

    2.必须会使用这个框架/类库,精通各种变通用法

    魔鬼都在细节中,如果有些用法根本不知道,可能你能看明白代码是什么意思,但是不知道它为什么这些写。

    3.先去找书,找资料,了解这个软件的整体设计。

    从全局的视角去看待,上帝视角理出主要核心架构设计,先森林后树叶。都有哪些模块? 模块之间是怎么关联的?怎么关联的?

    可能一下子理解不了,但是要建立一个整体的概念,就像一个地图,防止你迷航。

    在读源码的时候可以时不时看看自己在什么地方。就像「码哥字节」给大家梳理好了 Tomcat 相关架构设计,然后自己再尝试跟着 debug,这样的效率如虎添翼。

    4. 搭建系统,把源代码跑起来!

    Debug 是非常非常重要的手段, 你想通过只看而不运行就把系统搞清楚,那是根本不可能的!合理运用调用栈(观察调用过程上下文)。

    5.笔记

    一个非常重要的工作就是记笔记(又是写作!),画出系统的类图(不要依靠 IDE 给你生成的), 记录下主要的函数调用, 方便后续查看。

    文档工作极为重要,因为代码太复杂,人的大脑容量也有限,记不住所有的细节。 文档可以帮助你记住关键点, 到时候可以回想起来,迅速地接着往下看。

    要不然,你今天看的,可能到明天就忘个差不多了。所以朋友们记得收藏后多翻来看看,尝试把源码下载下来反复调试。

    错误方式陷入细节,不看全局:我还没弄清楚森林长啥样,就盯着叶子看 ,看不到全貌和整体设计思路。所以阅读源码学习的时候不要一开始就进入细节,而是宏观看待整体架构设计思想,模块之间的关系。

    还没学会用就研究如何设计:首先基本上框架都运用了设计模式,我们最起码也要了解常用的设计模式,即使是“背”,也得了然于胸。在学习一门技术,我推荐先看官方文档,看看有哪些模块、整体设计思想。然后下载示例跑一遍,最后才是看源码。

    看源码深究细节:到了看具体某个模块源码的时候也要下意识的不要去深入细节,重要的是学习设计思路,而不是具体一个方法实现逻辑。除非自己要基于源码做二次开发,而且二次开发也是基于在了解扎鞥个架构的情况下才能深入细节。

    组件设计-落实单一职责、面向接口思想

    当我们接到一个功能需求的时候,最重要的就是抽象设计,将功能拆解主要核心组件,然后找到需求的变化与不变点,将相似功能内聚,功能之间若耦合,同时对外支持可拓展,对内关闭修改。努力做到一个需求下来的时候我们需要合理的抽象能力抽象出不同组件,而不是一锅端将所有功能糅合在一个类甚至一个方法之中,这样的代码牵一发而动全身,无法拓展,难以维护和阅读。

    带着问题我们来分析 Tomcat 如何设计组件完成连接与容器管理。

    看看 Tomcat 如何实现将 Tomcat 启动,并且又是如何接受请求,将请求转发到我们的 Servlet 中。

    Catalina

    主要任务就是创建 Server,并不是简单创建,而是解析 server.xml 文件把文件配置的各个组件意义创建出来,接着调用 Server 的 init() 和 start() 方法,启动之旅从这里开始…,同时还要兼顾异常,比如关闭 Tomcat 还需要做到优雅关闭启动过程创建的资源需要释放,Tomcat 则是在 JVM 注册一个「关闭钩子」,源码我都加了注释,省略了部分无关代码。同时通过 await() 监听停止指令关闭 Tomcat。/**

    * Start a new server instance.

    */

    public void start() {

    // 若 server 为空,则解析 server.xml 创建

    if (getServer() == null) {

    load();

    }

    // 创建失败则报错并退出启动

    if (getServer() == null) {

    log.fatal("Cannot start server. Server instance is not configured.");

    return;

    }

    // 开始启动 server

    try {

    getServer().start();

    } catch (LifecycleException e) {

    log.fatal(sm.getString("catalina.serverStartFail"), e);

    try {

    // 异常则执行 destroy 销毁资源

    getServer().destroy();

    } catch (LifecycleException e1) {

    log.debug("destroy() failed for failed Server ", e1);

    }

    return;

    }

    // 创建并注册 JVM 关闭钩子

    if (useShutdownHook) {

    if (shutdownHook == null) {

    shutdownHook = new CatalinaShutdownHook();

    }

    Runtime.getRuntime().addShutdownHook(shutdownHook);

    }

    // 通过 await 方法监听停止请求

    if (await) {

    await();

    stop();

    }

    }

    通过「关闭钩子」,就是当 JVM 关闭的时候做一些清理工作,比如说释放线程池,清理一些零时文件,刷新内存数据到磁盘中…...

    「关闭钩子」本质就是一个线程,JVM 在停止之前会尝试执行这个线程。我们来看下 CatalinaShutdownHook 这个钩子到底做了什么。/**

    * Shutdown hook which will perform a clean shutdown of Catalina if needed.

    */

    protected class CatalinaShutdownHook extends Thread {

    @Override

    public void run() {

    try {

    if (getServer() != null) {

    Catalina.this.stop();

    }

    } catch (Throwable ex) {

    ...

    }

    }

    /**

    * 关闭已经创建的 Server 实例

    */

    public void stop() {

    try {

    // Remove the ShutdownHook first so that server.stop()

    // doesn't get invoked twice

    if (useShutdownHook) {

    Runtime.getRuntime().removeShutdownHook(shutdownHook);

    }

    } catch (Throwable t) {

    ......

    }

    // 关闭 Server

    try {

    Server s = getServer();

    LifecycleState state = s.getState();

    // 判断是否已经关闭,若是在关闭中,则不执行任何操作

    if (LifecycleState.STOPPING_PREP.compareTo(state) <= 0

    && LifecycleState.DESTROYED.compareTo(state) >= 0) {

    // Nothing to do. stop() was already called

    } else {

    s.stop();

    s.destroy();

    }

    } catch (LifecycleException e) {

    log.error("Catalina.stop", e);

    }

    }

    实际上就是执行了 Server 的 stop 方法,Server 的 stop 方法会释放和清理所有的资源。

    Server 组件

    来体会下面向接口设计美,看 Tomcat 如何设计组件与接口,抽象 Server 组件,Server 组件需要生命周期管理,所以继承 Lifecycle 实现一键启停。

    它的具体实现类是 StandardServer,如下图所示,我们知道 Lifecycle 主要的方法是组件的 初始化、启动、停止、销毁,和 监听器的管理维护,其实就是观察者模式的设计,当触发不同事件的时候发布事件给监听器执行不同业务处理,这里就是如何解耦的设计哲学体现。

    而 Server 自生则是负责管理 Service 组件。

    37c2c4464d2306d7b4d9e8c1ab21e64c.png

    接着,我们再看 Server 组件的具体实现类是 StandardServer 有哪些功能,又跟哪些类关联?

    0b3e500f3a14db18112309561e457d62.png

    在阅读源码的过程中,我们一定要多关注接口与抽象类,接口是组件全局设计的抽象;而抽象类基本上是模板方法模式的运用,主要目的就是抽象整个算法流程,将变化点交给子类,将不变点实现代码复用。

    StandardServer 继承了 LifeCycleBase,它的生命周期被统一管理,并且它的子组件是 Service,因此它还需要管理 Service 的生命周期,也就是说在启动时调用 Service 组件的启动方法,在停止时调用它们的停止方法。Server 在内部维护了若干 Service 组件,它是以数组来保存的,那 Server 是如何添加一个 Service 到数组中的呢?/**

    * 添加 Service 到定义的数组中

    *

    * @param service The Service to be added

    */

    @Override

    public void addService(Service service) {

    service.setServer(this);

    synchronized (servicesLock) {

    // 创建一个 services.length + 1 长度的 results 数组

    Service results[] = new Service[services.length + 1];

    // 将老的数据复制到 results 数组

    System.arraycopy(services, 0, results, 0, services.length);

    results[services.length] = service;

    services = results;

    // 启动 Service 组件

    if (getState().isAvailable()) {

    try {

    service.start();

    } catch (LifecycleException e) {

    // Ignore

    }

    }

    // 观察者模式运用,触发监听事件

    support.firePropertyChange("service", null, service);

    }

    }

    从上面的代码可以知道,并不是一开始就分配一个很长的数组,而是在新增过程中动态拓展长度,这里就是为了节省空间,对于我们平时开发是不是也要主要空间复杂度带来的内存损耗,追求的就是极致的美。

    除此之外,还有一个重要功能,上面 Caralina 的启动方法的最后一行代码就是调用了 Server 的 await 方法。

    这个方法主要就是监听停止端口,在 await 方法里会创建一个 Socket 监听 8005 端口,并在一个死循环里接收 Socket 上的连接请求,如果有新的连接到来就建立连接,然后从 Socket 中读取数据;如果读到的数据是停止命令“SHUTDOWN”,就退出循环,进入 stop 流程。

    Service

    同样是面向接口设计,Service 组件的具体实现类是 StandardService,Service 组件依然是继承 Lifecycle 管理生命周期,这里不再累赘展示图片关系图。我们先来看看 Service 接口主要定义的方法以及成员变量。通过接口我们才能知道核心功能,在阅读源码的时候一定要多关注每个接口之间的关系,不要急着进入实现类。public interface Service extends Lifecycle {

    // ----------主要成员变量

    //Service 组件包含的顶层容器 Engine

    public Engine getContainer();

    // 设置 Service 的 Engine 容器

    public void setContainer(Engine engine);

    // 该 Service 所属的 Server 组件

    public Server getServer();

    // --------------------------------------------------------- Public Methods

    // 添加 Service 关联的连接器

    public void addConnector(Connector connector);

    public Connector[] findConnectors();

    // 自定义线程池

    public void addExecutor(Executor ex);

    // 主要作用就是根据 url 定位到 Service,Mapper 的主要作用就是用于定位一个请求所在的组件处理

    Mapper getMapper();

    }

    接着再来细看 Service 的实现类:public class StandardService extends LifecycleBase implements Service {

    // 名字

    private String name = null;

    //Server 实例

    private Server server = null;

    // 连接器数组

    protected Connector connectors[] = new Connector[0];

    private final Object connectorsLock = new Object();

    // 对应的 Engine 容器

    private Engine engine = null;

    // 映射器及其监听器,又是观察者模式的运用

    protected final Mapper mapper = new Mapper();

    protected final MapperListener mapperListener = new MapperListener(this);

    }

    StandardService 继承了 LifecycleBase 抽象类,抽象类定义了 三个 final 模板方法定义生命周期,每个方法将变化点定义抽象方法让不同组件时间自己的流程。这里也是我们学习的地方,利用模板方法抽象变与不变。

    此外 StandardService 中还有一些我们熟悉的组件,比如 Server、Connector、Engine 和 Mapper。

    那为什么还有一个 MapperListener?这是因为 Tomcat 支持热部署,当 Web 应用的部署发生变化时,Mapper 中的映射信息也要跟着变化,MapperListener 就是一个监听器,它监听容器的变化,并把信息更新到 Mapper 中,这是典型的观察者模式。下游服务根据多上游服务的动作做出不同处理,这就是观察者模式的运用场景,实现一个事件多个监听器触发,事件发布者不用调用所有下游,而是通过观察者模式触发达到解耦。

    Service 管理了 连接器以及 Engine 顶层容器,所以继续进入它的 startInternal 方法,其实就是 LifecycleBase 模板定义的 抽象方法。看看他是怎么启动每个组件顺序。protected void startInternal() throws LifecycleException {

    //1. 触发启动监听器

    setState(LifecycleState.STARTING);

    //2. 先启动 Engine,Engine 会启动它子容器,因为运用了组合模式,所以每一层容器在会先启动自己的子容器。

    if (engine != null) {

    synchronized (engine) {

    engine.start();

    }

    }

    //3. 再启动 Mapper 监听器

    mapperListener.start();

    //4. 最后启动连接器,连接器会启动它子组件,比如 Endpoint

    synchronized (connectorsLock) {

    for (Connector connector: connectors) {

    if (connector.getState() != LifecycleState.FAILED) {

    connector.start();

    }

    }

    }

    }

    Service 先启动了 Engine 组件,再启动 Mapper 监听器,最后才是启动连接器。这很好理解,因为内层组件启动好了才能对外提供服务,才能启动外层的连接器组件。而 Mapper 也依赖容器组件,容器组件启动好了才能监听它们的变化,因此 Mapper 和 MapperListener 在容器组件之后启动。组件停止的顺序跟启动顺序正好相反的,也是基于它们的依赖关系。

    Engine

    作为 Container 的顶层组件,所以 Engine 本质就是一个容器,继承了 ContainerBase ,看到抽象类再次运用了模板方法设计模式。ContainerBase 使用一个 HashMap children = new HashMap<>(); 成员变量保存每个组件的子容器。同时使用 protected final Pipeline pipeline = new StandardPipeline(this); Pipeline 组成一个管道用于处理连接器传过来的请求,责任链模式构建管道。public class StandardEngine extends ContainerBase implements Engine {

    }

    Engine 的子容器是 Host,所以 children 保存的就是 Host。

    我们来看看 ContainerBase 做了什么...initInternal 定义了容器初始化,同时创建了专门用于启动停止容器的线程池。

    startInternal:容器启动默认实现,通过组合模式构建容器父子关系,首先获取自己的子容器,使用 startStopExecutor 启动子容器。public abstract class ContainerBase extends LifecycleMBeanBase

    implements Container {

    // 提供了默认初始化逻辑

    @Override

    protected void initInternal() throws LifecycleException {

    BlockingQueue startStopQueue = new LinkedBlockingQueue<>();

    // 创建线程池用于启动或者停止容器

    startStopExecutor = new ThreadPoolExecutor(

    getStartStopThreadsInternal(),

    getStartStopThreadsInternal(), 10, TimeUnit.SECONDS,

    startStopQueue,

    new StartStopThreadFactory(getName() + "-startStop-"));

    startStopExecutor.allowCoreThreadTimeOut(true);

    super.initInternal();

    }

    // 容器启动

    @Override

    protected synchronized void startInternal() throws LifecycleException {

    // 获取子容器并提交到线程池启动

    Container children[] = findChildren();

    List> results = new ArrayList<>();

    for (Container child : children) {

    results.add(startStopExecutor.submit(new StartChild(child)));

    }

    MultiThrowable multiThrowable = null;

    // 获取启动结果

    for (Future result : results) {

    try {

    result.get();

    } catch (Throwable e) {

    log.error(sm.getString("containerBase.threadedStartFailed"), e);

    if (multiThrowable == null) {

    multiThrowable = new MultiThrowable();

    }

    multiThrowable.add(e);

    }

    }

    ......

    // 启动 pipeline 管道,用于处理连接器传递过来的请求

    if (pipeline instanceof Lifecycle) {

    ((Lifecycle) pipeline).start();

    }

    // 发布启动事件

    setState(LifecycleState.STARTING);

    // Start our thread

    threadStart();

    }

    }

    继承了 LifecycleMBeanBase 也就是还实现了生命周期的管理,提供了子容器默认的启动方式,同时提供了对子容器的 CRUD 功能。

    Engine 在启动 Host 容器就是 使用了 ContainerBase 的 startInternal 方法。Engine 自己还做了什么呢?

    我们看下 构造方法,pipeline 设置了 setBasic,创建了 StandardEngineValve。/**

    * Create a new StandardEngine component with the default basic Valve.

    */

    public StandardEngine() {

    super();

    pipeline.setBasic(new StandardEngineValve());

    .....

    }

    容器主要的功能就是处理请求,把请求转发给某一个 Host 子容器来处理,具体是通过 Valve 来实现的。每个容器组件都有一个 Pipeline 用于组成一个责任链传递请求。而 Pipeline 中有一个基础阀(Basic Valve),而 Engine 容器的基础阀定义如下:final class StandardEngineValve extends ValveBase {

    @Override

    public final void invoke(Request request, Response response)

    throws IOException, ServletException {

    // 选择一个合适的 Host 处理请求,通过 Mapper 组件获取到合适的 Host

    Host host = request.getHost();

    if (host == null) {

    response.sendError

    (HttpServletResponse.SC_BAD_REQUEST,

    sm.getString("standardEngine.noHost",

    request.getServerName()));

    return;

    }

    if (request.isAsyncSupported()) {

    request.setAsyncSupported(host.getPipeline().isAsyncSupported());

    }

    // 获取 Host 容器的 Pipeline first Valve ,将请求转发到 Host

    host.getPipeline().getFirst().invoke(request, response);

    }

    这个基础阀实现非常简单,就是把请求转发到 Host 容器。处理请求的 Host 容器对象是从请求中拿到的,请求对象中怎么会有 Host 容器呢?这是因为请求到达 Engine 容器中之前,Mapper 组件已经对请求进行了路由处理,Mapper 组件通过请求的 URL 定位了相应的容器,并且把容器对象保存到了请求对象中。

    组件设计总结

    大家有没有发现,Tomcat 的设计几乎都是面向接口设计,也就是通过接口隔离功能设计其实就是单一职责的体现,每个接口抽象对象不同的组件,通过抽象类定义组件的共同执行流程。单一职责四个字的含义其实就是在这里体现出来了。在分析过程中,我们看到了观察者模式、模板方法模式、组合模式、责任链模式以及如何抽象组件面向接口设计的设计哲学。

    连接器之 I/O 模型与线程池设计

    连接器主要功能就是接受 TCP/IP 连接,限制连接数然后读取数据,最后将请求转发到 Container 容器。所以这里必然涉及到 I/O 编程,今天带大家一起分析 Tomcat 如何运用 I/O 模型实现高并发的,一起进入 I/O 的世界。

    I/O 模型主要有 5 种:同步阻塞、同步非阻塞、I/O 多路复用、信号驱动、异步 I/O。是不是很熟悉但是又傻傻分不清他们有何区别?

    所谓的I/O 就是计算机内存与外部设备之间拷贝数据的过程。

    CPU 是先把外部设备的数据读到内存里,然后再进行处理。请考虑一下这个场景,当程序通过 CPU 向外部设备发出一个读指令时,数据从外部设备拷贝到内存往往需要一段时间,这个时候 CPU 没事干了,程序是主动把 CPU 让给别人?还是让 CPU 不停地查:数据到了吗,数据到了吗……

    这就是 I/O 模型要解决的问题。今天我会先说说各种 I/O 模型的区别,然后重点分析 Tomcat 的 NioEndpoint 组件是如何实现非阻塞 I/O 模型的。

    I/O 模型

    一个网络 I/O 通信过程,比如网络数据读取,会涉及到两个对象,分别是调用这个 I/O 操作的用户线程和操作系统内核。一个进程的地址空间分为用户空间和内核空间,用户线程不能直接访问内核空间。

    网络读取主要有两个步骤:用户线程等待内核将数据从网卡复制到内核空间。

    内核将数据从内核空间复制到用户空间。

    同理,将数据发送到网络也是一样的流程,将数据从用户线程复制到内核空间,内核空间将数据复制到网卡发送。

    不同 I/O 模型的区别:实现这两个步骤的方式不一样。对于同步,则指的应用程序调用一个方法是否立马返回,而不需要等待。

    对于阻塞与非阻塞:主要就是数据从内核复制到用户空间的读写操作是否是阻塞等待的。

    同步阻塞 I/O

    用户线程发起read调用的时候,线程就阻塞了,只能让出 CPU,而内核则等待网卡数据到来,并把数据从网卡拷贝到内核空间,当内核把数据拷贝到用户空间,再把刚刚阻塞的读取用户线程唤醒,两个步骤的线程都是阻塞的。

    490e6b9cc76ef541fb1dbe840e44ed50.png

    同步非阻塞

    用户线程一直不停的调用read方法,如果数据还没有复制到内核空间则返回失败,直到数据到达内核空间。用户线程在等待数据从内核空间复制到用户空间的时间里一直是阻塞的,等数据到达用户空间才被唤醒。循环调用read方法的时候不阻塞。

    47af21c1d8fe1b435d0444ef0d19d50d.png

    I/O 多路复用

    用户线程的读取操作被划分为两步:用户线程先发起 select 调用,主要就是询问内核数据转备好了没?当内核把数据准备好了就执行第二步。

    用户线程再发起 read 调用,在等待内核把数据从内核空间复制到用户空间的时间里,发起 read 线程是阻塞的。

    为何叫 I/O 多路复用,核心主要就是:一次 select 调用可以向内核查询多个数据通道(Channel)的状态,因此叫多路复用。

    1d04b2d366acdb5c1e0a6975e8eafe77.png

    异步 I/O

    用户线程执行 read 调用的时候会注册一个回调函数, read 调用立即返回,不会阻塞线程,在等待内核将数据准备好以后,再调用刚刚注册的回调函数处理数据,在整个过程中用户线程一直没有阻塞。

    577394f1b53dd12a0c295d9a45f4d681.png

    Tomcat NioEndpoint

    Tomcat 的 NioEndpoit 组件实际上就是实现了 I/O 多路复用模型,正式因为这个并发能力才足够优秀。让我们一起窥探下 Tomcat NioEndpoint 的设计原理。

    对于 Java 的多路复用器的使用,无非是两步:创建一个 Seletor,在它身上注册各种感兴趣的事件,然后调用 select 方法,等待感兴趣的事情发生。

    感兴趣的事情发生了,比如可以读了,这时便创建一个新的线程从 Channel 中读数据。

    Tomcat 的 NioEndpoint 组件虽然实现比较复杂,但基本原理就是上面两步。我们先来看看它有哪些组件,它一共包含 LimitLatch、Acceptor、Poller、SocketProcessor 和 Executor 共 5 个组件,它们的工作过程如下图所示:

    5d24db7b7f08f9c81522bcc923edcec6.png

    正是由于使用了 I/O 多路复用,Poller 内部本质就是持有 Java Selector 检测 channel 的 I/O 时间,当数据可读写的时候创建 SocketProcessor 任务丢到线程池执行,也就是少量线程监听读写事件,接着专属的线程池执行读写,提高性能。

    自定义线程池模型

    为了提高处理能力和并发度, Web 容器通常会把处理请求的工作放在线程池来处理, Tomcat 拓展了 Java 原生的线程池来提升并发需求,在进入 Tomcat 线程池原理之前,我们先回顾下 Java 线程池原理。

    Java 线程池

    简单的说,Java 线程池里内部维护一个线程数组和一个任务队列,当任务处理不过来的时,就把任务放到队列里慢慢处理。

    ThreadPoolExecutor

    来窥探线程池核心类的构造函数,我们需要理解每一个参数的作用,才能理解线程池的工作原理。public ThreadPoolExecutor(int corePoolSize,

    int maximumPoolSize,

    long keepAliveTime,

    TimeUnit unit,

    BlockingQueue workQueue,

    ThreadFactory threadFactory,

    RejectedExecutionHandler handler) {

    ......

    }corePoolSize:保留在池中的线程数,即使它们空闲,除非设置了 allowCoreThreadTimeOut,不然不会关闭。

    maximumPoolSize:队列满后池中允许的最大线程数。

    keepAliveTime、TimeUnit:如果线程数大于核心数,多余的空闲线程的保持的最长时间会被销毁。unit 是 keepAliveTime 参数的时间单位。当设置 allowCoreThreadTimeOut(true) 时,线程池中 corePoolSize 范围内的线程空闲时间达到 keepAliveTime 也将回收。

    workQueue:当线程数达到 corePoolSize 后,新增的任务就放到工作队列 workQueue 里,而线程池中的线程则努力地从 workQueue 里拉活来干,也就是调用 poll 方法来获取任务。

    ThreadFactory:创建线程的工厂,比如设置是否是后台线程、线程名等。

    RejectedExecutionHandler:拒绝策略,处理程序因为达到了线程界限和队列容量执行拒绝策略。也可以自定义拒绝策略,只要实现 RejectedExecutionHandler 即可。默认的拒绝策略:AbortPolicy 拒绝任务并抛出 RejectedExecutionException 异常;CallerRunsPolicy 提交该任务的线程执行;``

    来分析下每个参数之间的关系:

    提交新任务的时候,如果线程池数 < corePoolSize,则创建新的线程池执行任务,当线程数 = corePoolSize 时,新的任务就会被放到工作队列 workQueue 中,线程池中的线程尽量从队列里取任务来执行。

    如果任务很多,workQueue 满了,且 当前线程数 < maximumPoolSize 时则临时创建线程执行任务,如果总线程数量超过 maximumPoolSize,则不再创建线程,而是执行拒绝策略。DiscardPolicy 什么都不做直接丢弃任务;DiscardOldestPolicy 丢弃最旧的未处理程序;

    具体执行流程如下图所示:

    aaa501e20ba9eee8b9698c5682ca0fbc.png

    Tomcat 线程池

    定制版的 ThreadPoolExecutor,继承了 java.util.concurrent.ThreadPoolExecutor。 对于线程池有两个很关键的参数:线程个数。

    队列长度。

    Tomcat 必然需要限定想着两个参数不然在高并发场景下可能导致 CPU 和内存有资源耗尽的风险。继承了 与 java.util.concurrent.ThreadPoolExecutor 相同,但实现的效率更高。

    其构造方法如下,跟 Java 官方的如出一辙public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue workQueue, RejectedExecutionHandler handler) {

    super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, handler);

    prestartAllCoreThreads();

    }

    在 Tomcat 中控制线程池的组件是 StandardThreadExecutor , 也是实现了生命周期接口,下面是启动线程池的代码@Override

    protected void startInternal() throws LifecycleException {

    // 自定义任务队列

    taskqueue = new TaskQueue(maxQueueSize);

    // 自定义线程工厂

    TaskThreadFactory tf = new TaskThreadFactory(namePrefix,daemon,getThreadPriority());

    // 创建定制版线程池

    executor = new ThreadPoolExecutor(getMinSpareThreads(), getMaxThreads(), maxIdleTime, TimeUnit.MILLISECONDS,taskqueue, tf);

    executor.setThreadRenewalDelay(threadRenewalDelay);

    if (prestartminSpareThreads) {

    executor.prestartAllCoreThreads();

    }

    taskqueue.setParent(executor);

    // 观察者模式,发布启动事件

    setState(LifecycleState.STARTING);

    }

    其中的关键点在于:Tomcat 有自己的定制版任务队列和线程工厂,并且可以限制任务队列的长度,它的最大长度是 maxQueueSize。

    Tomcat 对线程数也有限制,设置了核心线程数(minSpareThreads)和最大线程池数(maxThreads)。

    除此之外, Tomcat 在官方原有基础上重新定义了自己的线程池处理流程,原生的处理流程上文已经说过。前 corePoolSize 个任务时,来一个任务就创建一个新线程。

    还有任务提交,直接放到队列,队列满了,但是没有达到最大线程池数则创建临时线程救火。

    线程总线数达到 maximumPoolSize ,直接执行拒绝策略。

    Tomcat 线程池扩展了原生的 ThreadPoolExecutor,通过重写 execute 方法实现了自己的任务处理逻辑:前 corePoolSize 个任务时,来一个任务就创建一个新线程。

    还有任务提交,直接放到队列,队列满了,但是没有达到最大线程池数则创建临时线程救火。

    线程总线数达到 maximumPoolSize ,继续尝试把任务放到队列中。如果队列也满了,插入任务失败,才执行拒绝策略。

    最大的差别在于 Tomcat 在线程总数达到最大数时,不是立即执行拒绝策略,而是再尝试向任务队列添加任务,添加失败后再执行拒绝策略。

    代码如下所示:public void execute(Runnable command, long timeout, TimeUnit unit) {

    // 记录提交任务数 +1

    submittedCount.incrementAndGet();

    try {

    // 调用 java 原生线程池来执行任务,当原生抛出拒绝策略

    super.execute(command);

    } catch (RejectedExecutionException rx) {

    //总线程数达到 maximumPoolSize,Java 原生会执行拒绝策略

    if (super.getQueue() instanceof TaskQueue) {

    final TaskQueue queue = (TaskQueue)super.getQueue();

    try {

    // 尝试把任务放入队列中

    if (!queue.force(command, timeout, unit)) {

    submittedCount.decrementAndGet();

    // 队列还是满的,插入失败则执行拒绝策略

    throw new RejectedExecutionException("Queue capacity is full.");

    }

    } catch (InterruptedException x) {

    submittedCount.decrementAndGet();

    throw new RejectedExecutionException(x);

    }

    } else {

    // 提交任务书 -1

    submittedCount.decrementAndGet();

    throw rx;

    }

    }

    }

    Tomcat 线程池是用 submittedCount 来维护已经提交到了线程池,这跟 Tomcat 的定制版的任务队列有关。Tomcat 的任务队列 TaskQueue 扩展了 Java 中的 LinkedBlockingQueue,我们知道 LinkedBlockingQueue 默认情况下长度是没有限制的,除非给它一个 capacity。因此 Tomcat 给了它一个 capacity,TaskQueue 的构造函数中有个整型的参数 capacity,TaskQueue 将 capacity 传给父类 LinkedBlockingQueue 的构造函数,防止无限添加任务导致内存溢出。而且默认是无限制,就会导致当前线程数达到核心线程数之后,再来任务的话线程池会把任务添加到任务队列,并且总是会成功,这样永远不会有机会创建新线程了。

    为了解决这个问题,TaskQueue 重写了 LinkedBlockingQueue 的 offer 方法,在合适的时机返回 false,返回 false 表示任务添加失败,这时线程池会创建新的线程。public class TaskQueue extends LinkedBlockingQueue {

    ...

    @Override

    // 线程池调用任务队列的方法时,当前线程数肯定已经大于核心线程数了

    public boolean offer(Runnable o) {

    // 如果线程数已经到了最大值,不能创建新线程了,只能把任务添加到任务队列。

    if (parent.getPoolSize() == parent.getMaximumPoolSize())

    return super.offer(o);

    // 执行到这里,表明当前线程数大于核心线程数,并且小于最大线程数。

    // 表明是可以创建新线程的,那到底要不要创建呢?分两种情况:

    //1. 如果已提交的任务数小于当前线程数,表示还有空闲线程,无需创建新线程

    if (parent.getSubmittedCount()<=(parent.getPoolSize()))

    return super.offer(o);

    //2. 如果已提交的任务数大于当前线程数,线程不够用了,返回 false 去创建新线程

    if (parent.getPoolSize()

    return false;

    // 默认情况下总是把任务添加到任务队列

    return super.offer(o);

    }

    }

    只有当前线程数大于核心线程数、小于最大线程数,并且已提交的任务个数大于当前线程数时,也就是说线程不够用了,但是线程数又没达到极限,才会去创建新的线程。这就是为什么 Tomcat 需要维护已提交任务数这个变量,它的目的就是在任务队列的长度无限制的情况下,让线程池有机会创建新的线程。可以通过设置 maxQueueSize 参数来限制任务队列的长度。

    性能优化

    线程池调优

    跟 I/O 模型紧密相关的是线程池,线程池的调优就是设置合理的线程池参数。我们先来看看 Tomcat 线程池中有哪些关键参数:参数详情threadPriority线程优先级,默认是 5

    daemon是否是 后台线程,默认 true

    namePrefix线程名前缀

    maxThreads最大线程数,默认 200

    minSpareThreads最小线程数(空闲超过一定时间会被回收),默认 25

    maxIdleTime线程最大空闲时间,超过该时间的会被回收,直到只有 minSpareThreads 个。默认是 1 分钟

    maxQueueSize任务队列最大长度

    prestartAllCoreThreads是否在线程池启动的时候就创建 minSpareThreads 个线程,默认是 fasle

    这里面最核心的就是如何确定 maxThreads 的值,如果这个参数设置小了,Tomcat 会发生线程饥饿,并且请求的处理会在队列中排队等待,导致响应时间变长;如果 maxThreads 参数值过大,同样也会有问题,因为服务器的 CPU 的核数有限,线程数太多会导致线程在 CPU 上来回切换,耗费大量的切换开销。

    线程 I/O 时间与 CPU 时间

    至此我们又得到一个线程池个数的计算公式,假设服务器是单核的:

    线程池大小 = (线程 I/O 阻塞时间 + 线程 CPU 时间 )/ 线程 CPU 时间

    其中:线程 I/O 阻塞时间 + 线程 CPU 时间 = 平均请求处理时间。

    Tomcat 内存溢出的原因分析及调优

    JVM 在抛出 java.lang.OutOfMemoryError 时,除了会打印出一行描述信息,还会打印堆栈跟踪,因此我们可以通过这些信息来找到导致异常的原因。在寻找原因前,我们先来看看有哪些因素会导致 OutOfMemoryError,其中内存泄漏是导致 OutOfMemoryError 的一个比较常见的原因。

    其实调优很多时候都是在找系统瓶颈,假如有个状况:系统响应比较慢,但 CPU 的用率不高,内存有所增加,通过分析 Heap Dump 发现大量请求堆积在线程池的队列中,请问这种情况下应该怎么办呢?可能是请求处理时间太长,去排查是不是访问数据库或者外部应用遇到了延迟。

    java.lang.OutOfMemoryError: Java heap space

    当 JVM 无法在堆中分配对象的会抛出此异常,一般有以下原因:内存泄漏:本该回收的对象呗程序一直持有引用导致对象无法被回收,比如在线程池中使用 ThreadLocal、对象池、内存池。为了找到内存泄漏点,我们通过 jmap 工具生成 Heap Dump,再利用 MAT 分析找到内存泄漏点。jmap -dump:live,format=b,file=filename.bin pid

    内存不足:我们设置的堆大小对于应用程序来说不够,修改 JVM 参数调整堆大小,比如 -Xms256m -Xmx2048m。

    finalize 方法的过度使用。如果我们想在 Java 类实例被 GC 之前执行一些逻辑,比如清理对象持有的资源,可以在 Java 类中定义 finalize 方法,这样 JVM GC 不会立即回收这些对象实例,而是将对象实例添加到一个叫“java.lang.ref.Finalizer.ReferenceQueue”的队列中,执行对象的 finalize 方法,之后才会回收这些对象。Finalizer 线程会和主线程竞争 CPU 资源,但由于优先级低,所以处理速度跟不上主线程创建对象的速度,因此 ReferenceQueue 队列中的对象就越来越多,最终会抛出 OutOfMemoryError。解决办法是尽量不要给 Java 类定义 finalize 方法。

    java.lang.OutOfMemoryError: GC overhead limit exceeded

    垃圾收集器持续运行,但是效率很低几乎没有回收内存。比如 Java 进程花费超过 96%的 CPU 时间来进行一次 GC,但是回收的内存少于 3%的 JVM 堆,并且连续 5 次 GC 都是这种情况,就会抛出 OutOfMemoryError。

    这个问题 IDE 解决方法就是查看 GC 日志或者生成 Heap Dump,先确认是否是内存溢出,不是的话可以尝试增加堆大小。可以通过如下 JVM 启动参数打印 GC 日志:-verbose:gc //在控制台输出GC情况

    -XX:+PrintGCDetails //在控制台输出详细的GC情况

    -Xloggc: filepath //将GC日志输出到指定文件中

    比如 可以使用 java -verbose:gc -Xloggc:gc.log -XX:+PrintGCDetails -jar xxx.jar 记录 GC 日志,通过 GCViewer 工具查看 GC 日志,用 GCViewer 打开产生的 gc.log 分析垃圾回收情况。

    java.lang.OutOfMemoryError: Requested array size exceeds VM limit

    抛出这种异常的原因是“请求的数组大小超过 JVM 限制”,应用程序尝试分配一个超大的数组。比如程序尝试分配 128M 的数组,但是堆最大 100M,一般这个也是配置问题,有可能 JVM 堆设置太小,也有可能是程序的 bug,是不是创建了超大数组。

    java.lang.OutOfMemoryError: MetaSpace

    JVM 元空间的内存在本地内存中分配,但是它的大小受参数 MaxMetaSpaceSize 的限制。当元空间大小超过 MaxMetaSpaceSize 时,JVM 将抛出带有 MetaSpace 字样的 OutOfMemoryError。解决办法是加大 MaxMetaSpaceSize 参数的值。

    java.lang.OutOfMemoryError: Request size bytes for reason. Out of swap space

    当本地堆内存分配失败或者本地内存快要耗尽时,Java HotSpot VM 代码会抛出这个异常,VM 会触发“致命错误处理机制”,它会生成“致命错误”日志文件,其中包含崩溃时线程、进程和操作系统的有用信息。如果碰到此类型的 OutOfMemoryError,你需要根据 JVM 抛出的错误信息来进行诊断;或者使用操作系统提供的 DTrace 工具来跟踪系统调用,看看是什么样的程序代码在不断地分配本地内存。

    java.lang.OutOfMemoryError: Unable to create native threadsJava 程序向 JVM 请求创建一个新的 Java 线程。

    JVM 本地代码(Native Code)代理该请求,通过调用操作系统 API 去创建一个操作系统级别的线程 Native Thread。

    操作系统尝试创建一个新的 Native Thread,需要同时分配一些内存给该线程,每一个 Native Thread 都有一个线程栈,线程栈的大小由 JVM 参数-Xss决定。

    由于各种原因,操作系统创建新的线程可能会失败,下面会详细谈到。

    JVM 抛出“java.lang.OutOfMemoryError: Unable to create new native thread”错误。

    这里只是概述场景,对于生产在线排查后续会陆续推出,受限于篇幅不再展开。关注「码哥字节」给你硬货来啃!

    总结

    回顾 Tomcat 总结架构设计,详细拆解 Tomcat 如何处理高并发连接设计。并且分享了如何高效阅读开源框架源码思路,设计模式、并发编程基础是重中之重,读者朋友可以翻阅历史「码哥字节」的历史文章学习。

    推荐阅读

    拆解 Tomcat 核心组件,去体会 Tomcat 如何面向接口设计、落实单一职责的设计哲学思想。接着概括了 连接器涉及到的 I/O 模型,并对不同的 I/O 模型进行了详解,接着看 Tomcat 如何实现 NIO,如何自定义线程池以及队列实现高并发设计,最后简单分享常见的 OOM 场景以及解决思路,限于篇幅不再详细展开,关注「码哥字节」后续会分享各种线上故障排查调优思路,敬请期待…...

    有任何疑问或者计数探讨可以加个人微信:MageByte1024,一起学习进步。

    也可以通过公众号菜单加入技术群,里面有阿里、腾讯的大佬。

    437227b461a5c9c8d5c9316294ad188e.png

    编写文章不易,如果阅读后觉得有用,希望关注「码哥字节」公众号,点击「分享」、「点赞」、「在看」是最大的鼓励。

    展开全文
  • 优化配置参考一(APR与内存配置) 第一步:配置user登录tomcat ... ...阻塞式I/O操作,表示Tomcat使用的是传统Java I/O操作(即java.io包及其子包)。Tomcat7以下版本默认情况下是以bio模式运行的,..
  • 测试动机NodeJS因为它的非阻塞I/O和优秀的高并发性能受到越来越多的关注,而且NodeJS的服务器相对于Nginx其搭建非常简单,仅需很少的步骤和设置就可以搭建一个高性能的文件服务器。之前我有一台Tomcat服务器兼做文件...
  • 可以在tomcat\conf\server。xml中相应地配置。 默认Connector中没有设置最大并发数:=“假”重定向端口=“8443协议=”还可以打开上述注释,修改“=”25“=”75“默认最小并发数和最大并发数,实现并发控制。假如 4核...
  • 刷脉脉看到这个问题,不错的问题值得讨论。 之前做过高并发场景下的网关应用,也针对于jetty、netty、jvm进行过参数配置与调优,对于下面几个同学的讨论分析下。 程序猿关平 怎么...
  • 终止线程4种方式 sleep与wait区别 start与run区别 JAVA后台线程 JAVA锁 线程基本方法 线程上下文切换 同步锁与死锁 线程池原理 JAVA阻塞队列原理 Cycli cBarrier、CountDownLatch、Semaphor e的用法 volatile关键字...
  • Tomcat并发量与其配置息息相关, 一般的机器几百的并发量足矣, 如果设置太高可能引发各种问题, 内存、网络等问题也能在高并发下暴露出来, 因此, 配置参数的设置非常重要.1 Tomcat的3种运行模式1.1 BIO - 同步阻塞IO...
  • 应用升级MySQL驱动8.0后,在并发量较时,查看监控打点,Druid连接池拿到连接并执行SQL的时间大部分都超过200ms 对系统进行压测,发现出现大量线程阻塞的情况,线程dump信息如下: ...
  • 简单地说,启动多个线程调用同一实例的相同方法可以理解为高并发,如果需要,我可以给你一个简单的例子只通过java代码,有哪些方法可以保证并发超过100另一个响应是简单的建议,仅限于代码级和计算资源约束。...
  • 提升总响应时间,但是阻塞主请求线程,高并发时依然会造成线程数过多,CPU上下文切换; 全异步(Callback) Callback方式调用,使用场景:不考虑回调时间且只能对结果做简单处理,如果依赖服务是两个或两个以上服务,...
  • 本篇文章从实战角度,从问题识别,问题定位,问题分析,提出解决方案,实施解决方案,监控调优后的解决方案和调优后的观察等角度来与大家一起交流分享本次线上高并发调优整个闭环过程。一、项目简要情况概述该项目为...
  • 并发Tomcat线程数

    2020-12-22 01:13:22
    转自最近一直在解决线上一个问题,表现是:Tomcat每到凌晨会有一个高峰,峰值的并发达到了3000以上,最后的结果是Tomcat线程池满了,日志看很多请求超过了1s。服务器性能很好,Tomcat版本是7.0.54,配置如下:...
  • tomcat设置提高并发

    2021-07-31 09:20:34
    一个服务的实际并发量收到很多方面因素的影响,大致归类一下如下:1、数据库,这是web项目中最常见的瓶颈,解决方法一般都是通过cache2、远程接口调用,解决方法是选择性能的RPC框架,如dubbo+ZK等,使用长连接...
  • 压测工具采用了jmeter,服务器Tomcat,数据库oracle,系统linux,负载均衡nginx,缓存redis。这里提一下我们的产品,采用SpringBoot架构,功能十分强大,主动集成几乎所有功能;作为一个开发业务程序的程序员,不到...
  • 高并发业务架构设计高并发业务架构设计实践软件架构中的高并发思考什么是高并发高并发下系统设计问题的思考如何保障系统的高并发?数据层优化应用层优化前端优化多级缓存应用连接池详解线程池详解Java线程池Tomcat...
  • Netty和Tomcat

    2021-09-20 17:19:57
    1、Netty 是什么 ... 并发高---基于NIO 非阻塞IO,并发性能得到了很大提高 传输快---依赖了NIO的一个特性——零拷贝 封装好---较NIO和BIO代码量少 3、Netty的重要概念 (1)channel---一个连接或每一...
  • V8Java脚本。 节点本身V8Java脚本引擎... V8Java脚本引擎Nodejs并不具有高并发优势nodejs:为什么单线程支持高并发单线程解决高并发的思想是采用非阻塞,异步编程的思想。 简单的概括就是当遇到非常耗时的IO操作时,...
  • 高并发接口设计

    2021-07-09 19:33:51
    不会吧,不会吧,不会还有人在高并发场景下做成单体应用吧,假如某个接口并发量特别大,在不做限流的情况下是不是会把整个系统拖垮,其他功能也用不了,一个接口并发高导致服务器cpu或内存起飞,最终导致整个系统死...
  • implicit def execution...将阻塞代码从akka主调度器中分离出来执行上下文是一个好主意,就像你在这里做的那样,但是如果akka也使用这个执行上下文来运行代表这个路由的参与者,它将不起作用。 – 2014-10-27 18:25:32
  • 一、三种运行模式介绍Tomcat 有三种(bio,nio.apr) 运行模式,首先来简单介绍下biobio(blocking I/O),顾名思义,即阻塞式I/O操作,表示Tomcat使用的是传统的Java I/O操作(即java.io包及其子包)。Tomcat在默认情况下...
  • 高并发限流

    2021-03-17 11:27:57
    缓存缓存比较好理解,在大型高并发系统中,如果没有缓存数据库将分分钟被爆,系统也会瞬间瘫痪。使用缓存不单单能够提升系统访问速度、提高并发访问量,也是保护数据库、保护系统的有效方式。大型网站一般主要是“读...
  • 缓存缓存比较好理解,在大型高并发系统中,如果没有缓存数据库将分分钟被爆,系统也会瞬间瘫痪。使用缓存不单单能够提升系统访问速度、提高并发访问量,也是保护数据库、保护系统的有效方式。大型网站一般主要是“读...
  • 秒杀系统相信很多人见过,比如京东或者淘宝的秒杀,小米手机的秒杀,那么秒杀系统的后台是如何实现的呢?我们如何设计一个秒杀系统呢?...高并发 秒杀具有时间短、并发量大的特点,秒杀持续时间只
  • Java高并发编程指南

    2021-02-28 14:40:42
    随着移动互联网的发展,几乎所有主流的互联网应用都需要应对高并发场景,所以不管是Java初学者,还是Java开发老兵,了解和掌握Java高并发编程的相关知识都是非常必要的。本书以由浅入深的方式来对Java高并发编程的...
  • 本篇文章从实战角度,从问题识别,问题定位,问题分析,提出解决方案,实施解决方案,监控调优后的解决方案和调优后的观察等角度来与大家一起交流分享本次线上高并发调优整个闭环过程。一 项目简要情况概述该项目为...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 24,912
精华内容 9,964
关键字:

tomcat高并发阻塞