前言
學習一個優秀的框架,總要循序漸進,瞭解->使用->原理->源碼->改造。
材料
- 下載Tomcat-8.5.37 程序 https://tomcat.apache.org/download-80.cgi
- 下載Tomcat-8.5.37 源碼 http://archive.apache.org/dist/tomcat/tomcat-8/v8.5.37/src/
- 準備調試代碼,如下
public static void main(String args[]) throws Exception {
Bootstrap bootstrap=new Bootstrap();
bootstrap.start();
Thread.sleep(Integer.MAX_VALUE);
}
目錄
- ** Tomcat 組件分析**
- ** Tomcat 生產配置(網絡io模型,調優思路)**
- ** Tomcat 是如何啓動的?NIO從接受socket到我們的servlet?**
- ** Tomcat 如何打破雙親委派?**
0.簡介
Tomcat 是一個基於JAVA的WEB容器,其實現了JAVA EE中的 Servlet 與 jsp 規範,與Nginx,Apache 服務器不同在於一般用於動態請求處理;在架構設計上採用面向組件的方式設計,即整體功能是通過組件的方式拼裝完成。另外每個組件都可以被替換以保證靈活性。
1. Tomcat 組件分析
** 由上圖可知Tomcat組成如下 **
- 一個 Server 和 多個Service
- Connector 連接器
- HTTP 1.1
- SSL https
- AJP( Apache JServ Protocol) apache 私有協議,用於apache 反向代理Tomcat
- Container 啓動引擎
- Engine 引擎 catalina
- Host 虛擬機 基於域名 分發請求
- Context 隔離各個WEB應用 每個Context的 ClassLoader都是獨立
- Component
- Manager (管理器)
- logger (日誌管理)
- loader (載入器)
- pipeline (管道)
- valve (管道中的閥,filter)
** 他們的關係如下圖 **
** 也可以從server.xml文件中加深理解 **
2. Tomcat 生產配置(io模型,調優思路)
** 一般的部署思路 **
- 複製WAR包至Tomcat webapp 目錄。
- 執行starut.bat 腳本啓動。
- 啓動過程中war 包會被自動解壓裝載。
** 問題 **:多個項目在一起配置相互影響問題多多
** 生產環境部署策略 **
- 實現Tomcat程序和應用部署目錄部署相互分離
- 實現各個應用相互不影響
- 使用腳本啓動
我們只需要在啓動時指定CATALINA_HOME 與 CATALINA_BASE 參數即可實現。
啓動參數--------描述說明
JAVA_OPTS--------jvm 啓動參數 , 設置內存 編碼等 -Xms100m -Xmx200m -Dfile.encoding=UTF-8
JAVA_HOME--------指定jdk 目錄,如果未設置從java 環境變量當中去找。
CATALINA_HOME--------Tomcat 程序根目錄
CATALINA_BASE--------應用部署目錄,默認爲$CATALINA_HOME
CATALINA_OUT--------應用日誌輸出目錄:默認$CATALINA_BASE/log
CATALINA_TMPDIR--------應用臨時目錄:默認:$CATALINA_BASE/temp
創建目錄
payment
├─webapps
├─logs
├─temp
├─conf
reload.sh
#!/bin/bash
export JAVA_OPTS="-Xms100m -Xmx200m"
export JAVA_HOME=/root/svr/jdk/
export CATALINA_HOME=/usr/local/apache-tomcat-8.5.34
export CATALINA_BASE="`pwd`"
case $1 in
start)
$CATALINA_HOME/bin/catalina.sh start
echo start success!!
;;
stop)
$CATALINA_HOME/bin/catalina.sh stop
echo stop success!!
;;
restart)
$CATALINA_HOME/bin/catalina.sh stop
echo stop success!!
sleep 2
$CATALINA_HOME/bin/catalina.sh start
echo start success!!
;;
version)
$CATALINA_HOME/bin/catalina.sh version
;;
configtest)
$CATALINA_HOME/bin/catalina.sh configtest
;;
esac
exit 0
** 線程模型 **
Tomcat支持的IO模型說明
- BIO 阻塞式IO,即Tomcat使用傳統的java.io進行操作。該模式下每個請求都會創建一個線程,對性能開銷大,不適合高併發場景。優點是穩定,適合連接數目小且固定架構。
- NIO 非阻塞式IO,jdk1.4 之後實現的新IO。該模式基於多路複用選擇器監測連接狀態在通知線程處理,從而達到非阻塞的目的。比傳統BIO能更好的支持併發性能。Tomcat 8.0之後默認採用該模式
- APR 全稱是 Apache Portable Runtime/Apache可移植運行庫),是Apache HTTP服務器的支持庫。可以簡單地理解爲,Tomcat將以JNI的形式調用Apache HTTP服務器的核心動態鏈接庫來處理文件讀取或網絡傳輸操作。使用需要編譯安裝APR 庫
- AIO 異步非阻塞式IO,jdk1.7後之支持 。與nio不同在於不需要多路複用選擇器,而是請求處理線程執行完程進行回調調知,已繼續執行後續操作。Tomcat 8之後支持。
** 使用指定IO模型的配置方式 **
配置 server.xml 文件當中的 <Connector protocol="HTTP/1.1"> 修改即可。
默認配置 8.0 protocol=“HTTP/1.1” 8.0 之前是 BIO 8.0 之後是NIO
BIO
protocol=“org.apache.coyote.http11.Http11Protocol“
NIO
protocol=”org.apache.coyote.http11.Http11NioProtocol“
AIO
protocol=”org.apache.coyote.http11.Http11Nio2Protocol“
APR
protocol=”org.apache.coyote.http11.Http11AprProtocol“
** BIO 線程模型 (Tomcat8之後移除)**
Acceptor 負責接受連接(封裝提交task) 給線程池(去分配work線程),每個請求都又一個線程去處理
** NIO 線程模型講解 **
Acceptor 負責接受連接socket,然後會給Poller使用NIO select ,再 進行分配線程處理
** NIO2 線程模型講解 **
基於NIO,不再使用多路複用selector,基於事件的異步通知實現,監聽各種CompletionHandler,那麼爲什麼有了AIO 還需要NIO?適用場景不同,NIO適合處理較快的場景,不然要一直輪詢,如果處理時間長,效率就低下了。AIO 適合時間長的,例如相冊服務器,長時間的讀取,異步通知效率高
** 源碼實現 **
- 1.Http11Protocol Http BIO協議解析器
- JIoEndpoint
- Acceptor implements Runnable
- SocketProcessor implements Runnable
- JIoEndpoint
- 2.Http11NioProtocol Http Nio協議解析器
- NioEndpoint
- Acceptor implements Runnable
- Poller implements Runnable
- SocketProcessor implements Runnable
- NioEndpoint
** 調優 **
Connector
連接器:用於接收 指定協議下的連接 並指定給唯一的Engine 進行處理。
主要屬性:
protocol 監聽的協議,默認是http/1.1
port 指定服務器端要創建的端口號
minThread 服務器啓動時創建的處理請求的線程數
maxThread 最大可以創建的處理請求的線程數
enableLookups 如果爲true,則可以通過調用request.getRemoteHost()進行DNS查詢來得到遠程客戶端的實際主機名,若爲false則不進行DNS查詢,而是返回其ip地址
redirectPort 指定服務器正在處理http請求時收到了一個SSL傳輸請求後重定向的端口號
acceptCount 指定當所有可以使用的處理請求的線程數都被使用時,可以放到處理隊列中的請求數,超過這個數的請求將不予處理。內部有隊列
connectionTimeout 指定超時的時間數(以毫秒爲單位)
SSLEnabled 是否開啓 sll 驗證,在Https 訪問時需要開啓。
調休的關鍵點:爲什麼這麼調?不要憑着感覺調優
https://gitee.com/zhangxiangfeng/apache-tomcat-8-5-14-prod
- 使用線程池,優化策略
- 採用多個Acceptor 加速接受連接,處理加速 合理設置acceptCount acceptorThreadCount
- JVM設置合理的堆內存
3. Tomcat 是如何啓動的?
1.從這裏看到 執行 startup.sh == catalina.sh start
2.從catalina.sh 可以看出 org.apache.catalina.startup.Bootstrap “$@” start
3.到這裏就明白了簡單來說就是 Bootstrap.start()啓動,也徹底證明了Tomcat是純Java實現的應用容器
** 源代碼 **
> org.apache.catalina.startup.Bootstrap.start() 啓動入口函數
> org.apache.catalina.startup.Bootstrap.init() 1.使用類加載器工廠創建不同的類加載器 2.構造 Catalina,反射調用其內部的start的方法
> org.apache.catalina.startup.Catalina.start 1.調用start(啓動Acceptor(接受socket),SocketProcessor(處理socket,編解碼,然後給線程給run,出來HttpReq,HttpResp))
> org.apache.catalina.startup.Catalina.load()
> org.apache.catalina.startup.Catalina.initDirs() 初始化臨時目錄
> org.apache.catalina.startup.Catalina.initNaming() 初始化命名
> org.apache.catalina.startup.Catalina.createStartDigester() 構造主要的啓動者,設置了用來解析server.xml的參數
> org.apache.catalina.startup.Catalina.configFile() 根據約定規則去默認的base_home目錄conf/server.xml去獲取server.xml文件,下面使用org.xml.sax.InputSource解析該xml
> org.apache.tomcat.util.digester.Digester.parse(org.xml.sax.InputSource) 解析上一步的server文件,實例化Server,最終反射調用了org.apache.tomcat.util.IntrospectionUtils.callMethod1 調用setServer設置Server對象
> org.apache.catalina.startup.SetAllPropertiesRule.begin 設置HttpReq,HttpResp,用於之後的處理
> org.apache.catalina.Lifecycle.init() 初始化Server
> org.apache.catalina.Lifecycle.start() 啓動Server
> org.apache.catalina.core.StandardServer.startInternal:413 循環啓動所有的service(內部的engine+executors+mapperListener+connectors)都是調用其start方法
> org.apache.catalina.Lifecycle.start 循環啓動service
> org.apache.catalina.util.LifecycleBase.start 循環啓動connectors
> org.apache.coyote.ProtocolHandler.start 啓動 ProtocolHandler 協議處理器
> org.apache.tomcat.util.net.NioEndpoint.startInternal 內部先啓動(pollers,其次是啓動所有的Acceptor,代碼如下)
>// Start poller threads
pollers = new Poller[getPollerThreadCount()];
for (int i=0; i<pollers.length; i++) {
pollers[i] = new Poller();
Thread pollerThread = new Thread(pollers[i], getName() + "-ClientPoller-"+i);
pollerThread.setPriority(threadPriority);
pollerThread.setDaemon(true);
pollerThread.start();
}
//startAcceptorThreads();
> org.apache.tomcat.util.net.NioEndpoint.Poller#run 開始通過 selector.selectNow || selector.select(selectorTimeout)監聽
> org.apache.tomcat.util.net.NioEndpoint.Poller#events 循環所有進來的event,異步執行,放入
> org.apache.tomcat.util.net.NioEndpoint.PollerEvent#run 通過 socket.getIOChannel().keyFor(socket.getPoller().getSelector()) 處理某一個event
> java.nio.channels.SelectionKey.interestOps(int)
> org.apache.tomcat.util.net.NioEndpoint.Poller.processKey 處理NIO的selectKey
> org.apache.tomcat.util.net.AbstractEndpoint.processSocket 進一步處理
> org.apache.tomcat.util.net.SocketProcessorBase.run
> org.apache.tomcat.util.net.SocketProcessorBase.doRun 此方法是抽象的,又此類實現例如 NioEndpoint,Nio2Endpoint,AprEndpoint
> org.apache.coyote.Processor.process
> org.apache.coyote.AbstractProcessorLight.service
> org.apache.coyote.http11.Http11Processor.service 在這裏開始解析協議
> org.apache.catalina.connector.CoyoteAdapter.postParseRequest 開始實現Request,Response
> org.apache.catalina.core.ApplicationFilterChain.internalDoFilter Use potentially wrapped request from this point,Filter執行完畢就開始Servlet.service調用
> javax.servlet.Servlet#service 開始我們的應用
> org.apache.tomcat.util.net.AbstractEndpoint.startAcceptorThreads 啓動所有的Acceptor(請求入口接收者)
> java.lang.Runtime.addShutdownHook 添加鉤子函數,如果應用被系統kill掉,這裏就會調用stop方法優雅退出
源代碼調試比較複雜,簡單來說
- 構建Catalina 調用start
- 解析server.xml
- 初始化啓動Server
- Server 循環啓動所有的service(內部的engine+executors+mapperListener+connectors+pollers+startAcceptorThreads)都是調用其start方法
- 添加 addShutdownHook 鉤子函數,清理資源
4. Tomcat 如何打破雙親委派
篇幅夠長了,這裏不介紹什麼是雙親委派了
-
如何打破雙親委派 1.
重寫loadcalss
2.使用線程上下文類加載器
- 第一次破壞:JDK1.2引入雙親委派,之前的代碼都是自定義類加載器,爲了兼容,出現loadclass
- 第二次破壞: 雙親委派解決了基礎向上加載的問題,但是基礎的類依賴用戶的代碼如何處理,例如JNDI管理髮現用戶的資源,出現了 線程上下文類加載器(Thread Context ClassLoader)
- 第三次破壞: jrebel 熱加載和熱部署等的出現,java開頭的類用系統加載器,否則依賴自己的加載,樹狀加載轉網加載
-
爲什麼tomca要打破類加載,tomcat有自己的lib目錄,一些類要自己去加載
1.啓動Tomcat有三個線程異步啓動
2.org.apache.catalina.startup.Bootstrap.initClassLoaders 分別設置了上下文類加載器(用於加載約定lib下的jar,打破雙親委派)
private void initClassLoaders() {
try {
commonLoader = createClassLoader("common", null);
if( commonLoader == null ) {
// no config file, default to this loader - we might be in a 'single' env.
commonLoader=this.getClass().getClassLoader();
}
catalinaLoader = createClassLoader("server", commonLoader);
sharedLoader = createClassLoader("shared", commonLoader);
} catch (Throwable t) {
handleThrowable(t);
log.error("Class loader creation threw exception", t);
System.exit(1);
}
}
Tomcat 如果使用默認的類加載機制行不行?應用的不同lib如何隔離? 看這裏 https://blog.csdn.net/qq_38182963/article/details/78660779