Страничка курса: https://maxcom.github.io/scala-course-2022/
Сообщение теряется при:
Что гарантирует Akka?
Только одна попытка доставки:
«at most once delivery»
Потеря сообщений - проблема "модели"?
Нет, это часть модели отказоустойчивости.
Пример: кофейня готовит кофе и принимает платежи.
Платежи со счета клиента,
счет хранится в БД кофейни.
Наша кофейня использует идеальную БД,
плюс нет параллельных запросов
Счет каждого посетителя хранится в табличке
Списания - через UPDATE
Согласована ли связка ПО + БД?
Наивное предположение:
Что значит ошибка сети или таймаут?
Таймаут? Таймаут есть всегда
Итог - при сбое мы не знаем, списали деньги или нет.
Retry - возможно двойное списание, отказ от retry - возможна потеря денег
В асинхронной системе похожие проблемы:
Эти проблемы - проблемы как с акторами, так и без.
Решение проблемы
Сбой нельзя детектировать (в общем случае).
Сбой и таймаут не отличимы.
Таймаут — не конец операции.
Потеря сообщения при доставке - это вариант сбоя.
Не нужно решать проблему ненадежной доставки. Вместо этого нужно обеспечить требуемые гарантии при сбоях.
Положительный ответ только после записи в хранилище или выполнения действия
Откладывание выполнения с гарантиями - передача "эстафетной палочки". Похоже на передачу email.
Пример: гарантированное выполнение независимых запросов
Повторяем запрос, пока не получим подтверждение
object Requester {
sealed trait Protocol
case class Request(id: UUID, query: String,
replyTo: ActorRef[String]) extends Protocol
private implicit val timeout: Timeout = 10.seconds
def behavior(worker: ActorRef[Protocol]): Behavior[Protocol]
}
private case class Finished(request: Request, response: String)
extends Protocol
private case class Failed(id: UUID, ex: Throwable)
extends Protocol
Behaviors.setup { ctx =>
def start(rq: Request) = {
ctx.ask(worker, Request(rq.id, rq.query, _)) {
case Success(r) => Finished(rq, r)
case Failure(ex) => Failed(rq.id, ex)
}
rq.id -> rq
}
...
def process(processing: Map[UUID, Request]) =
Behaviors.receive[Protocol] {
case (ctx, rq: Request) =>
process(processing + start(ctx)(rq))
...
case (_, Finished(rq, response)) =>
rq.replyTo ! response
process(processing - rq.id)
case (_, Failed(id, ex)) =>
process(start(processing(id))
Behaviors.same // logging?
Таймаут?
Итог: "At least once delivery"
Проблема - дублирование действий.
Хорошо, когда действия идемпотентны.
Другой вариант: фильтруем дубли в worker по id.
Итог: "Exactly once delivery"
Еще проблема - порядок не сохраняется
Реализуем очередь
Перегоняем данные СУБД в Elasticsearch и Clickhouse
Требования:
Первый подход - нужна гарантированная очередь.
Это порождает массу проблем.
Появление нового в БД - событие, а не команда.
«Happy path & self heal»
«Happy path» — новые сообщения индексируем сразу; игнорируем сбои
«Self heal» – периодически проводим сверку и запускаем в обработку то, что не смогли обработать ранее.
Паттерн, реализованный в Akka.
Есть другие реализации.
Проблема: каскадные сбои при перегрузках запросами. Безумные политики retry.
Проблема: медленное восстановление после нормализации потока.
«Предохранитель», который можно вставлять между подсистемами.
«Разрывает цепь» при серии сбоев
«Замыкает цепь» после паузы и успешных пробных запросов.
В Akka реализован для Future.
Реализация требует ActorSystem
Пример:
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) // :Future[Result]
}
}
Это защитный механизм.
Не используем для регулировки скорости!
Проблема: быстрый producer + медленный consumer = Out Of Memory
При синхронных вызовах проблемы нет
Обратная связь от consumer к procuder.
Раньше делали сами - считали подтверждения для оценки размера очереди.
Это сильно усложняет актор.
Сейчас используем Akka Streams:
об этом на 10-й лекции.
Раздаем задачи пулу акторов
Похожи на акторы, low level реализация
Два механизма использования:
Простые стратегии:
object Calculator {
case class Calc(x: Long, replyto: ActorRef[Long])
def worker: Behavior[Calc] = Behaviors.receiveMessage {
case Calc(x, replyto) =>
replyto ! doWork(x)
Behaviors.same
}
...
}
def calculator: Behavior[Calc] = Behaviors.setup[Calc] { ctx =>
// round-robin по умолчанию
val pool = Routers.pool(poolSize = 16)(
Behaviors.supervise(worker)
.onFailure[Exception](SupervisorStrategy.restart))
val router = ctx.spawn(pool, "worker-pool")
Behaviors.receiveMessage[Calc] { msg =>
router ! msg
Behaviors.same
}
}
Проблема - сообщения перемешиваются
Актор - это не только вычисления, но и модель доступа к данным.
Пример - хранилище данных учетных записей
ConsistentHashingRouting - роутинг по ключу на основе сообщения
В классической Akka есть еще интересные роутеры
ScatterGatherFirstCompletedRouting
Отправляем всем, используем первый ответ.
Улучшаем время отклика, но выполняем работу N раз
TailChoppingRouting
Отправляем первому, через короткий таймаут второму и продолжаем, пока не получим первый ответ.
Улучшаем худшие времена отклика.
Напоминаю: