java開發的微信公衆號服務端生產環境中的兩個大坑

背景

本文的背景是一個用java開發的微信公衆號服務端的業務應用,使用的java開發包是weixin-java-tools該系統的部署結構式nginx+10個tomcat實例的集羣。 html

上線一段時間後,業務運營人員在微信公衆號上作了幾個活動,系統的訪問量增長了一些。就陸陸續續暴露了一些問題,而這些問題的形成的危害還很是大,其中有2個tomcat實例運行一段時間後就會沒法提供服務了。下面就詳細介紹這個問題。 java

問題描述

某天咱們的程序員小馬常常接到幾個短信報警說是2臺tomcat實例沒法提供服務了,他就只能重啓服務器,可是過幾十分鐘後,又會出現這樣的問題,他只能痛苦得一遍一遍得重啓tomcat服務器,最終實在是鬱悶就找到我幫他一塊兒看看究竟是什麼緣由。 nginx

查看jvm監控

我通過查看監控後,查看到了這樣的異常現象。 git

說明一下:上圖中的tomcat的線程最大數配置的是1000,所以這個tomcat已經達到了最大線程數(其中多餘的線程是jvm自啓動的一些線程以及應用程序其它的代碼啓動的一些線程)。而圖中出現的拐點是由於小馬哥重啓了tomcat,可是過段時間又會逐步上升。 程序員

查看線程棧列表

查看其它的正常的tomcat線程比較穩定,它們的線程數都在一個穩定狀態,而這些tomcat是負載均衡的狀態,它們的訪問量應該是差很少的,所以這2個tomcat的線程如此之多,不是由於訪問量過高,確定還有其它的願意,所以使用jstack將線程棧導出來,發現有大量的BLOCKED和WAITING狀態的線程。 github

BLOCKED狀態線程

"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)

WAITING狀態線程

"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)


問題分析及解決


BLOCKED狀態線程

根據線程中的信息找打鎖住行所在的源代碼,繼續追蹤該行的源代碼以下:
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


  • 在微信中調用api都須要accessToken,調用jsapi須要jsApiTicket。詳見http://mp.weixin.qq.com/wiki/2/88b2bf1265a707c031e51f26ca5e6512.html
  • accessToken的機制是每一個7200毫秒會過時,而且若從新獲取則上次獲取的會過時。
  • 本系統是在10個tomcat實例的集羣環境下面。
  • 本系統中的accessToken是存儲在內存中的,多個tomcat集羣的值沒法共享。
  • 多個tomcat集羣都會常常獲取,所以致使accessToken常常過時。
  • 獲取accessToken接口的調用次數有限制,每日2000次。
  • 若達到接口獲取上線,則沒法獲取accessToken,致使獲取accessToken始終失敗。
  • 代碼塊中有失敗重試默認3次的機制,並且每次衝時候會暫停線程1秒,且暫停時間每次增長一倍。
  • 所以會某個線程會在該處執行時間很是長,致使鎖長期被佔用,其它線程阻塞時間較長。


解決方案

從新實現accessToken和jsApiTicket存儲方案,將其存儲在共享的redis服務上。 spring

修改上線後,BLOCKED線程消失了,可是依舊有不少WAITING狀態的線程,所以繼續分析該狀態的代碼。 apache

WAITING狀態線程

分析線程棧中的代碼」at org.apache.http.pool.PoolEntryFuture.await(PoolEntryFuture.java:133)」通過查看源碼發現是由於調用微信api使用了java的組件httpclient,如本文中項目使用的是httpclient4.3.5。

而httpclient爲了複用http鏈接,使用了鏈接池技術,該處的等待線程就是在等待從鏈接池中得到鏈接,那有多是鏈接池中鏈接不夠,或者某些線程佔用鏈接時間過長致使的。所以繼續查看代碼和查找相關httpClient鏈接配置文檔得出以下結論:

httpclient鏈接配置所有爲默認

本項目中的httpclient的鏈接配置所有使用默認配置。使用HttpClients.createDefault();建立默認的httpclient對象,所有使用默認值。

httpclient鏈接的配置,參考了張開濤的博客:http://jinnianshilongnian.iteye.com/blog/2089792

鏈接池配置不合理

maxConnTotal和maxConnPerRoute

maxConnTotal是鏈接池總的最大鏈接數,用的是默認值20.

maxConnPerRoute是每一個路由最大鏈接數,本項目都是鏈接微信服務器,所以就是默認爲2的值,而這對於生產環境併發較高確實不合適。

http網絡鏈接配置不合理

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毫秒。

總結

  • 默認配置值必定不是最優的,有時候在正好碰到惡劣環境下反而是致命的問題。
  • 微信接口的性能比較差,尤爲是當服務器與微信api的網絡通信較差的時候,會是較大的問題。
  • 微信的accessToken和jspApiTicket在集羣環境下必定要共享存儲。
  • 涉及到網絡通信的鏈接超時必定要設置且不能太大。
  • 生產環境解決問題須要有儘可能多的日誌、監控、各類資源的使用狀況的信息。
相關文章
相關標籤/搜索