lealone / Lealone

比 MySQL 和 MongoDB 快10倍的 OLTP 关系数据库和文档数据库
Other
2.48k stars 519 forks source link

嵌入式场景多线程并发更新在生产环境导致数据损坏 #207

Closed cbqqkcel closed 1 year ago

cbqqkcel commented 1 year ago

2023-10-26T18:34:10.351+08:00 ERROR 16245 --- [nio-5220-exec-7] c.c.i.c.advice.GlobalResponseHandler : Throwable

java.lang.IllegalStateException: Position 0 [5.2.0/6] at org.lealone.common.util.DataUtils.newIllegalStateException(DataUtils.java:616) ~[lealone-common-5.2.0.jar!/:na] at org.lealone.storage.aose.btree.BTreeStorage.readPage(BTreeStorage.java:165) ~[lealone-aose-5.2.0.jar!/:na] at org.lealone.storage.aose.btree.page.PageReference.readPage(PageReference.java:199) ~[lealone-aose-5.2.0.jar!/:na] at org.lealone.storage.aose.btree.page.PageReference.getOrReadPage(PageReference.java:175) ~[lealone-aose-5.2.0.jar!/:na] at org.lealone.storage.aose.btree.page.NodePage.getChildPage(NodePage.java:51) ~[lealone-aose-5.2.0.jar!/:na] at org.lealone.storage.aose.btree.page.Page.gotoLeafPage(Page.java:267) ~[lealone-aose-5.2.0.jar!/:na] at org.lealone.storage.aose.btree.BTreeMap.getObjects(BTreeMap.java:178) ~[lealone-aose-5.2.0.jar!/:na] at org.lealone.transaction.aote.AOTransactionMap.getObjects(AOTransactionMap.java:563) ~[lealone-aote-5.2.0.jar!/:na] at org.lealone.db.index.standard.StandardPrimaryIndex.getRow(StandardPrimaryIndex.java:325) ~[lealone-db-5.2.0.jar!/:na] at org.lealone.db.table.StandardTable.getRow(StandardTable.java:230) ~[lealone-db-5.2.0.jar!/:na] at org.lealone.db.index.standard.StandardSecondaryIndex$StandardSecondaryIndexCursor.get(StandardSecondaryIndex.java:305) ~[lealone-db-5.2.0.jar!/:na] at org.lealone.db.index.standard.StandardSecondaryIndex$StandardSecondaryIndexCursor.get(StandardSecondaryIndex.java:299) ~[lealone-db-5.2.0.jar!/:na]

cbqqkcel commented 1 year ago

昨天异常太多了。我没有全部发上来。后来代码改了。没有测试代码了。 遇到过 Async callback is null, may be a bug! packetId 这个异常信息。 操作步骤是我一条一条的从 room 表查询把好的数据导出到新的 room2 表中遇到的。刚开始很快到后面就特别慢。

cbqqkcel commented 1 year ago

java.lang.StackOverflowError

昨天遇到最多的错是这个。查询一个表全部数据的时候。我觉得是数据坏了的问题导致的。很多表中的字段都是异常信息。

codefollower commented 1 year ago

同时出现 OutOfMemoryError 和数据损坏这两种情况,就说明没有比这个更糟糕的了,数据库出现任何错误提示都是不奇怪的。

这个大问题的原因总结起来就两个: 1. 不能在嵌入式场景下多线程使用 lealone 了,这个很明确是有问题,并且一定会导致数据损坏的;2. 把 -XX:MaxDirectMemorySize 这个参数去掉,没用的,还可能导致 OOM。

我看了你给我的数据,数据量很小的,比我压测 tpcc 少了几百倍,表结构也没什么特殊的。 换成 client-server 模式后试试吧。

cbqqkcel commented 1 year ago

clinet-server 依旧不行 因为客户着急用我昨天晚上把数据库临时迁移到 MySQL 了。 刚才从mysql导出的sql insert 语句数据都插入进去了。查询很多数据的时候还是会报错 insert 是用datagrip 命令行插入的。

image

cbqqkcel commented 1 year ago

为确保干净,5.2 的 zip我刚才又重新下载了一遍,data 目录下的数据全部删除重新搞得 image

是不是这个表有问题

cbqqkcel commented 1 year ago

image

cbqqkcel commented 1 year ago

要不我把sql文件和操作步骤发给你看看

codefollower commented 1 year ago

room 表有多少行数据?你在 client-server 模式下重新 insert 数据是在一张空的 room 表下进行,还是在原有的数据上进行?

cbqqkcel commented 1 year ago

26549 条数据。空表,全部都是重新来过的。

codefollower commented 1 year ago

26549 条数据根本就无关痛痒,你的内存那么大,写这点数据都是直接放内存中的,还轮不到写硬盘后数据损坏。 你直接把你导入数据的 sql 发到我邮箱吧,还是我直接操作调试能更快找到原因。

我只用256M内存压测 tpcc 上百万的记录都没问题,所以我就特别奇怪你的场景到底是哪出了问题。

cbqqkcel commented 1 year ago

26549 条数据根本就无关痛痒,你的内存那么大,写这点数据都是直接放内存中的,还轮不到写硬盘后数据损坏。 你直接把你导入数据的 sql 发到我邮箱吧,还是我直接操作调试能更快找到原因。

我只用256M内存压测 tpcc 上百万的记录都没问题,所以我就特别奇怪你的场景到底是哪出了问题。

步骤和文件发你了,如果是硬件问题那就尴尬了

codefollower commented 1 year ago

我定位到大概的原因了,当使用 from room limit 0,30000 查询时,记录数只要超过1万,lealone 默认就把结果集写到一个临时文件,写临时文件可能存在 bug,所以导致读取的时候出现 java.lang.IllegalStateException: Position 0 那个错误。 临时的解决方案就是执行 set max_memory_rows 1000000; 语句,把在内存中查到的结果集记录数调大一些,不写到临时文件。

insert 到 room 表的数据是没问题的,只是读查询结果集的临时文件出问题了。

forifido commented 1 year ago

-XX:MaxDirectMemorySize的作用: 大的DirectByteBuffer在堆内占用很少的内存空间,但是在堆外占用大量的内存,如果不加以限制,超过物理内存的大小就麻烦了。特别是,一些使用时间比较长的DirectByteBuffer对象,晋升到老年代,然后又变成垃圾了,如果老年代gc迟迟不触发,这部分堆外内存就不会释放。加了-XX:MaxDirectMemorySize以后,分配堆外内存如果超限制了,会触发一次堆内的gc,释放掉占坑不拉屎的DirectByteBuffer对象。 当然更好的办法是手工释放DirectByteBuffer,在java里面没有直接的办法,需要通过非标准api。

lealone 内部自己实现了一套 GC 算法,分配的 DirectByteBuffer 的大小是受 -Xmx 限制的,所以不会超过物理内存的大小,同时 GC 算法也会动态把 DirectByteBuffer 对象置 null,这时 JVM 的 GC 线程可以及时回收。 所以设置 -XX:MaxDirectMemorySize 参数对 lealone 是没有任何意义的,反而误让 JVM 干坏事了。

这里有点疑惑,你的意思是你能保证堆和直接内存的占用之和不超过 Xmx 的配置?

cbqqkcel commented 1 year ago

我定位到大概的原因了,当使用 from room limit 0,30000 查询时,记录数只要超过1万,lealone 默认就把结果集写到一个临时文件,写临时文件可能存在 bug,所以导致读取的时候出现 java.lang.IllegalStateException: Position 0 那个错误。 临时的解决方案就是执行 set max_memory_rows 1000000; 语句,把在内存中查到的结果集记录数调大一些,不写到临时文件。

insert 到 room 表的数据是没问题的,只是读查询结果集的临时文件出问题了。

非常有可能,我之前导出数据的时候 5000一次的查询就不会有问题。过1万就有可能出问题。

codefollower commented 1 year ago

-XX:MaxDirectMemorySize的作用: 大的DirectByteBuffer在堆内占用很少的内存空间,但是在堆外占用大量的内存,如果不加以限制,超过物理内存的大小就麻烦了。特别是,一些使用时间比较长的DirectByteBuffer对象,晋升到老年代,然后又变成垃圾了,如果老年代gc迟迟不触发,这部分堆外内存就不会释放。加了-XX:MaxDirectMemorySize以后,分配堆外内存如果超限制了,会触发一次堆内的gc,释放掉占坑不拉屎的DirectByteBuffer对象。 当然更好的办法是手工释放DirectByteBuffer,在java里面没有直接的办法,需要通过非标准api。

lealone 内部自己实现了一套 GC 算法,分配的 DirectByteBuffer 的大小是受 -Xmx 限制的,所以不会超过物理内存的大小,同时 GC 算法也会动态把 DirectByteBuffer 对象置 null,这时 JVM 的 GC 线程可以及时回收。 所以设置 -XX:MaxDirectMemorySize 参数对 lealone 是没有任何意义的,反而误让 JVM 干坏事了。

这里有点疑惑,你的意思是你能保证堆和直接内存的占用之和不超过 Xmx 的配置?

在 java 程序中肯定不能像 c++ 那样精确控制占用了多么内存,但是可以用个预估值,比如写入一条记录可以根据字段类型以及 jvm 分配一个对象占用多少字节做一个预估,然后动态累加,删除记录时再减去。启动时拿到-Xmx的最大值,取1/3,只要累加的内存使用量超过这个阈值了 lealone 就会启动 GC 任务,回收掉数据库中那些可以踢出去的对象。

codefollower commented 1 year ago

clinet-server 依旧不行 因为客户着急用我昨天晚上把数据库临时迁移到 MySQL 了。 刚才从mysql导出的sql insert 语句数据都插入进去了。查询很多数据的时候还是会报错 insert 是用datagrip 命令行插入的。

image

调试了代码,确实是查询记录数超过1万条后生成临时文件有 bug 导致的。

codefollower commented 1 year ago

java.lang.StackOverflowError

昨天遇到最多的错是这个。查询一个表全部数据的时候。我觉得是数据坏了的问题导致的。很多表中的字段都是异常信息。

这个问题我找到原因了,查表超过1万行时,写临时文件是在一个大事务中写所有记录,所以产生了很多次 page split,在对 page 标记脏页时使用了递归,递归太深,所以就堆栈溢出了。