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

Мы начнем с простой конструкции системы и покажем, как ее можно масштабировать. В процессе будут представлены несколько концепций, которые мы рассмотрим более подробно позже в этой серии. Следовательно, в этой статье дается широкий обзор этих концепций и того, как они помогают в масштабируемости - поистине головокружительный тур! Если вы пропустили первую часть этой серии, то вот она!

Базовая архитектура системы

Практически все крупномасштабные системы начинаются с малого и растут благодаря своему успеху. Обычно и разумно начинать со среды разработки, такой как Ruby on Rails или Django или эквивалентной, которая способствует быстрой разработке, чтобы быстро настроить и запустить систему. Типичная, очень простая архитектура программного обеспечения для «начальных» систем, которая очень похожа на то, что вы получаете со средами быстрой разработки, показана на рисунке 1. Она включает уровень клиента, уровень службы приложений и уровень базы данных. Если вы используете Rails или эквивалент, вы также получаете платформу, которая жестко соединяет шаблон модель-представление-контроллер (MVC) для обработки веб-приложений и объектно-реляционный сопоставитель (ORM), который генерирует SQL-запросы.

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

Служба приложений запускает код, который поддерживает интерфейс прикладного программирования (API), который клиенты используют для форматирования данных и отправки HTTP-запросов. После получения запроса служба выполняет код, связанный с запрошенным API. В процессе он может читать или записывать в базу данных, в зависимости от семантики API. Когда запрос завершен, сервер отправляет результаты клиенту для отображения в их приложении или браузере.

Многие системы концептуально выглядят именно так. Код службы приложений использует среду выполнения, которая позволяет обрабатывать несколько запросов от нескольких пользователей одновременно. Существует множество таких серверных технологий приложений - JEE и Spring для Java, Flask для Python - которые широко используются в этом сценарии.

Такой подход приводит к так называемой монолитной архитектуре. Единые монолитные сервисы усложняются по мере того, как приложение становится более многофункциональным. Это в конечном итоге затрудняет быстрое изменение и тестирование, и их объем выполнения может стать чрезвычайно тяжелым, поскольку все реализации API работают в одной и той же службе приложения.

Тем не менее, пока загрузка запросов остается относительно низкой, этой архитектуры приложения может быть достаточно. Сервис может обрабатывать запросы с неизменно низкой задержкой. Если нагрузка запросов продолжает расти, это означает, что в конечном итоге задержки будут расти, поскольку у сервера недостаточно ресурсов ЦП / памяти для объема параллельных запросов, и, следовательно, запросы будут обрабатываться дольше. В этих условиях наш единственный сервер перегружен и становится узким местом.

В этом случае первой стратегией масштабирования обычно является масштабирование аппаратного обеспечения службы приложений. Например, если ваше приложение работает на AWS, вы можете обновить свой сервер со скромного экземпляра t3.xlarge с 4 (виртуальными) ЦП и 16 ГБ памяти до экземпляра t3.2xlarge, «который удваивает количество виртуальных ЦП и памяти, доступной для приложение".

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

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

Уменьшить масштаб

Масштабирование зависит от возможности реплицировать службу в архитектуре и запускать несколько копий на нескольких серверных узлах. Запросы от клиентов распределяются по репликам, так что теоретически, если у нас есть N реплик, каждый серверный узел обрабатывает {# запросов / N} запросов. Эта простая стратегия увеличивает емкость приложения и, следовательно, масштабируемость.

Чтобы успешно масштабировать приложение, нам нужны два основных элемента в нашем дизайне. Как показано на рисунке 2, это:

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

Службы без отслеживания состояния. Чтобы балансировка нагрузки была эффективной и равномерно распределяла запросы, балансировщик нагрузки должен иметь возможность отправлять последовательные запросы от одного и того же клиента к разным экземплярам службы для обработки. Это означает, что реализации API в сервисах не должны сохранять информацию или состояние, связанные с сеансом отдельного клиента. Когда пользователь обращается к приложению, служба создает пользовательский сеанс, а уникальный идентифицированный сеанс управляется внутренне, чтобы идентифицировать последовательность пользовательских взаимодействий и отслеживать состояние сеанса. Классическим примером состояния сеанса является корзина покупок. Чтобы эффективно использовать балансировщик нагрузки, данные, представляющие текущее содержимое корзины пользователя, должны храниться где-то - обычно в хранилище данных, - чтобы любая реплика службы могла получить доступ к этому состоянию, когда она получает запрос в рамках пользовательского сеанса. На рисунке 2 это обозначено как Session Store.

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

Масштабирование имеет еще одну очень привлекательную особенность. Если одна из служб выйдет из строя, запросы, которые она обрабатывает, будут потеряны. Но поскольку отказавшая служба не управляет состоянием сеанса, эти запросы могут быть просто повторно выданы клиентом и отправлены другому экземпляру службы для обработки. Это означает, что приложение устойчиво к сбоям в программном и аппаратном обеспечении службы приложений, что повышает его доступность. Доступность - ключевая особенность распределенных систем, которую мы также подробно обсудим в следующей статье.

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

Масштабирование базы данных с кешированием

Масштабирование за счет увеличения количества процессоров, памяти и дисков на сервере базы данных может иметь большое значение для увеличения емкости системы. Например, на момент написания Google Cloud Platform может подготовить базу данных SQL на узле db-n1-highmem-96, который имеет 96 виртуальных ЦП, 624 ГБ памяти, 30 ТБ диска и может поддерживать 4000 соединения. Это будет стоить где-то от 6 до 16 тысяч долларов в год, что для меня звучит неплохо. Масштабирование - очень распространенная стратегия масштабирования базы данных.

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

В сочетании с масштабированием высокоэффективным подходом является как можно реже запрашивать базу данных в ваших службах. Этого можно достичь, используя распределенное кэширование на уровне служб. Кэш хранит в памяти недавно полученные и часто используемые результаты базы данных, поэтому их можно быстро получить, не создавая нагрузки на базу данных. Для данных, которые часто читаются и редко меняются, необходимо изменить логику обработки, чтобы сначала проверить распределенный кеш, например хранилище Redis или memcached. Эти технологии кеширования по сути представляют собой распределенные хранилища ключей и значений с очень простыми API. Эта схема проиллюстрирована на рисунке 3. Обратите внимание, что Session Store из рисунка 2 исчез. Это связано с тем, что мы можем использовать распределенный кеш общего назначения для хранения идентификаторов сеансов вместе с данными приложения.

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

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

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

Распространение базы данных

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

  1. Распределенные хранилища SQL от основных поставщиков, таких как Oracle и IBM. Это позволяет организациям относительно легко масштабировать свою базу данных SQL, сохраняя данные на нескольких дисках, которые запрашиваются несколькими репликами ядра СУБД. Эти несколько механизмов логически представляются приложению как единая база данных, что сводит к минимуму изменения кода.
  2. Распространены так называемые хранилища NoSQL от целого ряда поставщиков. В этих продуктах используются различные модели данных и языки запросов. Они распределяют данные между несколькими узлами, на которых работает ядро ​​базы данных, каждый со своим собственным локально подключенным хранилищем. Опять же, расположение данных прозрачно для приложения и обычно контролируется дизайном модели данных с помощью функций хеширования для ключей базы данных. Лидирующие продукты в этой категории - Cassandra, MongoDB и Neo4j.

На рисунке 4 показано, как наша архитектура включает распределенную базу данных. По мере роста объемов данных распределенная база данных имеет функции, позволяющие увеличивать количество узлов хранения. По мере добавления (или удаления) узлов данные, управляемые на всех узлах, повторно сбалансированы, чтобы попытаться обеспечить одинаковое использование ресурсов обработки и хранения на каждом узле.

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

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

Несколько уровней обработки

Любая реалистичная система, которую нам нужно масштабировать, будет иметь множество различных служб, которые взаимодействуют для обработки запроса. Например, для доступа к веб-странице на сайте Amazon.com может потребоваться более 100 различных служб, вызываемых, прежде чем совокупный ответ будет возвращен пользователю.

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

Этот дизайн также способствует наличию различных служб с балансировкой нагрузки на каждом уровне архитектуры. Например, на рисунке 6 показаны две реплицированные службы с выходом в Интернет, обе из которых использовали базовую службу, обеспечивающую доступ к базе данных. Каждая служба сбалансирована по нагрузке и использует кэширование для обеспечения высокой производительности и доступности. Этот дизайн часто используется, например, для предоставления службы для веб-клиентов и службы для мобильных клиентов, каждый из которых может масштабироваться независимо в зависимости от нагрузки, которую они испытывают. Его обычно называют паттерн Backend For Frontend (BFF).

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

Повышение отзывчивости

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

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

Однако на некоторые запросы на обновление можно успешно ответить без полного сохранения данных в базе данных. Например, лыжники и сноубордисты среди вас будут знакомы с системами сканирования билетов на подъемники, которые проверяют, есть ли у вас действующий пропуск для катания на подъемниках в этот день. Они также записывают, какие подъемы вы выполняете, время в пути и т. Д. После этого опытные лыжники / сноубордисты могут использовать мобильное приложение курорта, чтобы узнать, на скольких подъемниках они проезжают за день.

Когда человек ждет, чтобы попасть в лифт, сканер проверяет пропуск с помощью считывателя RFID-чипов. Информация о гонщике, подъемнике и времени затем отправляется через Интернет в службу сбора данных, управляемую горнолыжным курортом. Водителю лифта не нужно ждать этого обновления, так как задержка может замедлить процесс загрузки лифта. Водители лифтов также не ожидают, что смогут мгновенно использовать свое приложение для сбора этих данных. Они просто садятся в лифт, болтают с друзьями и планируют следующую пробежку.

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

Базовая архитектура для реализации этого подхода показана на рисунке 7.

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

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

Резюме и дополнительная литература

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

Еще одна область, которую обошли стороной в этой главе, - это архитектура программного обеспечения. Мы использовали термин сервисы для распределенных компонентов в архитектуре, которые реализуют бизнес-логику приложений и доступ к базе данных. Эти службы представляют собой независимо развернутые процессы, которые обмениваются данными с помощью механизмов удаленной связи, таких как HTTP. С точки зрения архитектуры, эти сервисы наиболее точно отражают сервисы в шаблоне сервисно-ориентированной архитектуры (SOA), устоявшемся архитектурном подходе к построению распределенных систем. Более современная эволюция этого подхода вращается вокруг микросервисов. Это, как правило, более связанные, инкапсулированные сервисы, способствующие непрерывной разработке и развертыванию.

Если вы хотите более подробно обсудить эти вопросы и вопросы архитектуры программного обеспечения в целом, то книга Марка Ричардса и Нила Форда - отличное место для начала.

Марк Ричардс и Нил Форд, Основы архитектуры программного обеспечения: инженерный подход, 1-е издание, O’Reilly Media, 2020

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

Джей Креппс, Ставя под сомнение лямбда-архитектуру,