Обзор Typesafe Stack 2.0 и введение в модель акторов на примере Akka 2.0

в 15:25, , рубрики: actor model, akka, java, play framework, scala, высокая производительность, метки: , , ,

image

Typesafe Stack — современная программная платформа, которая обеспечивает возможность создания легко масштабируемых программных систем на Java и Scala. Стек функционирует на JVM, включает в себя фрэймворки Akka 2.0 и Playframework 2.0. Ядром платформы, которое обеспечивает практически неограниченную масштабируемость разрабатываемой системы, является библиотека Akka, реализующая многозадачность на основе модели акторов.

Модель акторов

Строгое определение модели акторов, а также историю ее возникновения и фундаментальные концепции можно найти в википедии. Если определить коротко, модель акторов исходит из философии, что все вокруг является акторами. Это похоже на философию объектно-ориентированного программирования с тем лишь отличием, что в модели акторов вычисления по своей сути совпадают во времени, когда как в объектно-ориентированном программировании, как правило, программы выполняются последовательно. В модели акторов отсутствует разделяемое состояние, а обмен данными осуществляется с помощью механизма передачи сообщений от одного актора к другому. Каждый актор имеет очередь входящих сообщений, которые обрабатываются им последовательно. За счет отсутствующего разделяемого состояния и последовательной обработки сообщений распараллеливание вычислений в программах, в основе которых лежит модель акторов, сильно упрощается, а надежность таких программ возрастает.

Akka

Как написано выше, Akka реализует модель акторов на JVM. Акторы иначе называют легковесными потоками (green threads). Такие потоки действительно легковесные, их количество может достигать ~2.7 млн на 1 ГБ оперативной памяти, а количество передаваемых сообщений более 20 млн/сек на одном узле. Есть возможность организации взаимодействия акторов через сеть, что обеспечивает практически неограниченную масштабируемость разрабатываемого приложения. Более подробную информацию о возможностях Akka можно найти на официальном сайте.

Пример использования Typesafe и Akka

Оригинал: здесь

Начать использование Akka в составе Typesafe Stack довольно просто. Процесс настройки сводится к скачиванию дистрибутива стека и его установке. Хотя для разработки можно использовать как Java так и Scala, использование последнего делает программу особенно стройной и легкой для чтения и понимания, поэтому далее в примере будем использовать Scala.

Постановка задачи

В данном примере мы применим акторы для вычисления значения числа ПИ. Данная операция интенсивно использует процессор, поэтому Akka позволит реализовать параллельное вычисление, масштабируемое на многоядерной машине. Используемый алгоритм хорошо распараллеливается т.к. каждое из вычислений является независимым, что прекрасно проецируется на модель акторов. Формула для вычисления имеет следующий вид:

image

Основной актор разделит данный ряд на составляющие и отправит каждый из членов на вычисление вспомогательному актору (по одному вспомогательному актору на каждый член ряда). По завершению вычисления каждый из вспомогательных акторов вернет результат основному актору. После получения результатов всех вычислений, основной актор сформирует ответ.

Создание проекта

Во-первых необходимо создать проект из шаблона giter8 (Typesafe Stack должен быть установлен):
c:temp>g8 typesafehub/akka-scala-sbt

Akka 2.0 Project Using Scala and sbt

organization [org.example]: akka.tutorial
package [org.example]: akka.tutorial.first.scala
name [Akka Project In Scala]: PI Calculator
akka_version [2.0]:
version [0.1-SNAPSHOT]:

Applied typesafehub/akka-scala-sbt.g8 in pi-calculator

Теперь, когда проект создан, переходим в папку проекта:
c:temp>cd pi-calculator

Начинаем писать код

Прежде всего необходимо создать в соответствующей папке (./src/main/scala/akka/tutorial/first/scala/) файл Pi.scala и импортировать необходимые библиотеки:
import akka.actor._
import akka.routing.RoundRobinRouter
import akka.util.Duration
import akka.util.duration._

Создание шаблонов сообщений

Уточним архитектуру нашего приложения вкратце описанную выше. Имеется один Master актор, который создает множество Worker акторов и инициализирует вычисление. Для этого он делит всю задачу на мелкие операции и посылает эти операции для выполнения Worker акторам. После выполнения операций, Worker акторы возвращают результаты для агрегации. После завершения вычисления Master актор отправляет результат Listener актору, который выводит его на экран.

Опираясь на данное описание создадим сообщения, которые будут передаваться в системе:

  • Calculate — отправляется Master актору для старта вычисления
  • Work — отправляется Master актором Worker акторам, содержит описания операции для выполнения
  • Result — отправляется Worker акторами Master актору
  • PiApproximation — отправляется Master актором Listener актору, содержит результат вычисления и время, затраченное на вычисление

Сообщения, отправляемые акторам должны быть неизменяемыми для исключения общего изменяемого состояния. В Scala для этого отлично подходят case classes. Также мы создадим общий базовый trait для сообщений, который обозначим как sealed для исключения бесконтрольного добавления сообщений:

sealed trait PiMessage
case object Calculate extends PiMessage
case class Work(start: Int, nrOfElements: Int) extends PiMessage
case class Result(value: Double) extends PiMessage
case class PiApproximation(pi: Double, duration: Duration)

Создание Worker актора

Сейчас мы можем создать Worker актор. Это делается путем подмешивания трэйта Actor и определения метода receive. Данный метод является обработчиком входящих сообщений:

class Worker extends Actor {

  // calculatePiFor ...

  def receive = {
    case Work(start, nrOfElements) ⇒
      sender ! Result(calculatePiFor(start, nrOfElements)) // perform the work
  }
}

Как вы можете видеть, мы добавили обработчик сообщения Work, который создает ответное сообщение Result после выполнения операции calculatePiFor и посылает его обратно отправителю. Теперь реализуем calculatePiFor:

def calculatePiFor(start: Int, nrOfElements: Int): Double = {
  var acc = 0.0
  for (i ← start until (start + nrOfElements))
    acc += 4.0 * (1 - (i % 2) * 2) / (2 * i + 1)
  acc
}
Создание Master актора

Master актор немного сложнее, т.к. в его конструкторе создается циклический роутер для облегчения распределения операций между Worker акторами:

val workerRouter = context.actorOf(
  Props[Worker].withRouter(RoundRobinRouter(nrOfWorkers)), name = "workerRouter")

Теперь напишем сам Master актор. Данный актор создается со следующими параметрами:

  • nrOfWorkers — количество Worker акторов
  • nrOfMessages — общее количество цепочек операций
  • nrOfElements — количество операций в одной цепочке, вычисляемых каждым Worker актором

Итак имеем следующий код:

class Master(nrOfWorkers: Int, 
  nrOfMessages: Int, 
  nrOfElements: Int, 
  listener: ActorRef) extends Actor {

  var pi: Double = _
  var nrOfResults: Int = _
  val start: Long = System.currentTimeMillis

  val workerRouter = context.actorOf(
    Props[Worker].withRouter(RoundRobinRouter(nrOfWorkers)), name = "workerRouter")

  def receive = {
    // handle messages ...
  }

}

Кроме описанных выше параметров, в Master актор передается объект ActorRef, представляющий собой ссылку на Listener актор. Следует отметить, что передача сообщений между акторами всегда производится посредством таких ссылок.

Но это еще не все, т.к. мы не реализовали обработчики сообщений, которые и напишем сейчас:

def receive = {
  case Calculate ⇒
    for (i ← 0 until nrOfMessages) 
      workerRouter ! Work(i * nrOfElements, nrOfElements)
  case Result(value) ⇒
    pi += value
    nrOfResults += 1
    if (nrOfResults == nrOfMessages) {
      // Send the result to the listener
      listener ! PiApproximation(pi, duration = (System.currentTimeMillis - start).millis)
      // Stops this actor and all its supervised children
      context.stop(self)
    }
}

В целом по коду должно быть все понятно, следует только отметить, что после завершения вычисления и отправки результата на печать, Master актор останавливается командой context.stop(self).

Создание Listener актора

Реализация данного актора вполне простая. Он принимает сообщение PiApproximation от Master актора, печатает результат и останавливает систему акторов:

class Listener extends Actor {
  def receive = {
    case PiApproximation(pi, duration) ⇒
      println("ntPi approximation: tt%sntCalculation time: t%s"
        .format(pi, duration))
      context.system.shutdown()
  }
}
Написание объекта-приложения

Теперь осталось только написать объект-приложение и наша программа готова:

object Pi extends App {

  calculate(nrOfWorkers = 4, nrOfElements = 10000, nrOfMessages = 10000)

  // actors and messages ...

  def calculate(nrOfWorkers: Int, nrOfElements: Int, nrOfMessages: Int) {
    // Create an Akka system
    val system = ActorSystem("PiSystem")

    // create the result listener, which will print the result and 
    // shutdown the system
    val listener = system.actorOf(Props[Listener], name = "listener")

    // create the master
    val master = system.actorOf(Props(new Master(
      nrOfWorkers, nrOfMessages, nrOfElements, listener)),
      name = "master")

    // start the calculation
    master ! Calculate

  }
}
Запуск приложения

Проще всего запустить приложение из sbt. Для этого необходимо перейти в папку с приложением и набрать команду sbt. Затем выполняем следующую последовательность команд:
c:temppi-calculator>sbt
[info] Loading project definition from C:temppi-calculatorproject
[info] Set current project to PI Calculator (in build file:/C:/temp/pi-calculator/)
> compile
[success] Total time: 0 s, completed 20.03.2012 16:33:03

> run
[info] Running akka.tutorial.first.scala.Pi

Pi approximation: 3.1415926435897883
Calculation time: 423 milliseconds
[success] Total time: 1 s, completed 20.03.2012 16:33:25

Если уменьшить количество акторов до 1, то результат изменится:
> run
[info] Running akka.tutorial.first.scala.Pi

Pi approximation: 3.1415926435897883
Calculation time: 1160 milliseconds
[success] Total time: 2 s, completed 20.03.2012 16:35:16

Если увеличить до 8, то результат будет таким:
> run
[info] Running akka.tutorial.first.scala.Pi

Pi approximation: 3.1415926435897905
Calculation time: 388 milliseconds
[success] Total time: 1 s, completed 20.03.2012 16:36:55

Результат предсказуемый, учитывая то, что испытания проводились на 4-х ядерной машине. Так увеличение количества акторов до 16 не дает практически никакого прироста производительности:
> run
[info] Running akka.tutorial.first.scala.Pi

Pi approximation: 3.141592643589789
Calculation time: 372 milliseconds
[success] Total time: 1 s, completed 20.03.2012 16:40:04

Надеюсь было увлекательно. Следующий пост будет про Play 2.0. Удачи!

Автор: tolyasik

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js