1. 程式人生 > >JVM高手之路七(tomcat調優以及tomcat7、8效能對比)

JVM高手之路七(tomcat調優以及tomcat7、8效能對比)

 

   

因為每個鏈路都會對其效能造成影響,應該是全鏈路的修改壓測(ak大神經常說全鏈路大笑)。本次基本就是區域網,所以並沒有怎麼優化,其實也應該考慮進去的。

 

Linux系統引數層面的修改:

1、修改可開啟檔案數和使用者最多可開發程序數

命令:ulimit -n 655350

      ulimit –u 655350

可以通過ulimit –a檢視引數設定,不設定時預設為1024,預設情況下,你會發現請求數到到一定數值後,再也上不去了。

2、作業系統核心優化

net.ipv4.tcp_max_tw_buckets = 6000

timewait 的數量,預設是180000。

net.ipv4.ip_local_port_range = 1024 65000

允許系統開啟的埠範圍。

net.ipv4.tcp_tw_recycle = 1

啟用timewait 快速回收。

net.ipv4.tcp_tw_reuse = 1

開啟重用。允許將TIME-WAIT sockets 重新用於新的TCP 連線。

net.ipv4.tcp_syncookies = 1

開啟SYN Cookies,當出現SYN等待佇列溢位時,啟用cookies來處理。

net.core.somaxconn = 262144

web 應用中listen函式的backlog預設會給我們核心引數的net.core.somaxconn限制到128,而nginx定義的NGX_LISTEN_BACKLOG預設為511,所以有必要調整這個值。

net.core.netdev_max_backlog = 262144

每個網路介面接收資料包的速率比核心處理這些包的速率快時,允許送到佇列的資料包的最大數目。

net.ipv4.tcp_max_orphans = 262144

系統中最多有多少個TCP套接字不被關聯到任何一個使用者檔案控制代碼上。如果超過這個數字,故而連線將即刻被複位並打印出警告資訊。這個限制僅僅是為了防止簡單的DoS攻擊,不能過分依靠它或者人為地減小這個值,更應該增加這個值(如果增加了記憶體之後)。

net.ipv4.tcp_max_syn_backlog = 262144

記錄的那些尚未收到客戶端確認資訊的連線請求的最大值。對於有128M記憶體的系統而言,預設值是1024,小記憶體的系統則是128。

net.ipv4.tcp_timestamps = 0

時間戳可以避免序列號的卷繞。一個1Gbps的鏈路肯定會遇到以前用過的序列號。時間戳能夠讓核心接受這種“異常”的資料包。這裡需要將其關掉。

net.ipv4.tcp_synack_retries = 1

為了開啟對端的連線,核心需要傳送一個SYN 並附帶一個迴應前面一個SYN的ACK。也就是所謂三次握手中的第二次握手。這個設定決定了核心放棄連線之前傳送SYN+ACK包的數量。

net.ipv4.tcp_syn_retries = 1

在核心放棄建立連線之前傳送SYN 包的數量。

net.ipv4.tcp_fin_timeout = 1

如果套接字由本端要求關閉,這個引數決定了它保持在FIN-WAIT-2狀態的時間。對端可以出錯並永遠不關閉連線,甚至意外當機。預設值是60秒。2.2核心的通常值是180秒,3你可以按這個設定,但要記住的是,即使你的機器是一個輕載的WEB伺服器,也有因為大量的死套接字而記憶體溢位的風險,FIN-WAIT-2的危險性比FIN-WAIT-1要小,因為它最多隻能吃掉1.5K記憶體,但是它們的生存期長些。

net.ipv4.tcp_keepalive_time = 30

當keepalive 起用的時候,TCP傳送keepalive訊息的頻度。預設是2小時

核心引數優化設定在/etc/sysctl.conf檔案中。

 

上面2個都調整一樣的情況下,開始準備測試tomcat7(jdk7)與tomcat8(jdk8)的一些效能測試了。

由於各各原因複雜服務沒法測試,先僅僅是測試靜態頁面。

 

jvm層面優化:

Jdk7:

-Xms2G -Xmx2G -Xmn512m -XX:PermSize=512M -XX:MaxPermSize=512M -XX:+UseConcMarkSweepGC -XX:+CMSClassUnloadingEnabled -XX:+HeapDumpOnOutOfMemoryError -verbose:gc -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintGCDateStamps -Xloggc:/appl/gc.log -XX:CMSInitiatingOccupancyFraction=75 -XX:+UseCMSInitiatingOccupancyOnly

 

Jdk8:

-Xms2G -Xmx2G -Xmn512m -XX:MetaspaceSize=512M -XX:MaxMetaspaceSize=512M -XX:+UseConcMarkSweepGC -XX:+CMSClassUnloadingEnabled -XX:+HeapDumpOnOutOfMemoryError -verbose:gc -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintGCDateStamps -Xloggc:/appl/gc.log -XX:CMSInitiatingOccupancyFraction=75 -XX:+UseCMSInitiatingOccupancyOnly 

需要特別說明下:

元資料空間,專門用來存元資料的,它是jdk8裡特有的資料結構用來替代perm。

 

Jdk7:

出現了多次Full GC了。

其中,CMS-initial-mark和CMS-remark會stop-the-world。




 

所以選擇cms垃圾回收器,用jstat 相關命令看到的FGC每次都是加2的變化情況。

Jdk8:

一次Full GC也沒有發生。從這裡也可以看出tomcat8的實現機制比tomcat7的要好些(相同條件沒有產生多餘物件從而導致Full GC問題)。

 

需要特別說明下:

年輕代的gc日誌7和8略有不同

jdk8把日誌打得更全了 ,jdk8的gc日誌與jdk7的有所不同,聽大佬們說各各jdk的日誌都有所不同,其實這裡8的這個和7的意思一樣,只是7沒有表達出來而已。

 

通過壓力測試結果來看,jdk7每隔一段時間會出現tps大的下降,就是俗話說的卡頓。

而jdk8沒有啥卡頓現象


 

而jdk7的波動就特別明顯


 

該效果8比7的效果請求要好。

 

由於jdk7 gc日誌,


 

CMS開始回收tenured generation collection。這階段是CMS初始化標記的階段,從垃圾回收的“根物件”開始,且只掃描直接與“根物件”直接關聯的物件,並做標記,在此期間,其他執行緒都會停止。

 

tenured generation的空間是1572864K,在容量為1205558K時開始執行初始標記。

說明-XX:CMSInitiatingOccupancyFraction=75已經達到觸發(Background )CMS GC的條件。

 

應該擴大堆空間大小,在此修改僅僅是修改了堆其他不變,其他引數還是原來上面的引數

Jdk7,jdk8:

-Xms4G -Xmx4G -Xmn1365m

檢視gc日誌,發現的確都沒有FGC了,但是ygc差距很大,在此表示tomcat8(jdk8)比tomcat7(jdk7)好好像。

Jdk7 ygc時間過長:


 

Jdk8 ygc非常好:


 

其實對於ygc的分享特別複雜,jvm的引數調整算是小調,最關鍵的應該在產生物件的地方,即應用本事,採用合理的架構,合理的資料結構結合一些技巧來達到等。

由於測試的是靜態頁面,那麼只有tomcat程式碼了,表示8的實現比7的實現方面的確要好(有空去準備去讀讀tomcat原始碼到時候在分享分享)。

 

通過日誌檢視jdk7的老年代使用率很低,準備在此進行調整,在堆大小不變的情況下調整年輕代的大小。

Jdk7,jdk8都進行調整其他引數還保持上面不變。

-Xms4G -Xmx4G -Xmn3g 

效果有所改善(tps也張了200多),但是還是不如jdk8的,可能是tomcat內部實現8就是比7好。

次中間還嘗試過更大堆以及年輕代的調整 如6G 8G 10G等都沒有太大變化有些還不如4G點這個好,所以並不是堆空間設定越大越好。

Jvm目前只能調到這塊了,後續如果有啥發現或者大佬們的建議在調整。

 

 

Tomcat本身這塊的調優

 

Tomcat 7/8 的優化引數有點不一樣,最好按下面的方式看一下官網這個文件是否還保留著這個引數

啟動tomcat,訪問該地址,下面要講解的一些配置資訊,在該文件下都有說明的:

文件:http://127.0.0.1:8080/docs/config

你也可以直接看網路版本:

Tomcat 7 文件:https://tomcat.apache.org/tomcat-7.0-doc/config/

Tomcat 8 文件:https://tomcat.apache.org/tomcat-8.0-doc/config/

如果你需要檢視 Tomcat 的執行狀態可以配置tomcat管理員賬戶,然後登陸Tomcat後臺進行檢視。

 

 

在修改jvm引數之後tps怎麼都上不去的情況下面通過檢視執行緒dump


 

 

 

 

Tomcat7、tomcat8情況一樣,發現很多都堵塞在這塊了。

這塊涉及到程式碼這塊了(tomcat的原始碼這塊了)

通過檢視原始碼發現這塊是涉及到了tomcat執行緒池這塊了,稍微會詳細說明下,先看看一些簡單配置。


 

預設配置,可以配置寫那些值呢?


 

Tomcat8多了一個nio2


 

這個也比較重要


 

這個就是關於池的配置了,有那些引數和怎麼實現的呢?


 

Tomcat的實現在org.apache.catalina.core.StandardThreadExecutor

裡面的引數有


 

 

簡單理解就是:

maxThreads - Tomcat執行緒池最多能起的執行緒數

maxConnections - Tomcat最多能併發處理的請求(連線)

acceptCount - Tomcat維護最大的對列數

minSpareThreads - Tomcat初始化的執行緒池大小或者說Tomcat執行緒池最少會有這麼多執行緒。

比較容易弄混的是maxThreads和maxConnections這兩個引數:

 

maxThreads是指Tomcat執行緒池做多能起的執行緒數

maxConnections則是Tomcat一瞬間做多能夠處理的併發連線數。比如maxThreads=1000,maxConnections=800,假設某一瞬間的併發時1000,那麼最終Tomcat的執行緒數將會是800,即同時處理800個請求,剩餘200進入佇列“排隊”,如果acceptCount=100,那麼有100個請求會被拒掉。

 

注意:根據前面所說,只是併發那一瞬間Tomcat會起800個執行緒處理請求,但是穩定後,某一瞬間可能只有很少的執行緒處於RUNNABLE狀態,大部分執行緒是TIMED_WAITING,如果你的應用處理時間夠快的話。所以真正決定Tomcat最大可能達到的執行緒數是maxConnections這個引數和併發數,當併發數超過這個引數則請求會排隊,這時響應的快慢就看你的程式效能了。

 

 

這些僅僅是告訴我們,如果需要了解細節還需要閱讀下原始碼。有些讀了原始碼可能引數的理解更清楚了。


 

  1.   public class StandardThreadExecutor extends LifecycleMBeanBase
  2.   implements Executor, ResizableExecutor {
  3.   //預設執行緒的優先順序
  4.   protected int threadPriority = Thread.NORM_PRIORITY;
  5.   //守護執行緒
  6.   protected boolean daemon = true;
  7.   //執行緒名稱的字首
  8.   protected String namePrefix = "tomcat-exec-";
  9.   //最大執行緒數預設200個
  10.   protected int maxThreads = 200;
  11.   //最小空閒執行緒25個
  12.   protected int minSpareThreads = 25;
  13.   //超時時間為6000
  14.   protected int maxIdleTime = 60000;
  15.   //執行緒池容器
  16.   protected ThreadPoolExecutor executor = null;
  17.   //執行緒池的名稱
  18.   protected String name;
  19.   //是否提前啟動執行緒
  20.   protected boolean prestartminSpareThreads = false;
  21.   //佇列最大大小
  22.   protected int maxQueueSize = Integer.MAX_VALUE;
  23.   //為了避免在上下文停止之後,所有的執行緒在同一時間段被更新,所以進行執行緒的延遲操作
  24.   protected long threadRenewalDelay = 1000L;
  25.   //任務佇列
  26.   private TaskQueue taskqueue = null;
  27.    
  28.   //容器啟動時進行,具體可參考org.apache.catalina.util.LifecycleBase#startInternal()
  29.   @Override
  30.   protected void startInternal() throws LifecycleException {
  31.   //例項化任務佇列
  32.   taskqueue = new TaskQueue(maxQueueSize);
  33.   //自定義的執行緒工廠類,實現了JDK的ThreadFactory介面
  34.   TaskThreadFactory tf = new TaskThreadFactory(namePrefix,daemon,getThreadPriority());
  35.   //這裡的ThreadPoolExecutor是tomcat自定義的,不是JDK的ThreadPoolExecutor
  36.   executor = new ThreadPoolExecutor(getMinSpareThreads(), getMaxThreads(), maxIdleTime, TimeUnit.MILLISECONDS,taskqueue, tf);
  37.   executor.setThreadRenewalDelay(threadRenewalDelay);
  38.   //是否提前啟動執行緒,如果為true,則提前初始化minSpareThreads個的執行緒,放入執行緒池內
  39.   if (prestartminSpareThreads) {
  40.   executor.prestartAllCoreThreads();
  41.   }
  42.   //設定任務容器的父級執行緒池物件
  43.   taskqueue.setParent(executor);
  44.   //設定容器啟動狀態
  45.   setState(LifecycleState.STARTING);
  46.   }
  47.    
  48.   //容器停止時的生命週期方法,進行關閉執行緒池和資源清理
  49.   @Override
  50.   protected void stopInternal() throws LifecycleException {
  51.    
  52.   setState(LifecycleState.STOPPING);
  53.   if ( executor != null ) executor.shutdownNow();
  54.   executor = null;
  55.   taskqueue = null;
  56.   }
  57.    
  58.   //這個執行執行緒方法有超時的操作,參考org.apache.catalina.Executor介面
  59.   @Override
  60.   public void execute(Runnable command, long timeout, TimeUnit unit) {
  61.   if ( executor != null ) {
  62.   executor.execute(command,timeout,unit);
  63.   } else {
  64.   throw new IllegalStateException("StandardThreadExecutor not started.");
  65.   }
  66.   }
  67.    
  68.   //JDK預設操作執行緒的方法,參考java.util.concurrent.Executor介面
  69.   @Override
  70.   public void execute(Runnable command) {
  71.   if ( executor != null ) {
  72.   try {
  73.   executor.execute(command);
  74.   } catch (RejectedExecutionException rx) {
  75.   //there could have been contention around the queue
  76.   if ( !( (TaskQueue) executor.getQueue()).force(command) ) throw new RejectedExecutionException("Work queue full.");
  77.   }
  78.   } else throw new IllegalStateException("StandardThreadPool not started.");
  79.   }
  80.    
  81.   //由於繼承了org.apache.tomcat.util.threads.ResizableExecutor介面,所以可以重新定義執行緒池的大小
  82.   @Override
  83.   public boolean resizePool(int corePoolSize, int maximumPoolSize) {
  84.   if (executor == null)
  85.   return false;
  86.    
  87.   executor.setCorePoolSize(corePoolSize);
  88.   executor.setMaximumPoolSize(maximumPoolSize);
  89.   return true;
  90.   }
  91.   }

 

 

Tomcat的執行緒池的名字也叫作ThreadPoolExecutor,剛開始看原始碼的時候還以為是使用了JDK的ThreadPoolExecutor了呢,後面仔細檢視才知道是Tomcat自己實現的一個ThreadPoolExecutor,不過基本上都差不多。

 

看到這裡以為tomcat執行緒池的原理和jdk的執行緒池原理一樣了,其實不是的。

問題的關鍵在這裡


 

TaskQueue這個任務佇列是專門為執行緒池而設計的。優化任務佇列以適當地利用執行緒池執行器內的執行緒。

Jdk的execute執行策略:        優先offer到queue,queue滿後再擴充執行緒到maxThread,如果已經到了maxThread就reject

Tomcat的execute執行策略: 優先擴充執行緒到maxThread,再offer到queue,如果滿了就reject比較適合於業務處理需要遠端資源的場景


 

修改為:

 

  1.   <Executor name= "tomcatThreadPool" namePrefix="catalina-exec-"
  2.   maxThreads= "350" minSpareThreads="20" prestartminSpareThreads="true"/>


 

 

  1.   <Connector executor= "tomcatThreadPool" acceptCount="300000"
  2.   port= "8080" protocol="HTTP/1.1"
  3.   connectionTimeout= "20000"
  4.   redirectPort= "8443" />


由於可能是靜態頁面返回很快,設定500 800 1000執行緒效果都不怎麼明顯,如果是加專案應該會有所區別,所以執行緒池也並不是越多越好。

Tomcat7和tomcat8效能都有所提升,所以池很重要,但是8和7的tps在都提高了2000左右。在修改執行緒池之後,檢視jvm gc情況都良好,所以並沒有在此調整jvm引數了。

經過這麼多分析也瞭解到了tomcat該如何調優了,以及tomcat7、tomcat8的一些效能區別了。

由於測試的是靜態頁面,很多有些問題還沒有涉及到,後續如果測試服務估計需要修改,除錯排查的問題更多,到時候繼續檢視後續文章!!