Статьи

Состояние в масштабируемых архитектурах

Обработка состояния является одним из краеугольных камней разработки программного обеспечения. Большая часть деловой ценности, получаемой от программного обеспечения, зависит от государства. В зависимости от уровня абстракции, состояния и подхода к нему выглядит совершенно другая проблема. В этом посте мы увидим, что является общим для всех из них, и как современные потребности в масштабируемости подтолкнули к инновационным и обновляющим подходам.

Уровни абстракции

Вдохновением для этой статьи послужила презентация Теда Маласки о Spark . На этом снимке экрана Тед показывает список понятий, которые мы должны понимать при изучении кода:

обучения в код

Вы можете перестать читать здесь и потратить некоторое время на размышления о своих представлениях о состоянии на любом из этих уровней. Когда вы закончите, вернитесь сюда и давайте попробуем более формально определить, что такое состояние.

Определение государства

Если вы прошли через подобный ход мыслей, как я, эти понятия наверняка появятся: изменчивость, параллелизм, изоляция и область действия. Большинство из них связано с уровнем абстракции или стратегией управления состоянием, а не с самим определением. Давайте попробуем извлечь существенное из определения Википедии ):

Состояние цифровой логической схемы или компьютерной программы — это технический термин для всей хранимой информации в данный момент времени, к которой у схемы или программы есть доступ.

Данные в данный момент времени. Ни поведения, ни абстракций вокруг него, а чистых данных.

Из бумажных смол есть мощные идеи о состоянии с более радикальной концепции, чем масштабируемость: сложность.

И последнее замечание: тип сложности, который мы обсуждаем в этой статье, состоит в том, что затрудняет понимание больших систем. Именно это заставляет нас тратить огромные ресурсы на создание и обслуживание таких систем. Этот тип сложности не имеет ничего общего с теорией сложности — отраслью информатики, которая изучает ресурсы, потребляемые машиной, выполняющей программу.

Состояние на уровне обслуживания

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

Глобальное государство

Это самый свободный подход, например, public static не финальные переменные в Java. Как мы можем неофициально рассуждать о коде, который использует глобальные переменные? Нам нужно проверить весь код, который изменяет эту переменную, и эти строки кода могут находиться в совершенно не связанных частях системы. Необходимость рассуждать о множестве деталей одновременно неэффективна и опасна. Глобальное состояние вынуждает нас проводить тестирование черного ящика, поскольку части нашей системы связаны через глобальное состояние.

Априорные простые строки кода быстро «загрязняются» сложностью, вытекающей из состояния:

Ключевая проблема заключается в том, что тест (любого рода) в системе или компоненте, который находится в одном конкретном состоянии, вообще ничего вам не говорит о поведении этой системы или компонента, когда он оказывается в другом состоянии.

Объектно-ориентированное состояние программирования

В большинстве форм объектно-ориентированного программирования (ООП) объект рассматривается как состоящий из некоторого состояния вместе с набором процедур для доступа к этому состоянию и манипулирования им.

Как однажды сказал дядя Боб , парадигмы программирования, как это ни парадоксально, преуспевают, удаляя власть. Java преуспевает, потому что это лишило разработчиков возможности управлять памятью. ООП настоятельно рекомендует, чтобы код, изменяющий состояние, был связан с этим состоянием. Эта гарантия, наряду с предусловиями и постусловиями, значительно упрощает анализ и тестирование кода.

Использование наследования для совместного использования кода — плохая идея, основанная на этих предпосылках. Мутаторы состояний будут располагаться в разных файлах, даже если они в конечном итоге образуют один объект во время выполнения, что затрудняет чтение кода.

Состояние функционального программирования

FP пытается решить эту сложность, полностью избегая изменчивого состояния. Благодаря этому у нас могут быть функции, выходы которых полностью зависят от их входов. Однако избежать не так просто, как закрыть глаза. Есть несколько сценариев, где нам нужно обрабатывать состояние. Давайте использовать пример из функционального программирования в книге Scala :

1
2
3
4
5
6
7
8
case class Simple(seed: Long) extends RNG {
    def nextInt: (Int, RNG) = {
      val newSeed = (seed * 0x5DEECE66DL + 0xBL) & 0xFFFFFFFFFFFFL
      val nextRNG = Simple(newSeed)
      val n = (newSeed >>> 16).toInt
      (n, nextRNG)
    }
  }

Этот класс генератора случайных чисел зависит от предыдущего начального числа для генерации новых случайных чисел. Подход ООП будет сохранять старое семя как состояние объекта и изменять его каждый раз, nextInt вызывается nextInt . Подход FP заключает это семя в кортеж вдоль результата. Все является неизменным, и результат функции является детерминированным в зависимости от ввода. Это свойство называется ссылочной прозрачностью, и мы можем увидеть его силу, если подумать с точки зрения сочетаемости и модульности.

Актер Государство

Akka — это инструментарий, который реализует модель актера . Эта модель обрабатывает состояние с сочетанием ООП и ФП. В этом посте рассказывается о трех основных правилах параллелизма Akka.

Правило 1: каждый участник обрабатывает сообщения из своего почтового ящика последовательно (не одновременно). Поэтому клерк никогда не выбирает два или более сообщений и обрабатывает их рядом.

Правило 2: Если субъект отправляет другому субъекту два сообщения в определенном порядке, то получающий субъект получит их в том же порядке. Таким образом, клерки могут передавать работу своим коллегам, но, опять же, они должны извлекать одно сообщение за другим из папки «Входящие» и передавать их одно за другим коллеге.

Правило 3: Все сообщения, отправляемые актерам, должны быть неизменными объектами.

Каждый актер управляет своим собственным состоянием, но обработка происходит последовательно, поэтому блокировка не требуется. Это, плюс порядок доставки сообщений и неизменность обмена сообщениями, создает надежную, масштабируемую и простую для понимания модель для обработки состояния.

Сводка состояния на уровне обслуживания

К этому моменту мы уже рассмотрели переменные, операторы управления потоком (не очень релевантные для этого поста), функции, объекты и потоки. Мы сосредоточились на сложности больше, чем на масштабируемости, но я считаю, что эти идеи согласованы. Чтобы уменьшить сложность, связанную с состоянием на уровне обслуживания, мы узнали, что удобны следующие свойства:

  • Изоляция
  • Как можно больше избегать государства
  • Государство должно управляться специализированным программным обеспечением
  • неизменность
  • Государство и поведение сходство

Давайте посмотрим, как эти идеи применимы к системному уровню. Это соответствует распределению в списке кодов обучения, которое мы видели ранее.

Состояние на системном уровне

Состояние — это данные в данный момент времени. Но какова продолжительность жизни этого государства? Запрос ограничен? Сессия ограничена? Может ли он быть сброшен при сбое службы? Или это должно быть сохранено в длительном хранении?

  • Объем запроса: давайте представим, что объект содержит конечный автомат в течение срока службы HTTP-запроса. Эта область действительно коротка, поэтому мы можем сохранить состояние в памяти. В большинстве случаев мы могли бы просто повторить попытку, вместо того чтобы использовать что-то более сложное, например, Akka Persistance .
  • Область сеанса: HTTP — это протокол без сохранения состояния. Помните, что время является основным словом в нашем определении состояния. Это означает, что в HTTP нет встроенного механизма для отслеживания состояния. Концепция сеанса включает в себя логическую группу HTTP-запросов с некоторым внутренним состоянием. Классическим решением было сохранение этого состояния в памяти сервиса. Это решение противоречит горизонтальной масштабируемости . Основным моментом предоставления услуг без сохранения состояния является возможность направлять запросы различным службам в зависимости от работоспособности и текущей загрузки этих служб. Это невозможно, когда каждая отдельная служба связана с клиентами через это состояние сеанса.
  • Область видимости потока: это область анализа почти в реальном времени. Состояние ограничено окном времени для вычисления некоторой аналитики, такой как среднее, количество или макс.
  • Надежная область действия: некоторые данные будут стоять даже дольше, чем код, который их создал. Чтобы быть уверенным в его долговечности, нам нужно хранить его в каком-нибудь дисковом движке, таком как Cassandra , Hadoop или S3 .

Давайте сосредоточимся на состоянии, которое должно храниться в течение неопределенного периода времени.

База данных как единый источник государства

Нашим следующим источником вдохновения станет смысл книги «Обработка потоков» . Мартин Клеппманн делает портрет возрастающей сложности при использовании классической базы данных в качестве единого источника истины, SSOT.

Ранее мы говорили, что как можно больше избегать государства — это хорошая идея. Избегание — вводящий в заблуждение глагол, поэтому не создавайте впечатление, что служба без сохранения состояния не справляется с состоянием, она просто делегирует ее хранилищу данных, эксперту в этой нефункциональной области (долговечность, изоляция, согласованность, репликация …). То же самое с архитектурами без серверов, конечно, есть сервер, но вам не нужно работать с этой парадигмой, вы делегируете его экспертам.

Передача нашего длительного состояния в базу данных — хорошая идея. Однако что происходит, когда наша система начинает замедляться с увеличением нагрузки? Затем мы могли бы решить включить кеш. То же самое происходит, если нам нужны возможности полнотекстового поиска, а наша БД не справляется с этим эффективно. Внезапно у нас есть SSOT, по крайней мере, с двумя проекциями этого состояния, оптимизированный для различных шаблонов запросов. У нас есть два варианта синхронизации этих представлений о состоянии.

База данных в качестве основы для интеграции данных

Как мы уже говорили, базы данных являются экспертами в своем собственном домене. Интеграция данных с различными источниками и приемниками выходит за пределы этого домена. Мы хотели бы использовать тот же код, который мы используем для оперативных преобразований (с теми же процессами тестирования и развертывания), чтобы интегрировать данные. Давайте посмотрим, как мы можем решить эту проблему, используя код приложения.

Двойная запись для интеграции данных

При таком подходе приложение теперь отвечает за обновление каждого отдельного хранилища данных. Это звучит как хорошая идея, но это порождает пару проблем.

  • Условия гонки: если два клиента отправляют обновления в одну и ту же логическую запись, нет никакой гарантии, что эти обновления будут применены в разных хранилищах данных. Изоляция из свойств ACID помогает вам решить эту проблему параллелизма, и ее реализация на уровне обслуживания имеет некоторые сложности.
  • Связь: внезапно, то, что было простым приложением, которое пишет в базу данных, это монстр со связью с различными хранилищами данных. Этот подход не масштабируется организационно, поскольку владельцы этого сервиса должны будут обслуживать запросы для каждой отдельной команды.
  • Восстановление после сбоя: даже при отсутствии параллелизма у этого подхода есть проблема с атомарностью из свойств ACID. Что произойдет, если первое обновление прошло успешно, а последнее не удалось? Предоставлять распределенные гетерогенные транзакции не представляется возможным, поэтому, опять же, приложение должно будет обеспечить самостоятельную реализацию транзакций.

Размышления о производном состоянии

Прежде чем перейти к предлагаемому решению, давайте глубоко подумаем о концепции SSOT и полученных данных. Почему мы сохраняем разные взгляды на исходные данные? Давайте посмотрим на обоснование разных примеров:

  • Материализованные представления: наш SSOT будет иметь формат, который не будет удовлетворять каждому шаблону запроса. Возможно, это нормализованная реляционная база данных, так как нам нужно сэкономить на хранении, и мы искали строгие ссылочные ограничения. У нас может быть запрос с несколькими объединениями, который занимает много времени, поэтому мы решили сохранить это представление в базе данных, чтобы ускорить чтение. База данных позаботится об обновлении материализованного представления, как только будут обновлены исходные таблицы.
  • Индексы: каждая таблица, семейство столбцов или любая абстракция хранилища данных обеспечивают эффективный способ доступа к своим данным. Возможно, этого недостаточно для шаблонов запросов, поэтому нам нужно предоставить индекс. Опять же, база данных будет отвечать за ее обновление.
  • Кэши: некоторая часть наших данных будет доступна чаще. Кроме того, существуют физические хранилища, которые быстрее и дороже. Мы используем кэши для хранения «популярных» данных, «горячих» и доступных, храня их в свободном хранилище данных.
  • Реплики: наше государство является ядром нашего бизнеса. Мы не можем позволить себе потерять какие-либо данные или быть недоступными. Лучший способ защитить себя от повреждения / потери данных или недоступности узлов — это копировать наши данные в разные места.

Каково состояние нашей системы? Исходные или производные данные. Пока полученные данные имеют гарантии долговечности и согласованности, они представляют собой достоверное представление о состоянии системы в данный момент времени. Какая структура данных используется базами данных для реализации этих производных форм? Ответ — Журнал. Поскольку мы договорились о том, что мы хотим использовать разные источники данных, специализирующиеся на их собственных нефункциональных доменах, мы могли бы изучить и использовать уроки журнала, чтобы обеспечить надежные представления о состоянии нашей системы.

Журнал

Последнее вдохновение для этого поста — журналы «Я сердце» от Джея Крепса . Мы уже рассказывали о Kafka в этом блоге, и сегодня мы увидим, как Kafka, как реализация абстракции журнала, поможет нам синхронизировать производные формы нашего состояния.

Журнал — это структура данных, которая содержит упорядоченный набор сообщений. Этот порядок действительно важен для распределенных систем, как мы видели ранее. Kafka предоставляет реализацию публикации-подписки на основе таких понятий, как тема, раздел, брокер или потребитель. Идея проста, журнал становится центральной точкой интеграции. Никакие хранилища данных не будут напрямую общаться друг с другом, либо первоначальному производителю придется самостоятельно обрабатывать интеграцию данных.

Давайте рассмотрим свойства ACID, чтобы увидеть, как Kafka может помочь нам реализовать масштабируемое и простое для понимания распределенное решение:

  • Атомность: что произойдет, если у одного из потребителей журнала возникнут проблемы, и он не сможет записать в кеш или поисковик? Каждый потребитель хранит указатель со смещением в терминологии Кафки на последнее успешно использованное сообщение в журнале. Всякий раз, когда потребитель снова доступен, может использовать эти сообщения и завершить транзакцию. Можно сказать, что это в конечном итоге атомарно, и этого достаточно для большинства распределенных систем.
  • Изоляция: проблема условий гонки связана с отсутствием порядка. Порядок гарантирует, что журнал предоставляет, решает эту проблему.
  • Долговечность: у Кафки есть надежные гарантии долговечности. Сообщения записываются на диск и копируются в несколько брокеров, однако мы не должны использовать Kafka в качестве долговременного хранилища. Резервное копирование сообщений в S3 или Hadoop — это обычный подход.
  • Согласованность: потребители могут иметь разные скорости приема данных при использовании из журнала. Это потрясающее свойство, поскольку оно разъединяет производителей и потребителей, используя журнал в качестве буфера. Однако это подразумевает, что состояние нашей системы будет в конечном итоге согласованным, поскольку обработка выполняется асинхронно. Этого, как правило, достаточно для многих случаев использования, но это представляет проблему, когда обновление может привести систему в неверное состояние. Журнал по-прежнему будет полезной структурой данных для обеспечения упорядоченной и масштабируемой структуры данных, но нам нужно будет проверить, содержит ли новое обновление ограничения состояния нашей системы для одного из наших запрашиваемых хранилищ данных.

Сводка состояния на системном уровне

Одним из полезных изображений для визуализации состояния является позиция памяти, содержащая целое число. Это целое число будет обновляться время от времени, и если мы хотим не потерять обновления, эту позицию необходимо будет заблокировать или синхронизировать, что приведет к некоторой конкуренции.

Эта модель создает некоторые проблемы, когда мы предоставляем производные представления об этом состоянии. Теперь нам нужно синхронизироваться по сети, и это было доказано как сложная, ненадежная и не масштабируемая задача.

Альтернатива — перестать бороться за позиции памяти. Вместо обновления значений давайте сохраним историю, журнал изменений всего, что происходит в этой логической записи. Это называется источником событий в управляемом доменом дизайне, и это мощная идея, которая использовалась в качестве низкоуровневой детали реализации во многих базах данных.

Вывод

Я надеюсь, что после прочтения этого поста вы согласитесь со мной, что простота и масштабируемость совпадают при обработке состояния. Неизменность — это мощная идея, которая меняет способ разработки программного обеспечения. Отслеживание того, что произошло, а не просто окончательный снимок этих событий, дает бизнесу гибкость и понимание для улучшения своих продуктов. В то же время жизнь разработчиков и системных администраторов становится намного проще, поскольку эта история состояния дает информацию о том, почему и как работает система.

Спасибо за ваше время, не стесняйтесь отправлять ваши запросы и комментарии на felipefzdz .

Ссылка: Заявите о масштабируемой архитектуре от нашего партнера JCG Фелипе Фернандеса в блоге Crafted Software .