文档章节

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

杨武兵
 杨武兵
发布于 2015/12/10 20:52
字数 1926
阅读 1268
收藏 17

背景

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









© 著作权归作者所有

杨武兵

杨武兵

粉丝 277
博文 61
码字总数 123254
作品 1
昌平
架构师
私信 提问
weixin-java-tools 1.0.3 发布(新增企业号支持)

发布说明 refactor: 将原先公众号功能拆分到了weixin-java-mp中 add: issue #19 添加企业号支持(weixin-java-cp) add: 更新WxConsts,添加了更多菜单按钮类型 项目简介 weixin-java-tools是...

jarchan
2014/10/22
4.6K
10
weixin-java-tools 1.0.1 发布

发布说明 1. 提供了对微信消息加解密的支持。 项目简介 weixin-java-tools是一个用于开发微信公众号java工具集。 具有以下特性: 不基于Servlet、和其他MVC框架,仅作为工具使用,提供更多的...

jarchan
2014/10/19
1K
0
JDK,JRE和JVM之间的区别

JDK,JRE和JVM之间的区别 易百教程移动端:请扫描本页面底部(右侧)二维码并关注微信公众号,回复:"教程" 选择相关教程阅读或直接访问:http://m.yiibai.com 好多开发者学习 Java 编程有若干...

LYQ1990
2018/07/18
62
0
微信开发 Java SDK - Weixin Java Tools

微信开发 Java 开发工具包(SDK),支持包括微信支付、微信开放平台、小程序、企业号/企业微信、公众号(包括服务号和订阅号)等的后端开发。 本开发工具包基于chanjarster的同名SDK,增加了...

班纳睿
2016/12/22
120.1K
111
微信订阅号自定义菜单java开发

小弟应运营要求要更改公众号菜单,本以为是在公众号官网上进行配置即可,,谁知道 竟然是开发者模式编辑的。无奈,从未接触过公众号开发的我。有开始啃开了微信开发的api,原来的做这个得小伙...

明瞐
2018/11/21
72
0

没有更多内容

加载失败,请刷新页面

加载更多

OSChina 周六乱弹 —— 如果是个帅小伙你愿意和他出去吗

Osc乱弹歌单(2019)请戳(这里) 【今日歌曲】 小小编辑推荐:《Ghost 》游戏《死亡搁浅》原声 《Ghost 》游戏(《死亡搁浅》原声) - Au/Ra / Alan Walker 手机党少年们想听歌,请使劲儿戳...

小小编辑
今天
203
7
java通过ServerSocket与Socket实现通信

首先说一下ServerSocket与Socket. 1.ServerSocket ServerSocket是用来监听客户端Socket连接的类,如果没有连接会一直处于等待状态. ServetSocket有三个构造方法: (1) ServerSocket(int port);...

Blueeeeeee
今天
6
0
用 Sphinx 搭建博客时,如何自定义插件?

之前有不少同学看过我的个人博客(http://python-online.cn),也根据我写的教程完成了自己个人站点的搭建。 点此:使用 Python 30分钟 教你快速搭建一个博客 为防有的同学不清楚 Sphinx ,这...

王炳明
昨天
5
0
黑客之道-40本书籍助你快速入门黑客技术免费下载

场景 黑客是一个中文词语,皆源自英文hacker,随着灰鸽子的出现,灰鸽子成为了很多假借黑客名义控制他人电脑的黑客技术,于是出现了“骇客”与"黑客"分家。2012年电影频道节目中心出品的电影...

badaoliumang
昨天
16
0
很遗憾,没有一篇文章能讲清楚线程的生命周期!

(手机横屏看源码更方便) 注:java源码分析部分如无特殊说明均基于 java8 版本。 简介 大家都知道线程是有生命周期,但是彤哥可以认真负责地告诉你网上几乎没有一篇文章讲得是完全正确的。 ...

彤哥读源码
昨天
19
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部