5

我确实知道如何创建自己的 ExecutionContext 或导入全局播放框架。但我必须承认,我远不是关于多重上下文/执行服务如何在后面工作的专家。

所以我的问题是,为了更好的服务性能/行为,我应该使用哪个 ExecutionContext?

我测试了两个选项:

import play.api.libs.concurrent.Execution.defaultContext

implicit val executionContext = ExecutionContext.fromExecutorService(Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors()))

两者都产生了可比的性能。

我使用的动作在 playframework 2.1.x 中是这样实现的。SedisPool 是我自己的对象,带有一个普通的 sedis/jedis 客户端池的额外 Future 包装。

def testaction(application: String, platform: String) = Action {
    Async(
      SedisPool.withAsyncClient[Result] { client =>
        client.get(StringBuilder.newBuilder.append(application).append('-').append(platform).toString) match {
          case Some(x) => Ok(x)
          case None => Results.NoContent
        }
      })
  }

这种性能方面的表现与 Node.js 和 Go 中完全相同的函数一样好或稍慢。但仍然比 Pypy 慢。但是比 Java 中的同样事情要快得多(在这种情况下,使用 jedis 对 redis 进行阻塞调用)。我们使用 gatling 进行负载测试。我们在 redis 之上进行简单服务的技术“竞争”,标准是“与编码人员一样多的努力”。我已经使用 fyrie 测试了它(除了我不喜欢 API 的事实)它的行为几乎与这个 Sedis 实现相同。

但这不是我的问题。我只是想了解更多关于 playframework/scala 的这一部分。

有建议的行为吗?或者有人可以为我指出一个更好的方向吗?我现在开始使用 scala,我远不是专家,但我可以自己完成代码答案。

谢谢你的帮助。

更新 - 更多问题!

篡改池中的线程数后发现:Runtime.getRuntime().availableProcessors() * 20

为我的服务提供了大约 15% 到 20% 的性能提升(以每秒请求数和平均响应时间来衡量),这实际上使它比 node.js 和 go 稍微好一点(尽管几乎没有)。所以我现在有更多问题: - 我测试了 15 倍和 25 倍,而 20 似乎是一个最佳点。为什么?有任何想法吗?- 会有其他更好的设置吗?其他“甜蜜点”?- 20x 是最佳点还是取决于我正在运行的机器/jvm 的其他参数?

更新 - 有关该主题的更多文档

找到有关播放框架文档的更多信息。 http://www.playframework.com/documentation/2.1.0/ThreadPools

对于 IO,他们确实对我所做的事情提出了一些建议,但提供了一种通过 Akka.dispatchers 进行操作的方法,该方法可通过 *.conf 文件进行配置(这应该让我的操作很高兴)。

所以现在我正在使用

implicit val redis_lookup_context: ExecutionContext = Akka.system.dispatchers.lookup("simple-redis-lookup")

调度程序由

akka{
    event-handlers = ["akka.event.slf4j.Slf4jEventHandler"]
    loglevel = WARNING
    actor {
        simple-redis-lookup = {
            fork-join-executor {
                parallelism-factor = 20.0   
                #parallelism-min = 40
                #parallelism-max = 400
            }
        }
    }
}

它给了我大约 5% 的提升(现在正在关注它),并且一旦 JVM “热”,性能就会更加稳定。我的管理员很乐意在不重建服务的情况下使用这些设置。

我的问题仍然存在。为什么会有这个数字?

4

2 回答 2

11

我对优化的看法是:

  1. 看看单线程性能,然后
  2. 看看事情是如何并行的,然后
  3. 冲洗并重复,直到获得所需的性能或放弃。

单线程优化

单个线程的性能通常取决于代码的单个组件或部分,它可能是:

  • 一个受 CPU 限制的部分,实际上可能在从RAM读取时受到限制(这不是分页)。JVM 和更高级别的工具通常无法区分 CPU 和 RAM。性能分析器(例如 JProfiler)对于定位代码热点 非常有用)
    • 您可以通过优化代码以降低 CPU 使用率或 RAM 读/写率来提高性能
  • 分页问题,​​应用程序内存不足,正在向磁盘或从磁盘进行 分页
    • 您可以通过添加 RAM、减少内存使用、为进程分配更多物理 RAM 或减少操作系统上的内存负载来提高性能
  • 延迟问题,线程正在等待从套接字、磁盘或类似设备中读取数据,或者在将数据提交到磁盘时等待。
    • 您可以通过使用更快的磁盘(例如 spin rust -> SSD)、使用更快的网络(1GE -> 10GE)或通过提高您正在使用的网络应用程序的响应能力(调整数据库)来提高单线程性能

但是,如果您可以运行多个线程,那么单线程中的延迟就不会那么令人担忧了。当一个线程被阻塞时,另一个线程可以使用 CPU(用于交换上下文和替换 CPU 缓存中大部分项目的开销)。那么你应该运行多少个线程呢?

多线程

假设线程大约 50% 的时间花在 CPU 上,50% 的时间在等待 IO。在这种情况下,每个 CPU 可以被 2 个线程充分利用,您会看到 2 倍的吞吐量提升。如果线程花费大约 1% 的时间使用 CPU,那么您应该(在所有条件相同的情况下)能够同时运行 100 个线程。

但是,这可能会发生很多奇怪的效果:

  1. 上下文切换有(一些)成本,因此理想情况下您需要将它们最小化。如果您的延迟时间很少且较大,而不是频繁且较小,您将获得更好的整体系统性能。这种效果意味着将线程增加nx 倍,您将永远无法获得nx 倍的吞吐量提升。而过了一个临界点,随着你的增加n,你的表现也会随之下降
  2. 同步、信号量和互斥量。代码的小部分通常会获取信号量或互斥锁,以确保任何时候只有一个(或有限数量)线程可以进入。虽然只有几个线程,但这很少影响性能。但是,如果此代码块花费任何可观的时间,并且有很多线程,这将成为系统性能的门控因素。例如,假设一个受保护的单线程块需要 10 毫秒才能执行,例如通过查询数据库。因为一次只能进入一个线程,所以实际执行的最大线程数为 1000 毫秒/10 毫秒,即 100 个。所有其他线程最终将在此块的队列中彼此落后。
  3. 资源:当您增加并行度时,您正在加载各种以前轻负载的组件。随着这些变得越来越重,其他线程最终会阻塞等待来自它们的数据。最终,额外的并行性最终会在计算机上的所有线程中产生延迟。这些组件包括:
    1. 内存
    2. 磁盘通道
    3. 网络
    4. 网络服务(例如您的数据库)。我无法告诉您我已经优化了多少次 Java,以至于数据库限制了吞吐量。

如果发生这种情况,那么您需要重新考虑您的算法,更改服务器、网络或网络服务,或者降低并行度。

影响您可以运行多少线程的因素

从上面,您可以看到涉及大量因素。因此,线程/内核的最佳点是多种原因的意外,包括:

  • 您使用的 CPU 的性能,尤其是:
    • 核心数
    • SMT 与否 SMT
    • 缓存量
    • 速度
  • 你有多少 RAM 和内存总线的速度
  • 操作系统和环境:
    • 处理器上正在执行多少其他工作
    • Windows/Linux/BSD/等都有不同的多任务特性
    • JVM 版本(每个版本都有不同的特性,有些不同)
    • 网络上的流量和拥塞以及对相关交换机和路由器的影响
  • 你的代码
    • 你的算法
    • 您使用的库

根据经验,没有什么神奇的公式可以先验地计算出最佳线程数。就像您所做的那样,最好通过经验来解决这个问题(正如我上面所展示的)。如果您需要概括,您将需要在您选择的操作系统上对不同 CPU 架构、内存和网络的性能进行采样。

几个容易观察到的指标在这里很有用:

  • 每个核心的 CPU 利用率 - 帮助检测进程是否受 CPU 限制
  • 平均负载 - 这报告进程(或线程,如果使用 LWP)等待 CPU 的情况。如果这个数字比 CPU 内核的数量大,那么你的 CPU 内核肯定是 CPU 受限的。

如果您需要优化,请获得最好的分析工具。您将需要一个特定的工具来监控操作系统(例如 DTrace 用于 Solaris),以及一个用于 JVM(我个人喜欢 JProfiler)。这些工具将允许您精确放大我上面描述的区域。

结论

碰巧您的特定代码在特定的 Scala 库版本、JVM 版本、操作系统、服务器和 Redis 服务器上运行,因此每个线程大约 95% 的时间都在等待 I/O。(如果运行单线程,您会发现 CPU 负载约为 5%)。

这允许大约 20 个线程在此配置中以最佳方式共享每个 CPU。

这是最佳位置,因为:

  • 如果您运行的线程较少,您将浪费 CPU 周期等待数据
  • 如果您运行更多线程:
    • 您的架构的一个组件饱和(例如磁盘或您的 CPU<->RAM 总线)阻塞了额外的吞吐量(在这种情况下,您会看到 CPU 利用率低于或远低于 ~90%),或者
    • 线程上下文切换成本开始超过添加线程的增量增益(您会看到 CPU 利用率达到 > ~95%)
于 2013-05-14T10:57:57.333 回答
1

您是否尝试过更改线程池:

  • 使用 CachedThreadPool 而不是 FixedThreadPool,这样您就可以了解 ThreadPool 可能会增长多少,然后再对其进行加盖
  • 每个内核使用超过 1 个线程,也许是 2 个?
于 2013-04-25T12:09:09.123 回答