格雷姆林: ConcurrentModificationException和多线程问题

4

我的应用程序还没有上线,所以在它进入生产之前我正在测试我的 Gremlin 查询的性能。

为了测试,我正在使用一个查询,将边从一个顶点添加到 300 个其他顶点。它做了更多的事情,但这是简单的描述。我添加了这个提到的 300 的工作量只是为了测试。

如果我连续运行 300 次查询,几乎需要 3 分钟才能完成,并创建了 90,000 条边(300 x 300)。

我很担心,因为如果有 60,000 名用户同时使用我的应用程序,他们可能会在 2 分钟内使用此查询创建 90,000 条边,而在我的情况下,60,000 名用户同时在线并不算太多。 如果我有 1 百万名用户同时在线,我将需要许多服务器处于满负荷状态,这超出了我的预算。

然后我注意到当我的测试正在执行时,CPU 没有显示出很多活动,我不知道为什么,我不知道数据库在内部如何工作。 因此,我认为也许更真实的情况是同时调用我的查询,因为这就是真正用户要发生的事情,但当我尝试测试时,我得到了 ConcurrentModificationException。

据我所知,这个错误发生的原因是一个边或顶点在同时被 2 个查询读取或写入,这是我的应用程序中可能经常发生的事情,因为所有用户顶点都在不断地更改连接到同样的 4 个顶点,这些“冲突”将一直发生。

我正在使用通过 Node.js 使用 sockets 连接的 Gremlin Server 3.4.8 进行本地测试。当它进入生产时,我的计划是使用 AWS Neptune 作为我的数据库。

我该怎么做才能恢复希望呢?关于这个主题肯定有非常重要的东西我不知道,因为我不知道图形数据库在内部是如何工作的。

编辑

我实现了一个逻辑来重试查询请求,当收到错误时使用“指数回退”方法。这修复了 ConcurrentModificationException,但是在发送多个查询时,Gremlin Server 中存在许多问题,显示多线程在 Gremlin Server 中完全不受支持且不稳定,我们应该尝试在其他支持 Gremlin 的数据库中进行多线程操作,正如答案所说。我遇到了数据返回的随机不一致性以及来自数据库的 NegativeArraySize 和其他随机错误等问题,要注意这一点,以免浪费时间认为您的代码可能会出现错误,就像我遇到的那样。

2个回答

4
尽管TinkerPop和Gremlin试图提供与厂商无关的体验,但它们实际上只在其接口层面做到了这一点。因此,尽管您可能能够在JanusGraph、Neptune、CosmosDB等地方运行相同的查询,但您很可能会发现,根据查询的性质以及所讨论的图形优化该查询的能力程度,性能存在差异。
对于您的情况,考虑使用TinkerGraph,因为您正在本地运行测试。TinkerGraph是一种内存图,没有事务功能,并且对写入没有经过证明的线程安全性。如果您将大量写入工作负载应用于它,我可以想象容易生成ConcurrentModificationException。现在考虑一下JanusGraph。如果你以重写工作负载测试了它,你可能会发现,如果你的架构需要一个唯一的属性键,你会遇到大量的TemporaryLockingException错误,并且不得不修改您的代码进行带指数退避的事务重试。
这里的重点是,如果您的目标图形是Neptune,并且您已经针对正确性测试了遍历,现在关心性能,那么现在可能是时候在Neptune上进行负载测试,以查看是否存在任何问题了。
“我感到担忧,因为如果同时有60,000个用户使用我的应用程序,他们可能会在2分钟内创建90,000个边缘,并且在我的情况下,同时有60,000个用户并不多。如果我有100万个同时在线的用户,我将需要许多服务器处于满负荷状态,这超出了我的预算。”
您将需要制定一个现实的测试计划。 60000个用户同时按下“提交”按钮来触发此查询真的会发生吗?还是更可能有100,000个用户进行读取,并且每隔半秒钟有三个人碰巧点击“提交”按钮?
您的图增长速度似乎相当高,您在此处描述的预期使用情况很快就会将您的图放在数十亿个边缘的类别中(更不用说您可能有的其他写入)。您已经在拥有数十亿条边缘的图上测试了阅读工作负载吗?您明确在Neptune上测试过吗?您是否考虑过如何维护多十亿边缘的图形(例如,在需要新功能时更改模式,确保其正确增长等)?
所有这些问题都是修辞性的,只是旨在让您思考自己的方向。祝你好运!

1
你的回答非常有用,我曾经为寻找我认为是由多线程不支持数据库引起的错误而感到头痛,但实际上并非如此。我认为这应该是文档中首先显示的内容之一。我编辑了我的问题,警告访问者注意我的经验。 - fermmm
考虑在你的测试中使用 Neo4j 或 JanusGraph,因为它们具有良好的事务支持。Overlfowdb 可能是另一个选项,但我不确定它是否是线程安全的 - 我已经在这里发布了问题,希望有人能知道:https://twitter.com/spmallette/status/1313272097712599043 - stephen mallette
我已经尝试过JanusGraph,但是我遇到了同样的问题,它也不是线程安全的。我还没有尝试过Neo4j。 - fermmm
JanusGraph在这方面应该没有任何问题。你是说你用它遇到了ConcurrentModificationException异常吗?能提供完整的堆栈跟踪吗? - stephen mallette
不是的,但是如果在同时发送多个查询创建顶点,则在读取顶点时会出现java.lang.NullPointerException错误。如果我逐个发送顶点,则不会出现错误。此外,我还有另一个查询同时发送,用于创建许多边,然后在另一个查询中读取这些边时会得到重复的结果或其他随机不一致性,似乎同时发送多个查询以保存数据会生成数据损坏。 - fermmm
JanusGraph应该不会在同时执行多个查询时出现问题,而NullPointerException似乎是在并发查询情况下出现的奇怪问题。您可以考虑发布一个帖子在JanusGraph用户邮件列表上,介绍一下您正在做什么的背景信息。 - stephen mallette

1

虽然已经有一个被接受的答案,但我想提出另一种处理这个问题的方法。

我的想法是找出你是否真的需要在图上进行同步写操作。我建议当你收到请求时,只需使用此请求中的属性来获取子图/邻居,并继续业务逻辑。

同时将事件放入 SQS 或其他异步系统中,由 AWS Lambda 等异步系统负责写入。因为在 SQS + Lambda 中,您可以决定写并发性以适应您系统的舒适度(低于您的查询不会引起异常)。

进一步建议:您的写入爆炸半径异常高 - 在写入时,您的查询不应该触及那么多节点。您可以尝试将一些边缘转换为顶点,以减少半径。然后,在插入节点时,您只需向先前是边缘的顶点制作一个边缘,而不是向所有与此节点相关的顶点制作数百个边缘。希望这有意义。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接