-
Notifications
You must be signed in to change notification settings - Fork 516
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
嵌入式场景多线程并发更新在生产环境导致数据损坏 #207
Comments
查询数据库时报错了 |
这个是你自己打的 jar 包? |
是的 |
是基于什么时候的代码?你没有改动过吧? |
我看这个异常,是不是通过嵌入式的方式使用数据库的?堆栈没有看到调度服务线程的信息。 |
没有改过代码,是嵌入式的。 |
java.lang.IllegalStateException: Position 0 [5.2.0/6] |
这个表新的数据有问题,不查询新的数据不会出错。 |
用6.0.0-SNAPSHOT读这个数据还是一样的。估计是数据坏了 |
这个表的主键特别长,还有中文 |
如果直接内嵌在 tomcat 的容器里跑,并发读写 lealone 用的是 tomcat 的线程,这时就绕过 lealone 内部的调度服务线程了。嵌入式环境使用 lealone 然后用容器的并发线程读写会有并发访问的问题,这个我之前没有考虑到,我以为嵌入式场景是单线程的。 |
lealone 整个并发控制系统是完全依赖 lealone 调度服务线程的,如果调度服务线程不参与并发读写了,是一定会发生并发访问问题的。 |
lealone-tomcat 那个插件也是内嵌在 tomcat 中跑的,但是我用 lealone 的调度服务线程完全替换掉 tomcat 的线程池了,所以并发读写 lealone 是安全的。 |
那怎么办,想着删除今天的数据。也不能删的 |
主表的少量 page 因为并发读写出问题有可能损坏了,你需要把数据库备份一下,然后执行 repair table 语句修复一下看看还有问题没有。 修复完之后还是建议先改成 client-server 的方式访问数据库,目前这种直接内嵌在 tomcat 中用它的线程并发读写 lealone 我已经发现是会有并发问题然后容易导致数据损坏的。 |
repair table 没用。 |
直接去对应的表目录下,找到尾部数字最大的 chunk 文件,删掉它之后最新的数据就从上一个 chunk 文件开始。 |
表的 id 可以通过 information schema 的 db_objects 表查询 |
information schema 的 tables 表也可以查表 id |
select id,database_name from information_schema.databases; |
如果 redo_log 目录中还有数据,删除表的最新 chunk 之后是可以在重新启动时自动恢复的。 |
data\redo_log\archives 目录中的 redoLog_xxx 文件还保留了几天?虽然已经归档了,依然可以从这些归档的 redoLog 文件恢复到今天的数据的。不过我还没有开发工具从 archives 目录中的 redoLog_xxx 文件恢复,重启数据库时只是从未归档的 redoLog 文件恢复。 |
只有10-25和26日的 |
那个 lealone-5.2.0.jar 里面是不是没有 snakeyaml?没有的话就不读 lealone.yaml |
没有 |
你下个 zip 或 gz 的包,然后去 bin 目录下运行,这个才是完整的分发包。 |
data\redo_log 目录及其子目录下的所有文件都很重要,如果数据损坏后又不允许丢失一条数据,就可以通过 redo_log 目录中的文件恢复。从 redo_log 归档文件恢复数据的工具我还没做,如果你的数据不能丢失,我可以花点时间做个工具,你可以等一段时间,等我做完了我就把工具给你恢复数据,如果数据丢点没关系,那我就不着急做这个工具了,会把这个功能规划到 lealone 6.0 中。 |
可以丢失只要能回到今天之前。不过今天才发现之前的数据也有问题。 |
我这个项目是每月使用一次 |
之前的数据有几张表报 [2023-10-26 21:58:35] [HY000][50000] General error: "java.lang.StackOverflowError" [50000-0] |
这个我试了一下是个代码的 bug,一个线程准备做 GC 时,刚拿到当前使用的表对应的 StorageMap 的名字,另外一个线程就把表删除了,这时再用 StorageMap 的名字 get 出 StorageMap 对象就为 null 了,然后没有判断是否为 null 就去拿它调用 isClosed() 方法,最后就抛出 NullPointerException。 这个 bug 容易修复,加上 if(map != null) 即可。 |
这个问题我也重现出来了,原因是看到你配的参数是 set JAVA_OPTS=-ea -Xms1G -Xmx16G -XX:MaxDirectMemorySize=1G 我才突然明白问题是出在 -XX:MaxDirectMemorySize=1G 这个参数,把它去掉就好了,运行 java 开发的数据库不需要它。 以下是原因: 使用 java 开发的数据库,为了避免经常被 JVM GC 暂停影响会选择使用 java.nio.DirectByteBuffer 也就是 DirectMemory 来管理内存中的数据,lealone 也不例外,也大量使用了 DirectByteBuffer,并且 lealone 只在乎 -Xmx 这个参数,直接忽视 -XX:MaxDirectMemorySize,也就是说如果 -Xmx 设置为 16G,lealone 就会尽可能的把数据装载到 DirectByteBuffer 中,并不会考虑 -XX:MaxDirectMemorySize 这个参数,哪怕你设置了 1G,lealone 在装了 1G 的 DirectByteBuffer 后,看到最大 -Xmx 是 16G,还小得很呢,就继续加载数据到 DirectByteBuffer,但是 JVM 本身是要按 -XX:MaxDirectMemorySize 这个参数办事的,当 lealone 加载的 DirectByteBuffer 超过 1G了,JVM 就抛出 OutOfMemoryError 了。 我也是第一次碰到这个问题,-XX:MaxDirectMemorySize 这个参数在我的印象里我从来没用过,限制它的大小带来的好处我也不知道有哪些,缺点对于数据库来说就很致命了。 |
-XX:MaxDirectMemorySize的作用: 大的DirectByteBuffer在堆内占用很少的内存空间,但是在堆外占用大量的内存,如果不加以限制,超过物理内存的大小就麻烦了。特别是,一些使用时间比较长的DirectByteBuffer对象,晋升到老年代,然后又变成垃圾了,如果老年代gc迟迟不触发,这部分堆外内存就不会释放。加了-XX:MaxDirectMemorySize以后,分配堆外内存如果超限制了,会触发一次堆内的gc,释放掉占坑不拉屎的DirectByteBuffer对象。 当然更好的办法是手工释放DirectByteBuffer,在java里面没有直接的办法,需要通过非标准api。 |
lealone 内部自己实现了一套 GC 算法,分配的 DirectByteBuffer 的大小是受 -Xmx 限制的,所以不会超过物理内存的大小,同时 GC 算法也会动态把 DirectByteBuffer 对象置 null,这时 JVM 的 GC 线程可以及时回收。 |
就这个情况-Xmx16G -XX:MaxDirectMemorySize=1G这样设置是不合理的(其实在很多数情况下这个设置也不太合理,Xmx比MaxDirectMemorySize大太多)。 有的时候,光把DirectByteBuffer 对象置 null是不够的,现在java主流gc算法都是分代的比如G1和CMS,如果一个DirectByteBuffer能够在多个minor gc后保持存活,就晋升到老年代去了,有的程序老年代gc要很久才触发一次(比如大部分对象都是短生命周期的,都被minor gc干掉了,老年代就迟迟不会满),这样在老年代gc发生前,这个设置为null的DirectByteBuffer 都不会回收。 不分代的gc算法比如zgc下会如何我不太清楚,但现在使用最广泛的gc还是分代的。 |
只设置-Xmx是最好的,因为我看到报了一个错,网上说是MaxDirectMemorySize太小了。所以就调大了 |
没事的,这里只是讨论技术问题,如果下次遇到运行 lealone 出现任何问题,直接发来 github 就可以了,不用先去网上查的。 |
昨天异常太多了。我没有全部发上来。后来代码改了。没有测试代码了。 |
昨天遇到最多的错是这个。查询一个表全部数据的时候。我觉得是数据坏了的问题导致的。很多表中的字段都是异常信息。 |
同时出现 OutOfMemoryError 和数据损坏这两种情况,就说明没有比这个更糟糕的了,数据库出现任何错误提示都是不奇怪的。 这个大问题的原因总结起来就两个: 1. 不能在嵌入式场景下多线程使用 lealone 了,这个很明确是有问题,并且一定会导致数据损坏的;2. 把 -XX:MaxDirectMemorySize 这个参数去掉,没用的,还可能导致 OOM。 我看了你给我的数据,数据量很小的,比我压测 tpcc 少了几百倍,表结构也没什么特殊的。 |
要不我把sql文件和操作步骤发给你看看 |
room 表有多少行数据?你在 client-server 模式下重新 insert 数据是在一张空的 room 表下进行,还是在原有的数据上进行? |
26549 条数据。空表,全部都是重新来过的。 |
26549 条数据根本就无关痛痒,你的内存那么大,写这点数据都是直接放内存中的,还轮不到写硬盘后数据损坏。 我只用256M内存压测 tpcc 上百万的记录都没问题,所以我就特别奇怪你的场景到底是哪出了问题。 |
步骤和文件发你了,如果是硬件问题那就尴尬了 |
我定位到大概的原因了,当使用 from room limit 0,30000 查询时,记录数只要超过1万,lealone 默认就把结果集写到一个临时文件,写临时文件可能存在 bug,所以导致读取的时候出现 java.lang.IllegalStateException: Position 0 那个错误。 insert 到 room 表的数据是没问题的,只是读查询结果集的临时文件出问题了。 |
这里有点疑惑,你的意思是你能保证堆和直接内存的占用之和不超过 Xmx 的配置? |
非常有可能,我之前导出数据的时候 5000一次的查询就不会有问题。过1万就有可能出问题。 |
在 java 程序中肯定不能像 c++ 那样精确控制占用了多么内存,但是可以用个预估值,比如写入一条记录可以根据字段类型以及 jvm 分配一个对象占用多少字节做一个预估,然后动态累加,删除记录时再减去。启动时拿到-Xmx的最大值,取1/3,只要累加的内存使用量超过这个阈值了 lealone 就会启动 GC 任务,回收掉数据库中那些可以踢出去的对象。 |
这个问题我找到原因了,查表超过1万行时,写临时文件是在一个大事务中写所有记录,所以产生了很多次 page split,在对 page 标记脏页时使用了递归,递归太深,所以就堆栈溢出了。 |
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]
The text was updated successfully, but these errors were encountered: