【问题标题】:High CPU utilization when doing thousands of inserts with Hibernate使用 Hibernate 执行数千次插入时 CPU 使用率很高
【发布时间】:2015-06-17 19:56:12
【问题描述】:

我们最近使用 Hibernate 和 EntityManager(无 Spring)实现了 DB 绑定,以将记录写入数据库。为简单起见,我将仅讨论仅执行插入的过程的变体。 (另一个非常相似的过程会更新现有记录一次以设置状态,否则只会插入一堆记录。)

这个过程可以在每个事务中插入多达 10,000 条记录,尽管平均值少于该值,可能至少少了一半。我们可能在同一个 JVM 下同时在不同线程中运行该进程的几个实例。

我们遇到了一个生产问题,其中运行该进程的服务将机器上的所有 24 个内核都挂了出来。 (他们添加 12 只是为了适应这种情况。)我们已将这种高利用率缩小到 Hibernate。

我花了几天的时间研究并找不到任何可以提高我们性能的东西,除了将 hibernate.jdbc.batch_size 与 hibernate.order_inserts 一起使用。不幸的是,我们使用 IDENTITY 作为我们的生成策略,所以 Hibernate 可以/不会批处理这些插入。

我花了几天时间研究并没有发现在进行大量插入时的任何其他性能提示。 (我看过很多关于读取、更新和删除的技巧,但很少看到插入。)

我们有一个根 JobPO 对象。我们简单地调用它,所有的插入都是通过级联注释处理的。我们需要在单个事务中执行此操作。

我们只有 8 个要插入的不同表,但记录的层次结构有点复杂。

public void saveOrUpdate(Object dataHierarchyRoot) {
    final EntityManager entityManager = entityManagerFactory.createEntityManager();
    final EntityTransaction transaction = entityManager.getTransaction();

    try {
        transaction.begin();

        // This single call may result in inserting up to 10K records
        entityManager.merge(dataHierarchyRoot);
        transaction.commit();
    } catch (final Throwable e) {
        // error handling redacted for brevity
    } finally {
        entityManager.close();
    }
}

我们只创建一次 EntityManagerFactory。

有什么想法吗?

补充说明:

  • 没有人抱怨进程占用了太多内存

  • 对于只执行插入的过程的变体,我们可以只使用“persist”而不是“merge”。我们正在共享代码,因此我们进行了合并。我尝试转而坚持,但没有明显改善。

  • 我们确实有一些注释可以在一些字段上产生双向级联。我尝试删除这些,但对 Hibernate 不熟悉,无法正确保存。不过,据我了解,这似乎不会导致插入的性能下降。我没有使用显式的“反向”设置,因为这对于插入似乎也无关紧要。不过,我在这两个方面都有些摇摆不定。这方面还有改进的余地吗?

  • 我们在单个事务中运行 SQL Profiler。似乎没有什么不妥,我没有发现改进的余地。 (有大量的 exec sp_prepexec 语句,与插入的记录数大致相同。报告的就是这些。)

  • 在生产中表现出这种行为的代码在 commit() 之前对 entityManager.flush() 进行了显式调用。我在本地环境中删除了该代码。它没有带来明显的改善,但我不会加回去,因为我们没有理由调用flush()。

【问题讨论】:

  • 您提到 Hibernate 将所有 24 个内核都钉在一台机器上,完成插入需要多长时间?几秒钟或更多?如果所有 24 个内核都固定,则意味着您在业务逻辑中产生了大量线程。您能否详细解释一下您在插入时的多线程逻辑?
  • 对于 9K,大约需要 2 分钟。 DBA 表示数据库已尽可能地进行了调整。可以有多个进程实例在不同的线程中运行,为不同的对象调用我上面包含的代码。这些线程除了 EntityManagerFactory 之外不共享任何对象。
  • 我有过使用 Hibernate 在不到一秒的时间内插入包含 10000 条记录的树结构的经验。肯定有一些优化需要做。请分析您的代码以查找并修复效率低下的部分。为此,您可以使用 Yourkit。
  • 您是在单个事务中执行此操作吗?你有 batch_size 选项 > 1 吗?在最初的开发阶段,我们使用 Yourkit 获得了另一个开发人员资料,并报告说在 Hibernate 部分中没有“热点”。我会再试一次。我不抱太大希望。除了下面的建议,我不知道从代码的角度我们还能做什么。请注意,在提交之前调用 merge() 会占用至少一半的执行时间。目前我不知道是 merge() 还是 commit() 或者两者都占用了这么多 CPU。
  • 我不希望依赖 Hibernate 的合并,因为它会导致性能问题,因为整个过程超出了您的控制范围。您能否将加载数据和进行手动合并的过程分开?这使您可以分别对每个部分进行优化。可以使用 HQL 完成加载。根据您的模型的复杂程度,合并代码可能很简单也可能很复杂。

标签: java performance hibernate jpa transactions


【解决方案1】:

如果您为要保存的每个对象打开和关闭一个 Session,那么对于 10k 个对象,您实际上打开和关闭 10k 个 Session,刷新 10k 次并访问数据库进行 10k 次往返。

你们至少应该一起batch multiple entities

for (Object entity: entities) {    
    if(entity.getId() == null) {
        entityManager.persist(entity);
    } else {
        entityManager.merge(entity);
    }   
    if ((i % batchSize) == 0) {
        entityManager.getTransaction().commit();
        entityManager.clear();          
        entityManager.getTransaction().begin();
    }
}
entityManager.getTransaction().commit();
em.getTransaction().commit();

在此示例中,您实际上使用的是一个数据库连接,因此即使您使用连接池,您也不必获取/释放 10k 数据库连接。达到batchSize 阈值后会清除 Session,从而减少 JVM 垃圾回收。

如果您在会话中存储 10k 个实体并立即提交事务,您将遇到以下问题:

  • 数据库将持有更长时间的锁,并会创建大量撤消事务日志(以防您的数据库使用 MVCC)
  • 实体不会被垃圾回收,因为它们仍然附加到 Hibernate 会话

【讨论】:

  • 对于 10K 条记录,我们有一个事务和一个会话。保存根作业记录是导致插入 10K 记录的原因。在我提供的示例代码中,对 merge(object) 的单个调用将导致插入多达 10K 条记录。我们需要将所有这些记录保存在一个事务中。 (我们可以在没有事务的情况下做到这一点,但不能不跳槽。它们应该全部插入或不插入。这就是事务的用途。)
  • 理想情况下是的,但实际上添加大量资源需要大量资源。我想知道你真的不能把这 10k 分成更小的批次。
  • 谢谢。我们基本上保存了一个树结构,所以记录都有一个直接或间接的链接到根对象。更新我们的代码以将它们分批插入,我认为这是一种 hack,会使代码/测试复杂化很多。
  • 另外,如果这是解决方案,我会说 Hibernate 不是企业级的,尽管它被广泛使用。我们通常使用的本土版本可以在单个事务中处理这个 # 没有问题。如果事实证明这是 Hibernate 中唯一的解决方案,我们可能会考虑增强我们的本地解决方案。 (我们切换到 Hibernate 是因为我们的本地解决方案目前无法处理 BigInt 作为主键。)
  • 即使这不是一个简洁的解决方案,我也会将此选项(以及可能推荐的任何其他选项)提交给我们的架构师进行审查。感谢您的信息。
【解决方案2】:

好吧,您应该避免在每次更新时打开和关闭连接,因为这会损害性能。相反,您可以将持久性提供程序配置为使用批处理并设置合理的数量,然后执行批处理更新。

<persistence-unit name="pu" transaction-type="RESOURCE_LOCAL">
    <provider>org.hibernate.ejb.HibernatePersistence</provider>
        <properties>
            <property name="hibernate.dialect" value="org.hibernate.dialect.OracleDialect"/>
            <property name="hibernate.connection.username" value="***"/>
            <property name="hibernate.connection.password" value="***"/>
            <property name="hibernate.connection.driver_class" value="oracle.jdbc.OracleDriver"/>
            <property name="hibernate.connection.url" value="jdbc:oracle:thin:@***"/>
            <property name="hibernate.jdbc.batch_size" value="100"/>   
        </properties>
    </persistence-unit>

当您更新/插入循环时,这允许在单个命令中向数据库发送多个更新查询(这对您来说是透明的)。

Session session = SessionFactory.openSession();
Transaction tx = session.beginTransaction();
for ( int i=0; i<100000; i++ ) {
    Employee employee = new Employee(.....);
    session.save(employee);
}
tx.commit();
session.close();

参考:http://www.tutorialspoint.com/hibernate/hibernate_batch_processing.htm

【讨论】:

  • 正如我的帖子中提到的,我们不能使用 batch_size 属性,因为我们对主键使用了 GenerationType.IDENTITY 生成策略。现在改变我们的策略不是一种选择。
【解决方案3】:

解决方案(或者至少是一种大大降低 CPU 使用率的方法)是从合并切换到持久化。我在帖子中提到过,我尝试切换到坚持,但没有明显区别。

我随后找到了一种更好的方法来分析重负载,并且能够显示出改进。针对我正在运行的特定负载从持久切换到合并将平均 CPU 百分比从 16 降低到 5。

我们不需要合并。对于永久修复,我们需要稍微修改代码,以便能够使用相同的 EntityManager 来加载根对象,然后将其持久化(然后将级联完整的“树”结构)。这样,我们的对象就不会分离,也不需要使用合并。

感谢 ddalton 指出这个方向。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2022-12-05
    • 2020-11-13
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多