java开发的微信公众号服务端生产环境中的两个大坑
2017-01-04 14:30
316 查看
摘要: 我们开发的公众号,由于将功能开发完毕后,未对服务进行压力测试,因此用到的组件中的参数值全是默认的,服务上线后一段时间运行得倒没什么问题,随着服务得访问量增加,一些多线程并发的问题就逐步暴露出来了,有的问题还非常严重。
上线一段时间后,业务运营人员在微信公众号上做了几个活动,系统的访问量增加了一些。就陆陆续续暴露了一些问题,而这些问题的造成的危害还非常大,其中有2个tomcat实例运行一段时间后就会无法提供服务了。下面就详细介绍这个问题。
说明一下:上图中的tomcat的线程最大数配置的是1000,因此这个tomcat已经达到了最大线程数(其中多余的线程是jvm自启动的一些线程以及应用程序其它的代码启动的一些线程)。而图中出现的拐点是因为小马哥重启了tomcat,但是过段时间又会逐步上升。
BLOCKED状态线程
WAITING状态线程
在代码“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秒,且暂停时间每次增加一倍。
因此会某个线程会在该处执行时间非常长,导致锁长期被占用,其它线程阻塞时间较长。
修改上线后,BLOCKED线程消失了,但是依旧有很多WAITING状态的线程,因此继续分析该状态的代码。
而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;
都是使用的系统默认时间值,而这个值是一个比较大的值,对于生产环境来说是不合适的。
因此这些值对于生产环境来说均为不合理的值,因此我根据自己的生产环境的实际情况配置如下:
微信调用接口统计
平均耗时都要300毫秒。
微信接口的性能比较差,尤其是当服务器与微信api的网络通讯较差的时候,会是较大的问题。
微信的accessToken和jspApiTicket在集群环境下一定要共享存储。
涉及到网络通讯的连接超时一定要设置且不能太大。
生产环境解决问题需要有尽量多的日志、监控、各种资源的使用情况的信息。
原文地址:https://my.oschina.net/ywbrj042/blog/542453?p={{currentPage-1}}
背景
本文的背景是一个用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?p={{currentPage-1}}
相关文章推荐
- Java微信公众号开发--开发环境的搭建
- 微信公众号开发-开发环境搭建并通过java代码获取微信access_token
- 用java开发微信公众号:测试公众号与本地测试环境搭建(一)
- 用java开发微信公众号:测试公众号与本地测试环境搭建(一)
- 用java开发微信公众号:测试公众号与本地测试环境搭建(一)
- Javaweb服务端开发学习(一)--开发环境的配置
- java微信公众号开发(搭建本地测试环境)
- JAVA_OPTS 配置开发环境、生产环境等不同环境加载配置文件
- java微信公众号开发环境准备
- java微信公众号开发0.开发环境的搭建
- JAVA微信公众号开发第1篇之环境配置与开发接入
- Java学习笔记38:通过Spring Bean 注入static变量,来设计一套适合测试,开发,生产环境的配置项
- webpack开发和生产两个环境的配置详解
- Java日志框架-logback配置文件多环境日志配置(开发、测试、生产)(原始解决方法)
- java开发微信公众号:本地调试环境搭建与测试公众号
- 什么是JAVA开发环境、测试环境及生产环境,及它的过程
- java微信公众号开发(搭建本地测试环境)
- Mac下搭建Java服务端开发环境
- webpack开发和生产两个环境的配置详解
- [转载]让PHP支持大型项目-构建JSP、PHP与JAVA融为一体的开发环境