order压测记录.md 12.7 KB

购物车压测优化记录

异步消息发送优化

问题发现:order 服务cpu占用25%,gateway请求全部阻塞,说明order服务运行异常,order负载与gateway没有对应。

  1. 使用jstack –F 打印线程堆栈。

  2. 使用top (H命令)查看线程cpu状态,发现有一个线程始终占据100%,根据其线程号在线程堆栈中查询发现为VM Thread,说明垃圾回收线程一直忙。

  3. 通过jstat打印GC状态,发现老年代,新生代都已经接近100%。

  4. 确定为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数据库连接池大小优化

问题发现

##订单查询

  1. 订单列表

订单列表接口在gateway有两处服务调用(总数、列表),原处理为同步执行,由于两调用结果无依赖关系,因此可以修改总数获取为异步执行。

  1. 订单详情

订单详情目前整个接口为forceMaster强制使用主库,查询详情过程需要查询十几次数据库,使得order主库在压力较大,需要将部分级别较低的查询使用从库,如优惠信息查询、物流查询。

{width="5.768055555555556in" height="2.2604899387576554in"}

下单

###NGINX优化

待续...