为什么20x比率线程对IO的最佳位置?[以前:在playframework中使用哪个ExecutionContext?]

le-*_*ude 5 java jvm scala playframework playframework-2.1

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

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

我测试了两个选项:

import play.api.libs.concurrent.Execution.defaultContext
Run Code Online (Sandbox Code Playgroud)

implicit val executionContext = ExecutionContext.fromExecutorService(Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors()))
Run Code Online (Sandbox Code Playgroud)

两者都带来了可比的表现.

我使用的操作在playframework 2.1.x中实现如下.SedisPool是我自己的对象,带有额外的Future包装正常的sedis/jedis客户端池.

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
        }
      })
  }
Run Code Online (Sandbox Code Playgroud)

这个性能wize的行为与Node.js和Go中完全相同的函数一样好或稍慢.但仍然比Pypy慢.但是比Java更快(在这种情况下使用jedis使用阻塞调用redis).我们用加特林进行了测试.我们正在对redis之上的简单服务进行技术"竞争",标准是"与编码员一样努力".我已经使用fyrie测试了这个(除了我不喜欢API之外)它的行为与这个Sedis实现几乎相同.

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

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

谢谢你的帮助.

更新 - 更多问题!

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

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

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

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

对于IO,他们会为我所做的事情提供建议但是通过可通过*.conf文件配置的Akka.dispatchers提供了一种方法(这应该让我的操作开心).

所以现在我正在使用

implicit val redis_lookup_context: ExecutionContext = Akka.system.dispatchers.lookup("simple-redis-lookup")
Run Code Online (Sandbox Code Playgroud)

配置调度程序

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
            }
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

它给了我大约5%的提升(现在眼睛看它),并且一旦JVM"热",性能就会更稳定.我的系统管理员很乐意在不重建服务的情况下使用这些设置.

我的问题仍在那里.为什么这个数字?

And*_*ock 11

我对优化的看法是:

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

单线程优化

单个线程的性能通常会在代码的单个组件或部分上进行门控,它可能是:

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

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

多线程

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

然而,这是可能发生许多奇怪效果的地方:

  1. 上下文切换有(某些)成本,因此理想情况下您需要将它们最小化.如果您的延迟时间很少而且很大而不是频繁而且很小,那么您将获得更高的整体系统性能.这种效果意味着用nx 增加线程,你将永远无法完全n提高x吞吐量.在关键点之后,当你增加时n,你的表现会降低.
  2. 同步,信号量和互斥量.通常,代码的小区域会获取信号量或互斥量,以确保一次只能输入一个(或有限数量)的线程.虽然只有少数线程,但这很少影响性能.但是,如果此代码块花费任何可观的时间,并且有许多线程,则这将成为系统性能的门控因素.例如,想象一个受保护的单线程块,需要10ms才能执行,例如通过查询数据库.因为一次只能进入一个线程,所以实际执行的最大线程数为1000毫秒/ 10毫秒,或者100个.所有其他线程将在此块的队列中相互后面.
  3. 资源:当您增加并行性时,您将加载以前轻载组件的所有方式.由于这些负载变得更加严重,因此其他线程最终阻止等待来自它们的数据.最终,额外的并行性最终会在计算机上的所有线程中产生延迟.这些组件包括:
    1. 内存
    2. 磁盘通道
    3. 网络
    4. 网络服务(例如您的数据库).我不能告诉你我多少次优化Java到DB限制吞吐量的程度.

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

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

从上面可以看出,涉及到一定数量的因素.因此,线程/核心的最佳位置是多种原因的事故,包括:

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

根据经验,没有神奇的公式来计算先验最佳线程数.正如你所做的那样,这个问题最好通过经验解决(如上所示).如果需要进行概括,则需要在您选择的操作系统上对不同CPU架构,内存和网络的性能进行抽样.

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

  • 每个核心的CPU利用率 - 帮助检测进程是否受CPU限制
  • 平均负载 - 报告进程(或使用LWP时的线程)如何等待CPU.如果这个数字大大超过了CPU核心数,那么你的CPU核心绝对是CPU限制的.

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

结论

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

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

这是最佳点,因为:

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