Часть 7. Модель акторов и её применение для построения распределенных и отказоустойчивых приложений
Страничка курса: https://maxcom.github.io/scala-course-2018/
План задания
По Code Review:
Подключаем HTTP Client: build.sbt
libraryDependencies += ws
В контроллере:
import play.api.libs.ws.WSClient // не путаем с Java API!
class DemoController @Inject()(cc: ControllerComponents,
wsClient: WSClient)
Делаем HTTP-запросы
wsClient
.url(url) // куда обращаемся
.get // выполняем метод, получаем Future
.map(_.body[JsValue]) // читаем JSON
.foreach(v ⇒ println(Json.prettyPrint(v)))
Подробнее в документации.
Нет логина на vk.com?
что-нибудь придумаем
Предварительные шаги:
Смотрим документацию:
Знакомство с API ВКонтакте
Какие нужны права:
Пример URL:
https://oauth.vk.com/authorize?client_id=NNNNN&display=page&redirect_uri=https://oauth.vk.com/blank.html&scope=wall,friends,offline&response_type=token&v=5.52
не дает token - перелогиньтесь
/newsfeed - сервис оценки newsfeed
access_token не выкладывайте на gitlab!
Модель акторов - 1973 год,
использовалась как модель для описания параллельных систем.
Позже стала применяться в качестве базы для практических реализаций.
Популярность модель получила благодаря Erlang:
функциональному языку программирования и платформе,
разработанной Ericsson (1987 г.)
Дальше будем говорить про реализацию модели в библиотеке Akka.
Подключаем в проект: build.sbt
libraryDependencies +=
"com.typesafe.akka" %% "akka-actor" % "2.5.11"
в Play! зависимость уже и так есть
Актор — асинхронный объект
Объект | Актор | |
---|---|---|
Вызов | counter.incr(n) | counter ! Incr(n) counter.tell(Incr(n)) |
Запрос | counter.get() ⇒ Int |
counter ? Get counter.ask(Get) ⇒ Future[Int] |
В ООП логика моделируется взаимодействием объектов.
В модели акторов - взаимодействием акторов путем обмена сообщениями.
В ООП методы можно вызывать из разных потоков.
Потокобезопасность?
У актора обработка сообщений строго последовательна.
Акторы работают независимо в разных потоках.
Общение двух акторов последовательно
(очередь не
перемешивается).
class CounterActor extends Actor {
import CounterActor._
private var value: Long = 0 // state
override def receive = { // behavior
case Incr(n) ⇒ value += n
case Get ⇒ sender() ! value // или Status.Failure(ex)
}
}
object CounterActor { // protocol
case class Incr(n: Long)
case object Get
}
object Main extends App {
val system = ActorSystem("DemoSystem")
val counterActor: ActorRef =
system.actorOf(Props[CounterActor], name = "counter")
counterActor ! Incr(1)
println(Await.result(counterActor ? Get, 1.minutes))
}
ActorRef — идентификатор актора.
В Akka Typed идентификатор актора ActorRef[Protocol]
(этот API пока еще experimental)
Приватное состояние - не только переменные.
Актор может скрывать за собой внешние сущности:
Разделяемое изменяемое состояние + параллелизм = проблемы
Разделяемое изменяемое состояние + параллелизм =
«Share nothing» архитектура
Что можно отправлять актору?
Изменяемые объекты посылать нельзя!
Можно ли посылать функции?
Можно ли посылать внешние объекты, вроде коннектов к БД, сокетов, т.п.?
Пример актора: классификатор с обучением
Один актор на классификацию и обучение - просто, не эффективно
Актор для обучения, get запрос для модели - нужна immutable модель или функция копирования
Акторы могут быть расположены в разных виртуальных машинах и на разных серверах.
Взаимодействие акторов по сети не похоже на RPC (Remote Procedure Call).
Асинхронная логика больше похожа на взаимодействие по сети, чем на цепочку вызовов в одном потоке.
Факт: «Титаник» утонул из-за плохого разделения на отсеки.
асинхронные API или ask на другие акторы
Пример
class CacheActor(source: ⇒ Future[Int]) extends Actor {
private var value: Long = 0
override def receive = {
case Get ⇒
sender() ! value
case RefreshAndGet ⇒
import context.dispatcher // нужен ExecutionContext
source.onComplete {
??? // другой поток
}
}
}
override def receive = {
case Get ⇒
sender() ! value
case RefreshAndGet ⇒
source.onComplete {
case Success(v) ⇒ self ! Refresh(v)
case Failure(ex) ⇒ // handle failure
}
case Refresh(v) ⇒
value = v
// нужно еще отправить результат
}
sender() ?
case RefreshAndGet ⇒
source.onComplete {
case Success(v) ⇒
self ! Refresh(v)
sender() ! v // ошибка - тут sender() не определен
case Failure(ex) ⇒
}
case Refresh(v) ⇒
value = v
sender() ! v // ошибка - тут sender() это мы
Решение - сохраняем sender()
case RefreshAndGet ⇒
val replyTo = sender()
source.onComplete {
case Success(v) ⇒
self ! Refresh(v)
replyTo ! v
case Failure(ex) ⇒
}
В Akka Typed нет sender(), его явно передают
pipeTo: посылаем результат актору
import akka.pattern.pipe
source pipeTo self
Успех - отправляет результат,
ошибка - Status.Failure(ex)
Актор может переключать реализации receive:
private def waiting: Receive = {
case GetReady ⇒
context.become(ready)
}
private def ready: Receive = {
case TakeRest ⇒
context.become(waiting)
}
override def receive = waiting
Основные применения:
Пример: "immutable" counter
class CounterActor extends Actor {
import CounterActor._
private def counter(value: Long): Receive = {
case Get ⇒ sender() ! value
case Incr(n) ⇒ context.become(counter(value + n))
}
override def receive = counter(0)
}
Плюсы/минусы:
Можно временно отложить сообщения:
// extends Actor with Stash
private def waiting: Receive = {
case Process(task) ⇒
stash()
case GetReady ⇒
context.become(ready)
unstashAll()
}
private def ready: Receive = {
case TakeRest ⇒
context.become(waiting)
case Process(task) ⇒
???
}
Пример: последовательная обработка и Future
Вместо Stash иногда эффективнее
накапливать запросы
в state
Deadline - точка во времени
import scala.concurrent.duration._
val deadline: Deadline = 5 minutes fromNow
deadline.hasTimeLeft() // проверки
deadline.isOverdue()
// величина таймаута
val timeout: Duration = deadline.timeLeft
Таймеры
class CounterActor extends Actor with Timers {
import CounterActor._
// key для уникальности таймера
// msg - отправляемое сообщение
timers.startSingleTimer(key = Tick, msg = Tick, 1 minute)
timers.startPeriodicTimer(key = Tick, msg = Tick, 1 minute)
timers.cancel(Tick)
timers.cancelAll()
Зачем?
Receive Timeout - реакция на отсутствие сообщений
Защитный механизм для коротокоживущих акторов
context.setReceiveTimeout(5 minutes)
// отправляет ReceiveTimeout при отсутствии активности
Игнорирует сообщения расширяющие NotInfluenceReceiveTimeout
Виды ошибок в автомате с газировкой:
Иерархия: DriverActor → ConnectionActor → TransactionActor
Пример для ConnectionActor
override val supervisorStrategy =
OneForOneStrategy(maxNrOfRetries = 10,
withinTimeRange = 1 minute) {
case _: ConnectionResetException ⇒ Restart
case _: DatabaseWasDeletedException ⇒ Stop
case _: SharedMemoryCorruptionException ⇒ Escalate
}
Кроме быстрого перезапуска есть еще exponential back off
Пример: один запрос – три иерархии.
Ожидание остановки актора:
context.watch(anotherActorRef)
Либо ловим сообщение Terminated(actorRef), либо «death pact» — останавливаемся вместе.
Сообщение теряется при:
Только одна попытка доставки — «at most once delivery»
Потеря сообщений - проблема "модели"?
Пример: наша кофейня
использует идеальную БД
Счет каждого посетителя хранится в табличке
Списания - через UPDATE
Согласована ли связка ПО + БД?
Что значит ошибка сети или таймаут?
Таймаут? Таймаут есть всегда
В асинхронной системе похожие проблемы:
Эти проблемы - проблемы как с акторами, так и без.
Решение проблемы
За одно отметим:
Сбой нельзя детектировать (в общем случае).
Сбой и таймаут не отличимы.
Не нужно решать проблему ненадежной доставки. Вместо этого нужно обеспечить требуемые гарантии при сбоях.
А еще доставка внутри JVM гарантирована.
Положительный ответ только после записи в хранилище или выполнения действия
Откладывание выполнения с гарантиями - передача "эстафетной палочки"
Пример в сервисе индексации сообщений
«Happy path» — новые сообщений индексируем сразу; игнорируем сбои
«Self heal» – периодически проводим сверку и запускаем в обработку то, что не смогли обработать ранее.
«Предохранитель», которые можно вставлять между подсистемами.
«Разрывает цепь» при частых ошибках.
«Замыкает цепь» после таймаута и после успешных пробных запросов.
Можно использовать вне Akka.
Пример:
private val breaker = new CircuitBreaker(
scheduler = scheduler, // context.system.scheduler
maxFailures = 5,
callTimeout = 10.seconds,
resetTimeout = 1.minute
)
def process(task: Task): Future[Result] = {
breaker.withCircuitBreaker {
askAnotherService(task)
}
}
Проблема: быстрый producer + медленный consumer = Out Of Memory
Решение: выбрасываем то, что не успеваем обработать
Решение: замедляем producer'а (back pressure)
Раньше делали сами - считали подтверждения для оценки размера очереди.
Сейчас используем Akka Streams:
поговорим об этом на 8-м занятии.
Напоминаю:
Дополнительный материал
Раздаем задачи пулу акторов:
ConsistentHashingRouting - роутинг по ключу на основе сообщения
ScatterGatherFirstCompletedRouting
Отправляем всем, используем первый ответ.
Улучшаем время отклика, но выполняем работу N раз
TailChoppingRouting
Отправляем первому, через короткий таймаут второму и продолжаем пока не получим первый ответ.
Улучшаем худшие времена отклика.
Напоминаю: