关键词:groovyshell evaluate 内存泄露

grovvy 版本 2.4.10

1. 起因

代码在线上运行一段时间后,容器会自动重启,查看容器监控发现是内存溢出导致了自动重启。

查看 jvm 堆情况。堆内存的使用实际不大,但是 docker 实例的内存却又占满了,又因为 docker 实例并没有安装 agent 进程,说明很大可能是 JVM 堆外内存导致。

使用 jstat -gc pid 查看,发现 Metaspace 占用很大

回想该应用出现状况前的改动,主要是新增了 groovy 脚本执行任务。查看代码发现每次执行都会创建对象

public Object evaluate(String name, String script, Binding binding) {
    binding.setVariable("logger", LoggerFactory.getLogger("script-" + name));
    binding.setVariable("builder", new Builder());
    binding.setVariable("selector", this.selector);
    GroovyShell shell = new GroovyShell(binding);
    return shell.evaluate(script);
}

看监控类加载总量很大,猜测是因为生成很多临时 class 对象,无法 gc

2. 排查之后的具体原因

new GroovyShell() 未指定 patentClassloader

所以 GroovyShell 初始化的时候会自动 new 一个 classLoader 并设置 GroovyShell.class.getClassLoader() 为父 classloader

所以最终加载脚本文件的时候是交给 GroovyShell.class.getClassLoader() 处理的

执行 evaluate 方法,只传入了脚本字符串,直接 evaluate 字符串不会缓存脚本,所以每次执行都会重新加载一次,生成一个全新的 class 对象

但是 JVM 中的 Class 只有满足以下三个条件,才能被回收(也就是该 Class 被卸载 unload)

  • 类所有的实例都已经被 GC,也就是 JVM 中不存在该 Class 的任何实例。
  • 加载该类的 ClassLoader 已经被 GC。
  • 该类的 java.lang.Class 对象没有在任何地方被引用

由于实际负责加载类的 classloader 不会回收,导致 class 对象一直存在于 Metaspace 并且随着时间推移越来越多,并且启动时没有设置 Metaspace 最大值,导致 Metaspace 空间无限增长,最终容器自身 oom,自动重启

-XX:MetaspaceSize 表示触发 full gc 的容量。对于64位JVM来说,MetaspaceSize 默认为 20.75MB,-XX:MaxMetaspaceSize 默认值是无限大

多个单独索引是可能被同时使用到的

MySQL5.0之前,单表一次查询只能使用一个索引,无法同时使用多个索引分别进行条件扫描。但是从5.1开始,引入了 index merge 优化技术,对同一个表可以使用多个索引分别进行条件扫描。相关文档:http://dev.mysql.com/doc/refman/5.6/en/index-merge-optimization.html

index(a) index(b)

select * from table where a=1 and b=1 可能会用到索引 a 和 b(index_merge),也可能只用到 a 或者 b

mysql 会选择它觉得快的索引策略,但是 index_merge 不一定能比单索引查询快,这个要根据表里的数据具体分析。

如果没有单独按照 b 查询的场景,这里可以设置为联合索引 index(a, b),查询条件为 a=1 and b=2 时会比上述索引快一些

事务隔离级别

  • 读未提交(Read Uncommitted)
  • 读提交(Read Committed)
  • 可重复读(Repeated Read)
  • 串行化(Serializable)

默认是:可重复读(Repeated Read)

在事务隔离级别为「可重复读」的情况下,会出现「幻读」,这是网上以及各种面试经常看到的问题。

但是「幻读」如何定义,哪些现象被称为「幻读」,却很少有明确的说法。

网上能看到最多的关于幻读的描述是:一个事务相同的 sql 语句读取 2 次,得到的记录条数不一致(由于其他事务在第二次读取之前插入了符合查询条件的数据)。但是 mysql 中隔离级别设置为「可重复读」时,并不存在这个现象,实验环境为10.1.22-MariaDB,大家可以自己实验下。

关于「幻读」的定义还有另一个说法,我们模拟一个业务中比较常见的场景来说明。例如:

给用户给帖子点赞,用 uid 表示用户 id,tid 表示帖子 id,uid + tid 设置为唯一索引。
假如说这时候用户 1 要为帖子 2 点赞,代码做的事情就是:查询用户 1 对帖子 2 有没有点赞记录,如果有就什么都不做,没有则新增一条记录。假如这个操作并发了,A 事务查询点赞记录时没有查到,同时 B 事务查询点赞记录也没有查到,之后 A 事务插入了点赞数据,并且提交事务,然后 B 事务再插入这条数据的时候就会报唯一键冲突,引发异常。这个场景下 B 事务遇到的情况就属于「幻读」

因为前一个说法在 mysql 默认的隔离级别下不存在,据说是“由于 InnoDB 引擎的「可重复读」级别还使用了 MVCC,避免了这个问题”,所以我个人理解后一种才算是「幻读」

关于 mvcc 这里涉及数据库的「并发控制」,可以参考浅谈数据库并发控制 - 锁和 MVCC

这里我还想喷一下部分公司的面试官,上来就问如果出现了「幻读」怎么解决,而不提具体场景,事实上可能大家理解的「幻读」就不是一回事,并且「幻读」是一个抽象概念,不如直接说场景,不知道幻读的概念,不见得不能解决问题

网上关于幻读的解释

  1. 第一个事务对一个表中的数据进行了修改,这种修改涉及到表中的全部数据行。同时,第二个事务也修改这个表中的数据,这种修改是向表中插入一行新数据。那么,以后就会发生操作第一个事务的用户发现表中还有没有修改的数据行,就好象发生了幻觉一样。来源

mysql autocommit

表示除非显式地开始一个事务,否则每个查询都被当做一个单独的事务自动执行

关于行锁

InnoDB 通过索引来实现行锁,而不是通过锁住记录。因此,当操作的两条不同记录拥有相同的索引时,也可能会因为行锁而发生等待。

参考文献:

任何复杂的东西,都是在简单东西的基础上构建出来的,关键是思路

任何东西,刚开始都是简陋的,随着时间的推移变得更好,关键是迭代

所以不要怕东西做的不好看,或者功能不全,先出一个能用的版本再说,否则想得再好都是一场空

遇到一个复杂问题不要怂,要能够拆解开来变成简单问题,等解决的复杂问题足够多了,之前的“复杂问题”就转变成了“简单问题”,之后就可以解决更复杂的问题。数学、物理等学科就很好的印证了这一点

所以我们需要的能力有两个:

  1. 解决简单问题的能力
  2. 化复杂为简单的能力

这两个能力组合在一起,就可以升级打怪,完成越来越复杂的任务

其实我们从小到大的学习过程,就是在培养这样一种能力

我相信这两个能力,智力正常的人都能够掌握。差别在于掌握这个能力的快慢,人与人的差异也就体现在这里

如果设定一个能力值,再给足够长的时间,所有人都能达到这个能力水平。可惜人的生命有限,不是所有人都能达到

那么我们让自己更强,就取决于花在提升自我能力上的时间,但是提升自我需要跳出舒适区,是很费力的事情,这个就需要人的自控力,做到坚持不懈才行

综上,人的核心竞争力在于 自控力思路,思路我认为是是天生的东西,后天难以培养,所以对于大众来说,提升自控力是一个成功的不二法门,但是与人先天的惰性做斗争也是一件很难的事情,所以成功不易,加倍努力

思路这个概念有点宽泛,指天才的想法

这里要设置一个添加一个非 sudo 用户组的

例如要添加一个名为deploy的用户

  • 执行adduser deploy,按照提示添加用户
  • 执行visudo编辑 sudo 列表,不能直接vim /etc/sudoers,在# User privilege specification下面,加入以下内容
deploy ALL=(ALL) NOPASSWD:NOPASSWD:ALL
  • Ctrl + x,选择 Y,退出

然后就可以了

要改用户组的话,执行usermod -aG www-data deploy

www-data 是用户组

《平凡的一天》里有句歌词:每天早上七点半就自然醒,听到的时候我就在想,怎么可能呢,七点半自然醒那得是老年人的作息吧。没想到国庆回家之后,我甚至还能 7 点自然醒,简直 book 11,最关键的是一整天也不觉得累。

假期一过,返沪上班,总觉得每天都很累。早上醒来累,上班路上累,工作的时候倒是不觉得,下班路上又开始累,回了家也累。
早上 8 点醒来累,9 点醒来也累,晚上早下班累,晚下班也累。不知道这是什么毛病。

也不知道是工作的原因还是自己的原因,反正就是 累 类 泪 lay。

难受