购物车压测优化记录
异步消息发送优化
问题发现:order 服务cpu占用25%,gateway请求全部阻塞,说明order服务运行异常,order负载与gateway没有对应。
使用jstack –F 打印线程堆栈。
使用top (H命令)查看线程cpu状态,发现有一个线程始终占据100%,根据其线程号在线程堆栈中查询发现为VM Thread,说明垃圾回收线程一直忙。
通过jstat打印GC状态,发现老年代,新生代都已经接近100%。
确定为OOM后使用jmap导出dump堆存储文件。在eclipse中打开dump文件:
有个问题提示:有个线程池占用了92%的空间,分析该线程池列出其内部对象。如下图所示,
发现该线程池内部有大量的任务,具体的任务主要为UserVipHandler的消息处理方法和UnionPushHandler的消息处理方法。
解决OOM问题,order模块使用async注解消息处理器,其执行过程使用单独线程池(框架提供),队列为无界队列,导致消息过多时队列增长过大。原消息处理代码如下图:
Core模块已配置消息发送使用线程池达到异步(通过配置applicationEventMulticaster对象,spring框架会判断该对象是否存在,如果存在则会使用该消息处理器异步处理消息),无需再添加async注解(添加async,spring会对该消息处理方法进行拦截,在另外的线程池执行方法)。
解决方法: a)取消消息处理方法的async注解,保留EventListener(或同core模块中继承ApplicationListener实现消息处理handler)
b) core模块线程池异步消息使用线程池添加maxPoolSize参数,原默认为整形最大值,有可能导致oom。
推送开关
添加购物车查询后的推送开关:用户等级缓存,联盟推送。
解决问题:减少不重要的推送消息,通过zk动态配置开关,在运行状态切换消息推送功能,如下段发送消息入口代码首先判断开关,如果开关未开启则不进行消息推送。
private void publishUnionPushEvent(String userAgent, OrderCreationContext orderCreationContext) {
if(!configReader.getBoolean("order.submit.push.union.enable", true)){ //开关关闭,不发
return;
}
//订单推送
UnionContext unionContext = new UnionContext();
unionContext.setOrderCreationContext(orderCreationContext);
UnionPushOrderEvent event = new UnionPushOrderEvent();
event.setUserAgent(userAgent);
event.setUnionContext(unionContext);
publisher.publishEvent(event);
}
日志相关优化
A)Order服务日志按照重要级别整理
流量大情况设置非重要日志的级别均为WARN,重要的日志可以单独到制定文件中。
具体还在整理中。Gateway的controller可以关闭info级别。
B)日志输出管理
问题发现:该shotter是在一个log.debug()中作为参数传递,目的是截取输出字符串,但是日志级别比Debug高。因此需要注意debug日志的参数。
避免在log日志内进行大量计算,因为尽管日志级别别关闭,表达式计算过程还是会执行。
比如log.debug(“{}”,JSON. toJSON (obj)),尽管日志级别为更高级别,JSON. toJSON (obj)还是会被执行。
如果log.debug(“{}”,obj),直接使用对象tostring可以确保toString不被执行。
C) Gateway日志级别整理。
保留到warn级别。Cpu能下降20%。 下图是gateway在默认日志级别下的热点方法排序截图,可以看到前面的大部分方法均为log相关方法。 下图是gateway关闭日志后的cpu对比和线程数对比情况:在这次压测调整中cpu降低13%,线程数减少一半。
用户vip查询优化
A) vip信息优先查询本地缓存。
B) Vip模块redis使用nosync,不使用同步云。
解决问题:解决vip服务调用时间较长。
商品本地jar包优化
反射优化
问题分析:
刚开始Order服务cpu占用高70%,Jvisualvm分析热点方法:如下图所示
injectAttributeValue方法占用大量CPU,追踪该方法在ChargeGoods构造函数中被调用:ChargeGoods为购物车结算商品对象Bo,需要通过购物车商品Do与商品信息Do两个对象进行生成,ChargeGoods有60多个成员变量,通过反射设置,且本身每次查询过程都会构造多个ChargeGoods对象,因此反射效率可想而知。
改进方法:使用最简单的直接赋值方式转换bean。
缺点:在相应Do变化时需要手动修改ChargeGoods 该Bo的赋值过程。
效果:cpu降低接近20%
反序列化优化
原:从redis读取值后使用string2Value转成对象,其内部直接使用json.parse方法转成需要对象。
改进:对于string、Integer类型,直接使用类型转换,避免直接使用parse解析。
效果:减少string2Value方法的cpu时间。修改代码如下,带注释代码为优先判断部分。
public static <T> T string2Value(String value, Class<T> clazz) {
if(StringUtils.isEmpty(value)){
return null;
}
T t = null;
if (clazz.equals(String.class)) { //String优先
t = (T) value;
} else if (clazz.equals(Integer.class)) { //Integer优先
t = (T) Integer.valueOf(value);
} else {
t = JSON.parseObject(value, clazz);
}
return t;
}
字符串数组查询优化
原代码:“skn1, skn2, skn3, skn4, skn5, skn6, skn7, skn8”中查找是否包含某个skn(字符串),做法为split到一个数组,然后在数组中匹配查找目的skn。
改进方法:直接在字符串里面查找目的skn,注意头尾需要拼接下,号。
效果:在jvisalvm中该方法占用cpu时间有所降低,在本地自测belongsByString函数能提高10倍速度以上(在查找skn集合较大的情况下,优势更明显)。
/**
* 查找是srcValue中否包含judgevalue
* @param srcValue
* @param judgeValue
* @return
*/
private boolean belongs(String srcValue, String judgeValue) {
String[] belongArray = null;
if (judgeValue != null) {
belongArray = judgeValue.split(",");
}
return ArrayUtils.contains(belongArray, srcValue);
}
private boolean belongsByString(String srcValue, String judgeValue) {
if (StringUtils.isEmpty(judgeValue))
return false;
String tmp = "," + judgeValue + ",";
String tmp_key = "," + srcValue + ",";
return tmp.contains(tmp_key);
}
Redis值压缩优化
原:购物车商品在10件的时候,购物车信息大小在25k左右。
压缩原因:开始怀疑redis网卡达到瓶颈(1200M)。
解决方法:使用snappy压缩购物车信息作为值存入redis,读取时进行解压。
效果:order的cpu未有明显提升;redis的带宽降低到1/3左右。
后发现并非redis网卡有瓶颈,引入压缩主要能解决redis的存储量与带宽。
解压缩相关代码如下:由于目前框架中暂为提供redis写入二进制的接口,需要将压缩后的二进制转string进行读写。需要注意压缩后的二进制在转string时需要使用单字节类型编码,使用默认utf-8有可能会造成编码丢失。
public <T> T getWithCompress(CacheEnum cacheEnum, String postKey, Class<T> clazz) {
String key = cacheEnum.getCacheKey(postKey);
try {
String compressedVal = this.get(key);
if (StringUtils.isEmpty(compressedVal)) {
return null;
}
byte[] compressed = compressedVal.getBytes(Charset.forName("ISO8859-1")); //获取编码
byte[] uncompressed = Snappy.uncompress(compressed);
String value = new String(uncompressed, "UTF-8");
logger.info("get redis value operations. value is {}", value);
return BeanTool.string2Value(value, clazz);
} catch (Exception e) {
logger.warn("get redis value operation failed. key is {}", key, e);
}
return null;
}
public void putWithCompress(CacheEnum cacheEnum, String postKey, String value) {
byte[] compressed = new byte[0];
try {
compressed = Snappy.compress(value.getBytes("UTF-8"));
String compressedVal = new String(compressed, Charset.forName("ISO8859-1")); //生成string,注意为单字节
String key = cacheEnum.getCacheKey(postKey);
this.put(cacheEnum, postKey, compressedVal, true);
} catch (UnsupportedEncodingException e) {
logger.warn("put redis value un support failed. key is {}", postKey, e);
} catch (IOException e) {
logger.warn("put redis value io failed. key is {}", postKey, e);
}
}
部署优化
Gateway不与后台服务部署在同一机器。
效果:查询购物车tps从1.5k到2.5k。order服务能充分占用cpu。
Redis取值优化
原:获取一个值过程:判断缓存是否存在key,如果存在查询缓存。
改进:直接get值,不先判断key是否存在。
效果:查询缓存时间减少,减少与redis交换次数。
字符串相关
A. 避免使用substring截断字符串。
当时主要问题是这个操作在log.debug中,目的为了截断输出,debug虽然不打印日志,但是里面的表达式还是会执行,所以在jvusalvm中发下该方法占cpu时间较多。
##购物车编辑压测mark
查询操作减少优化
购物车压测场景:每个用户添加一件商品、选择、increase、decrease、删除并查询。 DBA统计有大量查询操作。如下图所示 分析代码,每次购物车编辑逻辑都会先查询购物车表(目的是获取用户的shoppingkey),uid和shoppingKey在用户第一次add之后就确定了1对1的关系,因此考虑将uid:shoppingkey存到redis 减少数据库select操作。 结果:aws主库数据库cpu有5%的降低。
order AWS写库优化
问题发现:购物车在编辑场景,cpu较高,100并发
innodb_flush_log_at_trx_commit=1 //每次提交均刷新到log
优化值=2, //每秒flush到磁盘一次
该参数相关配置说明连接:
http://blog.itpub.net/22664653/viewspace-1063134/
Cobar优化
问题发现: 购物车编辑场景,加大压力之后,tps未得到提高,且order、数据库压力均不高。将order服务直接连接数据库进行压测,发现tps有所提升。 分析使用cobar时cobar的状态,下图是cobar的线程运行状态,可以看到目前有四个执行线程池,每个线程池的sql分析线程和执行线程均为8,目前活动的执行线程均为8,且队列中有部分sql在等待执行
因此调整cobar的线程数
<!-- 系统参数定义,服务端口、管理端口,处理器个数、线程池等。 -->
<system>
<property name="serverPort">8066</property>
<property name="managerPort">9066</property>
<property name="initExecutor">4</property>
<property name="timerExecutor">4</property>
<property name="managerExecutor">4</property>
<property name="processors">4</property>
<property name="processorHandler">8</property>
<property name="processorExecutor">16</property>
<property name="dataNodeIdleCheckPeriod">15000</property>
<property name="monitorPeriod">5000</property>
<property name="readOnlyInSlave">true</property>
</system>
processors为线程池个数,通常和机器cpu个数对应,将processorExecutor执行线程数调整到16。 下图是增大cobar 执行线程数后的运行状态,可以看到执行sql任务队列已无排队情况,此时使用cobar的tps也能与直连数据库持平。
core数据库连接池大小优化
问题发现
##订单查询
- 订单列表
订单列表接口在gateway有两处服务调用(总数、列表),原处理为同步执行,由于两调用结果无依赖关系,因此可以修改总数获取为异步执行。
- 订单详情
订单详情目前整个接口为forceMaster强制使用主库,查询详情过程需要查询十几次数据库,使得order主库在压力较大,需要将部分级别较低的查询使用从库,如优惠信息查询、物流查询。
下单
###NGINX优化
待续...