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

来源:互联网 发布:爱淘宝买东西返利多少 编辑:程序博客网 时间:2024/05/20 09:26

摘要: 我们开发的公众号,由于将功能开发完毕后,未对服务进行压力测试,因此用到的组件中的参数值全是默认的,服务上线后一段时间运行得倒没什么问题,随着服务得访问量增加,一些多线程并发的问题就逐步暴露出来了,有的问题还非常严重。

背景

本文的背景是一个用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=2000weixin.mp.httpclient.connectTimeout=2000weixin.mp.httpclient.connectionRequestTimeout=500weixin.mp.httpclient.maxConnPerRoute=300weixin.mp.httpclient.maxConnTotal=300

微信调用接口统计
这里写图片描述
平均耗时都要300毫秒。

总结

  • 默认配置值一定不是最优的,有时候在正好碰到恶劣环境下反而是致命的问题。
  • 微信接口的性能比较差,尤其是当服务器与微信api的网络通讯较差的时候,会是较大的问题。
  • 微信的accessToken和jspApiTicket在集群环境下一定要共享存储。
  • 涉及到网络通讯的连接超时一定要设置且不能太大。
  • 生产环境解决问题需要有尽量多的日志、监控、各种资源的使用情况的信息。

原文地址:https://my.oschina.net/ywbrj042/blog/542453?p={{currentPage-1}}

0 0
原创粉丝点击