java开发的微信公众号服务端生产环境中的两个大坑

背景

本文的背景是一个用java开发的微信公众号服务端的业务应用,使用的java开发包是weixin-java-tools。该系统的部署结构式nginx+10个tomcat实例的集群。

上线一段时间后,业务运营人员在微信公众号上做了几个活动,系统的访问量增加了一些。就陆陆续续暴露了一些问题,而这些问题的造成的危害还非常大,其中有2个tomcat实例运行一段时间后就会无法提供服务了。下面就详细介绍这个问题。

问题描述

某天我们的程序员小马经常接到几个短信报警说是2台tomcat实例无法提供服务了,他就只能重启服务器,但是过几十分钟后,又会出现这样的问题,他只能痛苦得一遍一遍得重启tomcat服务器,最终实在是郁闷就找到我帮他一起看看到底是什么原因。

查看jvm监控

我经过查看监控后,查看到了这样的异常现象。

说明一下:上图中的tomcat的线程最大数配置的是1000,因此这个tomcat已经达到了最大线程数(其中多余的线程是jvm自启动的一些线程以及应用程序其它的代码启动的一些线程)。而图中出现的拐点是因为小马哥重启了tomcat,但是过段时间又会逐步上升。

查看线程栈列表

查看其它的正常的tomcat线程比较稳定,它们的线程数都在一个稳定状态,而这些tomcat是负载均衡的状态,它们的访问量应该是差不多的,因此这2个tomcat的线程如此之多,不是因为访问量太高,肯定还有其它的愿意,因此使用jstack将线程栈导出来,发现有大量的BLOCKED和WAITING状态的线程。

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进行更新操作。

既然大量的线程阻塞在该处,那说明有的线程在执行同步块中的代码非常慢,而其它的线程都在等待该线程释放锁,因此越来越多的线程都阻塞该处。问题就出在该代码处。继续分析该处代码发现了一个比较严重的坑,描述如下:


  • 在微信中调用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服务上。

修改上线后,BLOCKED线程消失了,但是依旧有很多WAITING状态的线程,因此继续分析该状态的代码。

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在集群环境下一定要共享存储。
  • 涉及到网络通讯的连接超时一定要设置且不能太大。
  • 生产环境解决问题需要有尽量多的日志、监控、各种资源的使用情况的信息。









转载于:https://my.oschina.net/ywbrj042/blog/542453

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值