一次无限读写的经历

先说明下本地的机器是i5 8核,8G内存,64位,jdk64位,1.8的版本
最近接手一个项目,就是在技术层面上面,实现大量数据的无限写入,场景是这样的:有一笔从opentsdb+hbase查出来的数据,了解到最大量时是大概是1秒钟一个参数20笔,最多80个参数,然后dump内存分析之后,发现一笔数据大约是50个字节,于是算了一笔帐,50 * 20 * 80 * 60 * 60 = 一个小时大约占有300M的内存,10个小时就会占用3G的内存,给其他的基本信息和类信息等预留1G的空间,考虑到生产上机器是8G的内存,还会部署一二个其他的应用,然后就设置-Xmx4096m -Xms4094m,初始化的堆和最大的堆都设置成4G,在本地作为测试,之所以初始化也设置一样,是考虑到这个应用几乎就是一个不断占用大内存过程的应用,这样可以避免不断收缩开辟新空间带来的内存开辟消耗,然后进行测试:先通过opentsdb往hbase写入24小时每秒10笔,40个参数,然后应用这边采用一批10小时的前面得到的结论进行保守测试,发现当第一批进行到30个参数左右,会报GC overhead limit exceeded,翻查资料增加-XX:-UseGCOverheadLimit,果不其然不再抛出此异常, 我了解到其实就是jvm给我们设置了一道屏障而已,好像就是说用98%的GC时间,但是只回收了2%的空间就会报这样的异常,解除屏障即可,GC效率低点就低点。同时用idea启动VisualVM图形界面进行分析,跟踪堆内存的情况,基本就是批量写完就到波底,再读完就到波峰的状态,波峰的瓶颈非常大,几乎就要内存突破,发现批量读的时候必须把所有的参数聚集才能做聚合运算(各参数的读实际上已经是并发读了),这部分的内存在代码层是无法优化的,那聚合转换的时候能不能做呢,就是说读一笔完整的参数就清空一笔,发现确实可以,用map remove移除之后,再获取此值,传入另外一个数据结构,进行最后的写入操作即可,同样想到是不是可以写入一笔的时候,再清除一笔呢,于是循环写入一笔数据的时候,这样可以用迭代器remove掉,就可以边写边释放内存了,就是从内存方面从代码层面上的优化,其实聚合参数的那个地方进行数据结构转换也很巧妙,这里实现了,不同数据量不同参数根据相同的时间戳进行聚合的逻辑,可以贴出来,大家可以交流下:

log.info("开始转换数据....");
Map<String, LinkedList<String>> allKeyTsListMap = new HashMap<>();
Map<String, Map> allKeyTvMap = new HashMap<>();
for (ResEntry keyEntry : keysEntries) {
    if(null != keyEntry) {
        Map resMap = keyEntry.getResMap();
        if(null != resMap){
            allKeyTsListMap.put(keyEntry.getKey(), new LinkedList<>(resMap.keySet()));
            allKeyTvMap.put(keyEntry.getKey(), resMap);
        }
    }
}
rows = new LinkedList<>();
while (true){
    //String ts = null;
    //List<String> tsList = entry.getValue();
    long minTs = Long.MAX_VALUE; //获取时间戳
    //获取最小的时间戳
    Long tempTs = null;
    for (LinkedList<String> value : allKeyTsListMap.values()) {
        if(value.size() > 0){
            tempTs = Long.parseLong(value.getFirst());
            if(null != tempTs && tempTs < minTs){
                minTs = tempTs;
            }
        }
    }


    if(tempTs == null){
        break;
    }


    List<String> row = new LinkedList<>();
    String strMinTs = String.valueOf(minTs);
    //时间戳
    row.add(DateFormatUtils.format(new Date(minTs), "yyyy-MM-dd HH:mm:ss SSS") + "\t");
    for (String h : dataHeaders) {
        Map tvMap = allKeyTvMap.get(h);
        if (null != tvMap) {
            //用remove提高内存利用率
            Object v = tvMap.remove(strMinTs);
            if(null != v){
                if(v instanceof BigDecimal){
                    BigDecimal bigDecimal = (BigDecimal)v;
                    row.add(bigDecimal.toPlainString());
                }/*else if(v instanceof Number){
                            row.add(v.toString());
                        } */else{
                    row.add(v.toString());
                }
                //移除之后,遍历其他参数时已经不会存在同样的时间戳
                allKeyTsListMap.get(h).remove(strMinTs);
            }
        } else {
            row.add("");
        }
    }
    rows.add(row);
}
log.info("转换数据完成!");
return this;

那还可不可以继续优化呢,于是想到两个Survivor默认和edon区的比例是8:1,而两个Survivor区总是有一个空间是不用的,这里是不是浪费太大的空间啊,于是设置-XX:SurvivorRatio=64,发现GC和Full GC的次数基本上减少一半,但是总的时间却没有减少,读一个星期左右1G多的数据,压缩之后500M左右,用时都是40分钟左右,没有明显的区别(原因不明),和其实再设置这个之前,我还设置了年轻代的空间是老年代的3年倍,也就是设置-Xmn3096,这样的Full GC的次数大大增加,后来了解到堆内存一定的情况,增加年轻代,老年代必然减少,而老年代是触发Full GC的时机,所有造成Full GC特别多,因此也没有明显的改善,打印GC信息用-XX:+PrintGCDetails。这样看来优化内存的方式似乎就没有用了,那是不是可以从收集器着手呢
默认GC收集器cpu和堆内存的情况(默认采用的是第二代Parallel GC并行收集器):
在这里插入图片描述
了解到目前G1收集器是最强大的,特别对于大内存的场景,对并行吞吐量和暂停是很优秀的,于是改用G1进行测试-XX:+UseG1GC,发现波底更低,波峰的持续时间更短,看来确实内存是得到了优化,但是总耗时却增加了几分钟,我理解是一个更大强度的榨取cpu,垃圾回收更深入更彻底的收集器,所以才会这样,当然我没有优化其他G1相关的参数进行测试。G1收集器下的堆内存和cpu情况
在这里插入图片描述
有关G1的详情信息,请参阅: https://docs.oracle.com/en/java/javase/11/gctuning/garbage-first-garbage-collector.html#GUID-CE6F94B6-71AF-45D5-829E-DEADD9BA929D

未完待续…

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值