后端調(diào)優(yōu)基礎(chǔ)——Tomcat調(diào)優(yōu)

jdk8和Tomcat8.5是JDK和Tomcat的史詩(shī)級(jí)提升,直接把單車(chē)變跑車(chē)撵割,所以如果你還是使用的jdk7和Tomcat8.5以后的版本,那可以考慮去線上換一下,但是不知道到時(shí)是架構(gòu)師打死你還是你打死架構(gòu)師

Tomcat的四種運(yùn)行模式:

  • BIO:同步阻塞模式澎蛛,Tomcat7及之前默認(rèn)的模式,性能最差蜕窿,Tomcat8.5后已拋棄
  • NIO:同步非阻塞模式谋逻,Tomcat8及之后默認(rèn)的模式,性能高桐经,現(xiàn)在的主流模式
  • AIO:異步非阻塞模式毁兆,由于純異步,性能最高
  • APR:需要額外安裝依賴庫(kù)阴挣,從操作系統(tǒng)級(jí)別解決異步IO气堕,大幅度的提高服務(wù)器的處理和響應(yīng)性能,也是Tomcat運(yùn)行高并發(fā)應(yīng)用的首選模式,但是使用困難,依賴系統(tǒng)的底層網(wǎng)絡(luò)包茎芭,團(tuán)隊(duì)沒(méi)有C++大神和熟悉Linux底層的大神還是別拿出來(lái)秀了

主流是NIO模式揖膜,我們主要學(xué)習(xí)NIO模式的

配置優(yōu)化

server status

通過(guò)配置可以看到Tomcat管理頁(yè)server status,利用server status幫我們獲取Tomcat的信息
我們需要修改一下配置文件,conf/tomcat-users.xml和webapps/manager/META-INF/context.xml梅桩。
找到conf/tomcat-users.xml

<role rolename="manager-gui"/>
<role rolename="admin-gui"/>
<role rolename="manager-script"/>
<role rolename="manager-jmx"/>
<role rolename="manager-status"/>
<user username="tzb" password="123456" roles="manager-gui,admin-gui,manager-script,manager-jmx,manager-status"/>

找到webapps/manager/META-INF/context.xml壹粟,將以下內(nèi)容注釋掉,我已經(jīng)注釋掉了宿百,這樣就能遠(yuǎn)程訪問(wèn)web manager了:
image.png
image.png

通過(guò)server status可以看到j(luò)vm的信息趁仙,還要很多的選項(xiàng)可供,例如應(yīng)用程序列表犀呼、JVM信息幸撕、NIO模型等
image.png

禁用ajp協(xié)議

ajp協(xié)議是基于TCP協(xié)議的,Tomct使用ajp協(xié)議主要是為了連接http apache服務(wù)器的外臂,http apache這個(gè)服務(wù)器已經(jīng)被Ngnix完爆了坐儿,如果沒(méi)有什么特殊癖好的話應(yīng)該沒(méi)人會(huì)用它了,但是這個(gè)配置會(huì)在程序中默認(rèn)跑著十個(gè)線程宋光,所以為了性能把這個(gè)沒(méi)用的功能去除貌矿,如果不是2020年2月以后下載的Tomcat的話,是沒(méi)有默認(rèn)禁用的罪佳,所以需要手動(dòng)禁掉

我的Tomcat是默認(rèn)禁止的逛漫,我在配置中開(kāi)啟了ajp,我們看到這里跑了十個(gè)線程,跑了線程又用不到赘艳,浪費(fèi)CUP的性能
image.png

在conf/server.xml中把這個(gè)配置注釋了就禁止掉ajp了(如果你有特殊的癖好酌毡,想打開(kāi)ajp,就把注釋打開(kāi)蕾管, 并且把secretRequired="true"修改為secretRequired="")
image.png

重啟服務(wù)器枷踏,查看server status,ajp已經(jīng)沒(méi)有了
image.png

自定義Tomcat線程池

Tomcat需要給每一個(gè)請(qǐng)求創(chuàng)建線程掰曾,Tomcat默認(rèn)的線程池最大線程數(shù)是200旭蠕,核心線程數(shù)是10,如何并發(fā)高的場(chǎng)景旷坦,Tomcat就得不斷的創(chuàng)建和銷(xiāo)毀線程掏熬,所以就得自定義線程池提高核心線程數(shù),這樣可以幫助我們提高性能秒梅。但是如果不是是連接請(qǐng)求特別多的場(chǎng)景旗芬,最后別亂改,核心線程是需要占用內(nèi)存的

先看看我們沒(méi)修改前的狀態(tài)捆蜀,我使用Jvisualvm工具監(jiān)控Tomcat的線程狀態(tài)疮丛,我們看到這里默認(rèn)是十個(gè)線程辆琅,由于我用的是Tomcat8.5所以運(yùn)行模式默認(rèn)是nio模式,線程的前綴是exec,記住這個(gè)名字后面用到的

image.png

修改server.xml文件这刷,打開(kāi)Executor的注釋婉烟,我配置的參數(shù)是:最大線程數(shù)150,核心線程數(shù)15暇屋,線程池名稱似袁,每個(gè)線程的前綴。
為了區(qū)別咐刨,我把線程的前綴改為tzb-nb-昙衅,接著把默認(rèn)的連接器配置注釋掉,打開(kāi)下面的連接器定鸟,讓自定義的連接線程池生效

image.png
image.png

重啟服務(wù)器而涉,查看Jvisualvm,我們看到連接池已經(jīng)生效了联予,整好十五個(gè)
image.png

Tomcat線程模型

我們主要講Tomcat8后的NIO,因?yàn)镹IO才是主流啼县,Tomcat的線程有很多,主線程叫做main是負(fù)責(zé)啟動(dòng)和關(guān)閉
Tomcat的主要線程

看監(jiān)控工具可以看到Tomcat的主要線程

Tomcat的主要線程
  • Acceptor

使用NIO的原生ServerSocketChannel的accept()方法監(jiān)聽(tīng)客戶的連接請(qǐng)求沸久,但是這個(gè)ServerSocketChannel是設(shè)置阻塞的季眷,所以當(dāng)沒(méi)有連接請(qǐng)求來(lái)時(shí),線程會(huì)阻塞在accept方法上卷胯。ServerSocketChannel設(shè)置的TCP連接隊(duì)列大小默認(rèn)是100子刮,TCP連接隊(duì)列就是把當(dāng)前連接信息存放到全連接隊(duì)列中,隊(duì)列中的連接信息等待ServerSocket.accpt()處理窑睁,Acceptor隊(duì)列由acceptCount控制挺峡,但是Tomcat保持的通道數(shù)默認(rèn)是10000,也就是ServerSocket.accpt()進(jìn)去的通道Tomcat能保持10000個(gè)担钮,由maxConnections控制橱赠。然后將ServerSocket.accpt()監(jiān)聽(tīng)獲取到的客戶端通道SocketChanel設(shè)置為非阻塞,同時(shí)將SocketChanel和注冊(cè)事件封裝成一個(gè)PollerEvent對(duì)象扔進(jìn)隊(duì)列SynchronizedStack中裳朋,SynchronizedStack內(nèi)部是數(shù)組病线,然后隊(duì)列里的PollerEvent等待Poller線程來(lái)注冊(cè)處理吓著,Poller線程內(nèi)部就是使用了NIO中的Selector多路復(fù)用器鲤嫡,PollerEvent對(duì)象主要是裝載了SocketChanel和注冊(cè)事件OP_REGISTER在Poller內(nèi)部其實(shí)就是給Selector注冊(cè)O(shè)P_READ,PollerEvent其實(shí)本身也是個(gè)線程绑莺。Poller線程是隊(duì)列的消費(fèi)者暖眼,Acceptor是生產(chǎn)者。這個(gè)Acceptor線程默認(rèn)只有1個(gè)纺裁,但是可以在Tomcat中配置數(shù)量

 public void bind() throws Exception {
        if (!getUseInheritedChannel()) {
//Acceptor的ServerSocketChannel
            serverSock = ServerSocketChannel.open();
            socketProperties.setProperties(serverSock.socket());
            InetSocketAddress addr = (getAddress()!=null?
new InetSocketAddress(getAddress(),getPort()):new InetSocketAddress(getPort()));
            serverSock.socket().bind(addr,getAcceptCount());//getAcceptCount()默認(rèn)是100
        } else {
                     //............................省略
//設(shè)置為阻塞的連接監(jiān)聽(tīng)通道
        serverSock.configureBlocking(true);
                //............................省略
    protected final void startAcceptorThreads() {
//Acceptor的線程個(gè)數(shù)诫肠,這個(gè)是可以通過(guò)配置文件配置的
        int count = getAcceptorThreadCount();
//創(chuàng)建Acceptor線程
        acceptors = new Acceptor[count];
        for (int i = 0; i < count; i++) {
            acceptors[i] = createAcceptor();
            String threadName = getName() + "-Acceptor-" + i;
            acceptors[i].setThreadName(threadName);
            Thread t = new Thread(acceptors[i], threadName);
            t.setPriority(getAcceptorThreadPriority());
            t.setDaemon(getDaemon());
//啟動(dòng)Acceptor線程
            t.start();
        }
    }
 protected class Acceptor extends AbstractEndpoint.Acceptor {
  //............................省略
        @Override
        public void run() {
  //............................省略
            while (running) {
//計(jì)數(shù)+1司澎,達(dá)到最大值則等待,tomcat設(shè)定個(gè)最大連接數(shù)是10000,達(dá)到這個(gè)閾值后栋豫,就會(huì)拒絕連接請(qǐng)求挤安,進(jìn)行阻塞,這個(gè)是用AQS阻塞隊(duì)列實(shí)現(xiàn)的
                    countUpOrAwaitConnection();

                    SocketChannel socket = null;
                    try {
//ServerSocketChannel開(kāi)始監(jiān)聽(tīng)連接
                        socket = serverSock.accept();
                  //............................省略
                    if (running && !paused) { 
                        if (!setSocketOptions(socket)) {//setSocketOptions設(shè)置socket的方法
                            closeSocket(socket);
                        }
              //............................省略
 private void closeSocket(SocketChannel socket) {
//斷開(kāi)連接計(jì)數(shù)器將會(huì)減1
            countDownConnection();
 protected boolean setSocketOptions(SocketChannel socket) {
        try {
          //將監(jiān)聽(tīng)到的SocketChannel 設(shè)置為非阻塞
            socket.configureBlocking(false);
            Socket sock = socket.socket();
        //............................省略
//注冊(cè)到ClientPoller的方法
            getPoller0().register(channel);
        //............................省略
    }
   public void register(final NioChannel socket) {
            //............................省略
            if ( r==null)
                  //封裝成PollerEvent丧鸯,與OP_REGISTER注冊(cè)事件綁定
                   r = new PollerEvent(socket,ka,OP_REGISTER);
            else r.reset(socket,ka,OP_REGISTER);
//添加到隊(duì)列
            addEvent(r);
        }
    public static class PollerEvent implements Runnable {
   //............................省略
        @Override
        public void run() {
//注冊(cè)事件蛤铜,我們看到注冊(cè)事件其實(shí)就是OP_READ讀事件
            if (interestOps == OP_REGISTER) {
                try {
//將事件和通道注冊(cè)到ClientPoller中的多路復(fù)用器中
                    socket.getIOChannel().register(
                            socket.getPoller().getSelector(), SelectionKey.OP_READ, socketWrapper);
             
            
  • ClientPoller

Tomcat8以后的NIO模式比Tomcat7以前強(qiáng)悍就是因?yàn)檫@個(gè)ClientPoller,ClientPoller是實(shí)現(xiàn)了Runnable丛肢,ClientPoller線程內(nèi)部有一個(gè)jdk原生的Selector對(duì)象围肥,也就是NIO的多路復(fù)用器。ClientPoller線程從隊(duì)列SynchronizedQueue中取出PollerEvent逐一啟動(dòng)蜂怎,啟動(dòng)后PollerEvent將SocketChanel和對(duì)應(yīng)的事件逐一注冊(cè)注冊(cè)到ClientPoller的Selector穆刻,Selector找出SocketChanel中就緒的SelectionKey,將SelectionKey和事件類型交給工作線程Exec進(jìn)行處理杠步。整個(gè)過(guò)程就是典型的NIO實(shí)現(xiàn)氢伟。Tomcat默認(rèn)啟動(dòng)Math.min(2,Runtime.getRuntime().availableProcessors())個(gè)ClientPoller線程,絕大部分的情況是2個(gè)ClientPoller幽歼,也就是兩個(gè)Selector需要處理成千上萬(wàn)的事件腐芍,非常繁忙

Poller的run方法部分代碼
Poller的run方法部分代碼
//processKey源碼大致邏輯,省略詳細(xì)代碼
 protected void processKey(SelectionKey sk, NioSocketWrapper attachment) {
                //............................省略
                if ( close ) {
                //............................省略
                } else if ( sk.isValid() && attachment != null ) {
                    if (sk.isReadable() || sk.isWritable() ) {
                              // 寫(xiě)事件
                          if ( attachment.getSendfileData() != null ) {
                            processSendfile(sk,attachment, false);
                        } else {
                            if (sk.isReadable()) {
                           //讀事件
                       if (!processSocket(attachment, SocketEvent.OPEN_READ, true)) {
                                    closeSocket = true;
                                }
                            }
                            if (!closeSocket && sk.isWritable()) {
                                  // 寫(xiě)事件
                          if (!processSocket(attachment, SocketEvent.OPEN_WRITE, true)) {
                                    closeSocket = true;
                                }
                            }
                            if (closeSocket) {
                          ............................
                                  // 關(guān)閉通道
                            }
                            ..................................
}
  • NioBlockingSelector.BlockPoller

NioBlockingSelector主要處理socketChanel的寫(xiě)操作试躏,也就是servlet的回寫(xiě)過(guò)程猪勇,NioBlockingSelector有兩個(gè)對(duì)象BlockPoller和Selector叫做sharedSelector。BlockPoller中也有一個(gè)Selector對(duì)象颠蕴,這個(gè)Selector主要是在blockingSelector.write時(shí)如果出現(xiàn)寫(xiě)失敗就把socketChanel注冊(cè)到BlockPoller的Selector不在占用ClientPoller的時(shí)間片泣刹,然后就利用CountDownLatch進(jìn)行阻塞這是Tomcat默認(rèn)的寫(xiě)操作過(guò)程,在BlockPoller的Selector注冊(cè)的socketChanel以后的讀寫(xiě)操作都是由這個(gè)Selector阻塞輪詢犀被。同時(shí)NioBlockingSelector是NioSelectorPool中一個(gè)成員對(duì)象椅您,NioSelectorPool中有兩個(gè)成員對(duì)象NioBlockingSelector和一個(gè)Selector叫做SHARED_SELECTOR,SHARED_SELECTOR主要在非默認(rèn)的寫(xiě)過(guò)程中處理由ClientPoller輪詢的socketChanel出現(xiàn)寫(xiě)失敗時(shí)寡键,為了節(jié)省ClientPoller寶貴的時(shí)間片socketChanel寫(xiě)事件會(huì)注冊(cè)到SHARED_SELECTOR上掀泳,這個(gè)SHARED_SELECTOR是叫做輔Selector,值得一提的是NioSelectorPool的SHARED_SELECTOR西轩、NioBlockingSelector的sharedSelector员舵、BlockPoller的Selector都是同一對(duì)象,這個(gè)輔Selector輪詢出現(xiàn)寫(xiě)事件失敗的socketChanel藕畔,由輔Selector負(fù)責(zé)這些socketChanel以后的讀寫(xiě)事件马僻,這樣減少線程間的切換,同時(shí)還可減輕主Selector的負(fù)擔(dān)注服。

public class NioSelectorPool {
//SHARED 默認(rèn)是true
    protected static final boolean SHARED =
        Boolean.parseBoolean(System.getProperty("org.apache.tomcat.util.net.NioSelectorShared", "true"));
    protected NioBlockingSelector blockingSelector;
    protected volatile Selector SHARED_SELECTOR;
    //........................省略
 public int write(ByteBuffer buf, NioChannel socket, Selector selector,
                     long writeTimeout, boolean block) throws IOException {
        if ( SHARED && block ) {//block 和SHARED 是默認(rèn)true韭邓,所以寫(xiě)事件默認(rèn)走這里
            //阻塞寫(xiě)操作
            return blockingSelector.write(buf,socket,writeTimeout);
        }
    //........................省略
                if ( keycount > 0 ) { //only write if we were registered for a write
//寫(xiě)操做措近,cnt -1為失敗
                    cnt = socket.write(buf); //write the data
                    if (cnt > 0) {                 
                        continue; 
                    }
//非阻塞寫(xiě)
                    if (cnt==0 && (!block)) break; //don't block
                }
                if ( selector != null ) {
      //非阻塞寫(xiě)失敗后重新注冊(cè)到SHARED_SELECTOR,不占用ClientPoller的時(shí)間片
                    if (key==null) key = socket.getIOChannel().register(selector, SelectionKey.OP_WRITE);
                    else key.interestOps(SelectionKey.OP_WRITE);
                    } else if (writeTimeout<0) {
                //SHARED_SELECTOR輪詢
                        keycount = selector.select();
                    } else {
                        keycount = selector.select(writeTimeout);
                    }
                }
//.....................................................................省略

ublic class NioBlockingSelector {

    protected Selector sharedSelector;

    protected BlockPoller poller;
//阻塞寫(xiě)
  public int write(ByteBuffer buf, NioChannel socket, long writeTimeout)
            throws IOException {
        SelectionKey key = socket.getIOChannel().keyFor(socket.getPoller().getSelector());
        try {
            while ( (!timedout) && buf.hasRemaining()) {
                if (keycount > 0) { //only write if we were registered for a write
                    int cnt = socket.write(buf); //write the data
                    written += cnt;
//寫(xiě)操做女淑,cnt -1為失敗
                    if (cnt > 0) {
                        time = System.currentTimeMillis(); //reset our timeout timer
                        continue; //we successfully wrote, try again without a selector
                    }
                }
                try {
                    if ( att.getWriteLatch()==null || att.getWriteLatch().getCount()==0) att.startWriteLatch(1);
//失敗則重新注冊(cè)寫(xiě)事件到BlockPoller 中
                    poller.add(att,SelectionKey.OP_WRITE,reference);
//利用CountDownLatch進(jìn)行阻塞
                    if (writeTimeout < 0) {
                        att.awaitWriteLatch(Long.MAX_VALUE,TimeUnit.MILLISECONDS);
                    } else {
                        att.awaitWriteLatch(writeTimeout,TimeUnit.MILLISECONDS);
                    }
            
    }
  • Exec

Exec是一個(gè)線程池很多博客叫它為Work,默認(rèn)的線程名稱是和server.xml中的一致瞭郑,核心線程默認(rèn)是10,最大線程數(shù)是200鸭你,可以通過(guò)我們上面的方法配置server.xml文件設(shè)置線程的數(shù)量凰浮,它主要是獲取ClientPoller輪詢出來(lái)的socketChanel后,做相應(yīng)的邏輯處理:先進(jìn)行三次握手苇本,然后調(diào)用Http11Processor的service方法解析HTTP協(xié)議袜茧,將http協(xié)議解析成鍵值對(duì)放入request,最終調(diào)用CoyoteAdapter的service方法將request和response傳入Tomcat的各種容器中執(zhí)行servlet的業(yè)務(wù)邏輯瓣窄。

image.png
//工作線程執(zhí)行run處理socketChanel
 public boolean processSocket(SocketWrapperBase<S> socketWrapper,
            SocketEvent event, boolean dispatch) {
        try {
            if (socketWrapper == null) {
                return false;
            }
            //將Socket封裝到這個(gè)對(duì)象笛厦,然后扔給工作線程執(zhí)行
            SocketProcessorBase<S> sc = processorCache.pop();
            if (sc == null) {
                sc = createSocketProcessor(socketWrapper, event);
            } else {
                sc.reset(socketWrapper, event);
            }
            //工作線程,我們?cè)谂渲梦募信渲玫腅xecutor,不配默認(rèn)是10個(gè)俺夕。最大數(shù)200
            Executor executor = getExecutor();
            //工作線程執(zhí)行工作
            if (dispatch && executor != null) {
                executor.execute(sc);
            } else {
                sc.run();
            }
//工作線程執(zhí)行的東西裳凸,主要是三次握手后開(kāi)始解析HTTP,然后關(guān)閉socket,省略部分代碼
  protected class SocketProcessor extends SocketProcessorBase<NioChannel> {
        public SocketProcessor(SocketWrapperBase<NioChannel> socketWrapper, SocketEvent event) {
            super(socketWrapper, event);
        }
        @Override
        protected void doRun() {
if (handshake == 0) {//三次握手已完成
                    SocketState state = SocketState.OPEN;
                    // Process the request from this socket
                    if (event == null) {
                        state = getHandler().process(socketWrapper, SocketEvent.OPEN_READ);
                    } else {
                        state = getHandler().process(socketWrapper, event);
                    }
                    if (state == SocketState.CLOSED) {
                        close(socket, key);
                    }
..........................
//邏輯處理主要是調(diào)用了這給ConnectionHandler處理socket的數(shù)據(jù)包
  protected static class ConnectionHandler<S> implements AbstractEndpoint.Handler<S> {
//省略  ...............................................            
//處理socket數(shù)據(jù)包劝贸,將HTTP其解析成request 對(duì)象姨谷,傳給Tomcat容器
        @Override
        public SocketState process(SocketWrapperBase<S> wrapper, SocketEvent status) {
//省略  ...............................................            
        do {
//省略  ...............................................            
            state = processor.process(wrapper, status);//真正繼續(xù)數(shù)據(jù)包的方法,這個(gè)processor是AbstractProcessorLight 類型的映九,但是process方法主要是調(diào)用子類Http11Processor類實(shí)現(xiàn)的service進(jìn)行處理
//省略  ...............................................            
                    }
      }
}

點(diǎn)進(jìn)process方法梦湘,里面調(diào)用了service

public abstract class AbstractProcessorLight implements Processor {
    @Override
    public SocketState process(SocketWrapperBase<?> socketWrapper, SocketEvent status)
//省略  ...............................................        
            throws IOException {
            } else if (status == SocketEvent.OPEN_READ) {
//這里是調(diào)用Http11Processor 的service方法
                state = service(socketWrapper);
       }
//省略  ...............................................        

Http11Processor 的service方法進(jìn)行系列的解析各種

public class Http11Processor extends AbstractProcessor {
//省略  ............................................... 
   @Override
    public SocketState service(SocketWrapperBase<?> socketWrapper)
        throws IOException {
//省略  ............................................... 
//經(jīng)過(guò)一系列的解析和設(shè)置后,將http的各種信息都存放到request中調(diào)用CoyoteAdapter的service方法將request, response傳遞到Tomcat容器
getAdapter().service(request, response);
//省略  ............................................... 
        }
}
Tomcat線程模型組件類的結(jié)構(gòu)圖
Tomcat線程模型動(dòng)態(tài)圖

NioEndpoint組件

NioEndpoint是Tomcat的NIO關(guān)鍵的類件甥,要理解tomcat的nio最主要就是對(duì)NioEndpoint的理解
image.png

這個(gè)我是摘抄別人的捌议,出處http://www.reibang.com/p/76ff17bc6dea
,有空得自己去看看《Tomcat的內(nèi)核設(shè)計(jì)剖析》

image.png

NioEndpoint它一共包含LimitLatch引有、Acceptor瓣颅、Poller、SocketProcessor譬正、Excutor5個(gè)部分宫补。LimitLatch是連接控制器,它負(fù)責(zé)維護(hù)連接數(shù)的計(jì)算曾我,nio模式下默認(rèn)是10000粉怕,達(dá)到這個(gè)閾值后,就會(huì)拒絕連接請(qǐng)求您单。Acceptor負(fù)責(zé)接收連接斋荞,默認(rèn)是1個(gè)線程來(lái)執(zhí)行荞雏,將請(qǐng)求的事件注冊(cè)到事件列表虐秦。有Poller來(lái)負(fù)責(zé)輪詢平酿,Poller線程數(shù)量是cpu的核數(shù)Math.min(2,Runtime.getRuntime().availableProcessors())。由Poller將就緒的事件生成SocketProcessor同時(shí)交給Excutor去執(zhí)行悦陋。Excutor線程池的大小就是我們?cè)贑onnector節(jié)點(diǎn)配置的maxThreads的值蜈彼。在Excutor的線程中,會(huì)完成從socket中讀取http request俺驶,解析成HttpServletRequest對(duì)象幸逆,分派到相應(yīng)的servlet并完成邏輯,然后將response通過(guò)socket發(fā)回client暮现。在從socket中讀數(shù)據(jù)和往socket中寫(xiě)數(shù)據(jù)的過(guò)程还绘,并沒(méi)有像典型的非阻塞的NIO的那樣,注冊(cè)O(shè)P_READ或OP_WRITE事件到主Selector栖袋,而是直接通過(guò)socket完成讀寫(xiě)拍顷,這時(shí)是阻塞完成的,但是在timeout控制上塘幅,使用了NIO的Selector機(jī)制昔案,但是這個(gè)Selector并不是Poller線程維護(hù)的主Selector,而是BlockPoller線程中維護(hù)的Selector电媳,稱之為輔Selector

NioEndpoint Nio的時(shí)序圖

Tomcat文件傳輸

sendfile零拷貝:

在普通的輸入輸出流進(jìn)行讀寫(xiě)時(shí)踏揣,實(shí)際上是進(jìn)行了多次上下文切換,應(yīng)用讀取數(shù)據(jù)時(shí)匾乓,先在內(nèi)核態(tài)將數(shù)據(jù)從磁盤(pán)讀取到內(nèi)核緩存捞稿,再切換到用戶態(tài)將數(shù)據(jù)從內(nèi)核緩存讀取到用戶緩存,在用戶態(tài)對(duì)數(shù)據(jù)進(jìn)行加工拼缝,然后再?gòu)挠脩魬B(tài)切換回內(nèi)核態(tài)括享,將用戶緩存數(shù)據(jù)拷貝到內(nèi)核緩存中,然后讀到socket中再到網(wǎng)卡

sendfile實(shí)質(zhì)是linux系統(tǒng)中一項(xiàng)優(yōu)化技術(shù)珍促,用以發(fā)送文件和網(wǎng)絡(luò)通信時(shí)铃辖,減少用戶態(tài)空間與磁盤(pán)倒換數(shù)據(jù),而直接在內(nèi)核級(jí)做數(shù)據(jù)拷貝猪叙,在Tomcat中是可以使用sendfile對(duì)一些靜態(tài)數(shù)據(jù)如:圖片娇斩、文件等進(jìn)行傳輸,這個(gè)sendfile是在Tomcat是默認(rèn)打開(kāi)的穴翩,可以有效的提高Tomcat的傳輸性能
零拷貝

compression文件壓縮:

compression可以對(duì)傳輸文件進(jìn)行壓縮有效解決文件傳輸?shù)膸拞?wèn)題犬第,在Tomcat不支持圖片壓縮,因?yàn)橐M(jìn)行上層的用戶態(tài)數(shù)據(jù)加工所以與sendfile互斥芒帕,Tomcat默認(rèn)關(guān)閉歉嗓,開(kāi)啟的話Tomcat會(huì)調(diào)用Filter利用jdk解壓縮流進(jìn)行解壓縮

//源碼中compression支持的壓縮格式中沒(méi)有圖片
  private String compressibleMimeType = "text/html,text/xml,text/plain,text/css," +
            "text/javascript,application/javascript,application/json,application/xml";
Accept-Encoding 和Content-Encoding

Accept-Encoding 和Content-Encoding是HTTP中用來(lái)對(duì)采用哪種編碼格式傳輸正文進(jìn)行協(xié)定的一對(duì)頭部字段。瀏覽器發(fā)送請(qǐng)求時(shí)背蟆,會(huì)在Request-heads攜帶Accept-Encoding會(huì)說(shuō)明自己支持的編碼列表鉴分,服務(wù)端在接收到請(qǐng)求后哮幢,從中挑選出一種用來(lái)對(duì)響應(yīng)信息進(jìn)行編碼,并通過(guò)Response-heads攜帶Content-Encoding來(lái)說(shuō)明服務(wù)端選擇的編碼信息志珍,瀏覽器在拿到響應(yīng)正文后橙垢,依據(jù)Content-Encoding進(jìn)行解壓。絕大部分是gzip格式

Tomcat重要配置參數(shù)

server.xml

自定義線程池我們上面講過(guò)了伦糯,現(xiàn)在有了一定的基礎(chǔ)更好介紹了

Executor標(biāo)簽是專門(mén)配置Exec線程池的柜某,專門(mén)用于處理多路復(fù)用器傳遞進(jìn)來(lái)的socketChanel,我們上面介紹過(guò)了
<Executor name="tomcatThreadPool"   
         namePrefix="tzb-nb-"   
         maxThreads="1000"   
         minSpareThreads="30"  
         maxIdleTime="60000"  
         maxQueueSize="1000000"  
         className="org.apache.catalina.core.StandardThreadExecutor"/>
        name:線程池名稱敛纲,用于 Connector中指定喂击。

        namePrefix:所創(chuàng)建的每個(gè)線程的名稱前綴。

        maxThreads:池中最大線程數(shù)淤翔。

        minSpareThreads:核心池線程數(shù)惭等。

        maxIdleTime:線程空閑時(shí)間,超過(guò)該時(shí)間后办铡,非核心線程會(huì)被銷(xiāo)毀辞做,默認(rèn)值為6000(1分鐘),單位毫秒寡具。

        maxQueueSize:在被執(zhí)行前最大線程排隊(duì)數(shù)目秤茅,任務(wù)數(shù)超出會(huì)執(zhí)行拒絕策略,默認(rèn)為Int的最大值
               
        className:線程池實(shí)現(xiàn)類童叠,未指定情況下框喳,默認(rèn)實(shí)現(xiàn)類為org.apache.catalina.core.StandardThreadExecutor。
        如果想使用自定義線程池首先需要實(shí)現(xiàn) org.apache.catalina.Executor接口厦坛。
Connector用于配置Tomcat運(yùn)行模式五垮、Acceptor、CilentPoller杜秸、sendfile放仗、是否開(kāi)啟文件壓縮
<Connector port="8080"   
          protocol="HTTP/1.1"   
          maxThreads="1000"   
          minSpareThreads="100"  
          acceptorThreadCount="2"
          acceptCount="1000"  
          maxConnections="1000"  
          connectionTimeout="20000"   
          maxHttpHeaderSize="8192"  
          compression="on"  
          compressionMinSize="2048"  
          redirectPort="8443"  
          URIEncoding="UTF-8" />

參數(shù)很多參考這里吧:https://blog.csdn.net/lijunwyf/article/details/84244209
我只列出幾個(gè)有意思的

port:代表Tomcat監(jiān)聽(tīng)端口,也就是網(wǎng)站的訪問(wèn)端口撬碟,默認(rèn)為8080诞挨,可以根據(jù)需要改成其他。

protocol:運(yùn)行模式呢蛤,可選類型有四種惶傻,分別為BIO,NIO其障,AIO和APR银室。
protocol="org.apache.coyote.http11.Http11NioProtocol"  //NIO  
protocol="org.apache.coyote.http11.Http11Nio2Protocol" //AIO  
protocol="org.apache.coyote.http11.Http11AprProtocol"  //ARP

acceptCount:就是Acceptor線程工作時(shí)通道的長(zhǎng)度也就是前面說(shuō)的SynchronizedStack,當(dāng)請(qǐng)求PollerEvent超出隊(duì)列時(shí)請(qǐng)求就會(huì)被拒絕,默認(rèn)是100蜈敢。
一般是設(shè)置的跟 maxThreads一樣或一半辜荠,此值設(shè)置的過(guò)大會(huì)導(dǎo)致排隊(duì)的請(qǐng)求超時(shí)而未被處理。所以這個(gè)值應(yīng)該是主要根據(jù)應(yīng)用的訪問(wèn)峰值與平均值來(lái)權(quán)衡配置扶认。

acceptorThreadCount:Acceptor線程的數(shù)量,默認(rèn)是1侨拦,如果在多核CPU架構(gòu)下殊橙,此值可以設(shè)置為2辐宾,官方不建議設(shè)定超過(guò)2個(gè)的值。

maxConnections:tomcat最大連接數(shù)也就是上面說(shuō)的LimitLatch膨蛮。NIO默認(rèn)是10000叠纹,超出則Acceptor線程就被阻塞了,即不再隊(duì)列中獲取已經(jīng)建立的連接敞葛。
但是它并不阻止新的連接的建立誉察,新的連接的建立過(guò)程不是Acceptor控制的,Acceptor僅僅是從隊(duì)列中獲取新建立的連接惹谐。
所以當(dāng)連接數(shù)已經(jīng)超過(guò)maxConnections后持偏,仍然是可以建立新的連接的,存放在上述acceptCount大小的隊(duì)列中氨肌,這個(gè)隊(duì)列里面的連接沒(méi)有被Acceptor獲取鸿秆,
就處于連接建立了但是不被處理的狀態(tài)。當(dāng)連接數(shù)低于maxConnections之后怎囚,Acceptor線程就不再阻塞

connectionTimeout:當(dāng)請(qǐng)求已經(jīng)被接受卿叽,但未被處理,也就是等待中的超時(shí)時(shí)間恳守。單位為毫秒考婴,默認(rèn)值為60000。

executor:就是把executor標(biāo)簽的內(nèi)容引過(guò)來(lái)催烘,不要它的話可以把executor的內(nèi)容復(fù)制到這里

URIEncoding:URL編碼字符集沥阱。

pollerThreadCount:ClientPoller的線程數(shù),默認(rèn)為2伊群。官方不建議設(shè)置大于2的值喳钟,因?yàn)殒i的競(jìng)爭(zhēng)會(huì)導(dǎo)致性能下降,事實(shí)上一個(gè)線程也足夠快速在岂。

useSendfile:是否開(kāi)啟sendfile特性奔则,默認(rèn)為true。對(duì)于web應(yīng)用而言蔽午,通常project中還會(huì)包含一定數(shù)量的靜態(tài)資源易茬,比如圖片、CSS、js抽莱、html等范抓,sendfile在一定程度上可以提高性能。

compression:是否開(kāi)啟壓縮食铐,這個(gè)屬性與useSendfile互斥匕垫,useSendfile開(kāi)啟了,這個(gè)認(rèn)關(guān)閉虐呻,compression是默認(rèn)關(guān)閉的

compressionMinSize:如果compression="on"象泵,則啟用此項(xiàng)。被壓縮前數(shù)據(jù)的最小值斟叼,也就是超過(guò)這個(gè)值后才被壓縮偶惠。如果沒(méi)有指定,這個(gè)屬性默認(rèn)為“2048”(2K)朗涩,單位為byte忽孽。

實(shí)戰(zhàn):

image.png

我們看到Acceptor線程線程是3個(gè)、Client線程是5個(gè)谢床,exec線程是15個(gè)和我上圖的配置一致

image.png

catalina.sh

使用JAVA_OPTS配置JVM參數(shù)

//例如:
JAVA_OPTS="-Dfile.encoding=UTF-8 -server -Xms2048m -Xmx2048m 
-XX:NewSize=512m -XX:MaxNewSize=1024m 
-XX:PermSize=256m -XX:MaxPermSize=256m -XX:MaxTenuringThreshold=10 -XX:NewRatio=2 -XX:+DisableExplicitGC"

下面是參考這里的https://blog.csdn.net/fly910905/article/details/78518599

  • 一般說(shuō)來(lái)兄一,您應(yīng)該使用物理內(nèi)存的 80% 作為堆大小。說(shuō)明:以上兩個(gè)參數(shù)關(guān)系到tomcat承受的訪問(wèn)性能识腿,但也要根據(jù)服務(wù)器實(shí)際內(nèi)存情況設(shè)定出革。有人建議Xms和Xmx的值取成一樣比較好,說(shuō)是可以加快內(nèi)存回收速度覆履。

  • Xms和Xmx這兩個(gè)值的大小一般根據(jù)需要進(jìn)行配置蹋盆。初始化堆的大小執(zhí)行了虛擬機(jī)在啟動(dòng)時(shí)向系統(tǒng)申請(qǐng)的內(nèi)存的大小。一般而言硝全,這個(gè)參數(shù)不重要栖雾。但是有的應(yīng)用程序在大負(fù)載的情況下會(huì)急劇地占用更多的內(nèi)存,此時(shí)這個(gè)參數(shù)就是顯得很重要伟众,假如虛擬機(jī)啟動(dòng)時(shí)配置使用的內(nèi)存比較小而在這種情況下有許多對(duì)象進(jìn)行初始化析藕,虛擬機(jī)就必須重復(fù)地增加內(nèi)存來(lái)滿足使用。由于這種原因凳厢,我們一般把-Xms和-Xmx設(shè)為相同大账胧,而堆的最大值受限于系統(tǒng)使用的物理內(nèi)存。一般使用數(shù)據(jù)量較大的應(yīng)用程序會(huì)使用持久對(duì)象先紫,內(nèi)存使用有可能迅速地增長(zhǎng)治泥。當(dāng)應(yīng)用程序需要的內(nèi)存超出堆的最大值時(shí)虛擬機(jī)就會(huì)提示內(nèi)存溢出,并且導(dǎo)致應(yīng)用服務(wù)崩潰遮精。因此一般建議堆的最大值配置為可用內(nèi)存的最大值的80%居夹。

  • 另外需要考慮的是Java提供的垃圾回收機(jī)制败潦。虛擬機(jī)的堆大小決定了虛擬機(jī)花費(fèi)在收集垃圾上的時(shí)間和頻度。收集垃圾能夠接受的速度和應(yīng)用有關(guān)准脂,應(yīng)該通過(guò)分析實(shí)際的垃圾收集的時(shí)間和頻率來(lái)調(diào)整劫扒。假如堆的大小很大,那么完全垃圾收集就會(huì)很慢狸膏,但是頻度會(huì)降低沟饥。假如您把堆的大小和內(nèi)存的需要一致,完全收集就很快湾戳,但是會(huì)更加頻繁贤旷。調(diào)整堆大小的的目的是最小化垃圾收集的時(shí)間,以在特定的時(shí)間內(nèi)最大化處理客戶的請(qǐng)求院塞。在基準(zhǔn)測(cè)試的時(shí)候遮晚,為確保最好的性能性昭,要把堆的大小設(shè)大拦止,確保垃圾收集不在整個(gè)基準(zhǔn)測(cè)試的過(guò)程中出現(xiàn)。

  • 假如系統(tǒng)花費(fèi)很多的時(shí)間收集垃圾糜颠,請(qǐng)減小堆大小汹族。一次完全的垃圾收集應(yīng)該不超過(guò) 3-5 秒。假如垃圾收集成為瓶頸其兴,那么需要指定代的大小顶瞒,檢查垃圾收集的周詳輸出,研究 垃圾收集參數(shù)對(duì)性能的影響元旬。一般說(shuō)來(lái)榴徐,您應(yīng)該使用物理內(nèi)存的 80% 作為堆大小。當(dāng)增加處理器時(shí)匀归,記得增加內(nèi)存坑资,因?yàn)榉峙淠軌虿⑿羞M(jìn)行,而垃圾收集不是并行的穆端。

Tomcat壓力測(cè)試

使用jmeter可以進(jìn)行對(duì)程序進(jìn)行壓力測(cè)試袱贮,根據(jù)用戶的并發(fā),調(diào)整Tomcat的線程數(shù)量

image.png

我使用一千個(gè)線程,一次并發(fā)一千体啰,循環(huán)十次

image.png

主要看吞吐量攒巍,根據(jù)吞吐量慢慢調(diào)試Tomcat的線程設(shè)置

image.png
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市荒勇,隨后出現(xiàn)的幾起案子柒莉,更是在濱河造成了極大的恐慌,老刑警劉巖沽翔,帶你破解...
    沈念sama閱讀 211,194評(píng)論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件兢孝,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)西潘,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,058評(píng)論 2 385
  • 文/潘曉璐 我一進(jìn)店門(mén)卷玉,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人喷市,你說(shuō)我怎么就攤上這事相种。” “怎么了品姓?”我有些...
    開(kāi)封第一講書(shū)人閱讀 156,780評(píng)論 0 346
  • 文/不壞的土叔 我叫張陵寝并,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我腹备,道長(zhǎng)衬潦,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 56,388評(píng)論 1 283
  • 正文 為了忘掉前任植酥,我火速辦了婚禮镀岛,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘友驮。我一直安慰自己漂羊,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,430評(píng)論 5 384
  • 文/花漫 我一把揭開(kāi)白布卸留。 她就那樣靜靜地躺著走越,像睡著了一般。 火紅的嫁衣襯著肌膚如雪耻瑟。 梳的紋絲不亂的頭發(fā)上旨指,一...
    開(kāi)封第一講書(shū)人閱讀 49,764評(píng)論 1 290
  • 那天,我揣著相機(jī)與錄音喳整,去河邊找鬼谆构。 笑死,一個(gè)胖子當(dāng)著我的面吹牛算柳,可吹牛的內(nèi)容都是我干的低淡。 我是一名探鬼主播,決...
    沈念sama閱讀 38,907評(píng)論 3 406
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼瞬项,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼蔗蹋!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起囱淋,我...
    開(kāi)封第一講書(shū)人閱讀 37,679評(píng)論 0 266
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤猪杭,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后妥衣,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體皂吮,經(jīng)...
    沈念sama閱讀 44,122評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡戒傻,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,459評(píng)論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了蜂筹。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片需纳。...
    茶點(diǎn)故事閱讀 38,605評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖艺挪,靈堂內(nèi)的尸體忽然破棺而出不翩,到底是詐尸還是另有隱情,我是刑警寧澤麻裳,帶...
    沈念sama閱讀 34,270評(píng)論 4 329
  • 正文 年R本政府宣布口蝠,位于F島的核電站,受9級(jí)特大地震影響津坑,放射性物質(zhì)發(fā)生泄漏妙蔗。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,867評(píng)論 3 312
  • 文/蒙蒙 一疆瑰、第九天 我趴在偏房一處隱蔽的房頂上張望眉反。 院中可真熱鬧,春花似錦乃摹、人聲如沸禁漓。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,734評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至伶跷,卻和暖如春掰读,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背叭莫。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,961評(píng)論 1 265
  • 我被黑心中介騙來(lái)泰國(guó)打工蹈集, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人雇初。 一個(gè)月前我還...
    沈念sama閱讀 46,297評(píng)論 2 360
  • 正文 我出身青樓拢肆,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親靖诗。 傳聞我的和親對(duì)象是個(gè)殘疾皇子郭怪,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,472評(píng)論 2 348