Масштабирование веб-сервиса

Эта статья является внутренним тренингом для моей команды, чтобы объяснить новичкам, какова практика и какие аспекты следует учитывать при масштабировании веб-сервиса.

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

  1. Загрузка чтения
  2. Загрузка записи
  3. Размер объема данных
  4. Загрузка задач
  5. Распределение пользователей

Кроме того, существует еще один уровень масштабирования, который подробно не рассматривается в этой статье, — масштабирование функций, но я немного опишу концепцию масштабирования функций в конце статьи.

Далее давайте развивать нашу систему шаг за шагом.

Голый металл

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

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

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

Однако даже для профессиональных серверов, где аппаратное обеспечение можно легко модернизировать и менять местами, существуют свои пределы. Предел здесь — это не только физический предел, но и предел бюджета. Например, SSD-накопитель емкостью 1 ТБ стоит более чем в два раза дороже, чем накопитель емкостью 512 ГБ. Кривая роста стоимости аппаратного обеспечения является экспоненциальной.

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

Многослойная архитектура

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

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

Другими словами, гораздо выгоднее увеличивать количество процессоров, чем увеличивать их размер. Поэтому для того, чтобы справиться с возросшим трафиком, мы обычно используем стратегию увеличения количества API.

Горизонтальное масштабирование

Чтобы добиться плавного масштабирования количества API, нам понадобится новая роль для распределения всего входящего трафика, называемая балансировщиком нагрузки. Балансировщик нагрузки распределяет трафик на основе своего алгоритма, распространенными алгоритмами являются RR (Round Robin) и LU (Least Used), но я рекомендую выбрать самый простой, RR, потому что сложный алгоритм создает дополнительную нагрузку на балансировщик нагрузки и делает его еще одним узким местом.

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

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

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

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

Разделение чтения-записи

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

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

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

Это можно увидеть в нескольких распространенных базах данных, таких как MySQL Replication и MongoDB ReplicaSet.

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

Есть ли способ одновременно поддерживать трафик и экономить средства? Да, кэширование.

Кэширование

Существует несколько типов кэширования, наиболее распространенными являются следующие:

  1. Кэш на стороне чтения
  2. Сеть доставки контента, CDN

У обоих подходов есть свои преимущества и недостатки, которые будут кратко проанализированы ниже.

Кэш на стороне чтения

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

  1. сначала считываются данные из кэша.
  2. если данные не существуют в кэше, читаем их из базы данных.
  3. затем записываем обратно в кэш.

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

CDN

Другой практикой кэширования является CDN.

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

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


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

Что касается кэша для чтения, то когда API обновляет базу данных, он может одновременно удалить данные в кэше, чтобы в следующий раз при чтении получить самые свежие данные. С другой стороны, CDN должны выполнять аннулирование через API, предоставляемые каждым поставщиком, что на практике сложнее, чем кэширование по принципу read-aside.

Узкие места при записи

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

  1. Репликация мастер-мастер
  2. Запись через кэш

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

Репликация «мастер-мастер

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

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

Типичным примером мастер-мастер репликации является Cassandra, которая очень масштабируема для записи и может поддерживать очень большое количество одновременных записей. С другой стороны, MySQL также поддерживает репликацию мастер-мастер, но одна из основных проблем репликации мастер-мастер в MySQL заключается в том, что репликация выполняется асинхронно в фоновом режиме, что приводит к потере самой важной характеристики MySQL — согласованности.

Если необходима репликация мастер-мастер, необходимо принять во внимание PACELC.

Репликация мастер-мастер — это техника, которая допускает сбой разделов, поэтому вы можете выбирать только между согласованностью и доступностью. Кроме того, MySQL может добиться согласованной репликации мастер-мастера с помощью внешних фреймворков, таких как Galera Cluster, что в свою очередь создает большие задержки.

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

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

Запись через кэш

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

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

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

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

Шардинг

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

Хотя в каждом из перечисленных кластеров имеется только одна база данных, все три кластера фактически являются кластерами баз данных, и к ним можно применить упомянутое разделение чтения-записи или репликацию «мастер-мастер». Более того, даже если API соответствуют кластеру баз данных по отдельности, это не означает, что API могут обращаться только к определенным кластерам. Например, API2 также может получить доступ к Cluster3.

Шардинг — это техника разделения большого набора данных на несколько меньших наборов данных. С помощью заранее заданных индексов, таких как ключ шарда (MongoDB) или ключ раздела (Cassandra), данные распределяются по соответствующим сущностям базы данных. Таким образом, для приложения, обращающегося к определенным данным, они будут находиться в определенной сущности базы данных, и если данные распределены достаточно равномерно, то нагрузка на отдельную сущность базы данных составляет 1/N, N — количество кластеров.

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

Как только мы преодолели проблему масштабирования трафика и объема данных, возникает следующая проблема: что делать, когда выполняемая задача становится достаточно большой, чтобы повлиять на производительность API и базы данных? Ответ заключается в том, чтобы разбить задачу на части.

Обмен сообщениями

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

Асинхронный подход уже упоминался во многих моих предыдущих статьях, то есть архитектура, управляемая событиями.

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

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

  • Как выбрать очередь сообщений? Существует четыре основных аспекта.
  • Что такое паттерн событийно-управляемой архитектуры? Шаблон проектирования, часть 1 и Шаблон проектирования, часть 2.
  • Как реализовать с минимальными усилиями? Решение.

Наконец, когда проект успешно реализован, пользователи находятся по всему миру. Если центр обработки данных расположен в одном месте, то для районов, которые физически находятся далеко, задержка будет заметна, что приведет к снижению уровня принятия пользователей. Итак, как же решить проблему масштабируемости пользователей?

Пограничные вычисления

Edge означает расположение системы как можно ближе к пользователю для обеспечения хорошего пользовательского опыта.

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

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

Другими словами, как сделать базу данных как можно ближе к пользователю и при этом иметь единую точку входа?

  • Осколки БД: Это относительно простой подход. Просто используйте регион в качестве ключа шарда и создайте шарды баз данных в каждом регионе. Тогда будет единая точка входа, которой в случае MongoDB является mongos.
  • Репликация мастер-мастер БД: репликация мастер-мастер также позволяет размещать различные объекты базы данных в разных регионах, но из-за физического расстояния репликация не очень эффективна, поэтому скорость синхронизации является потенциальной проблемой.
  • ETL данных: данные извлекаются из различных баз данных, преобразуются и загружаются в единое хранилище данных. Это наиболее распространенный способ, позволяющий аналитикам данных делать это без изменения структуры базы данных исходного приложения, а также предварительно обрабатывать данные и даже выбирать свое собственное привычное хранилище данных.

Заключение

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

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

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

  • Первородный грех микросервисов, часть 1
  • Первородный грех микросервисов, часть 2

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

Оцените статью
Procodings.ru
Добавить комментарий