欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  资讯频道

Kontraktor:Task、Actor调度的另一个选择

程序员文章站 2022-04-04 09:37:34
...
导读:

Java编程中,调度Task、Actor通常采用ExecutorsExecutorService。对无状态的任务,通常可以很好的胜任。但对于大量并发的有状态任务,需要使用Actor模型。
Kontraktor是一个Java编写的轻量级高效Actor模型实现。可以直接暴露Actor提供TCP服务、WebService或者WebSockets,从JavaScript客户端调用Actor方法,用JavaScript实现Actor并通过Java调用。
对无状态小任务单元,Executors可以很好的胜任。比如将计算任务分担到多个CPU上。然而,对于运行中的大任务单元Job调度,Executors只能做到次优(sub-optimal)。例如Actor或轻量级进程的消息调度。

许多Actor框架或类似的并发框架使用Executor service批量调度消息。由于Executor service是上下文不敏感的,因此会将单个Actor/Task消息安排多个线程或CPU处理。这会导致访问Actor、Process或Task状态时经常出现缓存未命中(cache miss)的情况。更糟糕的是,因为每个新的“Runnable”会把先前处理的Task缓存冲掉,所以CPU无法维持缓存的稳定。使用忙循环(busy-spin)会带来第二个问题。如果框架使用忙循环读取自己的队列,每个处理线程的CPU负载会升到100%。

借助Kontraktor 2.0,我实现了一种不同的调度机制——使用简单的度量标准测试应用实际需要的CPU资源,再进行水平式扩充。

Kontraktor:Task、Actor调度的另一个选择

每个Actor会固定分配到一个Workerthread(“DispatcherThread”)。调度器会定期重新调度Actor,根据信息判断是否需要把它们移动到另一个工作线程。

由于算法过于复杂通常会带来更高的运行时开销,实际调度时采用了一种非常简洁的方式:


    [1]如果消费循环连续处理N个消息没有休息(目前设置N=1000),就认定该线程超载。
    [2]一旦线程标记为“超载”,只要SUM_QUEUED_MSG(线程A上运行的Actor)大于SUM_QUEUED_MSG(新创建线程B上的 Actor),信箱(mailbox)中消息最多的Actor会移动到新的线程(直到#Threads == ThreadMax)。
    [3]如果#Threads == ThreadMax,Actor会根据目前收到的消息和“超载”信息重新分配。

问题:

  • 如果处理消息的时间差别很大,对消息队列的统计会产生误导。一种改进是为每个消息根据定期分析设定加权。可以简单地用每个Actor加权乘以队列大小。
  • 对爆发式负载会有延时,延迟结束后所有可用的CPU才能被真正地使用。
  • JIT真正起效前会有延迟,这会导致错误的分析数据,从而将错误放大(一段时间后能恢复正常,实际情况并没有那么糟糕)。

性能

为了对比自动化调度与Actor线程固定方式的开销,我运行了Computing-Pi测试(可参照前一篇博客)。这些数据并没有展示局部性(locality)带来的影响,只对固定方式与自动化调度进行了比较。

测试1 手动为每个Pi计算Actor分配一个线程,
测试2 一旦监测到实际的负载,总起启动一个worker并且自动进行比例调整。

Kontraktor:Task、Actor调度的另一个选择

(注意:示例要求kontraktor2.0-beta-2及更高版本。如果parkNanos选项启用,kontraktor的比例调整会限制在2、3个线程)

该测试运行了8次,每次运行会都会增加thread_max。

测试结果:

Kontraktor Autoscale(通常运行1个线程,然后比例调整到N个线程)


1 threads : 1527
2 threads : 1273
3 threads : 718
4 threads : 630
5 threads : 521
6 threads : 576
7 threads : 619
8 threads : 668

Kontraktor为每个Actor指定固定个数的线程(参见上面源码中被注释的行)


1 threads : 1520
2 threads : 804
3 threads : 571
4 threads : 459
5 threads : 457
6 threads : 534
7 threads : 615
8 threads : 659

结论

运行结果的区别很大程度上可归结与比例调整带来的延迟。对负载明确的情况,预先安排的Actor调度会更有效率。然而,考虑到服务器收到的请求会不断变化,自动化地调度是一种高效的选择。

将Executors/FJ与上面的调度策略进行对比,测试它们各自的缓存(cache)效果是很有意思的。不幸的是,Kontraktor不具备基于ExecutorService的消息分发,也没有针对Akka的调度策略实现。

此外,还需要一个示例Actors管理私有状态才可以观察缓存效果。