1 В избранное 0 Ответвления 0

OSCHINA-MIRROR/yu120-lemon-guide

Присоединиться к Gitlife
Откройте для себя и примите участие в публичных проектах с открытым исходным кодом с участием более 10 миллионов разработчиков. Приватные репозитории также полностью бесплатны :)
Присоединиться бесплатно
Клонировать/Скачать
Solution.md 440 КБ
Копировать Редактировать Web IDE Исходные данные Просмотреть построчно История
gitlife-traslator Отправлено 30.11.2024 18:34 ec8a065

Решение

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

[TOC]

Высокая доступность

Что такое высокая доступность?

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

Согласно Википедии, высокая доступность определяется как способность системы выполнять свои функции без прерывания, представляя степень доступности системы, которая является одним из критериев проектирования системы.

Сложность заключается в «непрерывности», и необходимо обеспечить 7 x 24 часа непрерывной работы без сбоев.

Почему нужна высокая доступность?

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

Оценка степени высокой доступности

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

Описание N из 9 Уровень доступности Годовое время простоя
Базовая доступность 2 из 9 99% 87,6 часов
Повышенная доступность 3 из 9 99% 8,8 часов
Возможность автоматического восстановления после сбоя 4 из 9 99,99% 53 минуты
Сверхвысокая доступность 5 из 9 99,999% 5 минут

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

Избыточность услуг

Стратегия избыточности

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

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

Отсутствие состояния

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

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

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

Хранение данных с высокой доступностью

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

По сравнению с вычислениями, есть фундаментальное различие: «данные перемещаются с одного компьютера на другой, требуя передачи по сети».

Сеть нестабильна, особенно между помещениями, задержка пинга может составлять несколько десятков или сотен миллисекунд, хотя миллисекунды почти незаметны для людей, для высокодоступных систем это фундаментальное отличие, означающее, что данные не согласованы в определённый момент времени. Согласно формуле «Данные + логика = бизнес», несогласованные данные приводят к несогласованным бизнес-результатам, но если данные не избыточны, невозможно гарантировать высокую доступность системы.

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

Известный CAP-теорема в области распределённых систем теоретически доказывает сложность обеспечения высокой доступности хранилищ, то есть высокая доступность хранилищ не может одновременно удовлетворять «согласованности, доступности и отказоустойчивости разделов», максимум может удовлетворить два из них, среди которых отказоустойчивость разделов в распределённой среде является обязательной, что означает, что при проектировании архитектуры мы должны сбалансировать согласованность и доступность бизнеса.

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

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

Репликация главный-подчиненный

Это наиболее распространённый и простой способ обеспечения высокой доступности хранилища, такой как MySQL, Redis и другие.

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

Недостатком является то, что клиент должен знать о существовании главного и подчинённого серверов и отправлять различные операции на разные серверы, кроме того, из-за задержки репликации главного и подчинённого серверов могут возникнуть проблемы с несогласованностью данных.

Автоматическое переключение главный-подчинённый

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

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

Мониторинг состояния главного сервера

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

Принятие решений о переключении

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

Обработка потери данных и конфликта данных

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

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

Разделение данных

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

Что такое разделение данных (segment, fragment, shard, partition), так это разделить набор данных на независимые и ортогональные подмножества данных, а затем распределить подмножества данных по различным узлам.

HDFS, MongoDB sharding. Режимы работы и их реализация

Режимы в основном основаны на фрагментированном подходе. При разработке фрагментации мы учитываем следующие аспекты:

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

Гибкость и асинхронность

Асинхронность

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

  1. После получения запроса сервер создаёт новый поток для обработки бизнес-логики и сначала отвечает клиенту.
  2. После получения запроса сервер сначала отвечает клиенту, а затем продолжает обработку бизнес-логики.
  3. После получения запроса сервер сохраняет информацию в очереди сообщений или базе данных и отвечает клиенту. Затем сервер обрабатывает бизнес-логику, читая информацию из очереди сообщений или базы данных.

Гибкость

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

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

Как достичь гибкости? На самом деле это понимание и оценка бизнеса. Гибкость — это скорее мышление, требующее глубокого понимания бизнес-сценариев.

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

Резервное копирование и отказоустойчивость

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

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

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

Если товарная цена в долларах должна быть рассчитана на основе цены товара в юанях и обменного курса, ошибка возникает на нижнем уровне доступа к данным. Если верхний уровень напрямую выполняет деление, это, безусловно, вызовет исключение java.lang.ArithmeticException: / by zero. В соответствии с нашим принципом, согласно которому любой уровень вызовов услуг ненадёжен, мы должны обрабатывать ошибки, чтобы они не распространялись, и гарантировать, что мы делаем всё возможное, чтобы определить услуги.

Балансировка нагрузки

Тема балансировки нагрузки, вероятно, глубоко укоренилась в сердцах каждого разработчика или проектировщика микросервисов. Реализация балансировки нагрузки включает аппаратное обеспечение, такое как F5 и A10, и программное обеспечение, такое как LVS, nginx и HAProxy. Алгоритмы балансировки нагрузки включают случайный выбор, циклический перебор и согласованное хеширование.

Отказоустойчивость Nginx и перенос нагрузки

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

Узел не работает

По умолчанию Nginx определяет, что узел не работает, на основе connect refuse и timeout. Когда количество отказов превышает max_fails, узел считается не работающим.

Восстановление узла

Когда количество отказов узла превышает max_fails, но не превышает fail_timeout, Nginx прекращает проверку этого узла до тех пор, пока не истечёт время простоя или все узлы не станут недоступными, после чего Nginx снова начнёт проверять узел.

ZK отказоустойчивый перенос нагрузки

При использовании ZK в качестве центра регистрации, обнаружение ошибок осуществляется самим ZK. Бизнес-логическая часть регистрируется в ZK через механизм сердцебиения, позволяя ZK знать, сколько доступных сервисов. Когда бизнес-логическая часть перезапускается или останавливается, она прекращает сердцебиение, и ZK обновляет список доступных сервисов.

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

Идентичность

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

Ограничение потока, плавление и отключение

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

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

Связь между плавлением и высокой доступностью заключается в следующем: микросервисы представляют собой сложную цепочку вызовов. Предположим, модуль A вызывает модуль B, модуль B вызывает модуль C, а модуль C вызывает модуль D. Модуль D сталкивается с серьёзной задержкой, вызывая задержку всей цепочки вызовов. A, B, C и D блокируют ресурсы друг друга, и если трафик большой, это может легко вызвать каскадный отказ. Плавление активно отбрасывает вызовы модуля D и обеспечивает максимальную функциональность системы. Плавление изолирует модули, обеспечивая максимальную функциональную доступность.

Управление услугами

Управление услугами

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

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

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

Эффективная система мониторинга микросервисов должна охватывать несколько уровней:

  • Мониторинг инфраструктуры: включает мониторинг сетевого оборудования, коммутаторов и маршрутизаторов. Стабильность этих компонентов напрямую влияет на стабильность сервисов верхнего уровня.
  • Системный мониторинг: охватывает физические серверы, виртуальные машины и операционные системы. Мониторинг ключевых показателей, таких как загрузка процессора, использование памяти и дисковый ввод-вывод, помогает обеспечить надёжную работу системы.
  • Мониторинг приложений: включает отслеживание производительности веб-приложений, количества запросов, времени отклика и ошибок. Также важен мониторинг производительности баз данных, включая медленные запросы и кэширование.
  • Бизнес-мониторинг: фокусируется на ключевых бизнес-процессах, таких как регистрация пользователей, оформление заказов и платежи. Этот уровень мониторинга предоставляет данные, важные для бизнес-аналитики и принятия стратегических решений.
  • Пользовательский опыт: мониторинг взаимодействия пользователей с сервисами, включая производительность, ошибки и проблемы с подключением. Это помогает понять, как пользователи воспринимают качество обслуживания.

Решения

Холодное резервирование

Холодное резервирование — это простой способ резервного копирования данных путём копирования файлов. Оно обеспечивает быстрое восстановление, но имеет ряд недостатков:

  1. Требуется остановка сервиса для выполнения резервного копирования.
  2. Существует риск потери данных между резервным копированием и восстановлением.
  3. Резервное копирование занимает много времени и требует дополнительных ресурсов.

Активное/пассивное резервирование (горячее резервирование)

Горячее резервирование позволяет поддерживать сервис в рабочем состоянии во время резервного копирования. Однако при восстановлении всё равно требуется временная остановка сервиса. Активное/пассивное резервирование может быть реализовано через:

  • Мастер/ведомый режим: основной узел обслуживает запросы, а ведомый узел синхронизируется с основным. При сбое основного узла ведомый становится активным.
  • Двунаправленное резервирование: оба узла могут обслуживать запросы, обеспечивая более эффективное использование ресурсов.

Резервирование в пределах города (локальное резервирование)

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

Двухуровневое резервирование

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

Распределённое резервирование

Распределённое резервирование подразумевает наличие нескольких центров обработки данных, расположенных в разных географических точках. Запросы распределяются между центрами, обеспечивая высокую доступность и устойчивость к сбоям. «Реализация технологии распределённой обработки запросов в условиях географического разделения (I): общее описание»

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

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

Распределённая обработка с географической избыточностью

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

Вспомним, как мы оптимизировали сетевую топологию? Мы ввели промежуточный узел, чтобы преобразовать сетевую топологию в звездообразную:

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

Реестр служб

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

Регистрация служб

Регистрация служб имеет две формы: клиентская регистрация и регистрация через прокси.

Клиентская регистрация

Клиентская регистрация означает, что служба сама отвечает за регистрацию и отмену регистрации. После запуска службы поток регистрации регистрируется в реестре, а при завершении работы службы отменяется регистрация.

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

Регистрация через прокси

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

Недостатком этой формы является то, что она добавляет ещё одну службу прокси, и служба прокси должна поддерживать высокую доступность.

Обнаружение служб

Обнаружение служб также делится на клиентское обнаружение и обнаружение через прокси.

Клиентское обнаружение

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

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

Обнаружение через прокси

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

Механизм пульса

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

Пассивное обнаружение

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

В приведённом выше примере служба A, экземпляр 2 вышел из строя и не может активно отправлять сообщения пульса в реестр. Через 15 секунд реестр удалит экземпляр 2.

Активное обнаружение

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

Реестр Dubbo

Основные функции

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

  • Регистрация службы: раскрытие URL поставщика услуг в реестре;
  • Отмена регистрации службы: удаление адреса поставщика услуг из реестра;
  • Подписка на службу: подписка на URL службы, необходимой для потребления, из реестра. При изменении списка служб реестр автоматически уведомляет подписчиков;
  • Отписка от службы: отмена подписки на прослушиватель службы из реестра;
  • Поиск службы: поиск списка определённых служб из реестра.

Особенности производства

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

  • Автоматическое переподключение: автоматическое повторное подключение после потери соединения с ZooKeeper или Redis;
  • Автоматическая смена: автоматическая смена после сбоя соединения с ZooKeeper или Redis;
  • Автоматическая очистка: автоматическая очистка данных в ZooKeeper или Redis после истечения срока действия (автоматическое снятие с учёта по истечении срока действия);
  • Автоматический отскок: автоматический отскок после успешного повторного подключения к ZooKeeper или Redis;
  • Автоматический повтор: бесконечный повтор после сбоя до успеха;
  • Автоматическое кэширование: клиент автоматически кэширует список служб подписки в локальной файловой системе JVM (обновляется при изменении уведомления).

Сравнение отраслевых решений

Ниже приведено сравнение компонентов по различным аспектам:

Решение Преимущества Недостатки Протокол доступа Алгоритм согласованности
ZooKeeper 1. Мощные функции, не только обнаружение служб; 2. Предоставляет механизм наблюдения для отслеживания состояния поставщиков услуг; 3. Широко используется, поддерживается такими микросервисными фреймворками, как Dubbo 1. Нет проверки работоспособности; 2. Необходимо интегрировать SDK в службу, сложность интеграции высока; 3. Не поддерживает мультицентры обработки данных TCP Paxos (CP)
Consul 1. Простота использования, простота интеграции; 2. Проверка работоспособности включена; 3. Поддержка мультицентров обработки данных; 4. Предоставление веб-интерфейса управления Не может получать уведомления об изменениях служб в реальном времени HTTP/DNS Raft (CP)
Nacos 1. Простота использования, подходит для Dubbo, Spring Cloud и других; 2. Модель AP, окончательная согласованность данных; 3. Реестр служб, централизованная конфигурация два в одном (два в одном не обязательно является преимуществом), предоставление интерфейса управления; 4. Чистый отечественный продукт, различные документы на китайском языке, прошёл испытание во время Double Eleven Недавно открытый исходный код, сообщество недостаточно активно, всё ещё существуют ошибки HTTP/DNS CP+AP
Eureka HTTP AP

Архитектура Консула

Консул реализует поддержку нескольких центров обработки данных на основе протокола сплетен (gossip protocol). Это делается для того, чтобы:

  • Не нужно было использовать адреса серверов для настройки клиентов; обнаружение сервисов происходит автоматически.
  • Проверка работоспособности не выполняется на сервере, а распределяется между серверами.

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

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

При изоляции служб Консул поддерживает настройку стратегий доступа на уровне служб, одновременно поддерживая классические и новые платформы, поддерживая распространение сертификатов TLS и шифрование «служба-служба».

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

Nacos — это API-шлюз.

Что такое API-шлюз?

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

Зачем нужен API-шлюз?

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

Проблемы Tomcat

  • Слишком много кеша. Tomcat использует множество технологий пулов объектов, и при ограниченном объёме памяти сборщик мусора легко запускается.
  • Копирование памяти. По умолчанию Tomcat использует память кучи, поэтому данные необходимо считывать в кучу, а серверная часть — Netty, имеет память вне кучи, которую необходимо копировать несколько раз.
  • Чтение тела Tomcat блокируется. Модель NIO и модель Reactor у Tomcat отличаются, чтение тела блокируется.
  • Количество ссылок на повторное использование Tomcat ограничено. По умолчанию 100 раз, после достижения 100 раз Tomcat добавит Connection:close в ответный заголовок, чтобы клиент закрыл соединение, иначе будет ошибка 400, если продолжить использовать это соединение для отправки.

Буфер Tomcat

На следующей диаграмме показана взаимосвязь буфера Tomcat:

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

Основные функции

  • Обратный прокси: аналогично Nginx, он реализует переадресацию внешних HTTP-запросов на внутренние RPC-запросы.
  • Динамическое обнаружение: добавление к центру микрослужб для динамического обнаружения экземпляров служб.
  • Балансировка нагрузки: распределение нагрузки на основе списка экземпляров внутренних служб.
  • Маршрутизация служб: маршрутизация вызовов различных служб на основе параметров запроса URL.

Функциональный дизайн

Публикация API

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

Разработчики начинают с создания API, заполняют параметры и генерируют сценарии DSL. Затем они могут протестировать API с помощью документации и функций MOCK. После завершения тестирования API для обеспечения стабильности при запуске платформа управления предоставляет функции утверждения выпуска, постепенного выпуска и отката версий. Во время работы API будет отслеживаться состояние сбоя вызова API и записываться журнал запросов. Как только обнаруживается аномалия, немедленно отправляется предупреждение. Наконец, для сервисов, которые больше не используются, выполняются операции отключения, ресурсы, используемые этими сервисами, восстанавливаются, и ожидается повторное включение. На протяжении всего жизненного цикла все процессы управляются конфигурацией и потоками, и разработчики полностью управляют ими самостоятельно, что значительно повышает эффективность разработки.

Центр конфигурации

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

  • Имя и группа: имя и принадлежность.
  • Запрос: информация об имени домена, пути и параметрах.
  • Ответ: результат сборки ответа, обработка исключений, заголовки, файлы cookie и другая информация.
  • Фильтры и FilterConfigs: компоненты API и информация о конфигурации, используемые API.
  • Invokers: правила и организация запросов для внутренних служб (RPC/HTTP/Function).

Маршрутизация API

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

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

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

Компоненты функций

Когда запрос потока достигает пути API и входит в службу, конкретная логика обработки определяется сценарием DSL, настроенным в конфигурации. Шлюз предоставляет множество интегрированных функциональных компонентов, включая отслеживание ссылок, мониторинг в реальном времени, журналы доступа, проверку параметров, аутентификацию, ограничение скорости, разрыв цепи, понижение уровня обслуживания и т. д., как показано ниже:

Преобразование протокола и вызов службы

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

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

Высокопроизводительный дизайн

Обеспечение стабильности

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

  • Управление потоком: пользователь может ограничить поток по UUID, приложению, IP-адресу и кластеру с нескольких точек зрения.
  • Кэширование запросов: разработчики могут включить кэширование для некоторых запросов без состояния, частых запросов и запросов с нечувствительными ко времени данными.
  • Тайм-аут управления: каждый API устанавливает время ожидания обработки, и шлюз быстро обрабатывает запросы с превышением времени ожидания.
  • Разрыв цепи и понижение уровня обслуживания: шлюз отслеживает статистику запросов в режиме реального времени и автоматически разрывает цепь и возвращает значения по умолчанию после достижения порога сбоя.

Самовосстановление

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

Переносимость

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

Решение состоит в том, чтобы перенести существующие веб-сервисы, предоставляющие внешние API, на шлюз API. API-шлюз предоставляет разработчикам набор инструментов для создания и тестирования программного обеспечения.

Процесс работы с API-шлюзом

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

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

Автоматическое создание DSL

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

Процесс настройки параметров сервиса включает в себя следующие шаги:

  • Импорт зависимостей интерфейса сервиса.
  • Получение информации о параметрах сервиса.
  • Создание шаблона JSON для тестовых случаев.
  • Определение правил сопоставления параметров.
  • Ввод данных в платформу управления и публикация API.

Этот процесс является трудоёмким и подвержен ошибкам. Для автоматизации процесса настройки параметров сервиса API-шлюз использует информацию из последней версии консоли управления сервисами. На основе этой информации шлюз автоматически генерирует данные JSON Mock для параметров сервиса. Затем эти данные объединяются с информацией из документации по API.

Повышение эффективности работы с API

Шлюз также предлагает инструменты, которые помогают ускорить работу с API:

  • Быстрое создание API — шлюз позволяет разработчикам создавать API с минимальным количеством информации.
  • Пакетная обработка — позволяет одновременно обновлять конфигурацию нескольких API.
  • Экспорт и импорт API — позволяет переносить конфигурации между различными средами разработки.

Использование пользовательских компонентов

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

Управление сервисами

Обычно один API соответствует одному сервису (RPC или HTTP). Однако иногда требуется объединить несколько сервисов для получения полного результата. Шлюз предоставляет возможность управлять такими сценариями, позволяя разработчикам вызывать несколько сервисов через один HTTP-запрос.

Контроль трафика

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

Мониторинг и оповещение

Шлюз обеспечивает мониторинг и оповещение о различных событиях, связанных с работой API. Мониторинг охватывает различные аспекты работы системы, такие как запросы, системные ресурсы и журналы. Оповещение позволяет получать уведомления о таких событиях, как превышение лимита запросов или сбой аутентификации. | 4 | API异常告警 | API发布失败、API检查异常时触发API异常告��ги | | 5 | 健康检查失败告警 | API心跳检查失败、网关节点不通时触发健康检查失败告��ги |

Ключевые аспекты проектирования

Асинхронный внешний вызов

На основе Netty реализуется асинхронный внешний вызов. Существует два основных способа реализации:

  • Способ 1: создание глобальной карты, передача requestId в онлайн-сообщении (без участия в удалённой передаче), использование requestId для сопоставления информации о восходящем потоке.
  • Способ 2: прямое инкапсулирование восходящей информации в контекст и передача её в онлайн-сообщении (не участвует в удалённой передаче).

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

Пул внешних вызовов

При использовании Netty для реализации микросервисов API-шлюза внешние вызовы должны быть объединены в пул. При проектировании необходимо учитывать следующие моменты:

  • Инициализация соответствующего соединения (слишком много или слишком мало не подходит).
  • Рассмотрение возможности автоматического расширения и сжатия соединений в соответствии с изменениями трафика.
  • Проверка доступности подключений перед их извлечением.
  • Соединения требуют двусторонней проверки сердцебиения.

Освобождение соединения

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

  • Connection:close.
  • Тайм-аут простоя, закрытие соединения.
  • Чтение тайм-аута, закрытие соединения.
  • Запись тайм-аута, закрытие соединения.
  • Fin, Reset.

Запись тайм-аута: writeAndFlush включает время кодирования Netty и время отправки запроса из очереди. Поэтому после того, как бэкэнд начинает отсчёт тайм-аута, он должен начинаться после успешного завершения flush, чтобы максимально приблизиться к времени ожидания бэкэнда (также существует время задержки сети и обработки ядра протокола).

Проектирование пула объектов

Для высокопроизводительных систем частое создание объектов не только приводит к выделению памяти, но и создаёт нагрузку на сборщик мусора. В процессе реализации часто используемые объекты (например, задачи в пуле потоков, StringBuffer и т. д.) переписываются для уменьшения частоты выделения памяти.

Переключение контекста

Хотя весь шлюз не связан с операциями ввода-вывода, асинхронность используется как в кодировании и декодировании ввода-вывода, так и в бизнес-логике. Есть две причины:

  • Предотвращение блокировки кода, написанного разработчиком.
  • Логирование бизнес-логики может быть довольно частым.

В случае всплеска трафика мы поддерживаем использование потоков push с помощью потоков IO Netty вместо этого. Здесь выполняется меньше работы, и здесь асинхронная модификация заменяется синхронной модификацией (через изменение конфигурации). Это снижает переключение контекста ЦП на 20 % и повышает общую пропускную способность. Аналогично Zuul2.

Мониторинг и оповещение

Уровень протокола:

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

Прикладной уровень:

  • Мониторинг времени выполнения. Включая медленные запросы, просроченные запросы, TP99, TP999 и другие.
  • QPS мониторинг и оповещения.
  • Мониторинг полосы пропускания и оповещения. Поддержка мониторинга запросов и ответов по строкам, заголовкам и телам отдельно.
  • Оповещения о кодах ответа. Особенно 400 и 404.
  • Мониторинг соединений. Соединения с конечными точками, а также соединения с внутренними службами. Мониторинг размера ожидающих отправки байтов внутренних служб.
  • Мониторинг неудачных запросов.
  • Предупреждения о колебаниях трафика. Колебания трафика либо указывают на проблемы, либо являются предвестниками проблем.

Решения

Shepherd API Gateway

Shepherd API Gateway

Mashape Kong

Доступный адрес: https://github.com/Kong/kong.

Kong

Soul

Доступный адрес: https://github.com/Dromara/soul.

Soul

Apiman

Доступный адрес: https://apiman.gitbooks.io/apiman-user-guide/user-guide/gateway/policies.html.

Apiman

Gravitee

Доступный адрес: https://docs.gravitee.io/apim_policies_latency.html.

Gravitee

Tyk

Доступный адрес: https://tyk.io/docs.

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

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

  • Простота использования. Платформа имеет интуитивно понятный пользовательский интерфейс, который упрощает процесс создания и управления API.
  • Гибкость. Tyk поддерживает различные типы API, такие как RESTful, SOAP и GraphQL.
  • Масштабируемость. Платформа может масштабироваться для обработки большого количества запросов, что делает её подходящей для крупных предприятий.
  • Безопасность. Tyk обеспечивает высокий уровень безопасности API с помощью таких функций, как аутентификация, авторизация и шифрование.

Traefik

Доступный адрес: https://traefik.cn.

Traefik — это современный обратный прокси-сервер HTTP, предназначенный для упрощения развёртывания микросервисных архитектур. Он поддерживает широкий спектр бэкендов, таких как Docker, Swarm, Kubernetes, Marathon, Mesos, Consul, Etcd, Zookeeper, BoltDB, Rest API и file. Traefik автоматически настраивает себя на основе изменений в бэкэнде, обеспечивая непрерывное обновление конфигурации.

Основные функции Traefik включают:

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

Малый леопард API Gateway

Доступный адрес: http://www.xbgateway.com.

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

Архитектура малого леопарда API Gateway включает:

  • Фасад. Предоставляет единый интерфейс для клиентов.
  • Парсер. Анализирует входные данные DSL и генерирует внутренние потоки данных. Также создаёт логику вызова задач.
  • Исполнитель. Выполняет фактические вызовы сервисов. Поддерживает RPC и HTTP.
  • DataProcessor. Преобразует данные в формат, необходимый для бизнес-сценариев.

Особенности малого леопарда API Gateway включают:

  • Децентрализованную архитектуру. Инженеры интегрированы в SDK. Решение является универсальным, и каждый сервис, которому требуются данные, может напрямую вызывать поставщика данных через платформу.
  • Параллельные и последовательные вызовы. Пользователи могут создавать собственные деревья вызовов сервисов в зависимости от сценария использования. Платформа выполняет параллельные вызовы, где это возможно, для оптимизации производительности.
  • JSON DSL. Описывает всю рабочую схему.
  • Поддержка JSONPath. Извлекает значения из результатов сервисов.
  • Внутренние функции и пользовательские команды. Обрабатывают полученные метаданные и создают необходимые результаты.
  • Визуализация дерева вызовов сервисов. Неважные или не срочные услуги или задачи могут быть отложены или приостановлены.

Снижение уровня услуг

  • Основные и неосновные услуги;
  • Поддержка снижения уровня, стратегия снижения уровня;
  • Сценарии восстановления связи, стратегии.

Автоматический выключатель

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

![Схема автоматического выключателя](images/Solution/Автоматический выключатель.png)

Состояние автоматического выключателя

Для каждого вызова службы (пути вызова) поддерживается состояние машины состояний, которое имеет три состояния:

  • Закрыто: состояние по умолчанию. Автоматический выключатель считает, что состояние прокси-службы хорошее, если доля отказов не достигает порога.
  • Открыто: автоматический выключатель обнаруживает, что доля отказов уже достигла порога, и считает, что прокси-служба неисправна. Он открывает переключатель, чтобы прекратить доступ к прокси-службе, и быстро возвращает ошибку.
  • Полуоткрыто: после открытия автоматического выключателя для обеспечения возможности автоматического восстановления доступа к прокси-сервису он переключается в полуоткрытое состояние, пытаясь вызвать прокси-сервис, чтобы проверить, восстановилась ли служба. Если вызов успешен, он возвращается в состояние Закрыто, в противном случае — в состояние Открыто.

Стратегия отключения

  • Доля отказов превышает пороговое значение в течение указанного времени;
  • Количество отказов превышает пороговое значение в течение указанного времени.

Стратегия восстановления

  • Доля отказов ниже порогового значения в течение указанного времени;
  • Количество отказов ниже порогового значения в течение указанного времени.

Отклоняющая стратегия

  • Выдача указанного исключения;
  • Обработка с использованием стратегии снижения уровня.

Общие проблемы

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

  1. Определение различной логики обработки после сбоя для разных исключений.
  2. Установка времени отключения, переключение в полуоткрытое состояние после превышения этого времени.
  3. Регистрация журналов сбоев для мониторинга.
  4. Активный перезапуск, например, для отключения, вызванного тайм-аутом соединения, можно использовать асинхронные потоки для проверки сети, такой как Telnet, и переключиться в полуоткрытое состояние для повторного вызова при обнаружении нормальной работы сети.
  5. Предоставление ручного переключателя для администраторов, автоматический выключатель может предоставить переключатель для ручного отключения.
  6. При повторных попытках можно использовать предыдущие неудачные запросы, но следует обратить внимание на то, разрешено ли это бизнес-логикой.

Применение

  • Быстрое отключение при сбое службы или обновлении;
  • Простая логика обработки сбоев;
  • Время отклика относительно велико, а установленное клиентом время ожидания чтения будет относительно длинным, чтобы предотвратить частые повторные попытки клиента, вызывающие проблемы с подключением и ресурсами потока.

Трассировка цепочки

![Трассировка цепочки: сравнение компонентов с открытым исходным кодом](images/Solution/Трассировка цепочки: сравнение компонентов с открытым исходным кодом.png)

ThreadContext

NDC (вложенный диагностический контекст) и MDC (сопоставленный диагностический контекст) являются двумя очень полезными классами log4j, которые используются для хранения контекста приложения (контекстной информации), что упрощает использование этой контекстной информации в журнале. NDC использует механизм, подобный стеку, для ввода и вывода контекстной информации, каждый поток сохраняет контекстную информацию отдельно. Например, сервлет может создать соответствующий NDC для каждого запроса, сохраняя такую информацию, как адрес клиента. MDC и NDC очень похожи, за исключением того, что MDC внутренне использует механизм карты для хранения информации, и каждый поток также сохраняет информацию отдельно, но информация хранится в карте с использованием ключа.

Принцип NDC и MDC заключается в использовании класса ThreadLocal в Java. Можно хранить информацию для разных потоков. Однако при использовании log4j2 сегодня было обнаружено, что NDC и MDC были заменены на ThreadContext.

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

NDC

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

  • Начало вызова:

    • NDC.push(message);
  • Удаление верхнего сообщения стека:

    • NDC.pop();
  • Очистка всех сообщений, обязательно вызывается перед выходом из потока, иначе это может привести к утечке памяти.

    • NDC.remove();
  • Шаблон вывода, обратите внимание на нижний регистр [%x]:

    • log4j.appender.stdout.layout.ConversionPattern=[%d{yyyy-MM-dd HH:mm:ssS}] [%x] : %m%n

MDC

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

  • Сохранение информации в контексте:

    • MDC.put(key, value);
  • Извлечение информации из контекста:

    • MDC.get(key);
  • Очищение информации с указанным ключом в контексте:

    • MDC.remove(key);
  • Полная очистка:

    • clear();
  • Шаблон вывода, обратите внимание на верхний регистр [%X{key}]:

    • log4j.appender.consoleAppender.layout.ConversionPattern = %-4r [%t] %5p %c %x - %m - %X{key}%n

В log4j 1.x использование MDC выглядит следующим образом:

@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
    try {
        // заполнение данных
        MDC.put(Contents.REQUEST_ID, UUID.randomUUID().toString());
        chain.doFilter(request, response);
    } finally {
        // завершение запроса очищает данные, в противном случае это может вызвать утечку памяти
        MDC.remove(Contents.REQUEST_ID);
    }
}

ThreadContext

В log4j 2.x ThreadContext заменяет MDC и NDC, и его использование выглядит следующим образом:

@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
    try {
        // заполнение данных
        ThreadContext.put(Contents.REQUEST_ID, UUID.randomUUID().toString());
        chain.doFilter(request, response);
    } finally {
        // завершение запроса очищает данные, в противном случае это может вызвать утечку памяти
        ThreadContext.remove(Contents.REQUEST_ID);
    }
}

Запись журнала

  • %X: выводит всю информацию в карте;
  • %X{key}: выводит указанную информацию;
  • %x: выводит всю информацию в стеке.

Пример формата журнала:

<PatternLayout pattern="%d{HH:mm:ss.SSS} [%t] %-5level %X{REQUEST_ID} %logger{36} - %msg%n" />

ThreadLocal

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

InheritableThreadLocal

InheritableThreadLocal — это решение для передачи потоков, предоставляемое JDK. Как следует из названия, когда текущий поток создаёт новый поток, новый поток унаследует значение текущего потока InheritableThreadLocal. Thread внутренне выделяет отдельный ThreadLocalMap для InheritableThreadLocal. При создании дочернего потока текущим потоком он проверяет, пуст ли ThreadLocalMap, и если нет, то он неглубоко копирует ThreadLocalMap в дочерний поток.

TransmittableThreadLocal

Transmittable ThreadLocal — это библиотека с открытым исходным кодом от Alibaba, которая наследует InheritableThreadLocal и оптимизирует передачу ThreadLocal при использовании пула потоков или других методов совместного использования потоков. Проще говоря, существуют специальные классы TtlRunnable и TtlCallable, которые считывают исходный объект ThreadLocal и значение объекта и сохраняют их в Runnable/Callable. При выполнении run или call они считывают сохранённые объекты ThreadLocal и значения из Runnable/Callable и помещают их в вызывающий поток. Zipkin — это один из открытых проектов Twitter, основанный на Google Dapper. Он направлен на сбор данных о времени обслуживания для решения проблем с задержкой в микросервисной архитектуре, включая сбор, хранение, поиск и отображение данных.

Архитектура Zipkin

В процессе работы сервисов создаётся множество цепочек информации, места генерации данных можно назвать репортёрами. Цепочки информации передаются через различные способы передачи, такие как HTTP, RPC и очереди сообщений Kafka, в сборщик Zipkin. После обработки Zipkin сохраняет цепочки информации в памяти. Системные администраторы могут запрашивать информацию о вызовах через пользовательский интерфейс.

Основные компоненты Zipkin

Zipkin имеет четыре основных компонента:

  1. Сборщик (Collector)

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

  1. Хранилище (Storage)

Изначально Zipkin Storage был разработан для хранения данных в Cassandra, поскольку Cassandra является масштабируемой и гибкой системой, широко используемой в Twitter. Помимо Cassandra, поддерживаются ElasticSearch и MySQL, и в будущем могут быть предложены сторонние расширения.

  1. Служба запросов (Query Service)

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

  1. Веб-интерфейс (Web UI)

Zipkin предоставляет базовый интерфейс поиска и поиска, который позволяет системным администраторам легко идентифицировать проблемы в сети на основе информации о вызове.

Алгоритм идемпотентности

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

  • Перехват и повторная передача злоумышленником: злоумышленник перехватывает запрос и повторно передаёт его.

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

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

Решения

Механизм токенов

Это распространённый метод реализации алгоритма идемпотентности для интерфейсов. Схема выглядит следующим образом:

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

Обратите внимание:

  • Для управления созданием и удалением токенов в Redis рекомендуется использовать Lua-скрипты для обеспечения атомарности операций.
  • Уникальный идентификатор может быть сгенерирован с использованием таких инструментов, как Baidu uid-generator или Leaf от Meituan.

На основе MySQL

Этот подход использует уникальные индексы таблиц MySQL. Схема выглядит так:

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

На основе Redis

Этот метод основан на команде SETNX в Redis для установки значения ключа. Команда SETNX устанавливает значение ключа только в том случае, если ключ не существует. Она возвращает 1 при успешной установке и 0 при неудаче. Схема выглядит так:

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

На основе параметров запроса

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

Однако во многих сценариях запросы не содержат уникальных параметров. Сначала рассмотрим простой случай, когда запрос имеет только одно поле reqParam. Можно использовать следующий подход для определения уникальности запроса: пользователь ID: имя интерфейса: параметры запроса.

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

Пример кода на Java:

String KEY = "user_opt:U="+userId + "M=" + method + "P=" + reqParamMD5;

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

Таким образом, объединив все идеи, можно создать следующую схему:

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

Распределённые идентификаторы

Основные требования к распределённым идентификаторам:

  • Глобальная уникальность
  • Упорядоченность
  • Высокая производительность

Схема распределённых идентификаторов выглядит так:

Существует два основных метода генерации распределённых идентификаторов:

UUID

Основан на UUID для генерации глобально уникальных идентификаторов. Хотя UUID может использоваться в некоторых сценариях, таких как генерация токенов, он не подходит для использования в качестве распределённого идентификатора из-за своей длины и отсутствия упорядоченности.

Преимущества: простота генерации без сетевого взаимодействия, глобальная уникальность. Недостатки: неупорядоченная строка без конкретной бизнес-информации. Применение: подходит для сценариев, где не требуется упорядоченность и длина не критична, например, генерация токенов.

Самоинкрементный идентификатор базы данных

Использование автоинкремента в базах данных может служить распределённым идентификатором. Однако этот подход имеет существенный недостаток: при высокой нагрузке MySQL сам становится узким местом системы. Не рекомендуется использовать его для распределённых сервисов.

SQL-код для создания таблицы с самоинкрементным идентификатором:

CREATE DATABASE `SEQ_ID`;
CREATE TABLE SEQID.SEQUENCE_ID (
    id bigint(20) unsigned NOT NULL auto_increment, 
    value char(10) NOT NULL default '',
    PRIMARY KEY (id),
) ENGINE=MyISAM;

insert into SEQUENCE_ID(value)  VALUES
``` **Преимущества и недостатки различных подходов к генерации идентификаторов (ID)**

**1. Простой подход с инкрементным ID**

* Преимущества:
    * Простота реализации.

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

**2. Многоглавный режим базы данных**

* Проблемы:
    * Если оба экземпляра MySQL начинают генерировать ID с 1, то могут возникнуть дубликаты.

* Решение:
    * Настройка параметров `AUTO_INCREMENT_OFFSET` и `AUTO_INCREMENT_INCREMENT`.
    * Для каждого экземпляра MySQL необходимо задать своё значение `AUTO_INCREMENT_OFFSET`, чтобы избежать дублирования ID.

* Преимущества:
    * Решает проблему с отказом одного узла.

* Недостатки:
    * Сложность масштабирования.
    * Высокая нагрузка на базу данных может привести к снижению производительности.
* Сценарии использования:
    * Подходят для проектов с небольшой нагрузкой и без необходимости масштабирования.

**3. Номерной режим**

* Описание:
    * Номера выделяются из базы данных пакетами.
    * Каждый пакет имеет свой диапазон номеров.
    * После использования пакета запрашивается новый диапазон.

* Таблица:
    | ID | Бизнес-тип | Максимальный ID | Шаг | Версия |
    |:--:|:----------:|:--------------:|:----:|:-----:|
    | 1  | 101       | 1000           | 2000 | 0     |

    * При использовании пакета номеров обновляется поле `версия`.

* Преимущества:
    * Распределённая генерация номеров, не зависящая от базы данных.
    * Меньшая нагрузка на базу данных.

* Недостатки:
    * Необходимо учитывать проблемы параллелизма при обновлении поля `версия`.
* Сценарии использования:
    * Подходят для проектов со средней нагрузкой и необходимостью распределённой генерации номеров.

**4. Режим Redis**

* Описание:
    * Использование команды `INCR` для атомарного увеличения значения.
    * Учёт проблем с сохранением данных в Redis.

* Режимы сохранения данных Redis:
    * RDB — периодическое сохранение данных на диск.
    * AOF — постоянное сохранение всех команд записи.

* Преимущества:
    * Атомарное увеличение значений.
    * Возможность чтения последовательности значений.

* Недостатки:
    * Зависимость от Redis может вызвать проблемы с надёжностью.
    * Увеличение нагрузки на сеть.
* Сценарии использования:
    * Подходят для проектов с невысокой нагрузкой и требованиями к упорядоченности значений.

**5. Алгоритм Snowflake**

* Структура идентификатора:
    * Знак (1 бит) — всегда равен нулю.
    * Временная метка (41 бит).
    * Идентификатор машины (5 бит).
    * Идентификатор центра обработки данных (5 бит).
    * Порядковый номер (12 бит).

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

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

**6. Uid-Generator от Baidu**

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

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

**7. Leaf от Meituan**

* Поддерживает режимы Snowflake и номерной.
* Позволяет переключаться между режимами в зависимости от требований проекта. **Создание и тестирование программного обеспечения: настройка базы данных и использование распределённого идентификатора**

**Третий шаг: конфигурация базы данных**

```properties
datasource.tinyid.names=primary
datasource.tinyid.primary.driver-class-name=com.mysql.jdbc.Driver
datasource.tinyid.primary.url=jdbc:mysql://ip:port/databaseName?autoReconnect=true&useUnicode=true&characterEncoding=UTF-8
datasource.tinyid.primary.username=root
datasource.tinyid.primary.password=123456

Четвёртый шаг: запуск tinyid-server и тестирование

# Получение распределённого инкрементного ID
http://localhost:9999/tinyid/id/nextIdSimple?bizType=test&token=0f673adf80504e2eaa552f5d791b644c'
Ответ: 3

# Пакетное получение распределённых инкрементных ID
http://localhost:9999/tinyid/id/nextIdSimple?bizType=test&token=0f673adf80504e2eaa552f5d791b644c&batchSize=10'
Ответ:  4, 5, 6, 7, 8, 9, 10, 11, 12, 13

Подключение к Java-клиенту

Первый шаг: импорт зависимостей

       <dependency>
            <groupId>com.xiaoju.uemc.tinyid</groupId>
            <artifactId>tinyid-client</artifactId>
            <version>${tinyid.version}</version>
        </dependency>

Второй шаг: настройка файла конфигурации

tinyid.server =localhost:9999
tinyid.token =0f673adf80504e2eaa552f5d791b644c

Третий шаг: test и tinyid.token — это данные, предварительно вставленные в таблицу базы данных, test — конкретный тип бизнеса, а tinyid.token — доступный тип бизнеса.

// Получение одного распределённого инкрементного ID
Long id = TinyId.nextId("test");
// Пакетное получение распределённых инкрементных ID
List<Long> ids = TinyId.nextId("test", 10);

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

  • Является ли эта реализация нереентерабельной блокировкой, когда один и тот же поток не может повторно получить блокировку до того, как освободит её (добавление полей записи информации о машине и потоке, при совпадении во время запроса — прямое распределение)?

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

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

На основе версии поля таблицы реализуется оптимистичный механизм блокировки. Обычно это достигается путём добавления поля version к таблице базы данных. При чтении данных номер версии считывается вместе с данными. При обновлении номер версии увеличивается на 1. Во время обновления номер версии сравнивается. Если номера совпадают, операция выполняется успешно. В противном случае обновление завершается неудачно. Фактически это процесс CAS.

Недостатки:

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

Реализуется на основе исключительной блокировки (for update). С помощью for update можно получить блокировку при выборе. База данных добавит исключительную блокировку к таблице во время процесса запроса. После того как запись заблокирована, другие потоки не могут добавить исключительную блокировку к этой строке. Можно считать, что поток, получивший исключительную блокировку, получает распределённую блокировку. Блокировка снимается с помощью команды connection.commit().

Преимущества:

— Простота реализации и понимания.

Недостатки:

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

Реализация:

CREATE TABLE `methodLock` (
      `id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键',
      `lock_key` varchar(64) NOT NULL DEFAULT '' COMMENT '锁的键值',
      `lock_timeout` datetime NOT NULL DEFAULT NOW() COMMENT '锁的超时时间',
      `remarks` varchar(255) NOT NULL COMMENT '备注信息',
      `update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    PRIMARY KEY ( `id` ),
    UNIQUE KEY `uidx_lock_key` ( `lock_key ` ) USING BTREE 
) ENGINE = INNODB DEFAULT CHARSET = utf8 COMMENT = '锁定中的方法';

Блокировка и снятие блокировки:

/**
 * 加锁
 */
public boolean lock() {
        // 开启事务
        connection.setAutoCommit(false);
        // 循环阻塞,等待获取锁
        while (true) {
            // 执行获取锁的sql
            String sql = "select * from methodLock where lock_key = xxx for update";
             // 创建prepareStatement对象,用于执行SQL
            ps = conn.prepareStatement(sql);
            // 获取查询结果集
            int result = ps.executeQuery();
            // 结果非空,加锁成功
            if (result != null) {
                return true;
            }
        }
    
        // 加锁失败
        return false;
}

/**
 * 解锁
 */
public void unlock() {
        // 提交事务,解锁
        connection.commit();
}

Redis

Проблемы с блокировками

Неатомарные операции

Операция добавления блокировки и последующего задания времени ожидания являются отдельными и неатомарными операциями. Решение:

Вариант 1: команда set

String result = jedis.set(lockKey, requestId, "NX", "PX", expireTime);
if ("OK".equals(result)) {
    return true;
}
return false;

В Redis команда set является атомарной операцией, позволяющей легко установить блокировку и задать время ожидания. Здесь:

— lockKey — идентификатор блокировки; — requestId — идентификатор запроса; — NX — установка значения только в том случае, если ключ не существует; — PX — задание времени ожидания в миллисекундах; — expireTime — время ожидания.

Вариант 2: Lua-скрипт

if (redis.call('exists', KEYS[1]) == 0) then
        redis.call('hset', KEYS[1], ARGV[2], 1); 
        redis.call('pexpire', KEYS[1], ARGV[1]); 
        return nil; 
end
if (redis.call('hexists', KEYS[1], ARGV[2]) == 1)
        redis.call('hincrby', KEYS[1], ARGV[2], 1); 
        redis.call('pexpire', KEYS[1], ARGV[1]); 
        return nil; 
end
return redis.call('pttl', KEYS[1]);

Забытая разблокировка

После получения блокировки необходимо дождаться истечения времени ожидания перед снятием блокировки. Несвоевременное снятие блокировки может вызвать проблемы. Рекомендуется следующий процесс:

![Процесс снятия блокировки Redis](images/Solution/Процесс снятия блокировки Redis.jpg)

Код для снятия блокировки:

try{
      String result = jedis.set(lockKey, requestId, "NX", "PX", expireTime);
      if ("OK".equals(result)) {
          return true;
      }
      return false;
} finally {
     unlock(lockKey);
}  

Снятие чужой блокировки

Можно снимать только свою блокировку, нельзя снимать чужие блокировки.

Решение 1: использование идентификатора запроса

Пример кода:

if (jedis.get(lockKey).equals(requestId)) {
    jedis.del(lockKey);
    return true;
}
return false;

Решение 2: использование Lua-скриптов

if redis.call('get', KEYS[1]) == ARGV[1] then 
    return redis.call('del', KEYS[1]) 
else 
    return 0 
end

Большое количество неудачных запросов

Что произойдёт в сценарии с большим количеством одновременных запросов? Предположим, что каждый 10 000-й запрос успешен. Затем каждый 10 000-й запрос также успешен. И так далее, пока не закончится товар. Это превращается в равномерное распределение «секундных» продаж, а не в желаемое поведение.

Решение: спин-блокировка

В течение определённого периода времени, например, 500 миллисекунд, непрерывно пытайтесь получить блокировку (по сути, это бесконечный цикл попыток получения блокировки), и если попытка успешна, немедленно возвращайтесь. Если попытка неудачна, подождите 50 миллисекунд и повторите попытку. Если время ожидания истекло, а блокировка не получена, вернитесь с ошибкой. Проблема реентерабельной блокировки

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

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

Псевдокод для добавления блокировки в методе рекурсии (вызовет исключение):

private int expireTime = 1000;
public void fun(int level,String lockKey,String requestId){
  try{
     String result = jedis.set(lockKey, requestId, "NX", "PX", expireTime);
     if ("OK".equals(result)) {
        if(level<=10){
           this.fun(++level,lockKey,requestId);
        } else {
           return;
        }
     }
     return;
  } finally {
     unlock(lockKey,requestId);
  }
}

Использование Redisson для реализации реентерабельных блокировок

Псевдокод:

private int expireTime = 1000;

public void run(String lockKey) {
  RLock lock = redisson.getLock(lockKey);
  this.fun(lock,1);
}

public void fun(RLock lock,int level){
  try{
      lock.lock(5, TimeUnit.SECONDS);
      if(level<=10){
         this.fun(lock,++level);
      } else {
         return;
      }
  } finally {
     lock.unlock();
  }
}

Проблема конкуренции блокировок

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

Блокировка чтения-записи

Особенности блокировки чтения-записи:

  • чтение и чтение являются общими и не исключают друг друга;
  • чтение и запись исключают друг друга;
  • запись и запись исключают друг друга.

Мы используем Redisson в качестве примера, который уже реализовал функцию блокировки чтения-записи. Псевдокод блокировки чтения:

RReadWriteLock readWriteLock = redisson.getReadWriteLock("readWriteLock");
RLock rLock = readWriteLock.readLock();
try {
    rLock.lock();
    //бизнес-операция
} catch (Exception e) {
    log.error(e);
} finally {
    rLock.unlock();
}

Псевдокод блокировки записи:

RReadWriteLock readWriteLock = redisson.getReadWriteLock("readWriteLock");
RLock rLock = readWriteLock.writeLock();
try {
    rLock.lock();
    //бизнес-операция
} catch (InterruptedException e) {
   log.error(e);
} finally {
    rLock.unlock();
}

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

Разбиение блокировки на разделы

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

Например, в сценарии «секундная распродажа» в настоящее время в инвентаре имеется 2000 товаров, и пользователи могут участвовать в распродаже. Чтобы предотвратить перепродажу, обычно можно заблокировать инвентарь. Если 1W пользователей конкурируют за одну и ту же блокировку, очевидно, пропускная способность системы будет очень низкой.

Чтобы повысить производительность системы, мы можем разделить инвентарь, например, на 100 частей, так что в каждой части будет 20 товаров для участия в распродаже.

Во время распродажи сначала получите хэш-значение идентификатора пользователя, затем разделите его на 100 по модулю. Пользователи с модулем 1 обращаются к разделу 1 инвентаря, пользователи с модулем 2 обращаются к разделу 2 инвентаря, пользователи с модулем 3 обращаются к разделу 3 инвентаря и так далее, пока пользователи с модулем 100 не обратятся к разделу 100 инвентаря.

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

Проблема блокировки тайм-аута

Если поток A успешно получает блокировку, но из-за длительной бизнес-логики превышает установленный тайм-аут, Redis автоматически освободит блокировку, полученную потоком A.

Решение: автоматическое продление срока действия

Функция автоматического продления срока действия заключается в том, чтобы запустить задачу таймера после получения блокировки, проверять каждые 10 секунд, существует ли блокировка, и обновлять срок действия, если она существует. Если продление происходит 3 раза, то есть через 30 секунд после этого, бизнес-метод всё ещё не выполнен, он больше не будет продлеваться.

Мы можем использовать класс TimerTask для реализации функции автоматического продления:

Timer timer = new Timer(); 
timer.schedule(new TimerTask() {
    @Override
    public void run(Timeout timeout) throws Exception {
        //автоматическое продление логики
    }
}, 10000, TimeUnit.MILLISECONDS);

После получения блокировки автоматически запустите задачу таймера, проверяйте каждые 10 секунд и автоматически обновляйте срок действия. Эта функция в Redisson называется «сторожевой пёс». Конечно, при реализации автоматического продления мы по-прежнему рекомендуем использовать Lua-скрипты, такие как:

if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then 
     redis.call('pexpire', KEYS[1], ARGV[1]);
     return 1; 
end;
return 0;

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

Проблемы с основной и резервной копиями

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

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

Решение: RedissonRedLock

Идея решения RedissonRedLock:

  1. Необходимо настроить несколько независимых сред Redis, например, мы настраиваем 5 сред здесь.
  2. Каждая среда имеет узел Redisson.
  3. Несколько узлов Redisson образуют RedissonRedLock.
  4. Среда включает одномашинный, главный-подчинённый, часовой и кластерный режимы, которые могут быть одним или несколькими смешанными.

Здесь мы берём ведущий-ведомый в качестве примера, структура выглядит следующим образом:

RedissonRedLock получает блокировку следующим образом:

  1. Получите информацию обо всех узлах Redisson, последовательно заблокируйте все узлы Redisson, предположим, что количество узлов равно N, пример равен 5.
  2. Если среди N узлов N/2 + 1 узел успешно заблокирован, то блокировка RedissonRedLock успешна.
  3. Если менее N/2 + 1 узлов успешно заблокированы среди N узлов, блокировка RedissonRedLock не удалась.
  4. Если во время процесса блокировки обнаруживается, что общее затраченное время на блокировку каждого узла превышает установленное максимальное время ожидания, немедленно вернитесь к сбою. Из текста можно сделать вывод, что использование алгоритма Redlock действительно решает проблему потери распределённой блокировки при сбое master-узла в сценариях с несколькими экземплярами. Однако это также приводит к некоторым новым проблемам:
  • Необходимость создания дополнительных сред и запроса большего количества ресурсов, что требует оценки затрат и эффективности.
  • Если есть N узлов Redisson, то для блокировки потребуется выполнить N операций, а для подтверждения успешной блокировки — как минимум N/2+1 операцию. Это увеличивает время блокировки и может привести к неэффективности.

В реальных бизнес-сценариях, особенно в условиях высокой параллельной нагрузки, RedissonRedLock используется не так часто. В распределённых средах нельзя обойти CAP-теорему: необходимо выбирать между согласованностью (Consistency), доступностью (Availability) и устойчивостью к разделению (Partition tolerance).

Если требуется обеспечить согласованность данных, то рекомендуется использовать CP-тип распределённой блокировки, например, Zookeeper, который основан на диске и может обеспечивать надёжность данных.

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

На практике в большинстве распределённых бизнес-сценариев достаточно использовать Redis для распределённой блокировки.

Для обеспечения атомарности операций блокировки и установки срока действия блокировки можно использовать Lua-скрипты с командами SETNX и EXPIRE. Пример кода на Java показывает, как реализовать эти операции.

Также упоминается проблема истечения срока блокировки из-за FullGC, когда блокировка автоматически освобождается. Предлагаются два решения этой проблемы:

  1. Имитация оптимистичной блокировки CAS путём добавления номера версии (токена).
  2. Использование механизма автоматического продления срока блокировки watch dog.

Описывается команда SET с параметрами NX и EX, которая позволяет установить значение ключа только в случае его отсутствия или если ключ уже существует. Клиент получает блокировку, если сервер возвращает OK, и не получает её, если возвращается NIL.

Пример кода на Java демонстрирует, как использовать команду set с параметрами NX, EX и timeout для реализации блокировки и разблокировки. Lua-скрипт

Этот фрагмент представляет собой Lua-скрипт, который проверяет, совпадает ли значение, полученное по ключу (key), со значением, переданным в качестве аргумента (value). Если значения совпадают, выполняется команда del, иначе возвращается 0.

Jedis.eval(String,list,list)

Эта команда выполняет Lua-скрипт. KEYS — это набор ключей, а ARGV — набор аргументов.

③. Повторные попытки

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

/**
 * Механизм повторных попыток
 * @param key идентификатор блокировки
 * @param value идентификатор клиента
 * @param timeOut время ожидания
 * @param retry количество повторных попыток
 * @param sleepTime интервал между попытками
 * @return результат
 */
public Boolean lockRetry(String key, String value, Long timeOut, Integer retry, Long sleepTime) {
    Boolean flag = false;
    try {
        for (int i=0;i<retry;i++) {
            flag = lock(key,value,timeOut);
            if(flag) {
                break;
            }
            Thread.sleep(sleepTime);
        }
    } catch (Exception e) {
        e.printStackTrace();
    }
    return flag;
}

Redisson

Redisson — это Java-библиотека, которая предоставляет реализацию In-Memory Data Grid на основе Redis. Она предлагает ряд распределённых объектов, таких как:

  • Reentrant Lock — повторно входящие блокировки;
  • Fair Lock — честные блокировки;
  • MultiLock — множественные блокировки;
  • RedLock — красные блокировки;
  • ReadWriteLock — блокировки чтения и записи.

Также Redisson предоставляет множество распределённых сервисов.

Особенности и функции

  • Поддержка различных режимов работы с Redis: одиночный узел, режим дозорного, главный/подчиненный и кластерный режим.
  • Возможность асинхронного выполнения и асинхронного потока выполнения.
  • Сериализация данных.
  • Автоматическое разделение данных в кластере.
  • Разнообразные распределённые объекты, такие как Object Bucket, Bitset, AtomicLong, Bloom Filter и HyperLogLog.
  • Распределенные коллекции, включая Map, Multimap, Set, SortedSet, List, Deque и Queue.
  • Реализация распределенных блокировок и синхронизаторов, таких как Reentrant Lock, Fair Lock, MultiLock, Red Lock, Semaphonre и PermitExpirableSemaphore.
  • Расширенные распределенные сервисы, такие как Remote Service, Live Object Service, Executor Service, Schedule Service и MapReduce.

Watch dog

В целом, Redisson предлагает следующие типы распределенных блокировок:

  • повторно входящие блокировки,
  • честные блокировки,
  • множественные блокировки,
  • красные блокировки,
  • блокировки чтения и записи,
  • семафоры,
  • истекающие семафоры,
  • закрытые блокировки.

Реализация

Для использования Redisson необходимо добавить зависимость в проект. Существует два способа добавления зависимости:

  1. Через Maven:
<!-- Способ 1: redisson-java -->
<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson</artifactId>
    <version>3.11.4</version>
</dependency>
  1. Через Spring Boot:
<!-- Способ 2: redisson-springboot -->
<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson-spring-boot-starter</artifactId>
    <version>3.11.4</version>
</dependency>

Затем необходимо определить интерфейс для работы с распределенными блокировками:

import org.redisson.api.RLock;
import java.util.concurrent.TimeUnit;

public interface DistributedLocker {

    RLock lock(String lockKey);

    RLock lock(String lockKey, int timeout);

    RLock lock(String lockKey, TimeUnit unit, int timeout);

    boolean tryLock(String lockKey, TimeUnit unit, int waitTime, int leaseTime);

    void unlock(String lockKey);

    void unlock(RLock lock);
}

И наконец, реализовать логику работы с распределёнными блокировками:

import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;

import java.util.concurrent.TimeUnit;

public class RedissonDistributedLocker implements DistributedLocker{

    private RedissonClient redissonClient;

    @Override
    public RLock lock(String lockKey) {
        RLock lock = redissonClient.getLock(lockKey);
        lock.lock();
        return lock;
    }

    // Другие методы интерфейса
}
``` **Высоконадёжный принцип работы RedLock (красной блокировки)**

Идея алгоритма RedLock заключается в том, что нельзя создавать блокировку только на одном экземпляре redis, а следует создавать её на нескольких экземплярах redis. Количество экземпляров должно быть n / 2 + 1. Блокировка считается успешной, если она успешно создана на большинстве узлов redis. Это позволяет избежать проблем, связанных с созданием блокировки только на одном узле redis.

**Zookeeper**

### Apache-Curator

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

* InterProcessMutex  это реализация справедливой блокировки, которая может быть повторно использована и является эксклюзивной.
* InterProcessSemaphoreMutex  эксклюзивная блокировка, которую нельзя использовать повторно.
* InterProcessReadWriteLock  блокировка чтения-записи.
* InterProcessSemaphoreV2  общий семафор.
* InterProcessMultiLock  многопользовательская блокировка (контейнер для управления несколькими блокировками как единым целым).

**Пример использования**
```java
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.TimeUnit;

import org.apache.curator.RetryPolicy;
import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.CuratorFrameworkFactory;
import org.apache.curator.framework.recipes.locks.InterProcessLock;
import org.apache.curator.framework.recipes.locks.InterProcessMultiLock;
import org.apache.curator.framework.recipes.locks.InterProcessMutex;
import org.apache.curator.framework.recipes.locks.InterProcessReadWriteLock;
import org.apache.curator.framework.recipes.locks.InterProcessSemaphoreMutex;
import org.apache.curator.framework.recipes.locks.InterProcessSemaphoreV2;
import org.apache.curator.framework.recipes.locks.Lease;
import org.apache.curator.retry.ExponentialBackoffRetry;
import org.apache.curator.utils.CloseableUtils;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;

public class DistributedLockDemo {

    // ZooKeeper 锁节点路径, 分布式锁的相关操作都是在这个节点上进行
    private final String lockPath = "/distributed-lock";
    // ZooKeeper 服务地址, 单机格式为:(127.0.0.1:2181),
    // 集群格式为:(127.0.0.1:2181,127.0.0.1:2182,127.0.0.1:2183)
    private String connectString="127.0.0.1:2181";
    // Curator 客户端重试策略
    private RetryPolicy retry;
    // Curator 客户端对象
    private CuratorFramework client1;
    // client2 用户模拟其他客户端
    private CuratorFramework client2;

    // 初始化资源
    @Before
    public void init() throws Exception {
        // 重试策略
        // 初始休眠时间为 1000ms, 最大重试次数为 3
        retry = new ExponentialBackoffRetry(1000, 3);
        // 创建一个客户端, 60000(ms)为 session 超时时间, 15000(ms)为链接超时时间
        client1 = CuratorFrameworkFactory.newClient(connectString, 60000, 15000, retry);
        client2 = CuratorFrameworkFactory.newClient(connectString, 60000, 15000, retry);
        // 创建会话
        client1.start();
        client2.start();
    }

    // 释放资源
    @After
    public void close() {
        CloseableUtils.closeQuietly(client1);
    }

    /**
     * InterProcessMutex:可重入、独占锁
     */
    @Test
    public void sharedReentrantLock() throws Exception {
        // 创建可重入锁
        InterProcessMutex lock1 = new InterProcessMutex(client1, lockPath);
        // lock2 用于模拟其他客户端
        InterProcessMutex lock2 = new InterProcessMutex(client2, lockPath);

        // lock1 获取锁
        lock1.acquire();
        try {
            // lock1 第2次获取锁
            lock1.acquire();
            try {
                // lock2 超时获取��ко, 因为��ко уже было занято lock1 клиентом, поэтому lock2 не удаётся получить блокировку, нужно дождаться освобождения lock1
                Assert.assertFalse(lock2.acquire(2, TimeUnit.SECONDS));
            } finally {
                lock1.release();
            }
        } finally {
            // 重入 блокировки должны соответствовать друг другу, если вы получаете 2 раза, отпустите 1 раз, то эта блокировка всё ещё занята,
            // если закомментировать следующую строку кода, то вы обнаружите, что блокировка не получена lock2
            lock1.release();
        }

        // После освобождения lock1 блокировка может быть получена lock2
        Assert.assertTrue(lock2.acquire(2, TimeUnit.SECONDS));
        lock2.release();
    }
    
    /**
     * InterProcessSemaphoreMutex: не может быть повторена, эксклюзивная блокировка
     */
    @Test
    public void sharedLock() throws Exception {
        InterProcessSemaphoreMutex lock1 = new InterProcessSemaphoreMutex(client1, lockPath);
        // lock2 используется для имитации других клиентов
        InterProcessSemaphoreMutex lock2 = new InterProcessSemaphoreMutex(client2, lockPath);

        // Получение объекта блокировки
        lock1.acquire();

        // Проверка возможности повторного входа
    ``` ```
// Потому что блокировка уже получена, возвращается false
Assert.assertFalse(lock1.acquire(2, TimeUnit.SECONDS)); // lock1 возвращает false
Assert.assertFalse(lock2.acquire(2, TimeUnit.SECONDS)); // lock2 возвращает false

// lock1 освобождает блокировку
lock1.release();

// lock2 успешно получает блокировку, потому что она была освобождена
Assert.assertTrue(lock2.acquire(2, TimeUnit.SECONDS));  // возвращает true
lock2.release();
System.out.println("Тестирование завершено");
}

/**
 * InterProcessReadWriteLock: блокировка чтения-записи.
 * Особенности: блокировка чтения-записи, реентерабельность
*/
@Test
public void sharedReentrantReadWriteLock() throws Exception {
    // Создаём объект блокировки чтения-записи, Curator реализует справедливую блокировку
    InterProcessReadWriteLock lock1 = new InterProcessReadWriteLock(client1, lockPath);
    // lock2 используется для имитации других клиентов
    InterProcessReadWriteLock lock2 = new InterProcessReadWriteLock(client2, lockPath);

    // Используем lock1 для имитации операции чтения
    // Используем lock2 для имитации операции записи
    // Получаем блокировку чтения (реализуется с помощью InterProcessMutex, поэтому реентерабельна)
    final InterProcessLock readLock = lock1.readLock();
    // Получаем блокировку записи (реализуется с помощью InterProcessMutex, поэтому реентерабельна)
    final InterProcessLock writeLock = lock2.writeLock();

    /**
     * Тестируемый объект блокировки чтения-записи
     */
    class ReadWriteLockTest {
        // Поле данных для изменения
        private Integer testData = 0;
        private Set<Thread> threadSet = new HashSet<>();

        // Запись данных
        private void write() throws Exception {
            writeLock.acquire();
            try {
                Thread.sleep(10);
                testData++;
                System.out.println("Запись данных \t" + testData);
            } finally {
                writeLock.release();
            }
        }

        // Чтение данных
        private void read() throws Exception {
            readLock.acquire();
            try {
                Thread.sleep(10);
                System.out.println("Чтение данных \t" + testData);
            } finally {
                readLock.release();
            }
        }

        // Ожидание завершения потоков, чтобы предотвратить выход текущего потока после вызова метода test,
        // что может привести к невозможности вывода информации на консоль
        public void waitThread() throws InterruptedException {
            for (Thread thread : threadSet) {
                thread.join();
            }
        }

        // Метод создания потока
        private void createThread(final int type) {
            Thread thread = new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        if (type == 1) {
                            write();
                        } else {
                            read();
                        }
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            });
            threadSet.add(thread);
            thread.start();
        }

        // Тестовый метод
        public void test() {
            for (int i = 0; i < 5; i++) {
                createThread(1);
            }
            for (int i = 0; i < 5; i++) {
                createThread(2);
            }
        }
    }

    ReadWriteLockTest readWriteLockTest = new ReadWriteLockTest();
    readWriteLockTest.test();
    readWriteLockTest.waitThread();
}

/**
* InterProcessSemaphoreV2: общий семафор
*/
@Test
public void semaphore() throws Exception {
    // Создание семафора, Curator использует справедливый механизм блокировки
    InterProcessSemaphoreV2 semaphore1 = new InterProcessSemaphoreV2(client1, lockPath, 6);
    // semaphore2 используется для имитации других клиентов
    InterProcessSemaphoreV2 semaphore2 = new InterProcessSemaphoreV2(client2, lockPath, 6);

    // Получение разрешения
    Lease lease1 = semaphore1.acquire();
    Assert.assertNotNull(lease1);
    // semaphore.getParticipantNodes() вернёт список узлов, участвующих в текущем семафоре, информация, полученная двумя клиентами, одинакова
    Assert.assertEquals(semaphore1.getParticipantNodes(), semaphore2.getParticipantNodes());

    // Попытка получить разрешение с таймаутом
    Lease lease2 = semaphore2.acquire(2, TimeUnit.SECONDS);
``` ```
Assert.assertNotNull(lease2);
Assert.assertEquals(semaphore1.getParticipantNodes(), semaphore2.getParticipantNodes());

// 获取多个许可, 参数为许可数量
Collection<Lease> leases = semaphore1.acquire(2);
Assert.assertTrue(leases.size() == 2);
Assert.assertEquals(semaphore1.getParticipantNodes(), semaphore2.getParticipantNodes());

// 超时获取多个许可, 第一个参数为许可数量
Collection<Lease> leases2 = semaphore2.acquire(2, 2, TimeUnit.SECONDS);
Assert.assertTrue(leases2.size() == 2);
Assert.assertEquals(semaphore1.getParticipantNodes(), semaphore2.getParticipantNodes());

// 目前 semaphore 已经获取 3 个许可, semaphore2 也获取 3 个许可, 加起来为 6 个, 所以他们无法再进行许可获取
Assert.assertNull(semaphore1.acquire(2, TimeUnit.SECONDS));
Assert.assertNull(semaphore2.acquire(2, TimeUnit.SECONDS));

// 释放一个许可
semaphore1.returnLease(lease1);
semaphore2.returnLease(lease2);
// 释放多个许可
semaphore1.returnAll(leases);
semaphore2.returnAll(leases2);
}

/**
 * InterProcessMutex :可重入、独占锁
 * InterProcessSemaphoreMutex : 不可重入、独占锁
 * InterProcessMultiLock: 多重共享锁(将多个锁作为单个实体管理的容器)
 */
@Test
public void multiLock() throws Exception {
InterProcessMutex mutex = new InterProcessMutex(client1, lockPath);
InterProcessSemaphoreMutex semaphoreMutex = new InterProcessSemaphoreMutex(client2, lockPath);
//将上面的两种锁入到其中
InterProcessMultiLock multiLock = new InterProcessMultiLock(Arrays.asList(mutex, semaphoreMutex));
// 获取参数集合中的所有锁
multiLock.acquire();
// 因为存在一个不可重入锁, 所以整个 multiLock 不可重入
Assert.assertFalse(multiLock.acquire(2, TimeUnit.SECONDS));
// mutex 是可重入锁, 所以可以继续获取锁
Assert.assertTrue(mutex.acquire(2, TimeUnit.SECONDS));
// semaphoreMutex  是不可重入锁, 所以获取锁失败
Assert.assertFalse(semaphoreMutex.acquire(2, TimeUnit.SECONDS));
// 释放参数集合中的所有锁
multiLock.release();
// interProcessLock2 中的锁已经释放, 所以可以获取
Assert.assertTrue(semaphoreMutex.acquire(2, TimeUnit.SECONDS));
}

Развёрнутый ответ на текст запроса:

В запросе представлен фрагмент кода на языке Java, который содержит тесты для проверки работы различных типов блокировок в многопоточной среде. В коде используются классы InterProcessMutex, InterProcessSemaphoreMutex и InterProcessMultiLock для управления доступом к общим ресурсам.

Также в запросе присутствует описание распределённой системы и её компонентов, а также методов ограничения потока запросов к системе. Описаны такие методы, как TPS (Transactions Per Second), HPS (Hits Per Second) и QPS (Queries Per Second).

Текст запроса не содержит вопросов или задач, которые требуют решения. ``` long generateToken = (currentTime - refreshTime) / 1000 * putTokenRate; //生成的令牌 =(当前时间-上次刷新时间)* 放入令牌速率 currentToken = Math.min(capacity, generateToken + currentToken); // 当前令牌数量 = 之前的桶内令牌数量+放入的令牌数量 refreshTime = currentTime; // 刷新时间

//桶里面还有令牌,请求正常处理 if (currentToken > 0) { currentToken--; //令牌数量-1 return true; }

return false;


Это похоже на язык Java.

```lua
-- 获取调用脚本时传入的第一个 key 值(用作限流的 key)
local key = KEYS[1]
-- 获取调用脚本时传入的第一个参数值(限流大小)
local limit = tonumber(ARGV[1])
-- 获取计数器的限速区间 TTL
local ttl = tonumber(ARGV[2])

-- 获取当前流量大小
local curentLimit = tonumber(redis.call('get', key) or "0")

-- 是否超出限流
if curentLimit + 1 > limit then
    -- 返回 (拒绝)
    return 0
else
    -- 没有超出 value + 1
    redis.call('INCRBY', key, 1)
    -- 如果 key 中保存的并发计数为 0,说明当前是一个新的时间窗口,它的过期时间设置为窗口的过期时间
    if (current_permits == 0) then
          redis.call('EXPIRE', key, ttl)
      end
    -- 返回 (放行)
    return 1
end

Этот фрагмент написан на языке Lua.

Данный фрагмент кода реализует алгоритм ограничения скорости запросов к Redis с использованием команд INCRBY и EXPIRE. Он получает ключ, лимит и TTL от вызывающего скрипта, проверяет текущий размер потока и увеличивает его на 1, если он не превышает лимит. Если текущий размер потока равен нулю, то устанавливается время жизни ключа равным TTL.

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

В следующем фрагменте также используется алгоритм токена.

-- key
local key = KEYS[1]
-- 最大存储的令牌数
local max_permits = tonumber(KEYS[2])
-- 每秒钟产生的令牌数
local permits_per_second = tonumber(KEYS[3])
-- 请求的令牌数
local required_permits = tonumber(ARGV[1])

-- 下次请求可以获取令牌的起始时间
local next_free_ticket_micros = tonumber(redis.call('hget', key, 'next_free_tickt_micros') or 0)

-- 当前时间
local time = redis.call('time')
-- time[1] 返回的为秒,time[2] 为 ms
local now_micros = tonumber(time[1]) * 1000000 + tonumber(time[2])

-- 查询获取令牌是否超时(传入参数,单位为 微秒)
if (ARGV[2] ~= nil) then
    -- 获取令牌的超时时间
    local timeout_micros = tonumber(ARGV[2])
    local micros_to_wait = next_free_ticket_micros - now_micros
    if (micros_to_wait> timeout_micros) then
        return micros_to_wait
    end
end

-- 当前存储的令牌数
local stored_permits = tonumber(redis.call('hget', key, 'stored_permits') or 0)
-- 添加令牌的时间间隔(1000000ms 为 1s)
-- 计算生产 1 个令牌需要多少微秒
local stable_interval_micros = 1000000 / permits_per_second

-- 补充令牌
if (now_micros> next_free_ticket_micros) then
    local new_permits = (now_micros - next_free_ticket_micros) / stable_interval_micros
    stored_permits = math.min(max_permits, stored_permits + new_permits)
    -- 补充后,更新下次可以获取令牌的时间
    next_free_ticket_micros = now_micros
end

-- 消耗令牌
local moment_available = next_free_ticket_micros
-- 两种情况:required_permits<=stored_permits 或者 required_permits>stored_permits
local stored_permits_to_spend = math.min(required_permits, stored_permits)
local fresh_permits = required_permits - stored_permits_to_spend;
-- 如果 fresh_permits>0,说明令牌桶的剩余数目不够了,需要等待一段时间
local wait_micros = fresh_permits * stable_interval_micros

-- Redis 提供了 redis.replicate_commands() 函数来实现这一功能,把发生数据变更的命令以事务的方式做持久化和主从复制,从而允许在 Lua 脚本内进行随机写入
redis.replicate_commands()
-- 存储剩余的令牌数:桶中剩余的数目 - 本次申请的数目
redis.call('hset', key, 'stored_permits', stored_permits - stored_permits_to_spread)
redis.call('hset', key, 'next_free_ticket_micros', next_free_ticket_micros + wait_micros)
redis.call('expire', key, 10)

-- 返回需要等待的时间长度
-- 返回为 0(moment_available==now_micros)表示桶中剩余的令牌足够,不需要等待
return moment_available - now_micros

Здесь также используется алгоритм токенов, который позволяет ограничивать скорость запросов к системе. Код получает параметры, такие как ключ, максимальный объём токенов, количество токенов в секунду и требуемое количество токенов для запроса. Затем он проверяет текущее состояние токенов и решает, можно ли выполнить запрос или нужно подождать. ``` limit_req_log_level warn;

Настройка (http, server, location) запуска без фильтрации. Включение не фильтрует запросы, но регистрирует журналы превышения скорости, по умолчанию отключено

limit_req_dry_run off; proxy_pass http://my_upstream; } error_page 555 /555json; location = /555json { default_type application/json; add_header Content-Type 'text/html; charset=utf-8'; return 200 '{"code": 666, "update":"Доступ в пиковые часы, пожалуйста, попробуйте позже"}'; }


**Ключ**: определение объекта ограничения скорости, $binary_remote_addr означает использование remote_addr для ограничения скорости, binary_ предназначен для уменьшения использования памяти.

**Зона**: определение области общей памяти для хранения информации о доступе, myRateLimit:10m означает область памяти размером 10M с именем myRateLimit. 1M может хранить информацию о доступе 16000 IP-адресов, а 10M может хранить информацию о доступе 16W IP-адресов.

**Скорость**: установка максимальной скорости доступа, rate=10r/s означает обработку максимум 10 запросов в секунду. Nginx фактически отслеживает информацию о запросах с точностью до миллисекунды, поэтому 10r/s на самом деле ограничивает обработку одного запроса каждые 100 миллисекунд. То есть после обработки предыдущего запроса и поступления ещё 20 запросов в течение следующих 100 миллисекунд они будут отклонены.

**Разрыв**: обработка всплесков трафика. burst=20 означает, что если одновременно поступит 21 запрос, Nginx обработает первый запрос, а остальные 20 запросов будут помещены в очередь, затем каждый раз через 100 мс из очереди будет извлекаться один запрос для обработки. Если количество запросов превышает 21, дополнительные запросы будут отклонены, и будет возвращён ответ 503. burst=20 nodelay означает немедленную обработку 20 запросов без задержки. Даже если эти 20 всплесков запросов будут немедленно обработаны, последующие запросы также не будут обрабатываться немедленно. **burst=20** эквивалентен выделению 20 мест в очереди, даже если запросы обрабатываются, эти 20 позиций могут быть освобождены только каждые 100 мс. **Третий шаг**: Lua-тесты

`/usr/local/lua_test/redis_test.lua`:

```lua
local redis = require "resty.redis"
local cache = redis.new()
cache.connect(cache, '127.0.0.1', '6379')
local res = cache:get("foo")
if res==ngx.null then
    ngx.say("This is Null")
    return
end
ngx.say(res)

/usr/local/lua_test/mysql_test.lua:

local mysql = require "resty.mysql"
local db, err = mysql:new()
if not db then
    ngx.say("failed to instantiate mysql: ", err)
    return
end

db:set_timeout(1000) -- 1 sec

-- or connect to a unix domain socket file listened
-- by a mysql server:
--     local ok, err, errno, sqlstate =
--           db:connect{
--              path = "/path/to/mysql.sock",
--              database = "ngx_test",
--              user = "ngx_test",
--              password = "ngx_test" }

local ok, err, errno, sqlstate = db:connect{
    host = "127.0.0.1",
    port = 3306,
    database = "test",
    user = "root",
    password = "123456",
    max_packet_size = 1024 * 1024 }

if not ok then
    ngx.say("failed to connect: ", err, ": ", errno, " ", sqlstate)
    return
end

ngx.say("connected to mysql.")

local res, err, errno, sqlstate =
    db:query("drop table if exists cats")
if not res then
    ngx.say("bad result: ", err, ": ", errno, ": ", sqlstate, ".")
    return
end

res, err, errno, sqlstate =
    db:query("create table cats "
             .. "(id serial primary key, "
             .. "name varchar(5))")
if not res then
    ngx.say("bad result: ", err, ": ", errno, ": ", sqlstate, ".")
    return
end

ngx.say("table cats created.")

res, err, errno, sqlstate =
    db:query("insert into cats (name) "
             .. "values (\'Bob\'),(\'\'),(null)")
if not res then
    ngx.say("bad result: ", err, ": ", errno, ": ", sqlstate, ".")
    return
end

ngx.say(res.affected_rows, " rows inserted into table cats ",
        "(last insert id: ", res.insert_id, ")")

-- run a select query, expected about 10 rows in
-- the result set:
res, err, errno, sqlstate =
    db:query("select * from cats order by id asc", 10)
if not res then
    ngx.say("bad result: ", err, ": ", errno, ": ", sqlstate, ".")
    return
end

local cjson = require "cjson"
ngx.say("result: ", cjson.encode(res))

-- put it into the connection pool of size 100,
-- with 10 seconds max idle timeout
local ok, err = db:set_keepalive(10000, 100)
if not ok then
    ngx.say("failed to set keepalive: ", err)
    return
end

-- or just close the connection right away:
-- local ok, err = db:close()
-- if not ok then
--     ngx.say("failed to close: ", err)
--     return
-- end
';

Четвёртый шаг: проверка результатов

$ curl 'http://127.0.0.1/lua_test' 
hello, lua

$ redis-cli set foo 'hello,lua-redis' 
OK 
$ curl 'http://127.0.0.1/lua_redis' 
hello,lua-redis

$ curl 'http://127.0.0.1/lua_mysql' 
connected to mysql. 
table cats created. 
3 rows inserted into table cats (last insert id: 1) 
result: [{"name":"Bob","id":"1"},{"name":"","id":"2"},{"name":null,"id":"3"}]

$ curl 'http://127.0.0.1/cats' 
{"errcode":400,"errstr":"expecting \\"name\\" or \\"id\\" query arguments"}

$ curl 'http://127.0.0.1/cats?name=bob' 
[{"id":1,"name":"Bob"}]

$ curl 'http://127.0.0.1/cats?id=2' 
[{"id":2,"name":""}]

$ curl 'http://127.0.0.1/mysql/bob' 
[{"id":1,"name":"Bob"}]

$ curl 'http://127.0.0.1/mysql-status' 
worker process: 32261

upstream backend 
  active connections: 0 
  connection pool capacity: 0 
  servers: 1 
  peers: 1

API экономика

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

Заёмный механизм Перевод текста на русский язык:

В отношении двух последовательных временных окон переключения, если в предыдущем временном окне количество запросов было использовано полностью, то первое временное окно может заранее использовать часть запросов из следующего временного окна (меньше, чем объём ресурсов в каждом временном окне, рекомендуется использовать не более 20% ресурсов). Если в этом временном окне также будут использованы все ресурсы, то будет активировано ограничение потока. Во втором временном окне будет использоваться меньше запросов, которые были заимствованы. При заимствовании части запросов во время переключения, если текущее временное окно «имеет задолженность» (то есть есть заимствованные запросы), то это временное окно не позволяет заимствовать ресурсы у следующего временного окна.

Распределённый кэш

Алгоритм устаревания

Наименее используемый алгоритм (LFU)

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

Наиболее недавно использованный алгоритм (LRU)

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

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

Преимущества: простая реализация, адаптация к горячим точкам доступа. Недостатки: чувствительность к случайным доступам, влияние на коэффициент попадания. Затраты на ведение журнала: время O(1), пространство O(N).

Адаптивный алгоритм замены кэша (ARC)

Разработанный в исследовательском центре IBM Almaden, этот алгоритм кэширования отслеживает записи LFU и LRU, а также вытесняет элементы кэша для достижения оптимального использования доступного кэша.

Первым пришёл — первым ушёл (FIFO)

FIFO — это аббревиатура от английского First In First Out, это тип очереди данных, который работает по принципу «первым пришёл — первым ушёл». Он отличается от обычной памяти отсутствием внешних линий чтения/записи адресов, что делает его очень простым в использовании, но недостатком является то, что данные могут быть записаны и прочитаны только последовательно, а чтение и запись данных осуществляется с помощью внутренней очереди указателей чтения/записи, которая автоматически увеличивается на 1.

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

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

Последний наиболее часто используемый алгоритм (MRU)

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

Стратегия обновления

Стратегии обновления кэша в основном делятся на три типа:

  • Кэширование в стороне (Cache Aside);
  • Чтение/запись через (Read/Write Through);
  • Асинхронная запись в кэш (Write Behind Caching).

Сценарии использования кэша

В распределённых системах либо обеспечивается строгая согласованность с использованием протоколов 2PC, 3PC или Paxos, либо предпринимаются отчаянные попытки снизить вероятность грязных данных при параллельной работе. Система кэширования применима в сценариях с нестрогой согласованностью, поэтому она относится к категории AP в CAP, обеспечивая только окончательную согласованность, как указано в теории BASE. Разнородные базы данных изначально не могут обеспечить строгую согласованность, мы просто стараемся минимизировать время несогласованности и достичь окончательной согласованности. В то же время применяется стратегия установки срока действия.

Анализ сценариев использования кэша

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

Кэширование в стороне

Кэширование в сторону (Cache Aside) является одним из наиболее широко используемых шаблонов кэширования. Правильное использование кэширования в сторону может значительно повысить производительность приложений, и оно может применяться как для операций чтения, так и для записи. Цель кэширования в сторону — максимально решить проблему несоответствия данных между кэшем и базой данных.

Чтение кэша в стороне

Процесс запроса на чтение выглядит следующим образом:

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

Запись кэша в сторону

Процесс записи запроса выглядит следующим образом:

  • При обновлении сначала обновляются данные в базе данных, затем данные удаляются из кэша.

Чтение/Запись через

В режиме чтения/записи через (Read/Write Through) сервер рассматривает кэш как основное хранилище данных. Приложения взаимодействуют с базой данных через абстрактный уровень кэша.

Прочитать через

Режим чтения через (Read Through) похож на режим кэширования в стороне, за исключением того, что программе не нужно управлять тем, откуда считывать данные (кэш или база данных). Вместо этого она напрямую считывает данные из кэша, и в этом сценарии кэш определяет, откуда запрашивать данные. Сравнение этих двух методов является преимуществом, поскольку оно делает код программы более лаконичным. Процесс Read Through выглядит следующим образом:

  • Считывание данных из кэша и немедленное возвращение.
  • Если данные не найдены, загрузка из базы данных, помещение данных в кэш и возврат ответа.

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

Написать через

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

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

Когда используется режим записи через, обычно также используется режим чтения через. Режим записи через подходит, когда:

  • Требуется частое чтение одних и тех же данных.
  • Нетерпимость к потере данных (по сравнению с режимом Write Behind) и несогласованность данных. Примером потенциального использования режима записи через является банковская система.

Написать за (асинхронная запись)

Написать за (Write Behind, также называемый Write Back) похож на Read/Write Through в том, что Cache Provider отвечает за чтение и запись кэша и базы данных. Однако между ними существует большое различие: Read/Write Through синхронизирует обновление кэша и данных, в то время как Write Behind обновляет только кэш, не обновляя базу данных напрямую, используя пакетную асинхронную запись для обновления базы данных.

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

  • Преимущества: быстрая скорость записи данных, подходящая для сценариев с частой записью.
  • Недостатки: несогласованность между кэшем и базой данных не является сильной, и её следует использовать с осторожностью в системах с высокими требованиями к согласованности. 最终一致性 — это особый случай слабой согласованности, когда система гарантирует достижение состояния согласованных данных в течение определённого времени.

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

Бизнес-задержка двойной очистки

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

  1. Сначала удалите кеш.
  2. Затем обновите базу данных.
  3. Подождите 1 секунду (эта секунда = максимальное время потребления, в основном это время ожидания завершения загрузки запросов, загружающих грязные данные).
  4. Снова удалите кеш.

Рисунок: Процесс отложенной двойной очистки.

В архитектуре с разделением чтения и записи:

  1. Сбросьте кеш.
  2. Обновите базу данных.
  3. Подождите 1 секунду (это время = максимальное время синхронизации главного и подчиненного + максимальное время потребления).
  4. Повторите удаление кеша.

Отложенная двойная очистка приводит к снижению пропускной способности.

Решение: сделать второе удаление асинхронным.

MQ механизм повторной попытки удаления

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

Рисунок: Решение проблемы удаления кеша.

Процесс:

  1. Обновите данные в базе данных.
  2. Кеш становится недоступным, что приводит к невозможности удаления данных из кеша.
  3. Когда удаление данных из кеша не удаётся, ключ, который необходимо удалить, отправляется в очередь сообщений (MQ).
  4. Приложение само потребляет ключ сообщения, которое необходимо удалить.
  5. После получения сообщения приложение удаляет кеш. Если подтверждение MQ-сообщения о том, что сообщение было использовано, получено после удаления кеша, если удаление кеша завершается неудачно, сообщение повторно ставится в очередь, и предпринимаются множественные попытки удалить кеш.

Оптимизация: используйте binlog MySQL для асинхронного удаления ключей.

Рисунок: Асинхронное решение проблемы удаления кеша на основе binlog.

Процесс:

  1. Обновите данные в базе данных.
  2. MySQL записывает данные обновления журнала в binlog.
  3. Canal подписывается и использует binlog MySQL, извлекает имя таблицы и идентификатор обновлённых данных.
  4. Вызов интерфейса удаления кеша приложения.
  5. Удалить данные из кеша Redis.
  6. Если Redis недоступен, отправьте имя таблицы и ID обновлённых данных в MQ.
  7. После получения сообщения приложение удаляет кеш. Если подтверждение MQ-сообщения о том, что сообщение было использовано, получено после удаления кеша, если удаление кеша завершается неудачно, сообщение повторно ставится в очередь, и предпринимаются множественные попытки удалить кеш, пока удаление кеша не будет успешным.

Выбор стратегии: удаление или обновление

При работе с кешем следует ли удалять кеш или обновлять его? В повседневной разработке обычно используется режим Cache-Aside. Некоторые пользователи могут спросить, почему при записи запроса используется удаление кеша вместо обновления кеша?

Рисунок: Поток записи в режиме Cache-Aside.

Мы рассматриваем удаление кеша или его обновление при работе с кешем? Давайте рассмотрим пример:

Рисунок: Пример потока записи в режиме Cache-Aside.

Последовательность действий:

  1. Поток A инициирует операцию записи, сначала обновляя базу данных.
  2. Поток B инициирует другую операцию записи, обновляя базу данных во второй раз.
  3. Из-за сетевых проблем поток B сначала обновляет кеш.
  4. Поток А обновляет кеш.

На этом этапе кеш сохраняет данные потока A (старые данные), а база данных сохраняет данные потока B (новые данные), что приводит к грязным данным. Если вы замените обновление кеша удалением кеша, проблема грязных данных не возникнет. Обновление кеша по сравнению с удалением кеша имеет два недостатка:

— Если значение, записанное в кеш, является результатом сложных вычислений, высокая частота обновлений кеша приведёт к потере производительности. — В сценарии с большим количеством операций записи и небольшим количеством операций чтения данные часто не считываются до их обновления, а затем обновляются, что также приводит к потере производительности (на самом деле, использование кеша в сценарии с большим количеством записей не очень рентабельно).

Порядок двойной записи

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

Вариант 1: сначала обновите базу данных, а затем обновите кеш

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

Пример:

A сначала обновляет базу данных до 123, но обновление кеша задерживается из-за проблем с сетью. В это время B обновляет базу данных до 456, а затем немедленно обновляет кеш до 456. Теперь запрос на обновление кеша от A достигает, и кеш обновляется до 123. На данный момент данные несовместимы, база данных содержит последние 456 данных, а кеш содержит старые 123 данных. Поскольку операции обновления базы данных и кеша не являются атомарными, эти две операции будут вставлены непосредственно в другие операции при высокой параллельной работе.

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

Вариант 2: сначала обновите кеш, а затем базу данных

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

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

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

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

Обновление кеша

Помимо стратегий истечения срока действия кеша, предоставляемых самим сервером кеширования (Redis по умолчанию предоставляет 6 стратегий), вы также можете настроить стратегию истечения срока действия в соответствии с конкретными бизнес-требованиями. Общие стратегии обновления включают:

LRU/LFU/FIFO: все они используются, когда кеш становится недостаточным. Они подходят для сценариев с ограниченной памятью, где данные редко меняются, и вероятность несогласованности данных очень мала. Например, некоторые данные, которые определяются как неизменяемые после определения. Истечение срока действия: установите срок действия для данных кеша. Подходит для бизнеса, который может терпеть определённое время несогласованности данных, например, описание рекламных акций. Активное обновление: если источник данных обновлён, активно обновляйте кеш. Для данных с высокими требованиями к согласованности, таких как системы транзакций и количество льготных купонов. Проблемы с кешем: лавины и дыры

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

Причины возникновения «лавины кеша»:

  • Одновременное истечение срока действия большого объёма данных.

Решения проблемы «лавины кеша»:

  • Равномерное установление сроков действия для кешированных данных с добавлением случайного числа.
  • Использование мьютекса для обеспечения того, чтобы только один запрос одновременно мог создавать кеш.
  • Применение многоуровневого кеша, где каждый уровень имеет свой собственный срок действия.
  • Контроль над запросами к базе данных через очередь сообщений (MQ).
  • Постоянное хранение «горячих» данных, которые часто запрашиваются, без истечения срока их действия.

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

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

Способы предотвращения «дыр в кеше»:

  • Мьютекс для контроля одновременного доступа к кешу.
  • Постоянное хранение «горячих» элементов без истечения срока действия.

Проблема проникновения в кеш

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

Причины проникновения в кеш:

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

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

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

Пример работы фильтра Блума: Предположим, у нас есть битовая карта длиной 8 и три хеш-функции. Мы хотим добавить элемент «A» в фильтр. Хеш-функции возвращают значения 3, 5 и 7 соответственно. Позиции 3, 5 и 7 устанавливаются в «1» на битовой карте. Теперь, если мы ищем элемент «B», хеш-функции могут вернуть значения 2, 4 и 6. Поскольку эти позиции не установлены в «1», можно сделать вывод, что элемента «B» нет в фильтре. Key的实例内存使用量远大于其他实例,что приводит к нехватке памяти и нагрузке на весь кластер.

Общие сценарии:

  • обсуждение популярной темы;
  • список подписчиков популярного аккаунта;
  • сериализованное изображение;
  • необработанные данные.

Влияние большого Key:

  • большой Key занимает много памяти, что затрудняет балансировку в Redis-кластере;
  • снижение производительности Redis;
  • длительные операции при удалении или истечении срока действия, вызывающие блокировку сервиса.

Как обнаружить большой Key:

  1. Команда redis-cli --bigkeys. Находит 5 типов данных (String, hash, list, set, zset) с наибольшим размером ключа. Однако выполнение этой команды может занять много времени, если в Redis много ключей.
  2. Получение файла RDB производственного Redis и анализ его с помощью rdbtools для создания CSV-файла, который затем можно импортировать в MySQL или другую базу данных для анализа и статистики.

Решения для оптимизации больших Key:

  • Для строковых данных: избегать хранения в Redis, использовать документно-ориентированную базу данных, такую как MongoDB, или кэшировать данные на CDN. Если необходимо хранить данные в Redis, лучше хранить их отдельно, а не вместе с другими ключами.
  • Установить порог длины значения. Если значение превышает порог, применить сжатие для уменьшения размера ключа-значения.
  • Оценить долю больших Key в общем объёме данных. Многие фреймворки используют пулинг, например Memcache, где можно заранее выделить пространство для больших объектов.
  • Разделить большие Key на несколько меньших ключей для независимого управления, что снижает затраты.
  • Указать разумный срок истечения для больших Key, чтобы избежать преждевременного удаления важных данных.

Другие вопросы

Предварительный нагрев кеша

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

Стратегия предварительного нагрева:

  • Небольшие объёмы данных: вручную загрузить данные при запуске проекта.
  • Большие объёмы данных: настроить периодическую задачу для обновления кеша.
  • Очень большие объёмы данных: обеспечить предварительный нагрев горячих данных.

Понижение уровня кеша

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

Разделение уровней понижения:

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

Горячие данные

В базе данных есть 20 миллионов записей, но только 2 миллиона хранятся в Redis. Как гарантировать, что данные в Redis являются горячими?

Для этого нужно рассмотреть стратегии выселения Redis:

  • volatile-lru: удаляет наименее использованные данные с установленным сроком действия.
  • volatile-ttl: удаляет данные, срок действия которых скоро истечёт.
  • volatile-random: случайным образом удаляет данные с установленным сроком действия.
  • allkeys-lru: при нехватке места удаляет наименее используемые ключи.
  • allkeys-random: случайно удаляет ключи при нехватке места.
  • no-eviction: предотвращает удаление данных, вызывая ошибку при нехватке места для новых данных.

После версии 4.0 добавлены ещё две стратегии:

  • volatile-lfu: удаляет наименее часто используемые данные с установленным сроком действия (отличается от lru).
  • allkeys-lfu: при нехватке места удаляет ключи, которые использовались реже всего.

Выбор стратегии зависит от распределения данных:

  • Если данные распределены по степенному закону, где некоторые данные используются чаще других, можно использовать allkeys-lru или allkeys-lfu.
  • Если распределение данных равномерное, то allkeys-random будет подходящим выбором. Многие проблемы с обработкой данных в системах разработки и тестирования программного обеспечения связаны с использованием очередей сообщений (MQ).

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

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

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

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

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

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

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

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

Эти подходы позволяют обеспечить надёжную и эффективную обработку данных в системах, использующих очереди сообщений. Заказ №001.getBytes()); // задержка 30 минут msg.setDelayTimeLevel(16); SendResult sendResult = producer.send(msg);

Очередь недоставленных сообщений

Когда сообщение не удаётся обработать в первый раз, очередь RocketMQ автоматически повторяет попытку обработки сообщения; после достижения максимального количества попыток, если обработка всё ещё не удалась, это означает, что потребитель не может корректно обработать сообщение в нормальных условиях, и в этом случае очередь RocketMQ не будет сразу удалять сообщение, а отправит его в специальную очередь для этого потребителя. В очереди RocketMQ сообщение, которое не удалось обработать в нормальных условиях, называется недоставленным сообщением (Dead-Letter Message), а специальная очередь, которая хранит недоставленные сообщения, называется очередью недоставленных сообщений (Dead-Letter Queue).

Недоставленное сообщение:

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

Очередь недоставленных сообщений:

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

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

%RETRY% имя группы потребителей; — %DLQ% имя группы потребителей.

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

  1. Обработка по расписанию. После того как пользователь размещает заказ, сначала создаётся информация о заказе, затем заказ добавляется в расписание (выполняется через 30 минут), и когда наступает указанное время, проверяется статус заказа. Если заказ не оплачен, он считается просроченным. Этот метод имеет очевидные недостатки при распределённом развёртывании серверов, поскольку требует координации с использованием распределённых блокировок и имеет низкую оперативность, оказывая давление на базу данных.

  2. Отложенная обработка. Когда пользователь размещает заказ, идентификатор заказа отправляется во все отложенные очереди, обрабатывается через 30 минут и проверяется статус заказа перед обработкой. Есть несколько способов реализации отложенной обработки:

    Собственная очередь DelayedQuene Java. Это собственная очередь задержки Java, которую можно использовать, если бизнес-логика не слишком сложна. Она использует память JVM для реализации и теряет данные при остановке, а масштабируемость не очень хорошая.

    Использование Redis для отслеживания истечения срока действия ключа. После размещения заказа ключ, соответствующий заказу, устанавливается в Redis. Через 30 минут ключ становится недействительным, и программа отслеживает истечение срока действия ключей и обрабатывает заказы (я также пробовал этот метод). Основным недостатком этого метода является то, что он может отслеживать только один ключ Redis, который не подходит для кластеров. Некоторые люди отслеживают каждый узел кластера Redis, но я считаю, что это не лучший способ. Если бизнес-логика не сложная, Redis можно развернуть на одном сервере.

    Реализация очереди недоставленных сообщений MQ.

Обработка просроченных транзакций

Транзакции

В большинстве случаев 99 % вызовов распределённых интерфейсов не требуют распределённых транзакций. Мониторинг (уведомления по электронной почте или SMS), ведение журнала и быстрое определение проблем после возникновения проблем, а затем их устранение, поиск решений и исправление данных могут помочь. Поскольку распределённые транзакции всегда связаны с затратами, эти затраты могут быть довольно высокими, особенно для небольших компаний. Кроме того, введение распределённых транзакций значительно увеличивает сложность кода, время разработки и снижает производительность и пропускную способность системы, делая систему более уязвимой и склонной к ошибкам. Конечно, если есть ресурсы для постоянного инвестирования, распределённые транзакции могут гарантировать 100 % согласованность данных без ошибок.

Что такое распределённая транзакция?

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

TCC и надёжная схема окончательной согласованности являются наиболее часто используемыми в производстве. TCC используется для обеспечения строгой согласованности, в основном для основных модулей, таких как транзакции/заказы. Окончательная согласованность обычно используется для периферийных модулей, таких как инвентарь, и уведомления отправляются через MQ для обеспечения окончательной согласованности.

Теория распределённых систем

Согласованность

Степень надёжности: строгая согласованность > последовательная согласованность > причинная согласованность > окончательная согласованность > слабая согласованность.

  • Строгая согласованность / линейная согласованность (Linearizability): после завершения записи требуется, чтобы любой считывающий запрос мог прочитать последнее значение.
  • Последовательная согласованность (Sequential Consistency): не гарантируется глобальный порядок операций, но каждая клиентская операция выполняется последовательно.
  • Причинная согласованность (Causal Consistency): гарантирует порядок только для параллельных операций с причинно-следственными связями.
  • Окончательная согласованность (Eventual Consistency): не гарантирует, что одни и те же данные на любом узле одинаковы в любое время, но данные на разных узлах в конечном итоге будут согласованы.
  • Слабая согласованность (Weak Consistency): после обновления данных некоторые или все последующие запросы могут не получить обновлённое значение.

ACID

ACID подчёркивает согласованность и является принципом проектирования традиционных реляционных баз данных. ACID — это четыре характеристики, которым должна соответствовать транзакция базы данных (например, MySQL) для правильного выполнения:

  • Атомарность (Atomicity): операции в транзакции либо выполняются полностью, либо не выполняются вовсе.
  • Согласованность (Consistency): система всегда должна находиться в состоянии строгой согласованности.
  • Изоляция (Isolation): выполнение одной транзакции не должно влиять на выполнение других транзакций.
  • Долговечность (Durability): изменения, внесённые в базу данных после успешной фиксации транзакции, являются постоянными.

CAP

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

  • Согласованность (Consistency): все копии данных в распределённой системе имеют одинаковое значение в одно и то же время.
  • Доступность (Availability): даже если часть узлов в кластере выйдет из строя, кластер всё равно сможет обрабатывать запросы чтения и записи от клиентов.
  • Устойчивость к разделению (Partition Tolerance): потеря или сбой информации в системе не влияет на непрерывную работу системы.

Эти три характеристики могут быть удовлетворены только двумя одновременно. Большинство систем следуют этому принципу:

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

BASE

BASE фокусируется на доступности. BASE является расширением CAP и предполагает, что даже если строгая согласованность не может быть достигнута, система может достичь конечной согласованности. BASE представляет собой аббревиатуру трёх понятий:

  • Базовая доступность (Basically Available): система допускает потерю части доступности при возникновении сбоя, обеспечивая при этом базовую доступность.
  • Мягкое состояние (Soft State): система может находиться в промежуточном состоянии, не влияя на общую доступность системы.
  • Конечная согласованность (Eventual Consistency): данные в системе в конечном счёте станут согласованными после определённого периода времени.

Теоретически BASE — это компромисс между согласованностью и доступностью в CAP. Основная идея заключается в следующем: мы не можем обеспечить строгую согласованность, но каждое приложение может выбрать подходящий способ достижения конечной согласованности в соответствии со своими бизнес-требованиями. Прямое отправление данных узлам B и D:

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

Антиэнтропия (Anti-entropy):

Энтропия в физике используется для измерения степени беспорядка в системе. Антиэнтропия, наоборот, направлена на устранение этого беспорядка. Gossip-протокол использует антиэнтропию для асинхронного восстановления различий в данных между узлами, обеспечивая окончательную согласованность. Существует три способа реализации антиэнтропии: push, pull и push-pull. В кластере узлы случайным образом выбирают другой узел через определённые промежутки времени и обмениваются данными для устранения различий. Однако выполнение антиэнтропии требует обмена данными между узлами, что приводит к высоким затратам на связь. Чтобы снизить затраты, можно использовать механизмы, такие как контрольная сумма, для уменьшения объёма данных и количества обменов.

Способ push:

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

Способ pull:

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

Способ push-pull:

Этот способ объединяет оба предыдущих способа, позволяя одновременно восстанавливать данные обоих узлов.

Хотя антиэнтропия является эффективным методом, её использование ограничено в динамических или больших распределённых системах, где количество узлов может быть значительным. В таких случаях можно использовать метод распространения слухов (Rumor mongering).

Распространение слухов (Rumor mongering):

Метод распространения слухов предполагает широкое распространение информации (слухов) после того, как узел получает новые данные. Узел становится активным и периодически связывается с другими узлами, передавая им новые данные, пока они не будут сохранены всеми узлами. Например, в приведённой схеме узел A передаёт новые данные узлам B и D, а узел B, получив новые данные, становится активным и передаёт их узлам C и D.

Распространение слухов эффективно в динамически изменяющихся распределённых системах.

Алгоритм Quorum NWR

Теория CAP определяет согласованность как сильную согласованность, которая означает, что после записи операции любые последующие чтения всегда получают обновлённое значение. Теория BASE определяет согласованность как окончательную согласованность, означающую, что после записи операции последующие чтения могут получить старые данные, но система в конечном итоге достигнет согласованности. Если система уже основана на модели окончательной согласованности (AP), и требуется временно обеспечить сильную согласованность из-за бизнес-требований, можно ли это реализовать? Один из способов — перестроить систему, но это слишком дорого. Другой способ — использовать алгоритм Quorum NWR. С помощью Quorum NWR можно настроить уровень согласованности.

Quorum NWR имеет три компонента: N, W и R, которые являются ключевыми элементами алгоритма. Комбинируя эти элементы, можно достичь желаемого уровня согласованности. Quorum NWR — это полезный алгоритм, который компенсирует недостаток сильной согласованности в системах AP и предоставляет гибкость в выборе уровня согласованности по требованию. Многие открытые фреймворки, такие как Elasticsearch и Kafka, используют Quorum NWR для настройки уровня согласованности, предоставляя варианты «any», «one», «quorum» и «all».

N (количество копий):

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

W (уровень согласованности записи):

W представляет уровень согласованности записи, указывающий, сколько копий должно быть успешно обновлено для успешной записи. Для примера возьмём DATA-2, где W = 2. Если клиент выполняет запись в DATA-2, необходимо обновить обе копии, чтобы операция записи считалась успешной.

R (уровень согласованности чтения):

R представляет уровень согласованности чтения, определяющий, сколько копий необходимо прочитать для получения последнего значения. Продолжая пример с DATA-2, если R = 2, то при чтении данных из DATA-2 необходимо прочитать обе копии и вернуть последнее значение.

Различные комбинации значений N, W и R приводят к разным эффектам согласованности:

  • Когда W + R > N, система обеспечивает сильную согласованность и возвращает обновлённые данные.
  • Когда W + R <= N, система может предоставить только окончательную согласованность, и есть вероятность возврата старых данных.

Например, рассмотрим DATA-2 с N = 3, W = 2 и R = 2. При записи данных в DATA-2 потребуется обновить две копии. Затем при чтении из DATA-2 будет прочитано две копии, и будет возвращено последнее значение, даже если одна из копий не была обновлена.

Алгоритм PBFT

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

По сравнению с алгоритмом устных сообщений, PBFT оптимизирует сложность сообщений с O(n ^ (f + 1)) до O(n^2), делая его более подходящим для реальных систем. Однако PBFT всё ещё требует значительного количества сообщений, особенно в больших кластерах. Например, для кластера из 13 узлов (где f = 4) требуется 237 сообщений для достижения консенсуса. Поэтому PBFT лучше подходит для небольших и средних распределённых систем. Подготовка

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

Поэтому после получения сообщения о предварительной подготовке B, C и D переходят в этап подготовки (Prepare) и передают сообщение о подготовке, содержащее инструкцию, другим узлам. Предположим, что предатель D хочет помешать достижению консенсуса, не отправляя сообщения.

Этап подачи заявки

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

На этом этапе узел (например, B) может выполнить инструкцию? Ответ по-прежнему отрицательный, потому что B не может подтвердить, получили ли A, C и D 2f идентичных сообщений о подготовке. Другими словами, на этом этапе B не знает, готовы ли A, C и D к выполнению инструкции.

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

Ответ

Когда определённый узел получает 2f + 1 подтверждённых сообщений о подаче заявки (где f — количество предателей), то есть большинство узлов достигли консенсуса и готовы выполнить инструкцию, этот узел выполняет инструкцию клиента и отправляет успешное сообщение клиенту после завершения выполнения.

В алгоритме PBFT в протоколе Биткоина клиент принимает решение о достижении консенсуса. Если клиент не получает ответ f + 1 в течение указанного времени, считается, что кластер вышел из строя и консенсус не достигнут. Клиент повторно отправит запрос.

Алгоритм PBFT решает проблему большинства, но не всех предателей. Алгоритм PoW решает эту проблему, увеличивая стоимость предательства.

Алгоритм доказательства работы (PoW)

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

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

Пример работы алгоритма PoW:

Предположим, у нас есть строка «tutuxiao». Мы добавляем число после строки и вычисляем хэш SHA256. Если первые четыре цифры хэша равны нулю, задача решена. Нам нужно выполнить 35024 вычисления, чтобы найти подходящее значение.

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

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

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

Двухфазный протокол фиксации обеспечивает полную согласованность, но низкую доступность и отказоустойчивость. Paxos и Raft — алгоритмы достижения строгой согласованности. Они обеспечивают высокую доступность и устойчивость к отказам. Почему в процессе работы Paxos половина или меньше Acceptor могут выйти из строя, и система всё равно будет работать?

Если половина или меньше Acceptor вышли из строя до того, как было определено окончательное значение (value), то в этом случае все Proposer будут конкурировать за право выдвинуть предложение. В итоге одно из предложений будет успешно принято. После этого половина или больше Acceptor примут это значение.

Как создать уникальный номер?

В статье «Paxos made simple» предлагается, чтобы все Proposer выбирали значения из непересекающихся наборов данных. Например, если в системе 5 Proposer, то каждому из них можно присвоить идентификатор j (от 0 до 4). Тогда каждый Proposer может использовать формулу 5 * i + j для определения номера своего предложения, где i — номер раунда.

Основная идея

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

Требования к отказоустойчивости

  • Система продолжает работать при выходе из строя половины или меньшего количества Acceptor.
  • После того как значение value было определено, оно остаётся неизменным даже при отказе половины или меньшего количества Acceptor.

Роли узлов

В протоколе Paxos есть три типа узлов:

  1. Proposer (предлагающий) — может быть несколько Proposer. Они предлагают решения (value). Value в контексте программирования может означать любую операцию, например, изменение значения переменной или назначение текущего primary узла. В протоколе Paxos эти операции абстрагируются как value. Разные Proposer могут предлагать разные, возможно противоречащие друг другу, значения. Однако в рамках одной итерации протокола Paxos принимается только одно значение.
  2. Acceptor (принимающий) — их количество N. Для принятия решения необходимо, чтобы более половины (N/2 + 1) Acceptor согласились с предложенным значением. Acceptor полностью независимы и равноправны.
  3. Learner (изучающий) — изучает принятые значения. Изучает результаты выбора различных Proposer и, если более половины Proposer приняли определённое значение, Learner запоминает его. Это похоже на механизм кворума, когда для принятия решения требуется согласие W = N/2 + 1 Acceptor, а Learner должен прочитать результаты не менее N/2+1 и не более N Acceptor, чтобы узнать принятое значение.

Процесс выборов

Процесс выборов состоит из двух этапов: подготовки и голосования.

На этапе подготовки:

  • P1a: Proposer отправляет Prepare-запрос. Proposer генерирует уникальный и возрастающий ProposalID и отправляет Prepare-запросы всем узлам Paxos. Запрос не содержит value, только ProposalID.
  • P1b: Acceptor отвечает на Prepare-запрос. Получив Prepare-запрос, Acceptor проверяет, превышает ли ProposalID значение самого большого ProposalID среди всех ранее принятых предложений. Если да:
    • сохраняет максимальное значение ProposalID как Max_N;
    • возвращает значение из последнего принятого предложения или пустое значение, если такого предложения ещё нет;
    • обещает не принимать предложения с ProposalID меньше Max_N. Если нет, то Acceptor не отвечает или возвращает ошибку.

На этапе голосования:

  • P2a: Proposer отправляет Accept-запрос. Через некоторое время после отправки Prepare-запросов Proposer собирает ответы от Acceptor:
    • если получено более половины ответов от Acceptor и все они пустые, Proposer отправляет accept-запрос со своим значением;
    • если получено более половины ответов от Acceptor и хотя бы один из них не пустой, Proposer отправляет accept-запрос с наибольшим значением ProposalID из полученных ответов;
    • если получено менее половины ответов от Acceptor, Proposer пытается увеличить ProposalID и повторяет P1a.
  • P2b: Acceptor отвечает на Accept-запрос. Получая Accept-запрос, Acceptor проверяет:
    • ProposalID запроса равен или больше сохранённого Max_N, тогда Acceptor возвращает успешное принятие и сохраняет значение и ProposalID;
    • иначе Acceptor не отвечает или возвращает отказ.
  • P2c: Proposer подсчитывает голоса. Через некоторое время Proposer получает ответы от Acceptor о принятии или отклонении предложения:
    • если более половины Acceptor приняли предложение, то оно считается успешным. Proposer может отправить широковещательное сообщение всем Proposer и Learner, уведомляя их о принятом значении;
    • если менее половины Acceptor приняли предложение или получен ответ об отклонении, Proposer пробует увеличить ProposalID и повторить P1a;
    • если получен один ответ о неудачном принятии, Proposer также пробует увеличить ProposalID и повторить P1a.

После нескольких раундов голосования достигается следующее состояние:

  • все Proposer успешно приняли свои предложения;
  • более половины Acceptor успешно приняли одно и то же предложение.

Ограничения

У протокола Paxos есть несколько ограничений:

  • P1: Acceptor должен принять первое полученное предложение.
  • P2a: Если предложение с определённым value было принято, то все последующие предложения от Acceptor должны иметь такое же value.
  • P2b: Если предложение с определённым value было принято, то все будущие предложения от Proposer должны иметь такое же value.
  • P2c: Если предложение с номером n имеет value v, то существует большинство Acceptor, которые либо не принимали предложения с номерами меньше n, либо уже приняли предложения с номером меньше n и максимальным value v.

Каждый раунд протокола Paxos состоит из этапа подготовки и этапа голосования. На этих этапах Proposer и Acceptor выполняют определённые действия. Взаимодействие между Proposer и Acceptor включает четыре типа сообщений:

Сообщение Этап Описание
Prepare Подготовка Proposer отправляет запрос Prepare всем Acceptor
Promise Подготовка Acceptor отвечает обещанием не принимать предложения с меньшим ProposalID
Accept Голосование Proposer запрашивает принятие предложения
Accepted Голосование Acceptor принимает предложение

В Raft в любой момент времени должен быть только один лидер (Leader), а в период нормальной работы — только Leader и последователи (Followers). Алгоритм Raft разделяет время на отдельные периоды (term), каждый из которых начинается с выборов лидера. После успешного избрания лидера он управляет кластером в течение всего периода. Если выборы лидера завершаются неудачно, то период заканчивается из-за отсутствия лидера.

Период (Term)

Алгоритм Raft делит время на периоды разной длины. Периоды обозначаются последовательными числами. Каждый период начинается с выборов (election), в ходе которых один или несколько кандидатов пытаются стать лидером. Кандидат, выигравший выборы, становится лидером на оставшийся срок периода. В некоторых случаях голоса разделяются, и тогда лидера не выбирают. Тогда начинается новый период, и сразу же начинаются новые выборы. Алгоритм Raft гарантирует, что в пределах одного периода существует только один лидер.

RPC (коммуникация)

Для коммуникации между серверами в алгоритме Raft используются удалённые вызовы процедур (RPC). Для передачи снимков достаточно двух типов RPC, но для передачи журналов добавлен третий тип. Всего существует три типа RPC:

  • RequestVote RPC — инициируется кандидатами во время выборов;
  • AppendEntries RPC — механизм сердцебиения лидера, при котором также происходит копирование журнала;
  • InstallSnapshot RPC — используется лидером для отправки снимков отстающим последователям.

Процесс выборов

Выборы лидера запускаются по истечении времени ожидания после последнего полученного от лидера сигнала «сердцебиения» (heartbeat). При запуске серверы становятся последователями. Лидер периодически отправляет сигналы «сердцебиение» всем последователям. Если последователь не получает сигнал «сердцебиение» от лидера в течение времени выборов, он ждёт случайный промежуток времени и затем инициирует выборы лидера. У каждого последователя есть часы, представляющие собой случайное значение, которое определяет, когда последователь ожидает стать лидером. Последователь увеличивает свой текущий период и становится кандидатом. Он сначала голосует за себя и отправляет другим серверам RequestVote RPC. Есть три возможных исхода:

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

Ограничения выборов

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

Копирование журнала

Цель копирования журнала — обеспечить согласованность данных. Процесс копирования журнала выглядит следующим образом:

После избрания лидер начинает получать запросы от клиентов. Лидер добавляет запрос как запись журнала (Log entries) в свой журнал, а затем параллельно отправляет RPC AppendEntries другим серверам для копирования этой записи. Когда эта запись копируется на большинство серверов, лидер применяет эту запись к своему состоянию и возвращает результат клиенту.

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

Журнал состоит из упорядоченных по индексу записей журнала. Каждая запись содержит номер периода (term), когда она была создана, и команду для выполнения состоянием машины. Запись считается зафиксированной, когда она копируется большинству серверов.

Согласованность журнала обеспечивается двумя гарантиями:

Если две записи в разных журналах имеют одинаковый индекс и номер периода, они содержат одну и ту же команду (поскольку лидер создаёт только одну запись для каждого индекса журнала в одном периоде). Если две записи в разных журналах имеют одинаковые индексы и номера периодов, все предыдущие записи также совпадают (при отправке дополнительных записей лидер отправляет индекс и номер периода предыдущей записи вместе с текущей записью. Если последователь обнаруживает несоответствие с собственным журналом, он отклоняет запись. Это называется проверкой согласованности).

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

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

Лидер проверяет, соответствует ли последняя запись последователя его собственной записи. Если нет соответствия, лидер пытается найти предыдущую запись, пока не найдёт совпадение, а затем заменяет последующие записи последователя. Таким образом, достигается согласованность главного и подчинённого журналов.

Безопасность

Raft вводит два ограничения для обеспечения безопасности:

Последователь, имеющий самые последние зафиксированные записи журнала, может стать лидером. Лидер может продвигать только зафиксированные записи текущего периода. Записи старого периода фиксируются косвенно (записи с меньшим индексом фиксации журнала фиксируются косвенно).

Сжатие журнала

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

Моментальный снимок содержит следующие данные:

Метаданные журнала, включая последний зафиксированный индекс записи и номер периода. Эти значения используются при проверке целостности первой записи AppendEntries после моментального снимка. Текущее состояние системы. Перевод текста:

Отброшенные записи и отправка снимков

Записи, которые были отброшены, лидер будет отправлять последователям в виде снимка (snapshot). Также снимок будет отправляться новой машине при её добавлении. Для отправки снимка используется RPC InstalledSnapshot.

Не стоит делать снимки слишком часто, иначе это приведёт к избыточному использованию дискового пространства. Но и не стоит делать их слишком редко, так как в случае перезапуска узла потребуется воспроизвести большой объём журналов, что повлияет на доступность. Рекомендуется создавать снимок после того, как журнал достигнет определённого размера. Создание снимка может занять много времени и повлиять на нормальную синхронизацию журналов. Эту проблему можно решить с помощью технологии copy-on-write, которая позволит избежать влияния создания снимка на нормальную синхронизацию журнала.

Изменения в составе участников

  • Проблемы, связанные с обычными изменениями в составе участников:
    • Может возникнуть ситуация, когда два разных лидера могут быть выбраны в одном периоде (проблема двух лидеров), один через старую конфигурацию, другой — через новую. В общем, проблема изменения состава участников заключается в том, что увеличение или уменьшение участников становится слишком большим, что приводит к отсутствию пересечения между старым и новым составом участников, вызывая проблему двух лидеров.
  • Решение проблемы изменения состава участников в один этап: Raft решает эту проблему, позволяя изменять состав участников только на одного участника за раз (если необходимо изменить больше участников, это делается последовательно несколько раз).

Общие вопросы

  1. На какие части делится Raft? Основные части включают выборы лидера, копирование журналов, сжатие журналов и изменение состава участников.
  2. Может ли любой узел инициировать выборы в Raft? Выборы в Raft могут быть инициированы в следующих случаях:
    • При запуске все узлы являются последователями, и в этом случае проводятся выборы для выбора лидера.
    • После сбоя лидера происходит повторный запуск выборов для выбора нового лидера.
    • Выборы инициируются при изменении состава участников.
  3. Какие условия должны быть выполнены для голосования за кандидата во время выборов в Raft? В Raft гарантируется, что выбранный лидер содержит все уже зафиксированные (в большинстве узлов кластера) записи журнала. Это обеспечение выполняется на этапе RequestVoteRPC, где кандидат отправляет свой собственный индекс последней записи журнала и term_id, а последователь, получив сообщение RequestVoteRPC, отклоняет голосование, если обнаруживает, что его журнал новее, чем в сообщении. Сравнение журналов основано на том, что если локальная последняя запись журнала имеет больший term id, то она обновляется, а если term id одинаковый, то сравнивается размер журнала (индекс больше).
  4. Как решается проблема согласованности данных в сети Raft при разделении? Если происходит разделение сети или сбой связи, лидер не может получить доступ к большинству последователей, поэтому он может нормально обновлять только тех последователей, к которым у него есть доступ, в то время как большинство последователей без лидера выбирают нового лидера, который затем принимает запросы от клиентов. Если связь восстанавливается, старый лидер становится последователем, и любые обновления, сделанные им во время разделения, не считаются зафиксированными и откатываются, принимая новые обновления от нового лидера (уменьшение запроса соответствует журналу).
  5. Как достигается согласованность данных в Raft? Согласованность данных достигается путём копирования журналов, где лидер добавляет запрос команды как новую запись журнала, а затем инициирует RPC ко всем последователям для синхронизации данных.
  6. Каковы особенности журналов в Raft? Журналы состоят из упорядоченных по номерам (log index) записей, каждая из которых содержит информацию о периоде (term), когда она была создана, и команду для выполнения в состоянии машины.
  7. В чём разница и преимущества Raft и Paxos?
    • В Raft существует ограничение на лидера, и только узел с самым свежим журналом может стать лидером, в отличие от multi-paxos, где любой узел может стать лидером.
    • Каждый период в Raft лидер имеет номер термина.
  8. Как обеспечивается фиксация данных в Raft, и что происходит с данными, которые не были зафиксированы при сбое лидера? Лидер будет блокировать последующие действия, отправляя запросы на копирование журнала последователям и ожидая завершения копирования всеми последователями. Данные, которые не были зафиксированы старым лидером, будут откачены, и новый лидер синхронизирует данные.
  9. Как реализуется сжатие журналов в Raft? Как обрабатываются добавления и удаления узлов? Чтобы предотвратить бесконечное увеличение журналов в реальных системах, что может привести к длительному времени восстановления при перезапуске системы и, следовательно, повлиять на доступность, Raft использует снимки для решения этой проблемы. Снимки содержат только метаданные журнала, такие как индекс и термин последней зафиксированной записи.

ZAB протокол

ZAB (Zookeeper Atomic Broadcast) — это протокол, специально разработанный для распределённой координации сервиса Zookeeper, обеспечивающий восстановление после сбоев и атомную трансляцию. Протокол ZAB определяет, что ZAB является протоколом поддержки для распределённого координационного сервиса Zookeeper, предназначенным для обеспечения устойчивости к сбоям и атомной трансляции. На основе этого протокола Zookeeper реализует архитектуру главного и резервного режима для поддержания согласованности данных между резервными копиями в кластере.

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

Трансляция сообщений

Процесс трансляции сообщений в ZAB использует протокол атомной трансляции, похожий на двухфазное подтверждение. Для клиентских запросов на запись все они принимаются лидером, который упаковывает запрос в транзакцию Proposal и отправляет его всем последователям. Затем, основываясь на ответах последователей, если более половины из них успешно отвечают, выполняется фиксация операции (сначала фиксируется сам лидер, затем отправляется фиксация всем последователям). Весь процесс трансляции состоит из трёх этапов:

  1. Копирование данных во все последователи.
  2. Ожидание ответов от последователей. Подтверждение считается успешным, если получено более половины успешных ответов.
  3. Когда более половины ответов успешны, выполняется фиксация, и лидер также фиксирует свои данные.

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

  • Лидер получает клиентский запрос и упаковывает его в транзакцию, присваивая транзакции уникальный идентификатор (ZXID), который представляет собой глобально увеличивающийся порядковый номер. ZAB должен гарантировать порядок транзакций, поэтому транзакции должны быть отсортированы по ZXID перед обработкой.
  • Между лидером и последователями есть очередь сообщений для развязки взаимодействия между ними.
  • В кластере Zookeeper только лидер может принимать запросы на запись, даже если последователь получает запрос от клиента, он перенаправит его лидеру для обработки.
  • Фактически, это упрощённая версия 2PC, которая не решает проблему единственной точки отказа. Позже мы обсудим, как ZAB решает проблему единственной точки отказа (то есть сбой лидера).

Восстановление после сбоя

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

  • ZAB гарантирует, что транзакции, зафиксированные лидером, в конечном итоге будут зафиксированы всеми серверами.
  • ZAB обеспечивает отбрасывание транзакций, которые только были предложены лидером, но не зафиксированы.

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

Синхронизация данных

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

Генерация ZXID

В дизайне идентификатора транзакции ZXID в протоколе ZAB ZXID представляет собой 64-битное целое число. Перевод текста на русский язык:

Цифры типа, где младшие 32 бита можно рассматривать как простой счётчик с приращением, для каждой транзакции клиента лидер генерирует новый Proposal транзакции и выполняет операцию +1 для этого счётчика. Старшие 32 бита представляют собой значение ZXID, полученное из локального журнала лидера, и извлекают соответствующее значение эпохи из этого ZXID, а затем добавляют к нему единицу.

Рисунок «ZAB данные синхронизации ZXD»

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

Двухфазная фиксация (2PC/XA)

Основная идея

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

Рисунок «Двухфазный протокол фиксации»

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

Двухфазная фиксация (Two-Phase Commit, 2PC) разделяет транзакцию на две части:

  • Фаза 1: подготовка (Prepare)
  • Фаза 2: подтверждение (Commit или Rollback)

Псевдокод

Выполнение кода {
    // Фаза 1
    aStatus = участник A.prepare()
    bStatus = участник B.prepare()

    // Фаза 2
    if aStatus and bStatus:
        участник A.commit()
        участник B.commit()
    else:
        участник A.rollback()
        участник B.rollback()
}

Проблемы

  • Синхронная блокировка: вся цепочка сообщений является последовательной, что требует ожидания ответа, который может занять много времени и не подходит для высококонкурентных сценариев.
  • Одиночная точка отказа: если координатор выходит из строя, участники будут блокированы навсегда (вторая фаза приведёт к тому, что все участники будут заблокированы в состоянии блокировки ресурсов).
  • Несогласованность данных: вторая фаза может привести к несогласованности данных из-за сетевых проблем или других причин, поскольку некоторые участники могут выполнить commit, а другие нет.

Подготовка (Prepare)

Этот протокол имеет две роли: узел A является координатором транзакции, узлы B и C являются участниками транзакции. Основной поток действий следующий:

  1. Координатор записывает команду в журнал.
  2. Отправляет команду prepare узлам B и C.
  3. Узлы B и C получают сообщение и решают, могут ли они зафиксировать транзакцию, исходя из своей ситуации.
  4. Результаты записываются в журнал.
  5. Результаты возвращаются координатору.

Подтверждение (Commit/Rollback)

Когда узел A получает подтверждения от узлов B и C, он выполняет следующие действия:

  1. Определяет, могут ли все координаторы зафиксировать транзакцию.
  2. Если да, то записывает в журнал и отправляет команду commit.
  3. Если нет, то записывает в журнал и отправляет команду rollback.
  4. Участники получают команды от координатора и выполняют их.
  5. Выполненные команды и результаты записываются в журнал.
  6. Результаты отправляются координатору.

Трёхфазная фиксация (3PC)

Трёхфазная фиксация (Three-Phase Commit, 3PC), основная цель которой — уменьшить время блокировки, вызванное сетевыми сбоями и другими проблемами.

  • Фаза 1: CanCommit
  • Фаза 2: PreCommit
  • Фаза 3: DoCommit

Псевдокод

Выполнение кода {
    // Фаза 1
    участник A.ping()
    участник B.ping()

    // Фаза 2
    aStatus=участник A.prepare().timeout(seconds).returnFalse()
    bStatus=участник B.prepare().timeout(seconds).returnFalse()

    // Фаза 3
    if aStatus and bStatus:
        участник A.commit().timeout(seconds).returnFalse()
        участник B.commit().timeout(seconds).returnFalse()
    else:
        участник A.rollback().timeout(seconds).returnFalse()
        участник B.rollback().timeout(seconds).returnFalse()
}

Улучшения включают:

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

3PC улучшает 2PC следующим образом:

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

Рисунок «Протокол трёхфазной фиксации»

CanCommit

Координатор отправляет участникам запрос CanCommit, и участники отвечают YES или NO.

PreCommit

На основе ответов участников координатор принимает решение, продолжать ли PreCommit:

  • Если все ответы участников положительные, координатор переходит к фазе Prepared.
  • Если любой из участников отвечает отрицательно или координатор не получает ответ в течение таймаута, координатор прерывает транзакцию.

DoCommit

Если координатор получает положительные ответы от участников, транзакция фиксируется:

  • Координатор отправляет запрос DoCommit участникам.
  • Участники фиксируют транзакцию и отправляют ACK координатору.
  • Координатор фиксирует транзакцию после получения ACK.

Если координатор не получил ACK от участников в течение таймаута или получил отрицательные ответы, координатор отменяет транзакцию:

  • Координатор отправляет участникам запросы на отмену.
  • Участники отменяют транзакцию.

Механизм компенсации (TCC)

Основная идея заключается в регистрации операций подтверждения и компенсации для каждой операции.

2PC и 3PC не подходят для сценариев с высокой степенью параллелизма. TCC, по сравнению с 2PC и 3PC, не блокирует весь ресурс, а использует механизм компенсации, преобразуя ресурсы в бизнес-логику, уменьшая степень детализации блокировки.

Псевдокод:

Выполнение кода{
    // Попытка, аналогичная авторизации кредитной карты, сначала замораживает баланс, но фактически не вычитает
    aStatus=Участник A. Заморозить ресурс(). зафиксировать()
    bStatus=Участник B. Заморозить ресурс(). зафиксировать()

    // Подтвердить, фактически вычесть (и разморозить)
    если aStatus и bStatus:
        (Участник A. Вычесть ресурс(). зафиксировать()). асинхронное выполнение()
        (Участник B. Вычесть ресурс(). зафиксировать()). асинхронное выполнение()

    еще:
        // Отменить, отменить заморозку
        (Участник А. Разморозить ресурс(). Зафиксировать()). асинхронное выполнение()
        (Участник В. Разморозить ресурс(). Зафиксировать()). асинхронное выполнение()
}

Преимущества TCC:

По сравнению с 2PC, TCC решает следующие проблемы:

  • Синхронная блокировка: введение таймаутов и компенсация после истечения таймаута.
  • Одиночная точка: координация осуществляется основным бизнесом, и система становится многоточечной с введением кластера.
  • Несогласованность данных: после введения компенсации согласованность контролируется системой управления бизнес-операциями.

Недостатки TCC: Применение инвазивности сильное: TCC, поскольку основан на бизнес-уровне, требует наличия трёх интерфейсов: try, confirm и cancel для каждой операции.

Сложность разработки высокая: объём кода большой, и для обеспечения целостности данных необходимо реализовать идемпотентность для confirm и cancel интерфейсов.

Этап try

Этот этап включает проверку ресурсов различных сервисов и их блокировку или резервирование.

Этап confirm

На этом этапе выполняется фактическая бизнес-операция без какой-либо бизнес-проверки. Используются только зарезервированные ресурсы этапа try. Операция confirm должна быть разработана с учётом идемпотентности. В случае сбоя требуется повторная попытка.

Этап cancel

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

Пример использования TCC

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

В нормальном процессе TCC всё ещё представляет собой двухэтапный протокол отправки. Однако при возникновении проблем он обладает определённой способностью к самовосстановлению. Если в любой транзакции участвует проблема, координатор может отменить предыдущую операцию, чтобы достичь окончательного согласованного состояния (например, отменить транзакцию или запросить транзакцию). Из процесса выполнения TCC также видно, что поставщик услуг должен предоставить дополнительную логику компенсации. Таким образом, один сервисный интерфейс после введения TCC может потребовать модификации в три типа логики:

  • Try: сначала вызывается цепочка вызовов сервиса для выполнения логики try.
  • Confirm: если всё в порядке, TCC распределяет выполнение логики подтверждения транзакции.
  • Cancel: если в логике try одного из сервисов возникает проблема, TCC распределит выполнение логики отмены транзакции для всех сервисов.

Обратите внимание: при разработке транзакций TCC интерфейсы операций cancel и confirm должны соответствовать идемпотентной конструкции.

Этап try

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

  • Сервис заказа: устанавливает промежуточное состояние «UPDATING» вместо непосредственной установки состояния «оплачено».
  • Сервис склада: использует поле для замораживания запасов для сохранения замороженных запасов, вместо прямого уменьшения запасов.
  • Сервис баллов: предварительно увеличивает баллы участников.
  • Сервис хранилища: создаёт документ о продаже со склада, но статус — UNKONWN.

Этап confirm

В зависимости от результата этапа try, этап confirm делится на два случая:

  • В идеальном случае все этапы try выполняются успешно, и каждый сервис выполняет логику подтверждения.
  • Если этап try какого-либо сервиса завершается неудачно, выполняется третий этап — cancel. Логика подтверждения этапа confirm обычно реализуется каждым сервисом самостоятельно:
  • Сервис заказа: логика подтверждения может изменить состояние заказа с «в обработке» на «оплачен».
  • Сервис склада: очищает поле для замораживания запасов и одновременно уменьшает фактические запасы.
  • Сервис баллов: очищает предварительное увеличение баллов участников и одновременно увеличивает фактические баллы участников.
  • Сервис хранилища: изменяет статус документа о продаже со склада на «создан».

Обратите внимание: на этапе confirm каждого сервиса могут возникнуть проблемы, и в этом случае обычно требуется TCC-фреймворк (например, ByteTCC, tcc-transaction, himly). TCC обычно записывает некоторые журналы активности распределённых транзакций, сохраняет информацию о каждом этапе и состоянии транзакции, обеспечивая окончательное согласование всей распределённой транзакции.

Этап cancel

Если этап try завершается неудачно, будет выполнен этап cancel. Например, для сервиса заказа можно реализовать логику отмены следующим образом: установить состояние заказа как «отменено». Для сервиса склада логика отмены заключается в том, чтобы освободить поле для замораживания запасов и вернуть запасы в состояние, доступное для продажи.

Обратите внимание: многие компании, стремясь упростить использование TCC, обычно разделяют один сервис на несколько интерфейсов. Например, сервис склада разделяет интерфейс уменьшения запасов на два под-интерфейса: интерфейс уменьшения и интерфейс восстановления уменьшения запасов, позволяя TCC гарантировать выполнение соответствующего интерфейса восстановления при сбое интерфейса уменьшения.

Надежная схема сообщений (MQ)

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

Принцип реализации:

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

Псевдокод:

execute(транзакционная тема, transId, участник){
    name = участник.имя()
    
    mq.setMsg(транзакционная тема, transId, name, 'ping')
    mStatus = участник.prepare();
    mq.setMsg(транзакционная тема, transId, name, 'prepare')
    if mStatus:
        excuted = база данных проверяет, была ли отправлена транзакция (transId, имя)
        if not excuted:
            участник.commit()
        mq.setMsg(транзакционная тема, transId, name, 'commit')
    else:
        участник.rollback()
        mq.setMsg(транзакционная тема, transId, name, 'rollback')
}

Инициатор A{
    transId = получить идентификатор транзакции()
    execute(транзакционная тема X, transId, инициатор A)
    name = инициатор A.имя()
    mq.подписаться на событие (транзакционная тема X, имя).вызвать функцию (e){
        transId = e.transacationId;
        запланированная задача (транзакционная тема X, transId, имя)
        if e.message == 'commit':
            //запустить следующий братский процесс
            mq.setMsg(транзакционная тема X, transId, инициатор B имя, "проснуться и работать")
        else if e.message == 'rollback':
            mq.delMsg(транзакционная тема X, transId)
    }
}

Участник B{
    name = участник B.имя()
    mq.подписаться на событие (транзакционная тема X, имя).вызвать функцию (e){
        transId = e.transacationId;
        запланированная задача (транзакционная тема X, tranId,имя)
        if e.message == 'проснуться и работать':
            execute(транзакционная тема X, transId, участник B)
        else if e.message == 'commit':
            mq.delMsg(транзакционная тема X, transId)
        else if e.message == 'rollback':
            //ручное управление, потому что обычно все могут быть успешными
    }
}

Преимущества и недостатки:

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

Вариант 1: локальная таблица сообщений

Локальная таблица сообщений основана на идее разделения распределённой транзакции на локальные транзакции. Эта идея исходит от eBay. Мы смотрим на следующую диаграмму, которая делит распределённую транзакцию на три части на основе локальной таблицы сообщений:

  • Надёжная служба сообщений: хранит сообщения, обычно через базу данных, поэтому её также называют локальной таблицей сообщений.
  • Производитель (верхний уровень обслуживания): производитель является инициатором вызова, отправляет сообщение.
  • Потребитель (нижний уровень обслуживания): потребитель является получателем вызова, получает сообщение.

① Надёжная служба сообщений

Служба надёжных сообщений — это отдельный сервис со своей собственной базой данных, основная функция которого заключается в хранении сообщений (включая информацию об интерфейсе вызова и уникальный номер глобального сообщения), обычно включая следующие состояния:

  • Ожидание подтверждения: верхний уровень обслуживания отправляет сообщение ожидания подтверждения.
  • Отправлено: верхний уровень обслуживания отправил подтверждающее сообщение.
  • Отменено (конечное состояние): верхний уровень обслуживания отправляет отменяющее сообщение.
  • Завершено (конечное состояние): нижний уровень обслуживания подтверждает завершение интерфейса. 生产者 выполняет локальную транзакцию, локальная транзакция выполняется успешно и после этого отправляется подтверждение сообщения; если локальное выполнение завершается неудачно, то отправляется сообщение об отмене.
  • В надёжном сервисе сообщений после получения сообщения изменяется состояние записи сообщения в локальной базе данных на «отправлено» или «отменено». Если это подтверждающее сообщение, то сообщение доставляется в очередь сообщений MQ (Message Queue). (Важно отметить, что изменение состояния сообщения и доставка в MQ должны происходить в одной транзакции, чтобы гарантировать успешное выполнение обоих действий или их неудачу.)

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

③ Потребитель

Поставщик услуг (потребитель сообщений) получает сообщения из MQ и выполняет локальную транзакцию. После успешного выполнения он уведомляет надёжный сервис сообщений о том, что обработка завершена, и сервис устанавливает состояние сообщения в таблице как «завершено». Здесь необходимо учитывать два случая:

  • Потребитель не может обработать сообщение или обрабатывает его успешно, но локальная транзакция завершается неудачно. В этом случае надёжный сервис сообщений может предоставить фоновое задание для непрерывной проверки сообщений с состоянием «отправлено», но ещё не завершённых в таблице сообщений. Затем сообщение повторно отправляется в MQ, позволяя нижестоящему сервису обработать его снова. Также можно использовать ZooKeeper, где потребитель уведомляет ZooKeeper, а производитель отслеживает изменения узлов в ZooKeeper и повторно отправляет сообщения.
  • Если сообщение отправляется повторно, логика интерфейса потребителя должна быть идемпотентной, чтобы предотвратить многократную обработку одного сообщения и возникновение дублирования данных или путаницы в бизнес-данных. Для этого потребитель может подготовить таблицу сообщений для проверки на дублирование. После обработки сообщения потребитель должен проверить эту запись в таблице, чтобы определить, была ли она успешно обработана. Если обработка прошла успешно, потребитель сразу же возвращает успех.

Заключение

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

Решение 2: Сервис сообщений

Многие открытые сервисы сообщений поддерживают распределённые транзакции, такие как RocketMQ и Kafka. Их концепция почти такая же, как у локальных таблиц сообщений/сервисов, за исключением того, что они объединяют функции надёжных сервисов сообщений и MQ для удобства использования. Этот подход иногда называют решением для обеспечения строгой согласованности надёжных сообщений. Рассмотрим RocketMQ в качестве примера, где отправка сообщений разделена на две фазы: подготовка и подтверждение.

① Подготовка

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

Обратите внимание: потребитель не может немедленно обработать HalfMsg, производитель может выполнить Commit или Rollback для завершения транзакции. Только после выполнения Commit для HalfMsg потребитель сможет обработать это сообщение.

② Подтверждение

  • Если локальная транзакция выполнена успешно, производитель отправляет сообщение Commit, содержащее предыдущий уникальный идентификатор HalfMsg, в сервис сообщений. Сервис изменяет состояние HalfMsg на «подтверждено» и уведомляет потребителя о выполнении транзакции.
  • Если локальная транзакция завершилась неудачно, производитель отправляет сообщение Rollback, содержащее уникальный идентификатор HalfMsg, в сервис сообщений. Сервис меняет состояние HalfMsg на «отклонено».

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

③ Механизм ACK

После обработки сообщения потребителем, если по какой-либо причине возникает ошибка, которая приводит к неудачному выполнению бизнес-логики, необходимо обеспечить возможность повторной обработки сообщения. RocketMQ предоставляет механизм ACK (Acknowledgement), согласно которому RocketMQ считает обработку успешной только после получения подтверждения от потребителя. Таким образом, потребитель может отправить сообщение ACK в RocketMQ после успешного выполнения бизнес-логики для подтверждения успешной обработки.

Пример применения

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

Цепочка транзакций

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

На схеме выше показано, что сервисы заказа, инвентаря и накопления баллов являются синхронными вызовами, поскольку они представляют собой основную цепочку транзакций. Мы можем использовать TCC (Two-Phase Commit) для обеспечения распределённой согласованности. Вызов хранилища через MQ является асинхронным, поэтому мы полагаемся на RocketMQ для реализации распределённых транзакций.

Выполнение транзакции

Теперь рассмотрим, как внедрение RocketMQ влияет на процесс выполнения транзакции в системе. Весь процесс выглядит следующим образом:

  • Когда пользователь инициирует оплату заказа, внешний интерфейс сначала отправляет сообщение half-msg в RocketMQ, получая успешный ответ (обратите внимание, что хранилище ещё не может обрабатывать сообщение, так как half-msg не подтверждено).
  • Затем внешний интерфейс вызывает основную цепочку транзакций. Если какой-либо сервис сталкивается с ошибкой, выполняется внутренний TCC для отката.
  • Если внешний интерфейс получает сигнал об ошибке от цепочки транзакций, он отправляет сообщение rollback в MQ, отменяя предыдущее half-msg.
  • Если цепочка транзакций завершается успешно, внешний интерфейс отправляет сообщение commit в MQ, подтверждая предыдущее half-msg, после чего хранилище может начать обработку сообщения.
  • Хранилище успешно обрабатывает сообщение и отправляет подтверждение (ACK) в RocketMQ, обеспечивая успешное завершение обработки.

Обратите внимание: если из-за сетевых проблем RocketMQ не получает сообщения commit или rollback от внешнего интерфейса, RocketMQ будет вызывать определённый интерфейс внешнего интерфейса для запроса статуса half-msg и определения необходимости commit или rollback. Перевод текста:

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

Существует две возможности после того, как система заказов получит сообщение:

  • Если уменьшение количества товара прошло успешно, статус заказа будет изменён на «подтверждённый заказ», и заказ будет выполнен успешно.
  • Если уменьшение количества товара не удалось, статус заказа будет изменён на «недействительный заказ», и заказ не будет выполнен.

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

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

TCC — это сервисная модель, которая состоит из двух этапов. Каждый бизнес-сервис должен реализовать три метода: try, confirm и cancel. Эти методы можно сопоставить с операциями Lock, Commit и Rollback в SQL-транзакциях.

  • Try — это начальная операция, которая выполняет предварительную проверку и подтверждает все бизнес-ресурсы.
  • Confirm — это операция подтверждения, выполняемая после успешного завершения проверки на этапе try. Она должна удовлетворять условию эквивалентности. Если выполнение confirm завершается неудачно, координатор транзакций будет продолжать попытки до тех пор, пока условие не будет выполнено.
  • Cancel — это отмена выполнения. На этапе try ресурсы, зарезервированные для этого заказа, будут освобождены. Как и confirm, cancel может потребовать повторных попыток.

Давайте рассмотрим, как добавить TCC в наш процесс оформления заказа и уменьшения количества товара на складе.

На этапе try система резервирует n единиц товара для этого заказа и создаёт два зарезервированных ресурса. На этапе confirm система использует зарезервированные ресурсы этапа try. В механизме транзакций TCC считается, что если ресурсы были успешно зарезервированы на этапе try, они будут полностью переданы на этапе confirm.

Если на этапе try одна из задач завершается неудачно, будет выполнена операция cancel, и зарезервированные на этапе try ресурсы будут освобождены.

Оформление заказа и уменьшение количества товара

Традиционный режим

Разделение на базы данных и таблицы

Seata — это промежуточное программное обеспечение, которое предлагает решение для распределённых транзакций. Существует несколько распространённых подходов к реализации распределённых транзакций, таких как двухфазная фиксация (2PC), основанная на протоколе XA, трёхфазная фиксация (3PC) и программирование на основе бизнес-слоя, такое как TCC. Также существуют решения, основанные на использовании очередей сообщений и таблиц для достижения окончательной согласованности.

2PC

Двухфазная фиксация основана на протоколе XA. Протокол XA состоит из двух частей: менеджера транзакций и локального менеджера ресурсов. Локальный менеджер ресурсов обычно реализуется базой данных, такой как Oracle или MySQL, которые поддерживают интерфейс XA. Менеджер транзакций выступает в роли глобального диспетчера.

2PC обеспечивает минимальную инвазивность для бизнеса. Его основным преимуществом является прозрачность использования, поскольку пользователи могут использовать распределённые транзакции на основе XA так же, как и локальные транзакции. Это позволяет обеспечить строгие гарантии свойств ACID для транзакций.

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

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

3PC

Трёхфазная фиксация является улучшенной версией 2PC, решая проблему блокировки в 2PC. В 2PC только координатор транзакций имеет механизм тайм-аута. В 3PC оба координатора и участники имеют механизмы тайм-аута, предотвращая блокировку участников после сбоя координатора. Кроме того, между первой и второй фазами добавляется фаза подготовки, обеспечивая согласованное состояние всех участников перед окончательной фиксацией.

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

TCC

Программирование на основе TCC также является разновидностью двухфазной фиксации. TCC отличается тем, что он реализуется на уровне бизнес-логики и включает три этапа: try, confirm и cancel для каждой бизнес-операции.

Например, при оформлении заказа и уменьшении количества товара этап try резервирует товар, этап confirm фактически уменьшает количество товара, а этап cancel отменяет уменьшение количества товара и освобождает зарезервированный товар в случае неудачи.

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

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

Распределённые транзакции на базе очередей сообщений

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

Процесс оформления заказа и уменьшения количества товара выглядит следующим образом:

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

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

Seata

Seata также эволюционировала из двухфазной модели фиксации и предоставляет различные режимы транзакций, такие как AT, TCC, SAGA и XA. Здесь мы сосредоточимся на режиме AT. Поскольку Seata основана на двухфазной фиксации, давайте рассмотрим, что происходит на каждом этапе. Рассмотрим пример оформления заказа и уменьшения количества товара или денег на счёте.

Сначала рассмотрим роли, используемые в распределённых транзакциях Seata:

  • Координатор транзакций (TC): глобальный координатор, управляющий глобальными и отдельными транзакциями. Он отвечает за принятие решений о фиксации или откате глобальных и отдельных транзакций.
  • Менеджер транзакций™: менеджер транзакций на уровне приложения, используемый для запуска/фиксации/отката всей транзакции (в методе вызова сервиса используется аннотация для запуска транзакции).
  • Диспетчер ресурсов (RM): диспетчер ресурсов, обычно представляющий базу данных приложения как отдельную транзакцию (Branch Transaction), управляющую взаимодействием с TC и сообщающую о состоянии отдельной транзакции. Он управляет фиксацией или откатом отдельной транзакции.

Seata реализует распределённые транзакции, создавая ключевую роль UNDO_LOG (журнал отмены), который представляет собой таблицу журнала отмены, создаваемую в каждой базе данных приложения для распределённой транзакции. Эта таблица служит для создания резервной копии данных перед обновлением, позволяя легко откатить изменения в случае возникновения ошибок.

Первый этап

Рассмотрим пример обновления поля name в таблице user.

update user set name = '小富最帅' where name = '程序员内点事'

Первым делом, Seata через JDBC Data Source Proxy анализирует SQL, извлекая метаданные SQL, включая тип (UPDATE), таблицу (user), условие (where name = 'программист') и т. д. Таблица 1

DATETIME(6),
gmt_modified DATETIME(6),
PRIMARY KEY (branch_id),
KEY idx_xid (xid)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8;

— таблица для хранения данных о блокировке.

CREATE TABLE IF NOT EXISTS lock_table
(
row_key VARCHAR(128) NOT NULL,
xid VARCHAR(96),
transaction_id BIGINT,
branch_id BIGINT NOT NULL,
resource_id VARCHAR(256),
table_name VARCHAR(32),
pk VARCHAR(36),
gmt_create DATETIME,
gmt_modified DATETIME,
PRIMARY KEY (row_key),
KEY idx_branch_id (branch_id)
) ENGINE = InnoDB DEFAULT CHARSET = utf8;

registry.conf

Файл registry.conf используется для настройки центра регистрации и конфигурационного центра. В настоящее время поддерживаются следующие виды центров регистрации: nacos, eureka, redis, zk, consul, etcd3, sofa. В данном случае используется eureka. Для конфигурационного центра поддерживаются nacos, apollo, zk, consul и etcd3.

После завершения настройки можно запустить seata-server в каталоге \seata\bin, чтобы завершить настройку сервера Seata.

Клиент Seata

После настройки сервера Seata можно создать три сервиса: order-server (для оформления заказов), storage-server (для уменьшения количества товаров на складе) и account-server (для учёта денежных средств на счетах). Каждый сервис регистрируется в eureka.

Конфигурация каждого сервиса включает:

spring:  
    application:  
        name: storage-server  
    cloud:  
        alibaba:  
            seata:  
                tx-service-group: my_test_tx_group  
    datasource:  
        driver-class-name: com.mysql.jdbc.Driver  
        url: jdbc:mysql://47.93.6.1:3306/seat-storage  
        username: root  
        password: root

# eureka 注册中心  
eureka:  
    client:  
        serviceUrl:  
            defaultZone: http://${eureka.instance.hostname}:8761/eureka/  
    instance:  
        hostname: 47.93.6.5  
        prefer-ip-address: true  

Процесс работы с сервисом: пользователь отправляет запрос на оформление заказа, локальный сервис order создаёт запись о заказе, затем через RPC удалённо вызывает сервисы storage для уменьшения количества товара на складе и account для списания денежных средств со счёта. Только если все три сервиса выполнят свою работу успешно, заказ будет оформлен. Если один из сервисов не сможет выполнить свою задачу, остальные сервисы откатятся назад.

Код для создания заказа:

@Override  
@GlobalTransactional(name = "create-order", rollbackFor = Exception.class)  
public void create(Order order) {  

    String xid = RootContext.getXID();  

    LOGGER.info("------->交易开始");  
    //本地方法  
    orderDao.create(order);  

    //远程方法 扣减库存  
    storageApi.decrease(order.getProductId(), order.getCount());  

    //远程方法 扣减账户余额  
    LOGGER.info("------->扣减账户开始order中");  
    accountApi.decrease(order.getUserId(), order.getMoney());  
    LOGGER.info("------->扣减账户结束order中");

    LOGGER.info("------->交易结束");  
    LOGGER.info("全局事务 xid: {}", xid);  
}  

Для реализации распределённой транзакции в режиме AT необходимо создать таблицу undo_log в соответствующей базе данных. Структура таблицы:

-- for AT mode you must to init this sql for you business database. the seata server not need it.  
CREATE TABLE IF NOT EXISTS `undo_log`  
(  
    `id`            BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT 'increment id',  
    `branch_id`     BIGINT(20) NOT NULL COMMENT 'branch transaction id',  
    `xid`           VARCHAR(100) NOT NULL COMMENT 'global transaction id',  
    `context`       VARCHAR(128) NOT NULL COMMENT 'undo_log context,such as serialization',  
    `rollback_info` LONGBLOB NOT NULL COMMENT 'rollback info',  
    `log_status`    INT(11) NOT NULL COMMENT '0:normal status,1:defense status',  
    `log_created`   DATETIME NOT NULL COMMENT 'create datetime',  
    `log_modified`  DATETIME NOT NULL COMMENT 'modify datetime',  
    PRIMARY KEY (`id`),  
    UNIQUE KEY `ux_undo_log` (`xid`, `branch_id`)  
) ENGINE = InnoDB AUTO_INCREMENT = 1 DEFAULT CHARSET = utf8 COMMENT ='AT transaction mode undo table';  

Тестирование Seata

В проекте используются следующие сервисы:

  • order-server: для оформления заказов;
  • storage-server: для уменьшения количества товаров на складе;
  • account-server: для учёта денежных средств.

Запуск сервисов и отправка запроса на оформление заказа. Данные в таблицах order, storage и account должны соответствовать ожидаемым результатам.

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

Рисунок 1. Таблица данных после отката глобальной транзакции.

ID Имя таблицы
1 Данные таблицы до отката
2 Данные таблицы после отката

Примечание: таблица содержит данные, которые демонстрируют изменения в таблице после выполнения отката.

Давайте посмотрим на журнал отката undo_log, поскольку Seata удаляет журнал отката очень быстро, нам нужно остановить процесс на одном из серверов, чтобы увидеть изменения в журнале отката.

Рисунок 2. Журнал отката.

Время ID транзакции Описание
время ID транзакции Описание

Примечание: в таблице содержится информация о выполненных транзакциях и их результатах.

В целом, мы рассмотрели пять решений для распределённых транзакций: 2PC, 3PC, TCC, MQ и Seata. Мы подробно изучили решение с использованием промежуточного программного обеспечения Seata. Однако независимо от того, какое решение мы выберем, при реализации в проекте необходимо быть осторожным и тщательно продумать его применение. Поскольку, за исключением определённых сценариев с сильной согласованностью данных, лучше избегать использования распределённых транзакций, если это возможно, поскольку они могут значительно снизить общую эффективность проекта, особенно в условиях высокой параллельности.

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

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

  2. Распределение данных по различным базам данных для уменьшения объёма данных в одной базе данных и облегчения нагрузки на неё. Это помогает улучшить производительность базы данных.

Решения:

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

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

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

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

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

Цели разделения:

  • Вертикальное разделение: разделение бизнес-данных.

  • Горизонтальное разделение: решение проблем с объёмом данных и производительностью.

Проблемы перед разделением:

  • Большой объём запросов: из-за ограниченных возможностей одного сервера по обработке транзакций в секунду (TPS), памяти и ввода-вывода. Решение: распределить запросы между несколькими серверами. Фактически, пользовательские запросы и выполнение SQL-запросов — это одно и то же, разница лишь в том, что пользовательские запросы проходят через шлюз, маршрутизатор и HTTP-сервер.

  • Слишком большая одна база данных: обработка данных ограничена одним сервером, дисковое пространство на сервере недостаточно, ввод-вывод становится узким местом. Решение: разделить базу данных на более мелкие части.

  • Одна таблица слишком большая: возникают проблемы с операциями CRUD, индексами и временем ожидания запросов. Решение: разделить таблицу на несколько меньших таблиц.

Вертикальное разделение

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

Преимущества оптимизации:

  1. Избежание конкуренции за ввод-вывод и уменьшение вероятности блокировки таблицы.
  2. Эффективное использование операций с популярными данными без влияния на менее популярные данные.

Принципы разделения:

  1. Неиспользуемые поля помещаются в отдельную таблицу.
  2. Большие поля, такие как текст и BLOB, выделяются в отдельные таблицы.
  3. Часто используемые столбцы помещаются в одну таблицу.

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

Преимущества оптимизации:

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

Горизонтальное разделение

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

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

Преимущества оптимизации:

  1. Решаются проблемы с данными, связанными с бизнесом.
  2. Улучшается управление, мониторинг, расширение и обслуживание различных бизнес-данных.
  3. Горизонтальное разделение баз данных может повысить ввод-вывод, количество подключений к базе данных, ресурсы процессора и другие возможности одного компьютера в сценариях с высокой параллельностью.

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

Правила разделения:

Хеш-функция по модулю

После применения хеш-функции по модулю данные, распределённые по базам данных и таблицам, становятся равномерно распределёнными. Это предотвращает проблемы с неравномерным распределением ресурсов. Однако, если объём данных резко возрастает, потребуется миграция данных, изменение маршрутизации и другие операции.

Разделение по диапазонам

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

Однородный хэш

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

Географическое разделение

Данные могут быть разделены на основе географических регионов, таких как Восточная Азия, Южная Азия и Северная Америка. Это часто используется в облачных сервисах.

Временное разделение

Данные, которые не использовались в течение определённого периода времени, могут быть перемещены в отдельную таблицу или базу данных. Это называется «холодные» и «горячие» данные. %SHARDINGSPHERE_PROXY_HOME%/lib каталог.

  1. Запуск сервиса:

    • Используя дефолтные настройки:

      sh %SHARDINGSPHERE_PROXY_HOME%/bin/start.sh

      По умолчанию порт запуска — 3307, а каталог конфигурационного файла — %SHARDINGSPHERE_PROXY_HOME%/conf/.

    • Настройка порта и каталога конфигурационных файлов:

      sh %SHARDINGSPHERE_PROXY_HOME%/bin/start.sh ${proxy_port} ${proxy_conf_directory}
  2. Использование ShardingSphere-Proxy

Выполнять команды MySQL или PostgreSQL для клиентов напрямую с ShardingSphere-Proxy. На примере MySQL:

mysql -u${proxy_username} -p${proxy_password} -h${proxy_host} -P${proxy_port}

ShardingSphere-Sidecar (TODO)

Определяется как Kubernetes-облачный нативный прокси для баз данных в форме Sidecar. Он предоставляет слой взаимодействия без централизации и без вторжения, известный как Database Mesh, или сетка баз данных.

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

ShardingSphere-Sidecar Architecture

  1. Конфигурация правил:

Редактируйте %SHARDINGSPHERE_SCALING_HOME%/conf/server.yaml. Подробности см. в руководстве пользователя.

  1. Включение зависимостей:

Если вы подключаетесь к базе данных PostgreSQL, дополнительные зависимости не требуются. Если вы подключаетесь к MySQL, загрузите mysql-connector-java-5.1.47.jar и поместите его в %SHARDINGSPHERE_SCALING_HOME%/lib каталог.

  1. Запуск службы:
sh %SHARDINGSPHERE_SCALING_HOME%/bin/start.sh
  1. Управление задачами:

Управляйте миграцией задач через соответствующие HTTP-интерфейсы. Подробности см. в руководстве пользователя.

Гибридная архитектура

ShardingSphere-JDBC использует децентрализованную архитектуру, подходящую для высокопроизводительных облегчённых OLTP-приложений на Java. ShardingSphere-Proxy обеспечивает статический вход и поддержку гетерогенных языков, подходит для OLAP-приложений и управления и обслуживания сегментированных баз данных.

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

ShardingSphere Hybrid Architecture

Функции:

  • Сегментирование данных:

    • Разделение на библиотеки и таблицы
    • Изоляция чтения и записи
    • Настройка стратегии сегментирования
    • Децентрализация распределённых первичных ключей
  • Распределённые транзакции:

    • Стандартизированные интерфейсы транзакций
    • Сильные согласованные транзакции XA
    • Гибкие транзакции
  • Управление базами данных:

    • Распределённое управление
    • Эластичное масштабирование
    • Визуализация отслеживания ссылок
    • Шифрование данных

Полномасштабное нагрузочное тестирование

Исследование и практика полномасштабного нагрузочного тестирования Dada

Полномасштабное нагрузочное тестирование в отрасли

Dada предлагает следующую схему нагрузочного тестирования: создание точной копии производственной среды и проведение тестирования в ней. Эта схема имеет низкую техническую сложность и проста в реализации, но она также имеет очевидные недостатки: затраты на человеческие ресурсы и оборудование растут по мере увеличения масштаба производственной среды. Поэтому мы изучили основные методы нагрузочного тестирования, используемые в отрасли, которые называются «маркировка трафика». Этот метод заключается в маркировке трафика запросов (HTTP, RPC, MQ) и использовании этих меток для отслеживания потока трафика между сервисами. Это позволяет отделить тестовый трафик от производственного трафика и обеспечить изоляцию данных на уровне баз данных, кэша и очередей сообщений.

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

Однако этот метод подходит только для сред с унифицированными промежуточными программами. Dada использует различные типы промежуточных программ, такие как ORM (Mybatis, Hibernate, JPA), и существует множество разнородных программ, включая Java и Python. Реализация метода «маркировки трафика» потребует значительных изменений в бизнес-логике, поэтому мы отказались от этого подхода.

Нагрузочное тестирование Dada

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

Процесс реализации этого решения включает в себя следующие шаги:

  • Абстрагирование всех узлов баз данных, кешей и очередей сообщений до отдельных узлов. Информация об узлах регистрируется в центре регистрации.
  • Регистрация всей информации об узлах в центре регистрации.
  • Подключение всех сервисов к «SDK управления трафиком». SDK управления трафиком может направлять запросы в соответствии с трафиком.

Наиболее важным компонентом этого решения является «SDK управления трафиком», задача которого состоит в том, чтобы направлять трафик в соответствии с типом маршрута. Как показано на рисунке ниже, он запускается следующим образом:

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

В результате в производственной среде формируется два маршрута: маршрут для обработки производственного трафика и маршрут для обработки тестового трафика.

Сравнение методов «маркировки трафика» и «маркировки устройств»: оба метода имеют свои преимущества и недостатки. Dada выбирает метод «маркировки устройств», исходя из соображений безопасности и затрат на изменения в системе.

Окончательный выбор: после сравнения двух методов было решено использовать метод «маркировки устройств».

Платформа нагрузочного тестирования

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

Платформа нагрузочного тестирования состоит из следующих основных компонентов:

  • Фронтенд-сервис: предоставляет функции создания, запуска и остановки нагрузочных тестов, а также отображения результатов.
  • Анализатор нагрузочных тестов: отвечает за анализ и хранение нагрузочных тестов.
  • Механизм нагрузочного теста: отвечает за планирование нагрузочных тестов и их выполнение на исполнителях (по расписанию и немедленно).
  • Обработчик результатов нагрузочного теста: отвечает за обработку возвращаемых значений нагрузочных тестов, статистический анализ, обработку исключений и генерацию отчётов.

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

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

Результаты нагрузочного тестирования отображаются в виде графиков и таблиц. ДадА: подход к нагрузочному тестированию

Перед, во время и после нагрузочного тестирования

  • Перед нагрузочным тестированием:

    • Анализ связей: анализ связей очень важен для определения того, какие сервисы должны быть развёрнуты при нагрузочном тестировании. В прошлом ДадА анализировала связи вручную, но этот метод был неэффективным, неточным, трудоёмким, и не позволял оперативно реагировать на изменения в производственной среде. Позже был внедрён APM (PinPoint), который имеет функцию анализа связей.

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

    • Подготовка плана оптимизации:

      • Неподготовленная война — это поражение. Нагрузочное тестирование проводится для выявления потенциальных проблем с производительностью системы при высокой нагрузке. Обычно перед нагрузочным тестом мы готовим план оптимизации, который включает следующие меры:
        • Увеличение размера пула потоков/пула соединений: расширение пула потоков (если загрузка процессора сервера не превышает порогового значения) / расширение сервисов бизнес-услуг.
        • Оптимизация MySQL мастер-реплик: оптимизация MySQL BinLog -> обновление конфигурации машины -> вертикальное разделение базы данных -> горизонтальное разделение базы данных.
        • Расширение пропускной способности Redis: автоматическое расширение пропускной способности.
        • Обработка накопленных сообщений MQ: расширение служб, обрабатывающих сообщения.
    • Из-за развития бизнеса в последние годы объём данных в одной таблице одной базы данных в системе логистики ДадА постоянно увеличивается. Система неоднократно сталкивалась с задержкой мастер-репликаций MySQL. Наиболее часто используемым методом оптимизации является оптимизация MySQL Binlog, которая в основном оптимизирует два параметра:

      • binlog_group_commit_sync_delay: указывает, сколько времени ждать после фиксации транзакции, прежде чем синхронизировать binlog с диском, по умолчанию 0, что означает отсутствие ожидания (в микросекундах).
      • binlog_group_commit_sync_no_delay_count: указывает, через сколько транзакций синхронизировать binlog с диском.
    • Однако у этой оптимизации есть и недостатки: после оптимизации время отклика интерфейса улучшается, поэтому необходимо учитывать, может ли бизнес выдержать увеличение времени отклика.

  • Во время нагрузочного тестирования:

    • Моделирование нагрузки: с развитием бизнеса модели нагрузочного тестирования также постоянно совершенствуются. От первоначального использования виртуальных рыцарей для моделирования нагрузки на основе целевых значений TPS до текущего моделирования активных рыцарей с учётом временных и пространственных факторов для создания более точных моделей. Модели делятся на две категории:

      • Модель данных: данные рыцаря, данные клиента, данные заказа.
      • Потоковая модель: отправка заказа, выполнение обязательств по доставке.
    • Данные модели:

      • Данные активных рыцарей, клиентов и заказов в производственной среде импортируются в теневой склад, очищаются от конфиденциальной информации (номер телефона, адрес и т. д.), и используются для нагрузочного тестирования. Этот метод прост и наиболее точно воспроизводит производственную среду.
    • Потоковые модели:

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

    • Время:

      • С точки зрения времени, в течение дня есть три пика: утренний пик, обеденный пик и вечерний пик. Каждый пиковый период имеет различное состояние для каждого интерфейса:

        • Утренний пик: высокий уровень запросов на отправку и получение заказов, низкий уровень запросов для других интерфейсов.
        • Обеденный и вечерний пики: высокий уровень запросов для деталей заказа, запросов на получение и завершение, низкий уровень для других интерфейсов.
      • Поэтому при нагрузочном тестировании ДадА разрабатывает два сценария тестирования на основе характеристик трёх пиковых периодов интерфейса, чтобы провести тестирование.

    • Пространство:

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

      • У ДадА есть интерфейс для просмотра заказов в пределах X километров, производительность которого тесно связана с количеством горячих точек и количеством заказов в каждой горячей точке. Чтобы понять эти аспекты, мы проанализировали поток во время больших акций, разделили всю страну на квадраты на основе geohash и подсчитали количество заказов и плотность рыцарей в каждом квадрате. Наконец, мы восстановили и увеличили масштаб в тестовой среде.

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

  • После нагрузочного тестирования:

    • После нагрузочного теста генерируется отчёт о тестировании, определяются и оптимизируются проблемы с производительностью, оценивается ёмкость системы и проводится анализ тестирования. Ниже приводится анализ тестирования.

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

Итоги и выгоды

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

  • Разделение потока: на основе маркировки потока ДадА разработала метод разделения потока на основе меток машин, который полностью изолирует поток тестирования от производственного потока.
  • Разделение данных: из соображений безопасности ДадА выбирает теневые склады, теневые кэши, теневые очереди и другие методы для достижения полной изоляции данных тестирования и производственных данных, что позволяет проводить нагрузочное тестирование в любое время суток.
  • Детализированная модель нагрузочного тестирования: модель нагрузочного тестирования должна быть максимально приближена к производственной среде для обеспечения точности результатов тестирования. Мы используем данные крупных акций в производственной среде в качестве ориентира, анализируем их с точки зрения времени и пространства и создаём поток, близкий к потоку крупных акций, чтобы обеспечить достоверность модели данных.

Общая выгода проекта также очевидна, она проявляется в следующих двух аспектах:

  • Стабильность: в настоящее время ДадА успешно выявляет более десяти проблем с производительностью во время каждой крупной акции в течение двух лет подряд, обеспечивая стабильность крупных акций.
  • Эффективность: по сравнению с первоначальным планом создания отдельной среды нагрузочного тестирования эффективность проекта значительно улучшилась. Текущая эффективность повысилась на 65%, а стоимость снизилась на 40%.

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

Функция Spring Cloud Config Apollo Nacos
Год выпуска 2014.9 2016.5 2018.6
Поддержка push-уведомлений в реальном времени Да (Spring Cloud Bus) Да (HTTP длинный опрос 1S) Да (HTTP длинный опрос 1S)
Управление версиями Да (Git) Да Да
Откат версий Да (Git) Да Да
-------------- ----------------------------------------------------------- ----------------------------------------------- -----------------------------------------
权限管理 Поддержка Поддержка Поддержка
Многокластеров Поддержка Поддержка Поддержка
Мульти-среда Поддержка Поддержка Поддержка
Мониторинг запросов Поддержка Поддержка Поддержка
Мультиязычность Только поддержка java Go, C++, java, Python, PHP, .net, OpenAPI Python, Java, Node.js, OpenAPI
Одномашинное развёртывание Config-server+Git+Spring Cloud Bus (поддержка конфигурации в реальном времени) Apollo-quikstart+MySQL Nacos одноузловой
Распределённое развёртывание Config-server+Git+MQ (сложное развёртывание) Config+Admin+Portal+MySQL (сложное развёртывание) Nacos+MySQL (простое развёртывание)
Проверка формата конфигурации Не поддерживается Поддерживается Поддерживается
Коммуникационный протокол HTTP и AMQP HTTP HTTP
Согласованность данных Git обеспечивает согласованность данных, Config-сервер считывает данные из Git База данных имитирует очередь сообщений, Apollo периодически считывает сообщения HTTP асинхронное уведомление
Чтение с одного узла 7 (ограничение потока) 9000 15000
Запись с одного узла 5 (ограничение потока) 1100 1800
Чтение с трёх узлов 21 (ограничение потока) 27000 45000
Запись с трёх узлов 5 (ограничение потока) 3300 5600
Документация Подробная Подробная Требует доработки (в настоящее время только документы, связанные с разработкой на java)

Apollo

Nacos

Spring Cloud Config

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

DubboArchitecture

Реестр служб

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

Redis

Zookeeper

Nacos

Стратегии обеспечения отказоустойчивости и высокой доступности кластера

Отказоустойчивость (Failover Cluster)

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

Быстрое обнаружение сбоев (Failfast Cluster)

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

Безопасный сбой (Failsafe Cluster)

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

Автоматическое восстановление после сбоя (Failback Cluster)

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

Параллельный вызов (Forking Cluster)

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

Широковещательный вызов (Broadcast Cluster)

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

Балансировка нагрузки

Случайная балансировка нагрузки (Random LoadBalance)

  • Случайная, установка вероятности на основе весов.
  • Вероятность столкновения на одном срезе высока, но распределение вызовов становится более равномерным по мере увеличения количества вызовов, и вероятность вызова также становится более равномерной после установки весов, что способствует динамической настройке весов провайдеров.

Циклическая балансировка нагрузки (RoundRobin LoadBalance)

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

Балансировка нагрузки с наименьшей активностью (LeastActive LoadBalance)

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

Согласованная балансировка нагрузки хэша (ConsistentHash LoadBalance)

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

Фильтр

AccessLogFilter

AccessLimitFilter

TraceFilter

TimeoutFilter

Используется на стороне провайдера для записи предупреждающего журнала для тайм-аута вызова службы.

MonitorFilter

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

CompatibleFilter

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

ExceptionFilter

На стороне провайдера выборочно упаковывает исключения. Исключения, не прошедшие проверку, напрямую выбрасываются, исключения JDK напрямую выбрасываются, исключения классов и интерфейсов в одном jar-пакете напрямую выбрасываются, а исключения, объявленные методом интерфейса службы, должны быть выброшены после упаковки в RpcResult.

Маршрутизация услуг

Закрытие заказа после истечения срока ожидания

В таких областях, как электронная коммерция и платёж, часто возникает ситуация, когда пользователь размещает заказ, но затем отказывается от оплаты. В этом случае заказ должен быть закрыт по истечении определённого периода времени. Внимательные читатели, вероятно, заметили, что эта логика присутствует в таких платформах, как Taobao и JD.com, причём время закрытия очень точное, с погрешностью в пределах 1 секунды. Как они это реализуют?

Существует несколько общих подходов:

  • Периодические задачи для закрытия заказов
  • Использование очередей RocketMQ с задержкой

Опубликовать ( 0 )

Вы можете оставить комментарий после Вход в систему

1
https://api.gitlife.ru/oschina-mirror/yu120-lemon-guide.git
git@api.gitlife.ru:oschina-mirror/yu120-lemon-guide.git
oschina-mirror
yu120-lemon-guide
yu120-lemon-guide
main