V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
summer7
V2EX  ›  Java

[虚心请教] JDK8 默认的配置环境下,10G 的堆内存 Full GC 一次要 16s 左右,该如何优化?处理一个请求内存耗费大约 900M

  •  1
     
  •   summer7 · 2019-10-16 12:42:58 +08:00 · 6862 次点击
    这是一个创建于 1625 天前的主题,其中的信息可能已经有所发展或是发生改变。
    第 1 条附言  ·  2019-10-16 22:26:36 +08:00
    补充描述:
    1、程序简要描述
    从底层数据库(非传统意义的数据库,像 limit 分页、存储过程等常见特性是不支持的)实时查询数据(暂定每次 30000,后期可能还会增大结果数),对查询数据做解析( json 解析以及字符串切割操作,必须操作),之后将解析后的数据存入 redis
    2、目前的实现方式:
    resultSet.next 遍历完存入一个 List 中,对 List 用并行流进行解析为 List<Map>结构(测试机器 32 核 cpu 跑满,30000 条数据需 2-4s,但是碰上 GC,耗时基本就是 20s+,观察 GC 日志是发生了 Full GC,16s+)
    ps:楼下有老哥提出 next 遍历出一条就解析一条,这个待我一试
    3、目前仅能采用这种单机应用的模式,甚至后期上线还可能是配置较低虚拟机,比开发测试机器差的很多的那种。。。
    4、另有关于 JDBC 的一些疑问,还请大佬指点(迷惑了好久)
    ResultSet rs = ps.executeQuery("xxxxx sql");
    while( rs.next){
    //取字段
    }
    问题 1:executeQuery 方法返回 ResultSet 时是否代表着数据库端已经将 sql 执行完毕?
    问题 2:ResultSet 对象存储的是什么?应该不是全部的 sql 执行结果吧。( debug 看了下当前用的数据库 resultSet.next 每次只是向数据库服务端发请求获取一部分数据放入本地内存,不知其他数据库是否也是这样?还是说每种数据库有不同的方式)
    第 2 条附言  ·  2019-10-16 23:01:10 +08:00
    看到各位老哥的回复,在此统一感谢一下,谢谢大家。


    根据各位的回复思考大概总结了下,在现有单机的情况下大概有以下几种方案觉得是可行的:
    1、next 遍历一条解析一条
    2、调整 young 和 old 的大小(准备把 young 调大一点测试验证一下)
    3、用 G1 垃圾收集器(刚刚突击了解了一下)
    4、堆外内存(还需要了解一下)


    其他的方案
    1、拆表(目前底层存储不允许,所有数据存在一张超大表中)
    2、分布式(这个在目前的条件不允许,只能用单机应用)
    3、换语言(这个嗯。。。有时间可以一试)



    再再再补充描述:
    Full GC 一次基本所有内存就全回收了( FullGC 一次 16s+),与刚启动占用内存基本一致。
    Full GC 原因观察:一小部分是因为 MetaSpace 满了(存疑,为什么这个也会涨),大部分原因是内存要吃满导致的。这个预计调整 young 大小可以减缓一大部分。
    63 条回复    2019-10-17 16:52:14 +08:00
    xuanbg
        1
    xuanbg  
       2019-10-16 13:13:09 +08:00
    拆分服务。。。。。。
    IamNotShady
        2
    IamNotShady  
       2019-10-16 13:14:33 +08:00
    一个请求 900M ? 换成 G1 试试
    chendy
        3
    chendy  
       2019-10-16 13:21:07 +08:00   ❤️ 1
    一个请求 900M…要么极端需求要么设计不合理…
    jimrok
        4
    jimrok  
       2019-10-16 13:23:59 +08:00
    把数据拆出来,放在 redis 或者 memcached 里面。否则堆会太大,一旦需要扫描堆就会比较耗时。
    reus
        5
    reus  
       2019-10-16 13:24:15 +08:00
    换最新版 jvm 和最先进的 GC 算法

    不换就不要奢求太多。
    coolcfan
        6
    coolcfan  
       2019-10-16 14:45:38 +08:00
    也许可以从生命周期的角度检查下请求处理过程中消耗的内存。
    lihongjie0209
        7
    lihongjie0209  
       2019-10-16 14:54:22 +08:00
    那也就是说你的这个服务的并发数量 = 10 ??
    summer7
        8
    summer7  
    OP
       2019-10-16 15:57:08 +08:00 via Android
    @lihongjie0209 是的,这个项目基本等于没有并发。 10 并发也到不了。
    summer7
        9
    summer7  
    OP
       2019-10-16 16:01:10 +08:00 via Android
    @chendy 需求大概是这样的:从数据库查出 30000 条数据,数据部分字段会包含长文本,单条数据约 200+字段,查出后需要对数据解析,据观察内存涨幅约等于 900m
    summer7
        10
    summer7  
    OP
       2019-10-16 16:10:34 +08:00 via Android
    @jimrok 数据流程大概是这样,第一步 jdbc 查库,第二步:对每条数据做解析,第三步:入 redis。 目前问题主要集中在解析这个过程 32 核机器跑满也需要 2-4s,稍微来个并发(不到 10 个),10g 的堆因为 gc 导致解析时间会上升到 20s+
    yidinghe
        11
    yidinghe  
       2019-10-16 16:15:57 +08:00
    1、确认有没有内存泄漏,也就是多次 GC 后程序还是能正常运行并且长期运行;
    2、能够对老年代进行非 STW 回收的垃圾收集器只有 G1。建议换 G1 ;
    3、进一步对内存使用进行剖析,减少不必要的对象持有;
    4、如果缓存数据占大头,那么换用 Redis/memcached 等独立进程的缓存方案。
    yidinghe
        12
    yidinghe  
       2019-10-16 16:17:05 +08:00
    “从数据库查出 30000 条数据,数据部分字段会包含长文本” 这时候应该改为读取一条处理一条的方式。
    summer7
        13
    summer7  
    OP
       2019-10-16 16:17:09 +08:00
    @jimrok 补充:是单次请求解析需 2-4s
    misaka19000
        14
    misaka19000  
       2019-10-16 16:21:07 +08:00
    用 C 语言重写
    learnshare
        15
    learnshare  
       2019-10-16 16:22:44 +08:00
    这需求或实现逻辑并不合理吧
    Immortal
        16
    Immortal  
       2019-10-16 16:24:46 +08:00
    单条数据约 200+字段= = 猛老哥
    u823tg
        17
    u823tg  
       2019-10-16 16:26:13 +08:00
    换语言? 我胡说的
    memedahui
        18
    memedahui  
       2019-10-16 16:28:35 +08:00
    @IamNotShady 我记得 java8 默认就是 G1 吧...还是我记错了
    lihongjie0209
        19
    lihongjie0209  
       2019-10-16 16:31:46 +08:00
    @misaka19000 #14 你用 c 语言管理 10G 内存试试, 没啥区别的。
    lihongjie0209
        20
    lihongjie0209  
       2019-10-16 16:32:20 +08:00
    @summer7 #9 一次查 3000 条试试?
    hikikomorimori
        21
    hikikomorimori  
       2019-10-16 17:51:36 +08:00
    考虑 Nosql?
    lihongjie0209
        22
    lihongjie0209  
       2019-10-16 18:03:35 +08:00
    @hikikomorimori #21 不管底层存储怎么样, 加载 30000 条数据就需要这么多的内存
    babyvox5th
        23
    babyvox5th  
       2019-10-16 18:09:27 +08:00
    补充一下技术优化之外的,SSD 上 3000MB.
    ipwx
        24
    ipwx  
       2019-10-16 18:11:40 +08:00 via Android
    不该想办法维护中间结果的表,降低每次请求计算量么
    bk201
        25
    bk201  
       2019-10-16 18:17:06 +08:00
    取少点不行吗?
    bobuick
        26
    bobuick  
       2019-10-16 18:21:56 +08:00
    假设你其他措施都做了, 然后单次要是 900m 是必要的数据. 然后也无法重新设计, 然后请求完数据后内存里这些数据就不用保存了的话, 不是应该在 young 区被回收么. 把 young 设的足够大一些. 老年代应该保持一天都不到一次的水平
    summer7
        27
    summer7  
    OP
       2019-10-16 18:36:26 +08:00 via Android
    @ipwx 嗯,以前 hbase 做存储时已经是结构化的数据了,但是后来换了数据库必须要调用方去解析这些数据。其实整体数据流程很简单,jdbc 查询,查完解析入库。
    bookit
        28
    bookit  
       2019-10-16 18:39:57 +08:00
    用 C 写一遍,最简单的那种,看需要多少内存
    sadfQED2
        29
    sadfQED2  
       2019-10-16 18:40:58 +08:00 via Android
    考虑 nosql?另外必须实时吗,不需要实时的话定时脚本计算,然后存缓存呢。最后,如果都不行就升级机器? 80 核以上,500+G 内存那种?
    sadfQED2
        30
    sadfQED2  
       2019-10-16 18:42:18 +08:00 via Android
    @sadfQED2 楼上的换语言没什么意义啊,数据已经那么大了,你用什么语言加载到内存都那么大,除非改算法
    BBCCBB
        31
    BBCCBB  
       2019-10-16 18:45:21 +08:00
    先用 G1 试试,然后增大堆内存再试试
    l8g
        32
    l8g  
       2019-10-16 18:48:15 +08:00
    1. 一次查询 3W 条记录,可以拆分一下
    2. 调整一下 Young 和 Old
    summer7
        33
    summer7  
    OP
       2019-10-16 18:50:55 +08:00
    @bobuick 感谢回复。young 设置大一点我会验证试一下的。这是第一次遇到这种 GC 问题,不知像互联网那些高并发项目,Full GC 频率的合理范围是多少呢?还请指教
    summer7
        34
    summer7  
    OP
       2019-10-16 18:52:09 +08:00
    @babyvox5th 数据 JDBC 查询完,就是内存操作了目前。
    summer7
        35
    summer7  
    OP
       2019-10-16 18:55:19 +08:00
    @l8g 感谢回复。楼上也有大佬提到修改 young 和 old,我会试一下的。 说起来,拆分查询也是一个头疼的事情,目前底层存储库不支持这种比如 limit 这种拆分查询
    summer7
        36
    summer7  
    OP
       2019-10-16 18:55:46 +08:00
    @BBCCBB 感谢回复。G1 会尝试一下的。
    summer7
        37
    summer7  
    OP
       2019-10-16 18:58:51 +08:00
    @bobuick 目前我单纯的本地写个 for 循环去触发查询接口,基本上每调用 9 次就会触发一次 Full GC,之后内存迅速下降,基本等于刚启动时候的内存。 改改参数看吧,也是第一次遇到这种问题
    summer7
        38
    summer7  
    OP
       2019-10-16 19:01:05 +08:00
    @sadfQED2 感谢回复。数据要求实时查询。加机器配置这个事情,难呀。
    summer7
        39
    summer7  
    OP
       2019-10-16 19:07:20 +08:00
    @yidinghe 感谢回复。
    1、确认后没有内存泄漏,一次 FullGC 之后内存基本和刚启动时一样.但是一次 10 并发,基本就要 Full GC 一次,楼下也有大佬提出修改 young 和 old,我试一下看看,将 young 设置大一点。
    2、对于取一条数据解析一条,嗯。。其实一直有个疑惑,while(resultSet.next ())的时候,这个 resultSet.next 是不是就相当于一个游标,其实数据还是存在于数据库端的?还是说不同数据库有不同的实现方式?
    shakoon
        40
    shakoon  
       2019-10-16 19:08:41 +08:00
    如果能在数据库上实现,用视图把表拆小、用存储过程进行你所说的解析,就试一下看看
    semut
        41
    semut  
       2019-10-16 19:28:17 +08:00
    一次请求 30000 条数据,设计不太合理,可以简单说说这个任务的目的,看下有没有其他方案实现
    phantomzz
        42
    phantomzz  
       2019-10-16 20:07:42 +08:00
    长文本存 elasticsearch,其他 200 字段该拆表拆表,将 30000 条数据水平分割到不同 机器 /进程 流式处理,全部处理完毕再汇聚。
    Jonz
        43
    Jonz  
       2019-10-16 20:15:58 +08:00
    关注下进展
    uyhyygyug1234
        44
    uyhyygyug1234  
       2019-10-16 20:54:28 +08:00
    @phantomzz 我感觉也是按照老哥的说法。

    900MB 3w 条数据 每条数据 30k

    这边 magic number 为啥要取 3w
    3w 条之间本身是否有关系,可否分割,分布式处理
    af463419014
        45
    af463419014  
       2019-10-16 21:02:13 +08:00
    用堆外内存

    处理的数据单独放在堆外内存,用完手动释放,可以跟 JVM 的 GC 分开
    JVM 堆内存就不需要 10G 这么大了
    pangliang
        46
    pangliang  
       2019-10-16 21:08:52 +08:00
    3 万条数据为啥要一次性读入内存? 用了 orm 吧? 查完库只能返回一个 3 万的 list, 然后遍历 list?
    直接 jdbc 里查, 完了用 jdbc 的 result.next 去循环直接处理, 不要拼到 list 再遍历;
    这样就不会有 3 万行在内存了
    如果还是不行, 检查下 jdbc 返回数据的方式; 可以设置, next 一次返回一行
    php 都有, java 不可能没有
    SoloCompany
        47
    SoloCompany  
       2019-10-16 21:14:27 +08:00 via iPad
    加并发控制,限制 slow operation 的并发数,预留 30%以上的空闲 heap
    mxalbert1996
        48
    mxalbert1996  
       2019-10-16 21:15:54 +08:00 via Android
    @sadfQED2 换语言的意义在于减少 GC 的时间啊
    summer7
        49
    summer7  
    OP
       2019-10-16 21:50:44 +08:00 via Android
    @phantomzz 其实在之前用 hbase 存储是分表的,但是每个表也是 200+字段的,之后底层数据库弃用了 hbase,把所有表数据汇聚在一张超大表中,数据量相当的大。
    感觉“合久必分”这句话太适合描述这种存储方式变化了
    summer7
        50
    summer7  
    OP
       2019-10-16 21:53:30 +08:00
    @summer7 合久必分,分久必合
    summer7
        51
    summer7  
    OP
       2019-10-16 21:57:20 +08:00
    @pangliang 感谢老哥的回复。源于对 JDBC 理解的不是很深,目前我的做法是 next 遍历完,30000 条数据存入 list 再交给具体的解析方法解析。 也有其他大佬和老哥你一样说,可以 next 遍历时一条一条解析,或许这个就是解决问题的办法吧,待我一试。
    neoblackcap
        52
    neoblackcap  
       2019-10-16 23:04:29 +08:00
    为什么需要一次读 3W 条数据?流式处理嘛,你业务逻辑不改,换 JVM 也未必有成效。
    毕竟假如全部都是新生代对象,但是在 GC 被触发的时候,这些对象很有可能还是活的,有其他业务代码在使用这它们。你这个整体并发一样上不去。
    这个东西还是得结合你的业务逻辑进行分析才行
    Buffer2Disk
        53
    Buffer2Disk  
       2019-10-17 00:01:22 +08:00
    改架构吧,这业务设计就不合理。。。
    swulling
        54
    swulling  
       2019-10-17 01:24:13 +08:00 via iPhone
    每次请求结束后都主动触发一次 GC 吧😄
    v2orz
        55
    v2orz  
       2019-10-17 08:31:32 +08:00
    换语言和堆外内存方案建议别搞
    换语言作用不大
    堆外内存,既然你们都设计成这样了,想想堆外内存管理估计也很难做好,不建议去踩
    一次少查点数据或者流式处理就差不多了,GC 换 G1
    softtwilight
        56
    softtwilight  
       2019-10-17 09:01:13 +08:00
    把 resultSet 包装为流,一条一条解析,十分省内存;
    return StreamSupport.stream(new Spliterators.AbstractSpliterator<Record>(
    Long.MAX_VALUE,Spliterator.ORDERED) {
    @Override
    public boolean tryAdvance(Consumer<? super Record> action) {
    try {
    if(!resultSet.next()) return false;
    action.accept(createRecord(resultSet));
    return true;
    } catch(SQLException ex) {
    throw new RuntimeException(ex);
    }
    }
    }, false).onClose(() -> closeConnectionAnd...())
    haochih
        57
    haochih  
       2019-10-17 09:20:16 +08:00
    jdbc 默认情况下的读会把 sql 语句的结果全部加载到内存中,这种大数据量的读可以考虑采用 stream read。详情参见 https://dev.mysql.com/doc/connector-j/8.0/en/connector-j-reference-implementation-notes.html 中 ResultSet 一节。
    tairan2006
        58
    tairan2006  
       2019-10-17 11:12:36 +08:00
    这架构有问题,数据拆开,或者改成流处理
    sorra
        59
    sorra  
       2019-10-17 12:10:09 +08:00
    JDBC ResultSet 有 vendor 差异性,默认情况下:
    - MySQL 和 PostgreSQL 会一次加载所有行到 JVM
    - Oracle 每次只加载 10 行到 JVM,DB 这边维持一个会话和偏移量,JVM 可以不停地推进偏移量,直到读完所有行
    这个行为可配置(ResultSet 可以 setFetchSize),详情见 vendor 的文档

    如果 vendor 支持 setFetchSize,你可以流式处理数据,不要都堆到内存里才全部处理

    如果 vendor 不支持 setFetchSize 和 LIMIT,也可以想想能不能在 SQL 语句上想想办法
    sorra
        60
    sorra  
       2019-10-17 12:15:03 +08:00
    Statement 也可以 setFetchSize,为了防止来不及,可以在 Statement 就设上
    jimrok
        61
    jimrok  
       2019-10-17 13:41:22 +08:00
    你的数据里有大文本是不太能降低内存的,大文本被装在进 java 要转换成 String 的对象,这些对象再进行解析,肯定要再产生若干小对象,gc 的负担肯定重。你这种还是提前做好,例如你要从小说里面找某个情节,那你把整篇小说读进来再查找肯定耗费内存。最后提前做好索引,这样根据索引,直接找到段落,返回段落信息就不需要消耗很多内存了。
    lazyfighter
        62
    lazyfighter  
       2019-10-17 14:21:05 +08:00
    可以写个定时任务建立中间表,甚至最终数据表,这样你的并发也能上来,把数据处理拆出来
    TJT
        63
    TJT  
       2019-10-17 16:52:14 +08:00
    流处理。
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   我们的愿景   ·   实用小工具   ·   3009 人在线   最高记录 6543   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 31ms · UTC 15:02 · PVG 23:02 · LAX 08:02 · JFK 11:02
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.