工作项目重点

135 阅读9分钟

1. 精准营销

在一次营销活动中, 用户填写电子问卷,然后我们根据用户填写的问卷信息去给用户推荐相关产品

由于推荐规则的多样性,易变性,以及时效性,须支持动态配置规则

一开始我考虑似乎可以用策略模式,后来发现完全不是一回事, 一般策略模式是根据某个字段的不同值选择不同的处理逻辑,但此处有多个字段,不同字段的组合有不同的逻辑,所以策略模式 pass;

后来考虑用责任链模型, 每一种规则判断是责任链的一环,将每个逻辑封装到一个方法中,然后将类信息配置到数据库,这样就可以组合判断, 但是这种模式有一个问题,就是每个逻辑一个方法,需要写在java代码里,这样不就是硬编码了吗, 考虑到规则很容易改变,所以也不考虑这个方案

最后研究发现有个规则引擎可以满足需求, 将判断逻辑配置在数据库中,根据一些标签可以批量拉取到对应的规则以及符合规则的推荐结果, 统一执行,这样就做到了推荐逻辑的动态可配置

规则引擎我是用jexl3实现的,可以配置js代码块,包装下可以执行java类

2. SAAS多租户方案

saas系统由一个单租户系统改造而来, 故需要在原系统上增加多租户代码改造

首先考虑的是库隔离,使用动态数据源的方式去切换库,但是很明显 如果saas租户少的话没问题,如果租户一多起来肯定对资源由极大浪费

其次我们考虑表隔离,发现也不现实,表实在太多了,而且租户多起来肯定也不行

后来我们考虑引入一张映射表,每次查库先查映射表,找到对应租户的数据id后再去查实际表, 考虑到映射字段难以确认,比如我需要根据创建时间范围查询,这个就很难操作,映射表中一般只存租户id与对应数据id的关系, 这样很容易导致索引失效

最后我们决定只改造orm层,使用mybatis拦截器对sql进行拦截,拼接多租户字段实现,这导致部分表索引失效,目前没有更好的方案

我个人认为saas的核心就是不同租户的需求定制和资源要求不一样,需求定制有时候很难抽象出来,资源的话也难评估,要考虑租户后期的扩缩容问题, 很多时候会出现执行大租户操作拖垮小租户,或者多个小租户拖垮大租户的情况, 不过这种问题也好解决,大租户单独拿出来单独部署数据库等资源, 其他中小租户直接扔saas中,毕竟saas的面向客户都是中小型为主的企业.

3. MinorGC问题

闲来无事观看每个服务的监控,突然发现堆内存使用为有规律的峰谷, 但是这个服务没啥流量,半夜里竟然也是这种规律波形图, 猜想肯定是哪个代码里在不停得new对象出来;

image.png

//启动参数
java -DAPP_DOMAIN=nova-activity -Xms1900m -Xmx1900m -XX:NewRatio=1 -XX:MaxDirectMemorySize=2m -XX:MetaspaceSize=256m -XX:MaxMetaspaceSize=512m  -jar nova-activity.jar

可以看到分配固定得1900MB堆内存,new ratio的比例是1,即 新生代老年代1:1 ,默认应该是1:2的,这里故意调大,因为这个项目是活动项目,有可能瞬时并发上去,而且有大数据量的文件读写,很需要新生代的大空间

然后使用jinfo看一下参数

Non-default VM flags: -XX:CICompilerCount=3 -XX:InitialHeapSize=1992294400 
-XX:MaxDirectMemorySize=2097152 
-XX:MaxHeapSize=1992294400 
-XX:MaxMetaspaceSize=536870912 
-XX:MaxNewSize=996147200 
-XX:MetaspaceSize=268435456 
-XX:MinHeapDeltaBytes=524288 
-XX:NewRatio=1 
-XX:NewSize=996147200 
-XX:OldSize=996147200 
-XX:+UseCompressedClassPointers 
-XX:+UseCompressedOops 
-XX:+UseFastUnorderedTimeStamps -XX:+UseParallelGC 

没毛病 新生代老年大空间都是996147200Byte,约996MB,

再看下服务内存使用图,差不多每次都是到900MB的时候内存就掉下去了

image.png

问题定位就是minorGC异常, 频率倒不高,10分钟一次,也不存在内存泄漏,不用管他也没问题

生产: jmap一下拿到内存以及线程结构,MAT分析下......

本地: 直接jvisualvm,鼠标点点就行了

首先映入眼帘的是线程结构

image.png

redisson为啥这么多活动线程?? 而且redisson配置的参数是5个active线程,这点存疑,至今未解决

不过看下数据是没啥问题的,虽然线程多,却没消耗什么资源

再看下堆内存, 内存占用最大的当然是byte[],string,object[], 看的头疼,没意思,估计不是这个问题

排查很久,突然发现有个线程在疯狂分配内存

image.png

RMI connection!!

这居然是jdk的东西,jdk有bug??肯定不可能,细想一下,不会是监控引起的吧

试验下,随便写个main方法,sleep住,打开jvisualvm看下,果然,也是这个线程引起的,

线上监控同理也会引起这个问题, 不过10分钟一次minorGC 一点毛病也没有, 正常高并发项目可能几十秒一次minorGC吧

对于minorGC,频率是相当高的,我们需要关注两个点,

  1. GC的STW时间,正常一般几十毫秒,如果达到秒级别,就得排查下了
  2. 对象存活时间是否太长? 主要原因比如高并发量挤占资源(或者锁竞争局部变量周期变长),string table里字符串太多(YGC扫描时间会变长,常见string.intern),缓存使用不当,累计大量对象不被释放,

排查方法:

  1. 局部变量的原因, 考虑并发数等原因
  2. 全局变量或者静态变量,可以排查下大对象,但是一般全局变量都是在线程启动的时候加载,内存基本不太会膨胀,这种情况下很快就会进入老年代的
  3. 看下线程堆栈,哪些线程的内存占用大或者内存增量大
  4. 最终只能看堆内存啦,大对象,多对象一个一个排查,总归会排查到的

4. 线上直接内存OOM问题

由于线上有个定时任务需要去扫描数据库里某张表的全量数据,然后写入某个文件上传 由于数据量不小 (百万级别),并且在快速膨胀,所以考虑分片读写 在分片读写的过程中,我使用了filechannel的类(filechannel在写大文件的时候是有一个直接内存以及零拷贝的优化),性能上应该会快一些,大概的实现方法是这样的

RandomAccessFile rcf = new RandomAccessFile(fName, "rw");
FileChannel channel = rcf.getChannel();
int outLoopSize = 100;
int innerLoopSize = 10000;
long offset = 0;

try {
    for (int i = 0; i < outLoopSize; i++) {
        StringBuilder builder = new StringBuilder();
        //造数据
        for (int j = 0; j < innerLoopSize; j++) {
            builder.append("139xxxxxxxx").append("||").append("||").append("固定值").append("||").append("139xxxxxxxx")
                    .append("||").append("0").append("||").append("12324123").append("||").append("139xxxxxxxx")
                    .append("||").append("三个字").append("||").append("3204811995432111231").append("||")
                    .append("2").append("||").append("0").append("||").append("123412").append("||")
                    .append("||").append("||").append(new Date()).append("||").append("X100").append("\n");
        }
        byte[] bytes = builder.toString().getBytes();
        System.out.println("分配直接内存空间" + bytes.length);
        //写文件
        channel.write(ByteBuffer.wrap(bytes), offset);

        channel.force(true);
        offset += bytes.length;
    }
} catch (Exception e) {
    e.printStackTrace();
} finally {
    try {
        channel.close();
        rcf.close();
    } catch (IOException e) {
        e.printStackTrace();
    }
}

channel.write方法如果传入的是HeapByteBuffer,则会在内存创建一个临时直接内存,然后将堆内存buffer拷贝过去,减少内核态与用户态的来回切换(因为写文件的时候用的nio,当写入缓冲区不够时会直接返回,这样就会引起内核态与用户态的来回切换)

然后就出现了一个奇怪的现象,在一次调用中,无论怎么去创建临时直接内存,每次gc都会回收掉,也就不会发生oom 但是在多次调用中,居然会OOM,这里的多次调用理解为多线程调用

问题方法定位:

查看channel.write方法,底层使用的时IOUtil.write方法(循环调用这个方法) image.png 其内部又分配了临时直接内存,Util.getTemporaryDirectBuffer()内部如下

public static ByteBuffer getTemporaryDirectBuffer(int var0) {
    if (isBufferTooLarge(var0)) {
        return ByteBuffer.allocateDirect(var0);
    } else {
        Util.BufferCache var1 = (Util.BufferCache)bufferCache.get();
        ByteBuffer var2 = var1.get(var0);
        if (var2 != null) {
            return var2;
        } else {
            if (!var1.isEmpty()) {
                var2 = var1.removeFirst();
                free(var2);
            }

            return ByteBuffer.allocateDirect(var0);
        }
    }
}

很明显,这里有两个点:

  1. isBufferTooLarge,内部是根据MAX_CACHED_BUFFER_SIZE判断,这个值受jdk.nio.maxCachedBufferSize参数控制,如果这个参数为空,则为无限大
  2. bufferCache, 居然又cache, 而且点进去,threadlocal对象,完犊子,肯定时直接内存对象被缓存住了,而且是每个线程缓存一次,这就可以解释OOM的问题了

问题找到了,channel.write(ByteBuffer.wrap(bytes), offset)这个地方有问题, 那么我们可以针对这个点优化下

RandomAccessFile rcf = new RandomAccessFile(fName, "rw");
FileChannel channel = rcf.getChannel();
int outLoopSize = 10;
int innerLoopSize = 2000;
long offset = 0;

//创建局部变量 bytebuffer ,这个对象被gc掉则会回收对应的直接内存
ByteBuffer byteBuffer = ByteBuffer.allocateDirect(2 * 1024 * 1024);

try {
    for (int i = 0; i < outLoopSize; i++) {
        StringBuilder builder = new StringBuilder();
        for (int j = 0; j < innerLoopSize; j++) {
            builder.append(i + "39xxxxxxxx").append("||").append("||").append("固定值").append("||").append("139xxxxxxxx")
                    .append("||").append("0").append("||").append("12324123").append("||").append("139xxxxxxxx")
                    .append("||").append("三个字").append("||").append("3204811995432111231").append("||")
                    .append("2").append("||").append("0").append("||").append("123412").append("||")
                    .append("||").append("||").append(new Date()).append("||").append("X100").append("\n");
        }
        byte[] bytes = builder.toString().getBytes();
        System.out.println("bytes大小" + bytes.length);
        byteBuffer.put(bytes, 0, bytes.length);
        byteBuffer.flip();
        channel.write(byteBuffer); //写入直接内存
        channel.force(true);
        byteBuffer.clear();
        offset += bytes.length;
    }
} catch (Exception e) {
    e.printStackTrace();
} finally {
    try {
        channel.close();
        rcf.close();
    } catch (IOException e) {
        e.printStackTrace();
    }
}