今天在新環境里部署tomcat, 剛開始啟動很快,關閉之后再啟動,卻發現啟動日志打印到
00:25:14.144 [localhost-startStop-1] INFO o.s.web.context.ContextLoader – Root WebApplicationContext: initialization completed in 6287 ms
一直hold著,tomcat程序也無法訪問,以為是程序哪里配置錯了,找了半天,甚至把spring的配置加載完全去掉才能啟動,why, 程序在開發環境可是刷刷刷就跑起來的
后來一直沒管這程序過了幾分鐘去看日志,發現tomcat 程序才啟動完畢,why?原來不是卡住,而是慢
用jstack 觀察一下啟動線程, 發現 C2 CompilerThread 占用cpu很高, 同時 org.apache.catalina.util.SessionIdGeneratorBase.createSecureRandom這里讀文件也產生阻塞,占用CPU也很高。
一、問題描述
在發布或重啟某線上某服務時(jetty8作為服務器),常常發現有些機器的load會飆到非常高(高達70),并持續較長一段時間(5分鐘)后回落(圖1),與此同時響應時間曲線(圖2)也與load曲線一致。注:load飆高的初始時刻是應用服務端口打開,流量打入時(load)。
圖1 發布時候load飆高
圖2 發布時候響應時間飆高
二、問題排查方法
發布時對資源使用情況進行監控。
1)通過top -H -p 查找cpu使用率較高的線程,發現2129和2130這兩個線程cpu使用較高。
圖3 查找cpu使用率較高的線程
2)通過jstack打印棧信息,并將線程號2129和2130轉換成16進制(printf “%xn” 2129),分別為851和852,發現這兩個線程是編譯線程(表1)。此外當這兩個線程cpu使用率降低后load以及響應時間也馬上恢復了正常,時間點非常吻合。
表1 cpu使用率較高的兩個線程詳細信息
java.lang.Thread.State: RUNNABLE
Locked ownable synchronizers:
– None
“C2 CompilerThread0” daemon prio=10 tid=0x00007fce48123000 nid=0x851 waiting on condition [0x0000000000000000]
java.lang.Thread.State: RUNNABLE
Locked ownable synchronizers:
– None
三、現象解釋
C2 CompilerThread線程項目啟動初期cpu使用率那么高,它在干什么呢?
Java程序在啟動的時候所有代碼的執行都處于解釋執行模式,只有在運行了一段時間后,根據代碼方法執行的次數,或代碼里循環的執行次數等達到一定的閾值才會編譯成機器碼,編譯成機器碼后執行效率會得到大幅提升,而隨著執行時間進一步拉長,JVM的各種更高級的編譯優化手段就會逐漸加上,例如if條件的執行狀況,逃逸分析等。這里的C2 CompilerThread線程干的就是編譯優化的活。
現在貌似可以解釋之前的現象了。
在程序剛啟動的時候,java還處于解釋執行模式,因此服務效率很低,響應時間緩慢,處理得慢了,load自然也就高了。而當流量持續不斷導入時,我們代碼的很多方法執行次數不斷增多,此時C2 CompilerThread線程不斷收集優化信息,并且開始將一些熱點代碼優化編譯成本地機器碼,因此該線程的cpu使用率增高。而當C2 CompilerThread線程完成初始編譯優化過程后,C2 CompilerThread線程的cpu使用率開始下降,與此同時優化后服務的性能大幅提升,服務響應時間也大大縮短,load也下降。
現在的癥結在于編譯優化過程持續時間較長,引起抖動。如何降低編譯優化的持續時間呢?
四、解決思路
1)預熱
如果在服務接受線上請求之前提前完成編譯優化過程,那么將能避免此種抖動情況。一般的做法是預熱,有兩種方法:
a)程序主動預熱:在啟動完成后,程序主動的訪問熱點的代碼,確保主要的熱點代碼已被編譯成機器碼后再放入流量,可通過-XX:+PrintCompilation來確認。
b)復制流量預熱:通過tcpcopy軟件拷貝一份線上nginx的流量進行預熱,完成之后再導入線上流量。
2)啟動多個線程進行編譯優化
如果能加快編譯優化速度,那也能降低解釋執行階段導致的抖動時間。因此可以多拿幾個線程來做編譯,加快達到高峰性能的速度。
可以使用-XX:CICompilerCount參數來設置編譯線程數目,這個值默認是2(之前在棧里看到有兩個編譯線程),我們可以加到4。
3)采用多層編譯
編譯方式有三種:1)Client模式;2)Server模式;3)Tiered模式。我們服務默認是Server模式。
Server模式是采用c2高級編譯的,會比較耗時且要運行一段時間才會觸發編譯。 Server模式的優點是編譯后程序效率較高;
Client模式比較輕量也比較快觸發(比Server模式觸發快),編譯優化后程序效率不如Server模式;
Tiered模式是Client模式和Server模式的折中,一開始會啟用Client模式,可以在啟動后更快的讓部分代碼先進入編譯優化階段,之后會啟動Server模式,達到程序效率最大優化的目的。
Oracle JDK 7里的HotSpot VM已經開始有比較好的Tiered編譯(tiered compilation)支持,可以設置參數-XX:+TieredCompilation來啟動Tiered模式,java 8默認就是Tiered模式。
圖4是截取的不同編譯方式的性能比較圖,橫坐標是時間,縱坐標是性能??梢钥闯鯰ired模式開始階段性能與C1相當,當到達某一時刻后性能與C2相當。
圖4 不同編譯模式的性能比較
五、結果分析
簡單起見采用方案2和方案3來進行優化。
采用方案2和3之后進行了多次發布,發布時除個別機器load達到10之外,基本沒有過高現象(在2~4范圍內),并且短時間(2分鐘)內,load都會降到較合理水平(2左右),較發布時的load來看,比優化前要好很多。
方案2和方案3只是降低了抖動持續的時間以及抖動強度,并不能完全避免抖動。真正能避免抖動的方案應該是方案1,通過預熱的方式實現平滑發布或重啟。
tomcat啟動時SessionIdGeneratorBase.createSecureRandom耗時5分鐘的問題
通常情況下,tomcat啟動只要2~3秒鐘,突然有一天,tomcat啟動非常慢,要花5~6分鐘,查了很久,終于在這篇文章找到了解決方案,博主牛人啊。
Tomcat 8啟動很慢,且日志上無任何錯誤,在日志中查看到如下信息:
Log4j:[2015-10-29 15:47:11] INFO ReadProperty:172 – Loading properties file from class path resource [resources/jdbc.properties]
Log4j:[2015-10-29 15:47:11] INFO ReadProperty:172 – Loading properties file from class path resource [resources/common.properties]
29-Oct-2015 15:52:53.587 INFO [localhost-startStop-1] org.apache.catalina.util.SessionIdGeneratorBase.createSecureRandom Creation of SecureRandom instance for session ID generation using [SHA1PRNG] took [342,445] milliseconds.
原因
Tomcat 7/8都使用org.apache.catalina.util.SessionIdGeneratorBase.createSecureRandom類產生安全隨機類SecureRandom的實例作為會話ID,這里花去了342秒,也即接近6分鐘。
SHA1PRNG算法是基于SHA-1算法實現且保密性較強的偽隨機數生成器。
在SHA1PRNG中,有一個種子產生器,它根據配置執行各種操作。
1)如果Java.security.egd 屬性或securerandom.source屬性指定的是”file:/dev/random”或”file:/dev/urandom”,那么JVM 會使用本地種子產生器NativeSeedGenerator,它會調用super()方法,即調用 SeedGenerator.URLSeedGenerator(/dev/random)方法進行初始化。
2)如果java.security.egd屬性或securerandom.source屬性指定的是其它已存在的URL,那么會調用SeedGenerator.URLSeedGenerator(url)方法進行初始化。
這就是為什么我們設置值為”file:///dev/urandom”或者值為”file:/./dev/random”都會起作用的原因。
在這個實現中,產生器會評估熵池(entropy pool)中的噪聲數量。隨機數是從熵池中進行創建的。當讀操作時,/dev/random設備會只返回熵池中噪聲的隨機字節。/dev/random非 常適合那些需要非常高質量隨機性的場景,比如一次性的支付或生成密鑰的場景。
當熵池為空時,來自/dev/random的讀操作將被阻塞,直到熵池收集到足夠的環境噪聲數據。這么做的目的是成為一個密碼安全的偽隨機數發生器,熵池要有盡可能大的輸出。對于生成高質量的加密密鑰或者是需要長期保護的場景,一定要這么做。
那么什么是環境噪聲?
隨機數產生器會手??來自設備驅動器和其它源的環境噪聲數據,并放入熵池中。產生器會評估熵池中的噪聲數據的數量。當熵池為空時,這個噪聲數據的收集是比較花時間的。這就意味著,Tomcat在生產環境中使用熵池時,會被阻塞較長的時間。
解決
有兩種解決辦法:
1)在Tomcat環境中解決
可以通過配置JRE使用非阻塞的Entropy Source。
在catalina.sh中加入這么一行:-Djava.security.egd=file:/dev/./urandom 即可。
加入后再啟動Tomcat,整個啟動耗時下降到Server startup in 2912 ms。
2)在JVM環境中解決
打開$JAVA_PATH/jre/lib/security/java.security這個文件,找到下面的內容:
securerandom.source=file:/dev/urandom
替換成
securerandom.source=file:/dev/./urandom
tomcat的缺省配置是不能長期穩定的運行的,也就是不適合生產環境,會出現死機的情況,讓他不斷的重啟。對于操作系統的優化來說,是盡可能的提高內存容量,提高cpu的頻率,保證文件系統的讀寫速率。
tomcat的優化主要有三方面,分為系統優化,tomcat自身優化,java虛擬機(jvm)調優,此處主要討論后兩種。
一、tomcat本身優化
1 工作方式選擇
為了提升性能,首先就要對代碼進行動靜分離,讓 Tomcat 只負責 jsp 文件的解析工作。如采用 Apache 和 Tomcat 的整合方式,他們之間的連接方案有三種選擇,JK、http_proxy 和 ajp_proxy。相對于 JK 的連接方式,后兩種在配置上比較簡單的,靈活性方面也一點都不遜色。但就穩定性而言不像JK 這樣久經考驗,所以建議采用 JK 的連接方式。
2 connector連接器的工作方式
Tomcat 連接器的三種方式: bio、nio 和 apr,三種方式性能差別很大,apr 的性能最優, bio 的性能最差。而 Tomcat 7 使用的 Connector 默認就啟用的 Apr 協議,但需要系統安裝 Apr 庫,否則就會使用 bio 方式。
3配置文件優化
(1) 線程池
tomcat為每個connector綁定一個線程池(默認最大線程數為200)。
配置方式如下:
<Executor name=”tomcatThreadPool” namePrefix=”catalina-exec-“
maxThreads=”500″ minSpareThreads=”20″ maxSpareThreads=”50″ maxIdleTime=”60000″/>
<Connector executor=”tomcatThreadPool”
port=”8080″ protocol=”HTTP/1.1″
URIEncoding=”UTF-8″
connectionTimeout=”30000″
enableLookups=”false”
disableUploadTimeout=”false”
connectionUploadTimeout=”150000″
acceptCount=”300″
keepAliveTimeout=”120000″
maxKeepAliveRequests=”1″
compression=”on”
compressionMinSize=”2048″
compressableMimeType=”text/html,text/xml,text/javascript,text/css,text/plain,image/gif,image/jpg,image/png”
redirectPort=”8443″ />
maxThreads:tomcat使用線程來處理請求,這個值表示tomcat可以創建的最大線程數,默認值為200。
minSpareThreads:最小空閑線程數,tomcat啟動時的初始化線程數,表示即便沒有請求,也要開啟這么多的線程等待,默認值是10。
maxSpareThreads:最大空閑線程數,一旦空閑的線程數超過這個值,tomcat就會關閉不在需要的socket線程。
maxThreads的值越大就會越消耗內存和CPU,因為CPU疲于處理線程上下文切換,就沒有精力處理請求了。具體的值要取決于系統參數及實際應用場景。線程池可以配置在tomcatTheadPool中,也可以直接配置在connector中,但不可以重復配置。
(2)URIEncoding:指定 Tomcat 容器的 URL 編碼格式,語言編碼格式這塊倒不如其它 WEB 服務器軟件配置方便,需要分別指定。
(3)connnectionTimeout: 網絡連接超時,單位:毫秒,設置為 0 表示永不超時,這樣設置有隱患的。通??稍O置為 30000 毫秒,可根據檢測實際情況,適當修改。
(4)enableLookups: 是否反查域名,以返回遠程主機的主機名,取值為:true 或 false,如果設置為false,則直接返回IP地址,為了提高處理能力,應設置為 false。
(5)disableUploadTimeout:上傳時是否使用超時機制。
(6)connectionUploadTimeout:上傳超時時間,畢竟文件上傳可能需要消耗更多的時間,這個根據你自己的業務需要自己調,以使Servlet有較長的時間來完成它的執行,需要與上一個參數一起配合使用才會生效。
(7)acceptCount:指定當所有可以使用的處理請求的線程數都被使用時,可傳入連接請求的最大隊列長度,超過這個數的請求將不予處理,默認為100個。
(8)keepAliveTimeout:長連接最大保持時間(毫秒),表示在下次請求過來之前,Tomcat 保持該連接多久,默認是使用 connectionTimeout 時間,-1 為不限制超時。
(9)maxKeepAliveRequests:表示在服務器關閉之前,該連接最大支持的請求數。超過該請求數的連接也將被關閉,1表示禁用,-1表示不限制個數,默認100個,一般設置在100~200之間。
(10)compression:是否對響應的數據進行 GZIP 壓縮,off:表示禁止壓縮;on:表示允許壓縮(文本將被壓縮)、force:表示所有情況下都進行壓縮,默認值為off,壓縮數據后可以有效的減少頁面的大小,一般可以減小1/3左右,節省帶寬。
(11)compressionMinSize:表示壓縮響應的最小值,只有當響應報文大小大于這個值的時候才會對報文進行壓縮,如果開啟了壓縮功能,默認值就是2048。
(12)compressableMimeType:壓縮類型,指定對哪些類型的文件進行數據壓縮。
(13)noCompressionUserAgents=”gozilla, traviata”: 對于以下的瀏覽器,不啟用壓縮。
如果已經對代碼進行了動靜分離,靜態頁面和圖片等數據就不需要 Tomcat 處理了,那么也就不需要配置在 Tomcat 中配置壓縮了。
以上是一些常用的配置參數屬性,當然還有好多其它的參數設置,還可以繼續深入的優化,HTTP Connector 與 AJP Connector 的參數屬性值,可以參考官方文檔的詳細說明:
https://tomcat.apache.org/tomcat-7.0-doc/config/http.html
https://tomcat.apache.org/tomcat-7.0-doc/config/ajp.html
二、JVM優化
JVM常見配置
堆設置
-Xms:初始堆大小
-Xmx:最大堆大小
-XX:NewSize=n:設置年輕代大小
-XX:NewRatio=n:設置年輕代和年老代的比值。如:為3,表示年輕代與年老代比值為1:3,年輕代占整個年輕代年老代和的1/4
-XX:SurvivorRatio=n:年輕代中Eden區與兩個Survivor區的比值。注意Survivor區有兩個。如:3,表示Eden:Survivor=3:2,一個Survivor區占整個年輕代的1/5
-XX:MaxPermSize=n:設置持久代大小
收集器設置
-XX:+UseSerialGC:設置串行收集器
-XX:+UseParallelGC:設置并行收集器
-XX:+UseParalledlOldGC:設置并行年老代收集器
-XX:+UseConcMarkSweepGC:設置并發收集器
垃圾回收統計信息
-XX:+PrintGC
-XX:+PrintGCDetails
-XX:+PrintGCTimeStamps
-Xloggc:filename
并行收集器設置
-XX:ParallelGCThreads=n:設置并行收集器收集時使用的CPU數。并行收集線程數。
-XX:MaxGCPauseMillis=n:設置并行收集最大暫停時間
-XX:GCTimeRatio=n:設置垃圾回收時間占程序運行時間的百分比。公式為1/(1+n)
并發收集器設置
-XX:+CMSIncrementalMode:設置為增量模式。適用于單CPU情況。
-XX:ParallelGCThreads=n:設置并發收集器年輕代收集方式為并行收集時,使用的CPU數。并行收集線程數。
常見的內存溢出有以下兩種:
Java.lang.OutOfMemoryError: PermGen space
java.lang.OutOfMemoryError: Java heap space
這里以tomcat環境為例
一、java.lang.OutOfMemoryError: PermGen space
PermGen space的全稱是Permanent Generation space,是指內存的永久保存區域,
這塊內存主要是被JVM存放Class和Meta信息的,Class在被Loader時就會被放到PermGen space中,它和存放類實例(Instance)的Heap區域不同,GC(Garbage Collection)不會在主程序運行期對PermGen space進行清理,所以如果你的應用中有很多CLASS的話,就很可能出現PermGen space錯誤,這種錯誤常見在web服務器對JSP進行pre compile的時候。如果你的WEB APP下都用了大量的第三方jar, 其大小超過了jvm默認的大小(4M)那么就會產生此錯誤信息了。
解決方法: 手動設置MaxPermSize大小
建議:將相同的第三方jar文件移置到tomcat/shared/lib目錄下,這樣可以達到減少jar 文檔重復占用內存的目的。
二、java.lang.OutOfMemoryError: Java heap space
JVM堆的設置是指java程序運行過程中JVM可以調配使用的內存空間的設置.JVM在啟動的時候會自動設置Heap size的值,其初始空間(即-Xms)是物理內存的1/64,最大空間(-Xmx)是物理內存的1/4??梢岳肑VM提供的-Xmn -Xms -Xmx等選項可進行設置。Heap size 的大小是Young Generation 和Tenured Generaion 之和。
提示:在JVM中如果98%的時間是用于GC且可用的Heap size 不足2%的時候將拋出此異常信息。
提示:Heap Size 最大不要超過可用物理內存的80%,一般的要將-Xms和-Xmx選項設置為相同,而-Xmn為1/4的-Xmx值。
解決方法:手動設置Heap size
Linux下修改JVM內存大小:
要添加在tomcat 的bin 下catalina.sh 里,位置cygwin=false前 。
# OS specific support. $var _must_ be set to either true or false.
JAVA_OPTS=”-Xms256m -Xmx512m -Xss1024K -XX:PermSize=128m -XX:MaxPermSize=256m”
cygwin=false
Windows下修改JVM內存大小:
情況一:解壓版本的Tomcat, 要通過startup.bat啟動tomcat才能加載配置
要添加在tomcat 的bin 下catalina.bat 里
rem Guess CATALINA_HOME if not defined
set CURRENT_DIR=%cd%后面添加,紅色的為新添加的.
set JAVA_OPTS=-Xms256m -Xmx512m -XX:PermSize=128M -XX:MaxNewSize=256m -XX:MaxPermSize=256m -Djava.awt.headless=true
情況二:安裝版的Tomcat下沒有catalina.bat
windows服務執行的是bin/tomcat.exe.他讀取注冊表中的值,而不是catalina.bat的設置.
修改注冊表HKEY_LOCAL_MACHINE/SOFTWARE/Apache Software Foundation/Tomcat Service Manager/Tomcat5/Parameters/JavaOptions
原值為
-Dcatalina.home=”C:/ApacheGroup/Tomcat 5.0″
-Djava.endorsed.dirs=”C:/ApacheGroup/Tomcat 5.0/common/endorsed”
-Xrs
加入 -Xms300m -Xmx350m
重起tomcat服務,設置生效
如何設置Tomcat的JVM虛擬機內存大小
可以給Java虛擬機設置使用的內存,但是如果你的選擇不對的話,虛擬機不會補償。可通過命令行的方式改變虛擬機使用內存的大小。如下表所示有兩個參數用來設置虛擬機使用內存的大小。
-Xms
JVM初始化堆的大小
-Xmx
JVM堆的最大值
這兩個值的大小一般根據需要進行設置。初始化堆的大小執行了虛擬機在啟動時向系統申請的內存的大小。一般而言,這個參數不重要。但是有的應用程序在大負載的 情況下會急劇地占用更多的內存,此時這個參數就是顯得非常重要,如果虛擬機啟動時設置使用的內存比較小而在這種情況下有許多對象進行初始化,虛擬機就必須 重復地增加內存來滿足使用。由于這種原因,我們一般把-Xms和-Xmx設為一樣大,而堆的最大值受限于系統使用的物理內存。一般使用數據量較大的應用程 序會使用持久對象,內存使用有可能迅速地增長。當應用程序需要的內存超出堆的最大值時虛擬機就會提示內存溢出,并且導致應用服務崩潰。因此一般建議堆的最大值設置為可用內存的最大值的80%。
Tomcat默認可以使用的內存為128MB,在較大型的應用項目中,這點內存是不夠的,需要調大。
Windows下,在文件/bin/catalina.bat,Unix下,在文件/bin/catalina.sh的前面,增加如下設置:
JAVA_OPTS=’-Xms【初始化內存大小】 -Xmx【可以使用的最大內存】’
需要把這個兩個參數值調大。例如:
JAVA_OPTS=’-Xms256m -Xmx512m’
表示初始化內存為256MB,可以使用的最大內存為512MB。
另 外需要考慮的是Java提供的垃圾回收機制。虛擬機的堆大小決定了虛擬機花費在收集垃圾上的時間和頻度。收集垃圾可以接受的速度與應用有關,應該通過分析 實際的垃圾收集的時間和頻率來調整。如果堆的大小很大,那么完全垃圾收集就會很慢,但是頻度會降低。如果你把堆的大小和內存的需要一致,完全收集就很快, 但是會更加頻繁。調整堆大小的的目的是最小化垃圾收集的時間,以在特定的時間內最大化處理客戶的請求。在基準測試的時候,為保證最好的性能,要把堆的大小 設大,保證垃圾收集不在整個基準測試的過程中出現。
如果系統花費很多的時間收集垃圾,請減小堆大小。一次完全的垃圾收集應該不超過 3-5 秒。如果垃圾收集成為瓶頸,那么需要指定代的大小,檢查垃圾收集的詳細輸出,研究 垃圾收集參數對性能的影響。一般說來,你應該使用物理內存的 80% 作為堆大小。當增加處理器時,記得增加內存,因為分配可以并行進行,而垃圾收集不是并行的。