7

我在 ZIO 应用程序中使用 Doobie,有时会出现死锁(应用程序完全冻结)。如果我只在一个内核上运行我的应用程序,或者如果我达到与数据库的最大并行连接数,就会发生这种情况。

我的代码如下所示:

def mkTransactor(cfg: DatabaseConfig): RManaged[Blocking, Transactor[Task]] =
    ZIO.runtime[Blocking].toManaged_.flatMap { implicit rt =>
      val connectEC = rt.platform.executor.asEC
      val transactEC = rt.environment.get.blockingExecutor.asEC

      HikariTransactor
        .fromHikariConfig[Task](
          hikari(cfg),
          connectEC,
          Blocker.liftExecutionContext(transactEC)
        )
        .toManaged
    }

  private def hikari(cfg: DatabaseConfig): HikariConfig = {
    val config = new com.zaxxer.hikari.HikariConfig

    config.setJdbcUrl(cfg.url)
    config.setSchema(cfg.schema)
    config.setUsername(cfg.user)
    config.setPassword(cfg.pass)

    config
  }

或者,我在 Hikari ( config.setLeakDetectionThreshold(10000L)) 上设置了泄漏检测参数,我得到泄漏错误,这不是由于处理数据库查询所花费的时间。

4

1 回答 1

10

Doobie 文档中有一个关于执行上下文和每个期望的很好的解释:https ://tpolecat.github.io/doobie/docs/14-Managing-Connections.html#about-transactors

根据文档,“等待连接到数据库的执行上下文”(connectEC在问题中)应该是有界的。

ZIO,默认情况下,只有两个线程池:

  1. zio-default-async– 有界,
  2. zio-default-blocking– 无界

所以很自然地相信我们应该使用zio-default-async它,因为它是有界的。

不幸的是,zio-default-async假设它的操作永远不会阻塞。这非常重要,因为它是ZIO解释器(它的运行时)用来运行的执行上下文。如果你阻止它,你实际上可以阻止ZIO程序的评估进程。当只有一个内核可用时,这种情况更常见。

问题是等待 DB 连接的执行上下文意味着阻塞,等待 Hikari 连接池中的可用空间。所以我们应该使用zio-default-async这个执行上下文。

下一个问题是:创建一个新的线程池和相应的执行上下文是否有意义connectEC?没有什么禁止您这样做,但可能没有必要这样做,原因有以下三个:

  • 您希望避免创建线程池,特别是因为您可能已经从 Web 框架、数据库连接池、调度程序等创建了多个线程池。每个线程池都有其成本。一些例子是:

    • 为 jvm JVM 管理更多内容
    • 消耗更多操作系统资源
    • 在线程之间切换,这部分在性能方面很昂贵
    • 使您的应用程序运行时更难理解(复杂的线程转储等)
  • ZIO线程池人体工程学开始针对它们的使用进行了很好的优化

  • 归根结底,您将不得不在某个地方管理超时,并且连接不是系统中最有可能有足够信息知道它应该等待多长时间的部分:不同的交互(即,在应用程序的外部部分,更接近使用点)可能需要不同的超时/重试逻辑。

话虽如此,我们发现了一个在生产中运行的应用程序中运行良好的配置:

// zio.interop.catz._ provides a `zioContextShift`

  val xa = (for {
    // our transaction EC: wait for aquire/release connections, must accept blocking operations
    te <- ZIO.access[Blocking](_.get.blockingExecutor.asEC)
  } yield {
    Transactor.fromDataSource[Task](datasource, te, Blocker.liftExecutionContext(te))
  }).provide(ZioRuntime.environment).runNow

  def transactTask[T](query: Transactor[Task] => Task[T]): Task[T] = {
    query(xa)
  }

我绘制了 Doobie 和 ZIO 执行上下文如何相互映射的图:https ://docs.google.com/drawings/d/1aJAkH6VFjX3ENu7gYUDK-qqOf9-AQI971EQ4sqhi2IY

更新:我在这里创建了一个包含 3 个该模式使用示例(混合应用程序、纯应用程序、ZLayer 应用程序)的存储库:https ://github.com/fanf/test-zio-doobie 欢迎任何反馈。

于 2020-10-15T12:24:08.573 回答