Когда использовать больше экземпляров одного и того же актера Akka?
Некоторое время я работал с Akka, но сейчас подробно изучаю его систему актеров. Я знаю, что есть исполнитель опроса потоков, исполнитель соединения вилки и исполнитель афинити. Я знаю, как работает диспетчер, и все остальное. Кстати, эта ссылка дает отличное объяснение
https://scalac.io/improving-akka-dispatchers
Однако, когда я экспериментировал с простым актером вызова и переключал контекст выполнения, у меня всегда была примерно одинаковая производительность. Я выполняю 60 запросов одновременно, и среднее время выполнения составляет около 800 мс, чтобы просто вернуть простую строку вызывающей стороне.
Я использую MAC с 8 ядрами (процессор Intel i7).
Итак, вот контексты выполнения, которые я пробовал:
thread-poll {
type = Dispatcher
executor = "thread-pool-executor"
thread-pool-executor {
fixed-pool-size = 32
}
throughput = 10
}
fork-join {
type = Dispatcher
executor = "fork-join-executor"
fork-join-executor {
parallelism-min = 2
parallelism-factor = 1
parallelism-max = 8
}
throughput = 100
}
pinned {
type = Dispatcher
executor = "affinity-pool-executor"
}
Итак, вопросы такие:
- Есть ли шанс получить лучшую производительность в этом примере?
- Что насчет экземпляров актеров? Как это важно, если мы знаем, что диспетчер планирует поток (используя контекст выполнения) для выполнения метода приема актора внутри этого потока для следующего сообщения из почтового ящика актора. Разве метод получения актера не похож на обратный вызов? Когда в игру вступает количество экземпляров актеров?
- У меня есть код, который выполняет Future, и если я запускаю этот код непосредственно из основного файла, он выполняется примерно на 100-150 мс быстрее, чем когда я помещаю его в акторы и выполняю Future из актора, передавая его результат отправителю. Что делает его медленнее?
Если у вас есть какой-то реальный пример с объяснением этого, это более чем приветствуется. Я читал некоторые статьи, но все в теории. Если я попробую что-то на простом примере, я получаю неожиданные результаты с точки зрения производительности.
Вот код
object RedisService {
case class Get(key: String)
case class GetSC(key: String)
}
class RedisService extends Actor {
private val host = config.getString("redis.host")
private val port = config.getInt("redis.port")
var currentConnection = 0
val redis = Redis()
implicit val ec = context.system.dispatchers.lookup("redis.dispatchers.fork-join")
override def receive: Receive = {
case GetSC(key) => {
val sen = sender()
sen ! ""
}
}
}
Звонящий:
val as = ActorSystem("test")
implicit val ec = as.dispatchers.lookup("redis.dispatchers.fork-join")
val service = as.actorOf(Props(new RedisService()), "redis_service")
var sumTime = 0L
val futures: Seq[Future[Any]] = (0 until 4).flatMap { index =>
terminalIds.map { terminalId =>
val future = getRedisSymbolsAsyncSCActor(terminalId)
val s = System.currentTimeMillis()
future.onComplete {
case Success(r) => {
val duration = System.currentTimeMillis() - s
logger.info(s"got redis symbols async in ${duration} ms: ${r}")
sumTime = sumTime + duration
}
case Failure(ex) => logger.error(s"Failure on getting Redis symbols: ${ex.getMessage}", ex)
}
future
}
}
val f = Future.sequence(futures)
f.onComplete {
case Success(r) => logger.info(s"Mean time: ${sumTime / (4 * terminalIds.size)}")
case Failure(ex) => logger.error(s"error: ${ex.getMessage}")
}
Код довольно простой, просто чтобы проверить, как он себя ведет.
1 ответ
Мне немного непонятно, о чем вы конкретно спрашиваете, но я возьму удар.
Если ваш диспетчер (и) (и, если то, что делает актер, связано с ЦП / памятью или с привязкой к вводу-выводу, фактическое количество доступных ядер (обратите внимание, что это становится тем более опасным, чем больше виртуализация) (спасибо, чрезмерная подписка центрального процессора...) и контейнеризация (спасибо, что ограничения cgroup на основе общих ресурсов и квот) вступают в игру)) позволяет одновременно обрабатывать m субъектов, и у вас редко / никогда не бывает более n субъектов с сообщением для обработки (m > n), пытаясь увеличить параллелизм через настройки диспетчера ничего вам не даст. (Обратите внимание, что в предыдущем случае любая задача, запланированная на диспетчере (ах), например,Future
обратный вызов, по сути, то же самое, что и актер).
n в предыдущем абзаце, очевидно, не больше, чем количество участников в приложении / диспетчере (в зависимости от того, в какой области мы хотим смотреть на вещи: я отмечу, что каждый диспетчер более двух (один для субъектов и фьючерсов, которые не блокируют и один для тех, кто это делает) - более сильный запах (если на Akka 2.5, вероятно, неплохая идея адаптировать некоторые из изменений 2.6 к настройкам диспетчера по умолчанию и запускать такие вещи, как удаленное взаимодействие / кластер, в их собственном диспетчере, чтобы они не голодали out; обратите внимание, что Alpakka Kafka по умолчанию использует свой собственный диспетчер: я бы не стал считать их против двух), поэтому в целом большее количество акторов подразумевает больший параллелизм, подразумевает большее использование ядра. Акторы сравнительно дешевы по сравнению с потоками, поэтому они не вызывают большого беспокойства.
Синглтон-субъекты (будь то на уровне узла или кластера (или даже, в крайних случаях, объекта)) могут многое сделать для ограничения общего параллелизма и пропускной способности: ограничение на одно сообщение за раз может быть очень эффективным дросселем (иногда это то, что вы хотите, часто это не так). Так что не бойтесь создавать недолговечных актеров, которые делают одну высокоуровневую вещь (они определенно могут обрабатывать более одного сообщения), а затем останавливаются (обратите внимание, что многие простые случаи этого можно сделать немного более легким способом с помощью фьючерсы). Если они взаимодействуют с какой-либо внешней службой, возможно, стоит сделать их дочерними элементами актора-маршрутизатора, который порождает новых дочерних элементов, если все существующие заняты (и т. не тратить много времени на обработку какого-либо сообщения,шансы, что это задушит систему, невелики. ВашRedisService
может быть хорошим кандидатом для такого рода вещей.
Также обратите внимание, что производительность и масштабируемость не всегда одно и то же, и улучшение одного ослабляет другое. Akka часто охотно жертвует производительностью в малом ради уменьшения деградации в целом.