本文的背景是一個用java開發的微信公衆號服務端的業務應用,使用的java開發包是weixin-java-tools。該系統的部署結構式nginx+10個tomcat實例的集羣。 html
上線一段時間後,業務運營人員在微信公衆號上作了幾個活動,系統的訪問量增長了一些。就陸陸續續暴露了一些問題,而這些問題的形成的危害還很是大,其中有2個tomcat實例運行一段時間後就會沒法提供服務了。下面就詳細介紹這個問題。 java
某天咱們的程序員小馬常常接到幾個短信報警說是2臺tomcat實例沒法提供服務了,他就只能重啓服務器,可是過幾十分鐘後,又會出現這樣的問題,他只能痛苦得一遍一遍得重啓tomcat服務器,最終實在是鬱悶就找到我幫他一塊兒看看究竟是什麼緣由。 nginx
我通過查看監控後,查看到了這樣的異常現象。 git
說明一下:上圖中的tomcat的線程最大數配置的是1000,所以這個tomcat已經達到了最大線程數(其中多餘的線程是jvm自啓動的一些線程以及應用程序其它的代碼啓動的一些線程)。而圖中出現的拐點是由於小馬哥重啓了tomcat,可是過段時間又會逐步上升。 程序員
查看其它的正常的tomcat線程比較穩定,它們的線程數都在一個穩定狀態,而這些tomcat是負載均衡的狀態,它們的訪問量應該是差很少的,所以這2個tomcat的線程如此之多,不是由於訪問量過高,確定還有其它的願意,所以使用jstack將線程棧導出來,發現有大量的BLOCKED和WAITING狀態的線程。 github
"http-1601-1000" daemon prio=10 tid=0x00007fb6709b1000 nid=0x673d waiting for monitor entry [0x00007fb604b0b000] java.lang.Thread.State: BLOCKED (on object monitor) at me.chanjar.weixin.mp.api.WxMpServiceImpl.getJsapiTicket(WxMpServiceImpl.java:136) - waiting to lock <0x00000007402d9a28> (a java.lang.Object) at com.jd.ql.cun.web.controller.CommonController.getSignature(CommonController.java:63) at sun.reflect.GeneratedMethodAccessor260.invoke(Unknown Source) at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25) at java.lang.reflect.Method.invoke(Method.java:597) at org.springframework.web.method.support.InvocableHandlerMethod.invoke(InvocableHandlerMethod.java:212) at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:126)
"http-1601-381" daemon prio=10 tid=0x00007f1fe827f800 nid=0x27f5 waiting on condition [0x00007f1fa03c1000] java.lang.Thread.State: WAITING (parking) at sun.misc.Unsafe.park(Native Method) - parking to wait for <0x00000007f9843b10> (a java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject) at java.util.concurrent.locks.LockSupport.park(LockSupport.java:158) at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await(AbstractQueuedSynchronizer.java:1987) at org.apache.http.pool.PoolEntryFuture.await(PoolEntryFuture.java:133) at org.apache.http.pool.AbstractConnPool.getPoolEntryBlocking(AbstractConnPool.java:282) at org.apache.http.pool.AbstractConnPool.access$000(AbstractConnPool.java:64) at org.apache.http.pool.AbstractConnPool$2.getPoolEntry(AbstractConnPool.java:177) at org.apache.http.pool.AbstractConnPool$2.getPoolEntry(AbstractConnPool.java:170) at org.apache.http.pool.PoolEntryFuture.get(PoolEntryFuture.java:102) at org.apache.http.impl.conn.PoolingHttpClientConnectionManager.leaseConnection(PoolingHttpClientConnectionManager.java:244) at org.apache.http.impl.conn.PoolingHttpClientConnectionManager$1.get(PoolingHttpClientConnectionManager.java:231) at org.apache.http.impl.execchain.MainClientExec.execute(MainClientExec.java:173) at org.apache.http.impl.execchain.ProtocolExec.execute(ProtocolExec.java:195) at org.apache.http.impl.execchain.RetryExec.execute(RetryExec.java:86) at org.apache.http.impl.execchain.RedirectExec.execute(RedirectExec.java:108) at org.apache.http.impl.client.InternalHttpClient.doExecute(InternalHttpClient.java:184) at org.apache.http.impl.client.CloseableHttpClient.execute(CloseableHttpClient.java:82) at org.apache.http.impl.client.CloseableHttpClient.execute(CloseableHttpClient.java:106) at me.chanjar.weixin.common.util.http.SimpleGetRequestExecutor.execute(SimpleGetRequestExecutor.java:36) at me.chanjar.weixin.common.util.http.SimpleGetRequestExecutor.execute(SimpleGetRequestExecutor.java:20) at com.jd.ql.cun.web.wx4jsdk.JdWxTestSupportMpServiceImpl.oauth2getAccessTokenExtension(JdWxTestSupportMpServiceImpl.java:91) at com.jd.ql.cun.web.controller.WeixinSecurityController.getOpenId(WeixinSecurityController.java:111)
public String getJsapiTicket(boolean forceRefresh) throws WxErrorException { if (forceRefresh) { wxMpConfigStorage.expireJsapiTicket(); } if (wxMpConfigStorage.isJsapiTicketExpired()) { synchronized (globalJsapiTicketRefreshLock) { if (wxMpConfigStorage.isJsapiTicketExpired()) { String url = "https://api.weixin.qq.com/cgi-bin/ticket/getticket?type=jsapi"; String responseContent = execute(new SimpleGetRequestExecutor(), url, null); JsonElement tmpJsonElement = Streams.parse(new JsonReader(new StringReader(responseContent))); JsonObject tmpJsonObject = tmpJsonElement.getAsJsonObject(); String jsapiTicket = tmpJsonObject.get("ticket").getAsString(); int expiresInSeconds = tmpJsonObject.get("expires_in").getAsInt(); wxMpConfigStorage.updateJsapiTicket(jsapiTicket, expiresInSeconds); } } } return wxMpConfigStorage.getJsapiTicket(); }
在代碼「synchronized (globalJsapiTicketRefreshLock) {「處使用了synchronized 同步鎖,對全局共享對象globalJsapiTicketRefreshLock進行了加鎖操做,主要是防止多個線程同時對jsapiTicket進行更新操做。 web
既然大量的線程阻塞在該處,那說明有的線程在執行同步塊中的代碼很是慢,而其它的線程都在等待該線程釋放鎖,所以愈來愈多的線程都阻塞該處。問題就出在該代碼處。繼續分析該處代碼發現了一個比較嚴重的坑,描述以下: redis
從新實現accessToken和jsApiTicket存儲方案,將其存儲在共享的redis服務上。 spring
修改上線後,BLOCKED線程消失了,可是依舊有不少WAITING狀態的線程,所以繼續分析該狀態的代碼。 apache
分析線程棧中的代碼」at org.apache.http.pool.PoolEntryFuture.await(PoolEntryFuture.java:133)」通過查看源碼發現是由於調用微信api使用了java的組件httpclient,如本文中項目使用的是httpclient4.3.5。
而httpclient爲了複用http鏈接,使用了鏈接池技術,該處的等待線程就是在等待從鏈接池中得到鏈接,那有多是鏈接池中鏈接不夠,或者某些線程佔用鏈接時間過長致使的。所以繼續查看代碼和查找相關httpClient鏈接配置文檔得出以下結論:
本項目中的httpclient的鏈接配置所有使用默認配置。使用HttpClients.createDefault();建立默認的httpclient對象,所有使用默認值。
httpclient鏈接的配置,參考了張開濤的博客:http://jinnianshilongnian.iteye.com/blog/2089792
maxConnTotal和maxConnPerRoute
maxConnTotal是鏈接池總的最大鏈接數,用的是默認值20.
maxConnPerRoute是每一個路由最大鏈接數,本項目都是鏈接微信服務器,所以就是默認爲2的值,而這對於生產環境併發較高確實不合適。
httpclient的請求配置都沒有配置,使用默認配置信息。
this.connectionRequestTimeout = -1;
this.connectTimeout = -1;
this.socketTimeout = -1;
都是使用的系統默認時間值,而這個值是一個比較大的值,對於生產環境來講是不合適的。
所以這些值對於生產環境來講均爲不合理的值,所以我根據本身的生產環境的實際狀況配置以下:
weixin.mp.httpclient.socketTimeout=2000 weixin.mp.httpclient.connectTimeout=2000 weixin.mp.httpclient.connectionRequestTimeout=500 weixin.mp.httpclient.maxConnPerRoute=300 weixin.mp.httpclient.maxConnTotal=300
平均耗時都要300毫秒。