Решение
Введение: сбор информации о технологиях, связанных с высокой доступностью, таких как идемпотентность, ограничение скорости, понижение уровня обслуживания, отключение, транзакции, кэширование, разделение базы данных и т. д.!
[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. Режимы работы и их реализация
Режимы в основном основаны на фрагментированном подходе. При разработке фрагментации мы учитываем следующие аспекты:
Гибкость и асинхронность
Асинхронность
Чем дольше время вызова, тем выше риск тайм-аута. Чем сложнее логика выполнения шагов, тем больше вероятность сбоя. Если это разрешено бизнес-логикой, пользовательский вызов может предоставлять только необходимые результаты, а не синхронные результаты, которые могут быть обработаны в другом месте асинхронно, что снижает риск тайм-аутов и упрощает сложность бизнес-процессов. Конечно, асинхронность имеет много преимуществ, таких как развязка и т. д., здесь мы рассматриваем только доступные перспективы. Асинхронность обычно реализуется тремя способами:
Гибкость
Что такое гибкость? Представьте себе сценарий, в котором наша система добавляет баллы к каждой транзакции пользователя. Что делать, если после завершения транзакции возникает проблема с сервисом, добавляющим баллы? Должны ли мы отменить заказ или позволить ему пройти через систему, обрабатывая проблему с баллами позже?
Гибкость — это подход в нашей бизнес-среде, который позволяет предоставлять пользователям максимально возможный сервис, вместо того чтобы требовать 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 и обеспечивает максимальную функциональность системы. Плавление изолирует модули, обеспечивая максимальную функциональную доступность.
Управление услугами
Управление услугами
Модули услуг тесно связаны друг с другом, но модули услуг имеют разные уровни важности в бизнесе. Например, модуль заказа может быть ключевым компонентом для интернет-магазинов, и проблемы с ним могут напрямую повлиять на доходы компании. С другой стороны, модуль поиска может быть важным, но его важность не сравнима с модулем заказа. Таким образом, при управлении услугами необходимо чётко определять уровни важности различных модулей услуг, чтобы лучше контролировать и распределять ресурсы. Это зависит от конкретных компаний, и стандарты могут различаться. Для интернет-магазина определение уровней услуг может основываться на количестве запросов пользователей и доходах. Сервисная градация не только определяет важность и приоритет в определении сбоев, но также играет ключевую роль в мониторинге сервисов и обеспечении их высокой доступности.
Мониторинг сервисов является важным элементом управления микросервисами. Качество мониторинга напрямую влияет на качество предоставляемых микросервисных услуг. Эффективная система мониторинга позволяет оперативно получать информацию о состоянии сервисов, что критически важно для обеспечения надёжности и стабильности системы. Надёжность и стабильность являются необходимыми условиями для достижения высокой доступности.
Мониторинг сервисов в основном направлен на прогнозирование рисков и раннее обнаружение проблем. Если система оснащена системой оповещения о сбоях, она может автоматически устранять ошибки или уведомлять персонал о необходимости вмешательства.
Эффективная система мониторинга микросервисов должна охватывать несколько уровней:
Холодное резервирование — это простой способ резервного копирования данных путём копирования файлов. Оно обеспечивает быстрое восстановление, но имеет ряд недостатков:
Горячее резервирование позволяет поддерживать сервис в рабочем состоянии во время резервного копирования. Однако при восстановлении всё равно требуется временная остановка сервиса. Активное/пассивное резервирование может быть реализовано через:
Локальное резервирование обеспечивает защиту от сбоев в пределах одного центра обработки данных. Это решение подходит для небольших компаний или для защиты отдельных сервисов.
Двухуровневое резервирование предполагает наличие двух центров обработки данных в разных местах. Один центр обрабатывает запросы, а другой служит резервным. В случае сбоя основного центра запросы перенаправляются на резервный. Это обеспечивает более высокую доступность и защиту от крупных сбоев.
Распределённое резервирование подразумевает наличие нескольких центров обработки данных, расположенных в разных географических точках. Запросы распределяются между центрами, обеспечивая высокую доступность и устойчивость к сбоям. «Реализация технологии распределённой обработки запросов в условиях географического разделения (I): общее описание»
Таким образом, в этой области нельзя проводить балансировку нагрузки. Использование ведущего и ведомого вместо балансировки нагрузки естественным образом решает проблему конфликтов.
На самом деле, распределённая обработка запросов с географическим разделением и распределённая обработка с географической избыточностью очень похожи, структура балансировки нагрузки более проста, поэтому при проектировании архитектуры системы не нужно учитывать слишком много факторов, достаточно выполнить традиционные ограничения трафика, аварийное переключение и другие операции. Но на самом деле балансировка нагрузки — это лишь временный шаг, конечная цель — перейти к географической избыточности. Потому что помимо проблемы конфликта данных, балансировка нагрузки также не может обеспечить горизонтальное расширение.
Согласно идее распределённой обработки с географическим разделением, можно нарисовать схему распределённой обработки с географической избыточностью. Степень входа и выхода каждого узла равна 4, и в этом случае сбой любого узла не повлияет на бизнес. Однако из-за расстояния каждая операция записи будет иметь большее время задержки. Время задержки не только влияет на пользовательский опыт, но и приводит к большему конфликту данных. В случае серьёзного конфликта данных использование распределённых блокировок также становится дороже. Это приведёт к увеличению сложности системы и снижению пропускной способности. Поэтому схема выше не применима.
Вспомним, как мы оптимизировали сетевую топологию? Мы ввели промежуточный узел, чтобы преобразовать сетевую топологию в звездообразную:
После преобразования в вышеуказанную схему сбой любого города не повлияет на данные. Для существующего трафика запросов он будет перераспределён на новый узел (желательно перераспределить на ближайший город). Чтобы решить проблему безопасности данных, нам нужно только обработать центральный узел. Однако требования к центральному городу будут выше, чем к другим городам. Например, скорость восстановления, целостность резервных копий и т. д., здесь пока не обсуждаются. Предположим, что центр полностью безопасен.
Реестр служб в основном предоставляет унифицированный стандартный базовый компонент для распределённых сервисов для публикации и обнаружения, так что пользователи могут напрямую использовать простой интерфейс для реализации функций публикации и обнаружения сервисов.
Регистрация служб имеет две формы: клиентская регистрация и регистрация через прокси.
Клиентская регистрация означает, что служба сама отвечает за регистрацию и отмену регистрации. После запуска службы поток регистрации регистрируется в реестре, а при завершении работы службы отменяется регистрация.
Эта форма имеет недостаток, заключающийся в том, что логика регистрации и отмены регистрации тесно связана с бизнес-логикой службы, и если служба разработана на разных языках, необходимо адаптировать множество наборов логики регистрации служб.
Регистрация через прокси означает, что отдельная служба прокси отвечает за регистрацию и отмену регистрации. Когда поставщик услуг запускается, он уведомляет службу прокси определённым способом, после чего служба прокси берёт на себя ответственность за регистрацию в реестре.
Недостатком этой формы является то, что она добавляет ещё одну службу прокси, и служба прокси должна поддерживать высокую доступность.
Обнаружение служб также делится на клиентское обнаружение и обнаружение через прокси.
Клиентское обнаружение означает, что клиент сам отвечает за запрос адреса доступных служб из реестра и выбор экземпляра для вызова на основе алгоритма балансировки нагрузки после получения списка всех доступных адресов экземпляров.
Преимущество этой формы заключается в том, что клиенты могут контролировать алгоритм балансировки нагрузки. Недостатком является то, что получение списка адресов экземпляров, балансировка нагрузки и т.д. тесно связаны с бизнес-логикой служб, и все службы должны быть изменены и перезапущены, если обнаружение служб или балансировка нагрузки изменятся.
Обнаружение через прокси означает добавление службы маршрутизации, которая отвечает за обнаружение доступных служб и предоставление списка доступных экземпляров. Если потребитель хочет вызвать службу A, он может напрямую отправить запрос службе маршрутизации. Служба маршрутизации выбирает экземпляр из доступного списка экземпляров и пересылает запрос на основе настроенного алгоритма балансировки нагрузки, и, если экземпляр недоступен, служба маршрутизации может повторить попытку самостоятельно, и потребителю не нужно об этом знать.
Если у службы есть несколько экземпляров, и один из экземпляров выходит из строя, реестр может немедленно обнаружить это и удалить этот экземпляр из списка, который называется снятием с эксплуатации. Как реализовать снятие с эксплуатации? Обычно используемый метод в отрасли — это механизм пульса, который может быть реализован двумя способами: активно и пассивно.
Пассивное обнаружение означает, что службы активно отправляют сообщения пульса в реестр, интервал настраивается, например, устанавливается на 5 секунд, и реестр удалит экземпляр из списка через 15 секунд после того, как не получит сообщение пульса от экземпляра службы в течение трёх периодов.
В приведённом выше примере служба A, экземпляр 2 вышел из строя и не может активно отправлять сообщения пульса в реестр. Через 15 секунд реестр удалит экземпляр 2.
Активное обнаружение означает, что реестр активно инициирует и отправляет сообщения пульса всем экземплярам в списке каждые несколько секунд. Если сообщение пульса не было успешно отправлено или не получено в течение нескольких периодов, оно будет активно удалять экземпляр.
С точки зрения пользователя, основное внимание уделяется пяти основным интерфейсам реестра, которые позволяют выполнять динамическую публикацию, динамическое обнаружение и изящное завершение работы служб:
ZooKeeper может столкнуться с некоторыми неизвестными проблемами, поэтому необходимы функции для решения различных возможных проблем, чтобы повысить качество продукта. Эти функции обычно не ощущаются пользователями напрямую, но их роль заключается в обеспечении высококачественных услуг:
Ниже приведено сравнение компонентов по различным аспектам:
Решение | Преимущества | Недостатки | Протокол доступа | Алгоритм согласованности |
---|---|---|---|---|
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:
Из диаграммы видно, что Tomcat хорошо инкапсулирован снаружи, но внутри по умолчанию будет три раза копирования.
Основные функции
Функциональный дизайн
Публикация API
Используя интерфейс управления API-шлюзом, разработчики могут легко управлять полным жизненным циклом API, как показано на следующем рисунке:
Разработчики начинают с создания API, заполняют параметры и генерируют сценарии DSL. Затем они могут протестировать API с помощью документации и функций MOCK. После завершения тестирования API для обеспечения стабильности при запуске платформа управления предоставляет функции утверждения выпуска, постепенного выпуска и отката версий. Во время работы API будет отслеживаться состояние сбоя вызова API и записываться журнал запросов. Как только обнаруживается аномалия, немедленно отправляется предупреждение. Наконец, для сервисов, которые больше не используются, выполняются операции отключения, ресурсы, используемые этими сервисами, восстанавливаются, и ожидается повторное включение. На протяжении всего жизненного цикла все процессы управляются конфигурацией и потоками, и разработчики полностью управляют ими самостоятельно, что значительно повышает эффективность разработки.
Центр конфигурации
Центр конфигурации API-шлюза хранит соответствующую конфигурацию API, используя настраиваемый язык DSL для описания, который используется для передачи конфигурации маршрутизации, правил и компонентов API в центр данных API-шлюза. Центр конфигурации разработан с использованием унифицированного сервиса управления конфигурацией и локального кэширования для реализации динамической конфигурации без простоев. Конфигурация API выглядит следующим образом:
Маршрутизация API
После обнаружения конфигурации API в центре данных API-шлюз создаст маршрут информации о запросе и конфигурации API в памяти. Обычно в пути HTTP-запроса есть переменные пути. Из соображений производительности не используется сопоставление регулярных выражений, а используются две структуры данных:
Одна из них представляет собой структуру MAP без переменных пути, где ключ — это полная информация о домене и пути, а значение — конкретная конфигурация API.
Другая — структура дерева префиксов, содержащая переменные пути. Сначала выполняется точное сопоставление листьев, затем узлы переменных помещаются в стек, если совпадение не найдено, верхний элемент стека выталкивается, а элементы того же уровня переменных помещаются в стек. Если совпадение всё ещё не найдено, процесс продолжается до тех пор, пока не будет найден (или не найден) узел пути и не завершится.
Компоненты функций
Когда запрос потока достигает пути API и входит в службу, конкретная логика обработки определяется сценарием DSL, настроенным в конфигурации. Шлюз предоставляет множество интегрированных функциональных компонентов, включая отслеживание ссылок, мониторинг в реальном времени, журналы доступа, проверку параметров, аутентификацию, ограничение скорости, разрыв цепи, понижение уровня обслуживания и т. д., как показано ниже:
Преобразование протокола и вызов службы
Последний шаг вызова API — преобразование протокола и вызов службы. Работа, выполняемая шлюзом, включает в себя получение параметров HTTP-запроса, контекста локальных параметров, сборку параметров внутренней службы, завершение преобразования протокола HTTP во внутреннюю службу и вызов внутренней службы для получения результатов ответа и преобразования их в результаты ответа HTTP.
На этом рисунке показан пример вызова внутренней RPC-службы. Используя выражение JsonPath для извлечения значений параметров из разных частей HTTP-запроса, значения заменяются соответствующими частями параметров RPC-запроса для создания сценария DSL службы, и, наконец, универсальный вызов RPC завершает этот сервисный вызов.
Высокопроизводительный дизайн
Обеспечение стабильности
Предоставляются некоторые стандартные меры обеспечения стабильности для обеспечения доступности самого шлюза и внутренних служб. Как показано ниже:
Самовосстановление
Сервисы шлюза поддерживают эластичное масштабирование, которое может быстро расширяться или сокращаться в зависимости от показателей, таких как использование ЦП. Кроме того, шлюз поддерживает быстрое удаление проблемных узлов и более детальное удаление проблемных компонентов.
Переносимость
Для служб, которые уже предоставляют внешние API, разработчики стремятся снизить эксплуатационные расходы и повысить эффективность последующей разработки за счёт переноса их на шлюзы API. Для некоторых неосновных API можно рассмотреть возможность постепенного внедрения. Однако для некоторых основных API функция постепенного внедрения на уровне машины является слишком грубой и не может обеспечить достаточную гибкость для поддержки процесса проверки серого масштаба.
Решение состоит в том, чтобы перенести существующие веб-сервисы, предоставляющие внешние API, на шлюз API. API-шлюз предоставляет разработчикам набор инструментов для создания и тестирования программного обеспечения.
Процесс работы с API-шлюзом
API-шлюз предлагает разработчикам сервис, который позволяет проводить предварительное тестирование API перед их окончательным внедрением.
Автоматическое создание DSL
Разработчики используют графический интерфейс платформы управления шлюзом для настройки параметров API. Однако параметры сервиса всё ещё необходимо настраивать вручную.
Процесс настройки параметров сервиса включает в себя следующие шаги:
Этот процесс является трудоёмким и подвержен ошибкам. Для автоматизации процесса настройки параметров сервиса API-шлюз использует информацию из последней версии консоли управления сервисами. На основе этой информации шлюз автоматически генерирует данные JSON Mock для параметров сервиса. Затем эти данные объединяются с информацией из документации по API.
Повышение эффективности работы с API
Шлюз также предлагает инструменты, которые помогают ускорить работу с API:
Использование пользовательских компонентов
В дополнение к стандартным компонентам шлюз поддерживает использование пользовательских компонентов для реализации специфической логики. Разработчики могут использовать эти компоненты для выполнения таких задач, как проверка подписи или обработка результатов.
Управление сервисами
Обычно один API соответствует одному сервису (RPC или HTTP). Однако иногда требуется объединить несколько сервисов для получения полного результата. Шлюз предоставляет возможность управлять такими сценариями, позволяя разработчикам вызывать несколько сервисов через один HTTP-запрос.
Контроль трафика
Для обеспечения безопасности шлюз предлагает различные механизмы контроля трафика, включая аутентификацию, авторизацию, регулирование, изоляцию кластеров и другие функции.
Мониторинг и оповещение
Шлюз обеспечивает мониторинг и оповещение о различных событиях, связанных с работой API. Мониторинг охватывает различные аспекты работы системы, такие как запросы, системные ресурсы и журналы. Оповещение позволяет получать уведомления о таких событиях, как превышение лимита запросов или сбой аутентификации. | 4 | API异常告警 | API发布失败、API检查异常时触发API异常告��ги | | 5 | 健康检查失败告警 | API心跳检查失败、网关节点不通时触发健康检查失败告��ги |
На основе Netty реализуется асинхронный внешний вызов. Существует два основных способа реализации:
Способ 1 требует независимого обслуживания отдельной глобальной таблицы отображения, а также рассмотрения вопросов тайм-аута запроса и потери данных, иначе может возникнуть проблема с постоянным увеличением памяти.
При использовании Netty для реализации микросервисов API-шлюза внешние вызовы должны быть объединены в пул. При проектировании необходимо учитывать следующие моменты:
HTTP-соединения являются эксклюзивными, поэтому при освобождении необходимо проявлять особую осторожность. Необходимо убедиться, что сервер ответил, прежде чем освобождать соединение. Также следует обратить внимание на обработку закрытого соединения, включая:
Запись тайм-аута: writeAndFlush включает время кодирования Netty и время отправки запроса из очереди. Поэтому после того, как бэкэнд начинает отсчёт тайм-аута, он должен начинаться после успешного завершения flush, чтобы максимально приблизиться к времени ожидания бэкэнда (также существует время задержки сети и обработки ядра протокола).
Для высокопроизводительных систем частое создание объектов не только приводит к выделению памяти, но и создаёт нагрузку на сборщик мусора. В процессе реализации часто используемые объекты (например, задачи в пуле потоков, StringBuffer и т. д.) переписываются для уменьшения частоты выделения памяти.
Хотя весь шлюз не связан с операциями ввода-вывода, асинхронность используется как в кодировании и декодировании ввода-вывода, так и в бизнес-логике. Есть две причины:
В случае всплеска трафика мы поддерживаем использование потоков push с помощью потоков IO Netty вместо этого. Здесь выполняется меньше работы, и здесь асинхронная модификация заменяется синхронной модификацией (через изменение конфигурации). Это снижает переключение контекста ЦП на 20 % и повышает общую пропускную способность. Аналогично Zuul2.
Уровень протокола:
Прикладной уровень:
Доступный адрес: https://github.com/Kong/kong.
Доступный адрес: https://github.com/Dromara/soul.
Доступный адрес: https://apiman.gitbooks.io/apiman-user-guide/user-guide/gateway/policies.html.
Доступный адрес: https://docs.gravitee.io/apim_policies_latency.html.
Доступный адрес: https://tyk.io/docs.
Tyk — это платформа управления API, которая позволяет разработчикам легко создавать, публиковать, защищать и монетизировать свои API. Она предоставляет инструменты для управления жизненным циклом API, включая разработку, тестирование, развёртывание и управление версиями. Tyk также обеспечивает безопасность API с помощью аутентификации, авторизации и шифрования.
Платформа предлагает несколько функций, которые делают её привлекательной для разработчиков:
Доступный адрес: https://traefik.cn.
Traefik — это современный обратный прокси-сервер HTTP, предназначенный для упрощения развёртывания микросервисных архитектур. Он поддерживает широкий спектр бэкендов, таких как Docker, Swarm, Kubernetes, Marathon, Mesos, Consul, Etcd, Zookeeper, BoltDB, Rest API и file. Traefik автоматически настраивает себя на основе изменений в бэкэнде, обеспечивая непрерывное обновление конфигурации.
Основные функции Traefik включают:
Доступный адрес: http://www.xbgateway.com.
Малый леопард API Gateway — корпоративный API Gateway, который решает такие задачи, как аутентификация, авторизация, безопасность, управление трафиком, кэширование, маршрутизация сервисов, преобразование протоколов, оркестровка сервисов, разрыв цепи, публикация серого цвета, мониторинг и оповещение.
Архитектура малого леопарда API Gateway включает:
Особенности малого леопарда API Gateway включают:
Автоматический выключатель — это механизм защиты микросервисной цепочки от эффекта лавины. Когда в цепочке микросервисов происходит сбой или время отклика слишком велико, происходит снижение уровня обслуживания, что приводит к отключению вызовов данного узла микросервиса и быстрому возврату ошибочного ответа. После обнаружения того, что узел микросервиса снова отвечает нормально, вызовы цепочки восстанавливаются. Архитектура автоматического выключателя представлена на схеме:

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

NDC (вложенный диагностический контекст) и MDC (сопоставленный диагностический контекст) являются двумя очень полезными классами log4j, которые используются для хранения контекста приложения (контекстной информации), что упрощает использование этой контекстной информации в журнале. NDC использует механизм, подобный стеку, для ввода и вывода контекстной информации, каждый поток сохраняет контекстную информацию отдельно. Например, сервлет может создать соответствующий NDC для каждого запроса, сохраняя такую информацию, как адрес клиента. MDC и NDC очень похожи, за исключением того, что MDC внутренне использует механизм карты для хранения информации, и каждый поток также сохраняет информацию отдельно, но информация хранится в карте с использованием ключа.
Принцип NDC и MDC заключается в использовании класса ThreadLocal в Java. Можно хранить информацию для разных потоков. Однако при использовании log4j2 сегодня было обнаружено, что NDC и MDC были заменены на ThreadContext.
Основная цель трассировки микросервисов — быстро определить точку отказа, используя метод MDC для добавления идентификатора транзакции ко всем журналам, созданным каждой транзакцией, так что только этот идентификатор необходим для сбора всех соответствующих журналов в системе для анализа и определения конкретной проблемы.
NDC хранит контекст с помощью механизма стека, который является независимым для каждого потока, а дочерний поток копирует контекст из родительского потока. Его методы вызова следующие:
Начало вызова:
Удаление верхнего сообщения стека:
Очистка всех сообщений, обязательно вызывается перед выходом из потока, иначе это может привести к утечке памяти.
Шаблон вывода, обратите внимание на нижний регистр [%x]:
MDC хранит контекст через механизм карты, который независим для каждого потока, и дочерний поток будет копировать контекст из родительского потока. Методы вызова следующие:
Сохранение информации в контексте:
Извлечение информации из контекста:
Очищение информации с указанным ключом в контексте:
Полная очистка:
Шаблон вывода, обратите внимание на верхний регистр [%X{key}]:
В 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);
}
}
В 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);
}
}
Пример формата журнала:
<PatternLayout pattern="%d{HH:mm:ss.SSS} [%t] %-5level %X{REQUEST_ID} %logger{36} - %msg%n" />
В рамках полнофункциональной системы отслеживания основной функцией Trace является передача информации через ThreadLocal. Однако в реальной бизнес-среде может использоваться асинхронный вызов, что приведёт к потере информации о трассировке и нарушению целостности цепочки.
InheritableThreadLocal — это решение для передачи потоков, предоставляемое JDK. Как следует из названия, когда текущий поток создаёт новый поток, новый поток унаследует значение текущего потока InheritableThreadLocal. Thread внутренне выделяет отдельный ThreadLocalMap для InheritableThreadLocal. При создании дочернего потока текущим потоком он проверяет, пуст ли ThreadLocalMap, и если нет, то он неглубоко копирует ThreadLocalMap в дочерний поток.
Transmittable ThreadLocal — это библиотека с открытым исходным кодом от Alibaba, которая наследует InheritableThreadLocal и оптимизирует передачу ThreadLocal при использовании пула потоков или других методов совместного использования потоков. Проще говоря, существуют специальные классы TtlRunnable и TtlCallable, которые считывают исходный объект ThreadLocal и значение объекта и сохраняют их в Runnable/Callable. При выполнении run или call они считывают сохранённые объекты ThreadLocal и значения из Runnable/Callable и помещают их в вызывающий поток. Zipkin — это один из открытых проектов Twitter, основанный на Google Dapper. Он направлен на сбор данных о времени обслуживания для решения проблем с задержкой в микросервисной архитектуре, включая сбор, хранение, поиск и отображение данных.
В процессе работы сервисов создаётся множество цепочек информации, места генерации данных можно назвать репортёрами. Цепочки информации передаются через различные способы передачи, такие как HTTP, RPC и очереди сообщений Kafka, в сборщик Zipkin. После обработки Zipkin сохраняет цепочки информации в памяти. Системные администраторы могут запрашивать информацию о вызовах через пользовательский интерфейс.
Zipkin имеет четыре основных компонента:
Как только сборщик получает цепочку отслеживания данных, Zipkin проверяет, хранит и индексирует её, а затем вызывает интерфейс хранения для сохранения данных для последующего поиска.
Изначально Zipkin Storage был разработан для хранения данных в Cassandra, поскольку Cassandra является масштабируемой и гибкой системой, широко используемой в Twitter. Помимо Cassandra, поддерживаются ElasticSearch и MySQL, и в будущем могут быть предложены сторонние расширения.
После того как данные отслеживания были сохранены и проиндексированы, веб-интерфейс может вызывать службу запросов для поиска любых данных, помогая системным администраторам быстро определять проблемы в сети. Служба запросов предоставляет простой API JSON для поиска и извлечения данных.
Zipkin предоставляет базовый интерфейс поиска и поиска, который позволяет системным администраторам легко идентифицировать проблемы в сети на основе информации о вызове.
Повторная отправка от клиента: операции, такие как регистрация пользователя или создание товаров, требуют от клиента отправки данных на сервер. Если клиент случайно отправляет данные несколько раз, сервер получит несколько запросов и создаст несколько записей в базе данных. Это ошибка, вызванная отсутствием алгоритма идемпотентности.
Перехват и повторная передача злоумышленником: злоумышленник перехватывает запрос и повторно передаёт его.
Тайм-аут и повторная попытка: при использовании сторонних сервисов для вызовов иногда возникают проблемы с сетью, поэтому обычно добавляется механизм повторных попыток. Если первая попытка вызова была выполнена наполовину, и произошла сетевая ошибка, повторная попытка может привести к ошибкам из-за грязных данных.
Повторяющееся потребление сообщений: при обработке сообщений с помощью промежуточного программного обеспечения и ручном подтверждении получения сообщения, если потребитель неожиданно отключается, сообщение будет повторно поставлено в очередь. Когда другое потребительское приложение повторно обрабатывает сообщение, отсутствие алгоритма идемпотентности может вызвать ошибки, такие как дублирование данных в базе данных, конфликты данных и повторное использование ресурсов.
Механизм токенов
Это распространённый метод реализации алгоритма идемпотентности для интерфейсов. Схема выглядит следующим образом:
Клиент сначала отправляет запрос на получение токена, сервер генерирует уникальный идентификатор в качестве токена и сохраняет его в Redis. Затем этот токен возвращается клиенту. Во время второго вызова клиент должен предоставить этот токен. Сервер проверяет токен и, если проверка успешна, выполняет бизнес-логику и удаляет токен из Redis. В случае неудачной проверки, что указывает на отсутствие соответствующего токена в Redis, операция считается повторной и возвращает соответствующий результат клиенту.
Обратите внимание:
На основе 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
Первый шаг: импорт зависимостей
<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.
Недостатки:
Реализуется на основе исключительной блокировки (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();
}
Операция добавления блокировки и последующего задания времени ожидания являются отдельными и неатомарными операциями. Решение:
Вариант 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]);
После получения блокировки необходимо дождаться истечения времени ожидания перед снятием блокировки. Несвоевременное снятие блокировки может вызвать проблемы. Рекомендуется следующий процесс:

Код для снятия блокировки:
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:
Здесь мы берём ведущий-ведомый в качестве примера, структура выглядит следующим образом:
RedissonRedLock получает блокировку следующим образом:
В реальных бизнес-сценариях, особенно в условиях высокой параллельной нагрузки, RedissonRedLock используется не так часто. В распределённых средах нельзя обойти CAP-теорему: необходимо выбирать между согласованностью (Consistency), доступностью (Availability) и устойчивостью к разделению (Partition tolerance).
Если требуется обеспечить согласованность данных, то рекомендуется использовать CP-тип распределённой блокировки, например, Zookeeper, который основан на диске и может обеспечивать надёжность данных.
Если необходима высокая доступность, то лучше выбрать AP-тип блокировки, такой как Redis, основанный на памяти и обеспечивающий хорошую производительность, но с риском потери данных.
На практике в большинстве распределённых бизнес-сценариев достаточно использовать Redis для распределённой блокировки.
Для обеспечения атомарности операций блокировки и установки срока действия блокировки можно использовать Lua-скрипты с командами SETNX и EXPIRE. Пример кода на Java показывает, как реализовать эти операции.
Также упоминается проблема истечения срока блокировки из-за FullGC, когда блокировка автоматически освобождается. Предлагаются два решения этой проблемы:
Описывается команда 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. Она предлагает ряд распределённых объектов, таких как:
Также Redisson предоставляет множество распределённых сервисов.
Особенности и функции
Watch dog
В целом, Redisson предлагает следующие типы распределенных блокировок:
Реализация
Для использования Redisson необходимо добавить зависимость в проект. Существует два способа добавления зависимости:
<!-- Способ 1: redisson-java -->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.11.4</version>
</dependency>
<!-- Способ 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;
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, элементы с наименьшей частотой доступа удаляются первыми. Этот метод используется редко, поскольку он не может отвечать за элементы, которые после первоначального высокого уровня доступа долго не используются.
Этот алгоритм кэширования помещает недавно используемые элементы ближе к вершине кэша. Когда новый элемент запрашивается, LRU помещает его в начало кэша. Когда кэш достигает своего предела, элементы, к которым обращались ранее, удаляются с конца кэша. Здесь используется дорогостоящий алгоритм, и необходимо отслеживать «возрастные метки», чтобы точно показать, когда элемент был доступен. Кроме того, после удаления элемента алгоритмом LRU «возрастная метка» изменяется вместе с другими элементами.
После доступа к кэшу вероятность повторного доступа к нему в ближайшее время высока. Можно отслеживать последнее время доступа каждого элемента кэша, и элементы с самым длинным временем без доступа будут удалены первыми.
Преимущества: простая реализация, адаптация к горячим точкам доступа. Недостатки: чувствительность к случайным доступам, влияние на коэффициент попадания. Затраты на ведение журнала: время O(1), пространство O(N).
Разработанный в исследовательском центре IBM Almaden, этот алгоритм кэширования отслеживает записи LFU и LRU, а также вытесняет элементы кэша для достижения оптимального использования доступного кэша.
FIFO — это аббревиатура от английского First In First Out, это тип очереди данных, который работает по принципу «первым пришёл — первым ушёл». Он отличается от обычной памяти отсутствием внешних линий чтения/записи адресов, что делает его очень простым в использовании, но недостатком является то, что данные могут быть записаны и прочитаны только последовательно, а чтение и запись данных осуществляется с помощью внутренней очереди указателей чтения/записи, которая автоматически увеличивается на 1.
Элементы, которые попадают в кэш раньше, имеют большую вероятность того, что они больше не будут доступны. Поэтому при удалении элементов кэша следует выбирать те, которые находились в кэше дольше всего. Очередь может реализовать эту стратегию:
Преимущества: простота реализации, подходит для сценариев линейного доступа. Недостатки: неспособность адаптироваться к конкретным горячим точкам доступа, плохой коэффициент попадания кэша. Затраты на ведение журнала: время O(1), пространство O(N).
Этот алгоритм кэширования удаляет элементы, к которым последний раз обращались чаще всего. Алгоритм MRU хорошо справляется с ситуациями, когда элементы остаются доступными в течение длительного времени.
Стратегии обновления кэша в основном делятся на три типа:
В распределённых системах либо обеспечивается строгая согласованность с использованием протоколов 2PC, 3PC или Paxos, либо предпринимаются отчаянные попытки снизить вероятность грязных данных при параллельной работе. Система кэширования применима в сценариях с нестрогой согласованностью, поэтому она относится к категории AP в CAP, обеспечивая только окончательную согласованность, как указано в теории BASE. Разнородные базы данных изначально не могут обеспечить строгую согласованность, мы просто стараемся минимизировать время несогласованности и достичь окончательной согласованности. В то же время применяется стратегия установки срока действия.
Кэширование в сторону (Cache Aside) является одним из наиболее широко используемых шаблонов кэширования. Правильное использование кэширования в сторону может значительно повысить производительность приложений, и оно может применяться как для операций чтения, так и для записи. Цель кэширования в сторону — максимально решить проблему несоответствия данных между кэшем и базой данных.
Процесс запроса на чтение выглядит следующим образом:
Процесс записи запроса выглядит следующим образом:
В режиме чтения/записи через (Read/Write Through) сервер рассматривает кэш как основное хранилище данных. Приложения взаимодействуют с базой данных через абстрактный уровень кэша.
Режим чтения через (Read Through) похож на режим кэширования в стороне, за исключением того, что программе не нужно управлять тем, откуда считывать данные (кэш или база данных). Вместо этого она напрямую считывает данные из кэша, и в этом сценарии кэш определяет, откуда запрашивать данные. Сравнение этих двух методов является преимуществом, поскольку оно делает код программы более лаконичным. Процесс Read Through выглядит следующим образом:
Этот шаблон представляет собой ещё один уровень абстракции поверх кэширования в сторону, делая код программы более кратким и снижая нагрузку на источник данных. Процесс выглядит следующим образом:
Все операции записи в режиме записи через (Write Through) проходят через кэш. Каждый раз, когда данные записываются в кэш, кэш сохраняет данные в соответствующей базе данных, и обе операции выполняются в одной транзакции. Таким образом, только две успешные операции приводят к окончательному успеху. Использование задержки записи гарантирует согласованность данных. Когда поступает запрос на запись, абстрактный слой кэша также обновляет данные источника и кэша, процесс выглядит следующим образом:
Когда используется режим записи через, обычно также используется режим чтения через. Режим записи через подходит, когда:
Написать за (Write Behind, также называемый Write Back) похож на Read/Write Through в том, что Cache Provider отвечает за чтение и запись кэша и базы данных. Однако между ними существует большое различие: Read/Write Through синхронизирует обновление кэша и данных, в то время как Write Behind обновляет только кэш, не обновляя базу данных напрямую, используя пакетную асинхронную запись для обновления базы данных.
В этом режиме согласованность кэша и базы данных не является строгой, и системы с высокими требованиями к согласованности должны использовать его с осторожностью. Однако он подходит для сценариев с частыми операциями записи, таких как механизм буфера пула InnoDB в MySQL. Как показано на рисунке выше, приложение обновляет два фрагмента данных, Cache Provider немедленно записывает их в кэш, но только через некоторое время они записываются в базу данных партиями. Преимущества и недостатки следующие:
Здесь мы выделяем окончательную согласованность, потому что она является очень уважаемой моделью согласованности в рамках слабой согласованности и также широко используется в области больших распределённых систем данных.
Бизнес-задержка двойной очистки
Как избежать грязных данных при удалении кеша перед обновлением базы данных? Используйте стратегию отложенной двойной очистки.
Рисунок: Процесс отложенной двойной очистки.
В архитектуре с разделением чтения и записи:
Отложенная двойная очистка приводит к снижению пропускной способности.
Решение: сделать второе удаление асинхронным.
MQ механизм повторной попытки удаления
Независимо от того, используется ли отложенная двойная очистка или стратегия «сначала операция с базой данных, затем удаление кеша», может произойти сбой второго шага удаления кеша, что приведёт к несогласованности данных. Можно ввести механизм повторной попытки удаления кеша для решения этой проблемы.
Рисунок: Решение проблемы удаления кеша.
Процесс:
Оптимизация: используйте binlog MySQL для асинхронного удаления ключей.
Рисунок: Асинхронное решение проблемы удаления кеша на основе binlog.
Процесс:
Выбор стратегии: удаление или обновление
При работе с кешем следует ли удалять кеш или обновлять его? В повседневной разработке обычно используется режим Cache-Aside. Некоторые пользователи могут спросить, почему при записи запроса используется удаление кеша вместо обновления кеша?
Рисунок: Поток записи в режиме Cache-Aside.
Мы рассматриваем удаление кеша или его обновление при работе с кешем? Давайте рассмотрим пример:
Рисунок: Пример потока записи в режиме Cache-Aside.
Последовательность действий:
На этом этапе кеш сохраняет данные потока A (старые данные), а база данных сохраняет данные потока B (новые данные), что приводит к грязным данным. Если вы замените обновление кеша удалением кеша, проблема грязных данных не возникнет. Обновление кеша по сравнению с удалением кеша имеет два недостатка:
— Если значение, записанное в кеш, является результатом сложных вычислений, высокая частота обновлений кеша приведёт к потере производительности. — В сценарии с большим количеством операций записи и небольшим количеством операций чтения данные часто не считываются до их обновления, а затем обновляются, что также приводит к потере производительности (на самом деле, использование кеша в сценарии с большим количеством записей не очень рентабельно).
Порядок двойной записи
В случае двойной записи следует сначала обновить базу данных или сначала обновить кеш? В режиме Cache-Aside некоторые пользователи всё ещё сомневаются, что при записи запроса следует сначала обновить базу данных? Почему бы не обновить кеш первым? Например, одни и те же данные существуют как в базе данных, так и в кеше. Теперь вам нужно обновить эти данные. Независимо от того, сначала обновляется база данных или кеш, оба метода имеют проблемы.
Вариант 1: сначала обновите базу данных, а затем обновите кеш
Рисунок: Порядок двойной записи: сначала база данных, потом кеш.
Пример:
A сначала обновляет базу данных до 123, но обновление кеша задерживается из-за проблем с сетью. В это время B обновляет базу данных до 456, а затем немедленно обновляет кеш до 456. Теперь запрос на обновление кеша от A достигает, и кеш обновляется до 123. На данный момент данные несовместимы, база данных содержит последние 456 данных, а кеш содержит старые 123 данных. Поскольку операции обновления базы данных и кеша не являются атомарными, эти две операции будут вставлены непосредственно в другие операции при высокой параллельной работе.
Рисунок: Пример порядка двойной записи: сначала база данных, потом кеш.
Вариант 2: сначала обновите кеш, а затем базу данных
Рисунок: Порядок двойной записи: сначала кеш, потом база данных.
Пример: обновление кеша успешно, данные являются последними, но обновление базы данных не удалось и откатилось назад, всё ещё старые данные. Это также связано с неатомарными операциями.
Рисунок: Пример порядка двойной записи: сначала кеш, потом база данных.
Обратите внимание: хотя порядок операций с базой данных и кешем отличается, он также может привести к несовместимости данных. Однако вероятность этого невелика, поскольку причиной грязных данных обычно является сбой удаления кеша. Далее мы проанализируем ситуацию сбоя удаления кеша и то, как обеспечить согласованность.
Обновление кеша
Помимо стратегий истечения срока действия кеша, предоставляемых самим сервером кеширования (Redis по умолчанию предоставляет 6 стратегий), вы также можете настроить стратегию истечения срока действия в соответствии с конкретными бизнес-требованиями. Общие стратегии обновления включают:
LRU/LFU/FIFO: все они используются, когда кеш становится недостаточным. Они подходят для сценариев с ограниченной памятью, где данные редко меняются, и вероятность несогласованности данных очень мала. Например, некоторые данные, которые определяются как неизменяемые после определения. Истечение срока действия: установите срок действия для данных кеша. Подходит для бизнеса, который может терпеть определённое время несогласованности данных, например, описание рекламных акций. Активное обновление: если источник данных обновлён, активно обновляйте кеш. Для данных с высокими требованиями к согласованности, таких как системы транзакций и количество льготных купонов. Проблемы с кешем: лавины и дыры
В результате одновременного истечения срока действия большого количества данных, все запросы начинают обращаться напрямую к базе данных. Это приводит к резкому увеличению нагрузки на базу данных и может вызвать её сбой. Такое явление называется «лавиной кеша».
Причины возникновения «лавины кеша»:
Решения проблемы «лавины кеша»:
Если срок действия «горячего» элемента данных истекает, и большое количество запросов пытается получить доступ к этому элементу, база данных может быть перегружена и выйти из строя. Это явление называется «дырой в кеше».
«Дыра в кеше» похожа на «лавину кеша», но является более узким понятием. Она возникает, когда срок действия конкретного «горячего» элемента истекает и множество запросов пытаются получить к нему доступ.
Способы предотвращения «дыр в кеше»:
Проблема проникновения в кеш
Проникновение в кеш происходит, когда данные, необходимые пользователю, отсутствуют как в кеше, так и в базе данных. В результате большое количество таких запросов перегружает базу данных.
Причины проникновения в кеш:
Методы предотвращения проникновения в кеш включают блокировку несанкционированных запросов, использование фильтров Блума и предварительную загрузку пустых объектов в кеш при отсутствии данных в базе.
Фильтр Блума представляет собой структуру данных, которая позволяет быстро определить, существует ли элемент в наборе данных. Он состоит из битовой карты и нескольких хеш-функций. Когда элемент добавляется в фильтр, каждая хеш-функция используется для вычисления его позиции в битовой карте. Затем эта позиция устанавливается в значение «1». При поиске элемента хеш-функции используются снова для определения его позиций в битовой карте, и если все они равны «1», считается, что элемент присутствует в фильтре.
Пример работы фильтра Блума: Предположим, у нас есть битовая карта длиной 8 и три хеш-функции. Мы хотим добавить элемент «A» в фильтр. Хеш-функции возвращают значения 3, 5 и 7 соответственно. Позиции 3, 5 и 7 устанавливаются в «1» на битовой карте. Теперь, если мы ищем элемент «B», хеш-функции могут вернуть значения 2, 4 и 6. Поскольку эти позиции не установлены в «1», можно сделать вывод, что элемента «B» нет в фильтре. Key的实例内存使用量远大于其他实例,что приводит к нехватке памяти и нагрузке на весь кластер.
Общие сценарии:
Влияние большого Key:
Как обнаружить большой Key:
Решения для оптимизации больших Key:
Предварительный нагрев кеша — это процесс загрузки соответствующих данных в кеш-систему после запуска системы. Это позволяет избежать запросов к базе данных при каждом запросе пользователя, предоставляя предварительно нагретые данные из кеша. Без предварительного нагрева Redis будет пустым при запуске, что может вызвать нагрузку на базу данных при высоком трафике.
Стратегия предварительного нагрева:
Понижение уровня кеша означает предоставление менее точных или неполных данных, когда система сталкивается с высокой нагрузкой, сбоями или недоступностью основных сервисов. Цель — сохранить доступность основных функций, даже если качество обслуживания снижается. Понижение уровня может быть автоматическим или настраиваемым вручную.
Разделение уровней понижения:
В базе данных есть 20 миллионов записей, но только 2 миллиона хранятся в Redis. Как гарантировать, что данные в Redis являются горячими?
Для этого нужно рассмотреть стратегии выселения Redis:
После версии 4.0 добавлены ещё две стратегии:
Выбор стратегии зависит от распределения данных:
Если после успешной отправки заказа система не может сразу же зачислить пользователю баллы, это может быть связано с проблемами обработки данных. Например, если после успешного выполнения операции «отправить сообщение» система не выполняет операцию «зачислить баллы», то данные будут несогласованными.
Проблемы с согласованностью данных могут возникать, когда обработка сообщения завершается неудачно. В этом случае часть данных сохраняется в базе данных, а другая часть — нет. Чтобы избежать таких проблем, рекомендуется использовать механизмы обеспечения согласованности данных, такие как транзакции или двухфазная фиксация.
Также существует проблема потери сообщений, которая может возникнуть из-за различных факторов, таких как сбои в работе сети, проблемы с хранением данных или ошибки в обработке сообщений. Для решения этой проблемы можно использовать механизмы повторной отправки сообщений или сохранения истории сообщений для последующей обработки.
Ещё одна проблема — нарушение порядка сообщений. Это может произойти, например, при использовании асинхронных методов обработки сообщений, когда порядок обработки сообщений не гарантируется. Для обеспечения порядка сообщений можно использовать механизмы упорядочивания сообщений или проверки их целостности.
Кроме того, существует проблема накопления сообщений, когда скорость обработки сообщений ниже скорости их поступления. Это может привести к задержкам в обработке запросов и снижению производительности системы. Для предотвращения накопления сообщений можно использовать методы балансировки нагрузки или оптимизации обработки сообщений.
Наконец, использование очередей сообщений может усложнить систему, поскольку необходимо учитывать дополнительные компоненты, такие как серверы очередей и механизмы обработки сообщений. Однако преимущества использования очередей сообщений обычно перевешивают эти сложности.
Для решения этих проблем можно использовать различные подходы, такие как:
Эти подходы позволяют обеспечить надёжную и эффективную обработку данных в системах, использующих очереди сообщений. Заказ №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% имя группы потребителей.
Проблема с обработкой просроченных заказов является сложной задачей. Существует два решения этой проблемы:
Обработка по расписанию. После того как пользователь размещает заказ, сначала создаётся информация о заказе, затем заказ добавляется в расписание (выполняется через 30 минут), и когда наступает указанное время, проверяется статус заказа. Если заказ не оплачен, он считается просроченным. Этот метод имеет очевидные недостатки при распределённом развёртывании серверов, поскольку требует координации с использованием распределённых блокировок и имеет низкую оперативность, оказывая давление на базу данных.
Отложенная обработка. Когда пользователь размещает заказ, идентификатор заказа отправляется во все отложенные очереди, обрабатывается через 30 минут и проверяется статус заказа перед обработкой. Есть несколько способов реализации отложенной обработки:
— Собственная очередь DelayedQuene Java. Это собственная очередь задержки Java, которую можно использовать, если бизнес-логика не слишком сложна. Она использует память JVM для реализации и теряет данные при остановке, а масштабируемость не очень хорошая.
— Использование Redis для отслеживания истечения срока действия ключа. После размещения заказа ключ, соответствующий заказу, устанавливается в Redis. Через 30 минут ключ становится недействительным, и программа отслеживает истечение срока действия ключей и обрабатывает заказы (я также пробовал этот метод). Основным недостатком этого метода является то, что он может отслеживать только один ключ Redis, который не подходит для кластеров. Некоторые люди отслеживают каждый узел кластера Redis, но я считаю, что это не лучший способ. Если бизнес-логика не сложная, Redis можно развернуть на одном сервере.
— Реализация очереди недоставленных сообщений MQ.
Обработка просроченных транзакций
В большинстве случаев 99 % вызовов распределённых интерфейсов не требуют распределённых транзакций. Мониторинг (уведомления по электронной почте или SMS), ведение журнала и быстрое определение проблем после возникновения проблем, а затем их устранение, поиск решений и исправление данных могут помочь. Поскольку распределённые транзакции всегда связаны с затратами, эти затраты могут быть довольно высокими, особенно для небольших компаний. Кроме того, введение распределённых транзакций значительно увеличивает сложность кода, время разработки и снижает производительность и пропускную способность системы, делая систему более уязвимой и склонной к ошибкам. Конечно, если есть ресурсы для постоянного инвестирования, распределённые транзакции могут гарантировать 100 % согласованность данных без ошибок.
Что такое распределённая транзакция?
Распределённая транзакция — это когда участники транзакции, серверы поддержки транзакций и серверы ресурсов, а также менеджеры транзакций расположены на разных узлах распределённой системы. Одна большая операция состоит из множества мелких операций, которые выполняются на различных сервисах. Эти мелкие операции либо выполняются успешно вместе, либо не выполняются вообще.
TCC и надёжная схема окончательной согласованности являются наиболее часто используемыми в производстве. TCC используется для обеспечения строгой согласованности, в основном для основных модулей, таких как транзакции/заказы. Окончательная согласованность обычно используется для периферийных модулей, таких как инвентарь, и уведомления отправляются через MQ для обеспечения окончательной согласованности.
Теория распределённых систем
Степень надёжности: строгая согласованность > последовательная согласованность > причинная согласованность > окончательная согласованность > слабая согласованность.
ACID подчёркивает согласованность и является принципом проектирования традиционных реляционных баз данных. ACID — это четыре характеристики, которым должна соответствовать транзакция базы данных (например, MySQL) для правильного выполнения:
Основная идея теории CAP заключается в том, что любая сетевая система обмена данными может обеспечить только две из трёх характеристик данных: согласованность, доступность и устойчивость к разделению.
Эти три характеристики могут быть удовлетворены только двумя одновременно. Большинство систем следуют этому принципу:
BASE фокусируется на доступности. BASE является расширением CAP и предполагает, что даже если строгая согласованность не может быть достигнута, система может достичь конечной согласованности. BASE представляет собой аббревиатуру трёх понятий:
Теоретически 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.
Распространение слухов эффективно в динамически изменяющихся распределённых системах.
Теория 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 приводят к разным эффектам согласованности:
Например, рассмотрим DATA-2 с N = 3, W = 2 и R = 2. При записи данных в DATA-2 потребуется обновить две копии. Затем при чтении из DATA-2 будет прочитано две копии, и будет возвращено последнее значение, даже если одна из копий не была обновлена.
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 — номер раунда.
В протоколе Paxos есть три типа узлов:
Процесс выборов состоит из двух этапов: подготовки и голосования.
На этапе подготовки:
На этапе голосования:
После нескольких раундов голосования достигается следующее состояние:
У протокола Paxos есть несколько ограничений:
Каждый раунд протокола 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:
Процесс выборов
Выборы лидера запускаются по истечении времени ожидания после последнего полученного от лидера сигнала «сердцебиения» (heartbeat). При запуске серверы становятся последователями. Лидер периодически отправляет сигналы «сердцебиение» всем последователям. Если последователь не получает сигнал «сердцебиение» от лидера в течение времени выборов, он ждёт случайный промежуток времени и затем инициирует выборы лидера. У каждого последователя есть часы, представляющие собой случайное значение, которое определяет, когда последователь ожидает стать лидером. Последователь увеличивает свой текущий период и становится кандидатом. Он сначала голосует за себя и отправляет другим серверам RequestVote RPC. Есть три возможных исхода:
Ограничения выборов
Все записи журнала в протоколе Raft добавляются только лидером и только на последователях. Лидер содержит все ранее зафиксированные записи журнала, и кандидат на роль лидера должен иметь все зафиксированные записи. Поскольку записи журнала передаются только от лидера к последователю, если выбранный лидер не имеет всех зафиксированных записей, эти записи будут потеряны, что недопустимо. Это ограничение выборов: кандидат на роль лидера содержит все зафиксированные записи журнала.
Копирование журнала
Цель копирования журнала — обеспечить согласованность данных. Процесс копирования журнала выглядит следующим образом:
После избрания лидер начинает получать запросы от клиентов. Лидер добавляет запрос как запись журнала (Log entries) в свой журнал, а затем параллельно отправляет RPC AppendEntries другим серверам для копирования этой записи. Когда эта запись копируется на большинство серверов, лидер применяет эту запись к своему состоянию и возвращает результат клиенту.
Каждая операция клиента включает команду, которая будет выполнена состоянием машины. Лидер добавляет эту команду как новую запись в журнал, параллельно отправляя RPC другим серверам, чтобы они скопировали эту информацию. Если запись успешно скопирована, лидер применяет её к своему состоянию машины и возвращает ответ клиенту. Если последователь выходит из строя, работает медленно или возникают проблемы с сетью, лидер будет продолжать попытки, пока все последователи не скопируют все записи журнала.
Журнал состоит из упорядоченных по индексу записей журнала. Каждая запись содержит номер периода (term), когда она была создана, и команду для выполнения состоянием машины. Запись считается зафиксированной, когда она копируется большинству серверов.
Согласованность журнала обеспечивается двумя гарантиями:
Если две записи в разных журналах имеют одинаковый индекс и номер периода, они содержат одну и ту же команду (поскольку лидер создаёт только одну запись для каждого индекса журнала в одном периоде). Если две записи в разных журналах имеют одинаковые индексы и номера периодов, все предыдущие записи также совпадают (при отправке дополнительных записей лидер отправляет индекс и номер периода предыдущей записи вместе с текущей записью. Если последователь обнаруживает несоответствие с собственным журналом, он отклоняет запись. Это называется проверкой согласованности).
Нештатные ситуации с журналами могут возникнуть, если старый лидер не полностью скопировал все записи в журнале. Новый лидер может столкнуться с несогласованностью журнала со старыми последователями: старые последователи могут потерять некоторые записи лидера, содержать записи, которых нет у лидера, или иметь записи, которые есть у обоих. Потерянные или дополнительные записи могут сохраняться в нескольких периодах.
Чтобы обеспечить согласованность журналов, лидер принудительно копирует свои записи на последователей. Несогласованные записи последователей заменяются записями лидера. Лидер ищет согласованное место в журнале последователей и заменяет последующие записи.
Лидер проверяет, соответствует ли последняя запись последователя его собственной записи. Если нет соответствия, лидер пытается найти предыдущую запись, пока не найдёт совпадение, а затем заменяет последующие записи последователя. Таким образом, достигается согласованность главного и подчинённого журналов.
Безопасность
Raft вводит два ограничения для обеспечения безопасности:
Последователь, имеющий самые последние зафиксированные записи журнала, может стать лидером. Лидер может продвигать только зафиксированные записи текущего периода. Записи старого периода фиксируются косвенно (записи с меньшим индексом фиксации журнала фиксируются косвенно).
Сжатие журнала
В реальных системах журналы не могут бесконечно расти, иначе при перезапуске системы потребуется много времени для воспроизведения, что повлияет на доступность. Raft решает эту проблему, выполняя моментальные снимки всей системы. Все журналы до моментального снимка можно удалить (старые данные уже сохранены на диск). Каждый экземпляр независимо выполняет моментальный снимок своего состояния и может выполнять моментальный снимок только зафиксированных журналов.
Моментальный снимок содержит следующие данные:
Метаданные журнала, включая последний зафиксированный индекс записи и номер периода. Эти значения используются при проверке целостности первой записи AppendEntries после моментального снимка. Текущее состояние системы. Перевод текста:
Отброшенные записи и отправка снимков
Записи, которые были отброшены, лидер будет отправлять последователям в виде снимка (snapshot). Также снимок будет отправляться новой машине при её добавлении. Для отправки снимка используется RPC InstalledSnapshot.
Не стоит делать снимки слишком часто, иначе это приведёт к избыточному использованию дискового пространства. Но и не стоит делать их слишком редко, так как в случае перезапуска узла потребуется воспроизвести большой объём журналов, что повлияет на доступность. Рекомендуется создавать снимок после того, как журнал достигнет определённого размера. Создание снимка может занять много времени и повлиять на нормальную синхронизацию журналов. Эту проблему можно решить с помощью технологии copy-on-write, которая позволит избежать влияния создания снимка на нормальную синхронизацию журнала.
Изменения в составе участников
Общие вопросы
ZAB (Zookeeper Atomic Broadcast) — это протокол, специально разработанный для распределённой координации сервиса Zookeeper, обеспечивающий восстановление после сбоев и атомную трансляцию. Протокол ZAB определяет, что ZAB является протоколом поддержки для распределённого координационного сервиса Zookeeper, предназначенным для обеспечения устойчивости к сбоям и атомной трансляции. На основе этого протокола Zookeeper реализует архитектуру главного и резервного режима для поддержания согласованности данных между резервными копиями в кластере.
С точки зрения проектирования, ZAB похож на Raft. Процесс репликации в ZAB аналогичен двухфазному подтверждению (2PC), и ZAB требует, чтобы более половины последователей успешно ответили, прежде чем выполнить фиксацию, что значительно уменьшает блокировку синхронизации и повышает доступность.
Процесс трансляции сообщений в ZAB использует протокол атомной трансляции, похожий на двухфазное подтверждение. Для клиентских запросов на запись все они принимаются лидером, который упаковывает запрос в транзакцию Proposal и отправляет его всем последователям. Затем, основываясь на ответах последователей, если более половины из них успешно отвечают, выполняется фиксация операции (сначала фиксируется сам лидер, затем отправляется фиксация всем последователям). Весь процесс трансляции состоит из трёх этапов:
Используя эти три шага, можно поддерживать согласованность данных между узлами кластера. Фактически, между лидером и последователями существует очередь сообщений, которая служит для развязки их взаимодействия и позволяет асинхронно обрабатывать запросы, избегая синхронной блокировки. Есть некоторые дополнительные детали:
Когда лидер выходит из строя, вступает в силу режим восстановления после сбоя (сбой означает потерю связи с более чем половиной последователей). ZAB определил два принципа:
Поэтому ZAB разработал алгоритм выбора лидера, способный гарантировать, что новый выбранный лидер обладает транзакциями с наибольшим ZXID среди всех серверов кластера, что позволяет новому выбранному лидеру иметь все зафиксированные транзакции. Преимущество такого подхода заключается в том, что новый выбранный лидер может пропустить шаг проверки фиксации и отбрасывания транзакций.
После восстановления после сбоя, перед началом официальной работы (приёмом клиентских запросов), лидер сначала проверяет, были ли все транзакции успешно зафиксированы последователями, чтобы убедиться в согласованности данных. Цель состоит в том, чтобы сохранить согласованность данных. После успешной синхронизации всех последователей лидер добавит эти серверы в список доступных серверов.
В дизайне идентификатора транзакции ZXID в протоколе ZAB ZXID представляет собой 64-битное целое число. Перевод текста на русский язык:
Цифры типа, где младшие 32 бита можно рассматривать как простой счётчик с приращением, для каждой транзакции клиента лидер генерирует новый Proposal транзакции и выполняет операцию +1 для этого счётчика. Старшие 32 бита представляют собой значение ZXID, полученное из локального журнала лидера, и извлекают соответствующее значение эпохи из этого ZXID, а затем добавляют к нему единицу.
Рисунок «ZAB данные синхронизации ZXD»
Старшие 32 бита определяют уникальность каждого лидера, а младшие 32 — уникальность транзакций в каждом лидере. Кроме того, это позволяет последователям идентифицировать различных лидеров. Это упрощает процесс восстановления данных. На основе этой стратегии, когда последователь присоединяется к лидеру, лидер сравнивает последний отправленный ZXID на своём сервере с ZXID последователя и принимает решение о том, следует ли откатить или синхронизироваться, в зависимости от результата сравнения.
Основная идея
Участники сообщают координатору об успешном или неудачном результате операции, после чего координатор решает, должны ли участники зафиксировать операцию или отменить её, основываясь на ответах всех участников.
Рисунок «Двухфазный протокол фиксации»
В MySQL транзакции завершаются с помощью системы журналов. Двухфазный протокол может использоваться для одномашинных централизованных систем, где менеджер транзакций координирует несколько менеджеров ресурсов, или для распределённых систем, где глобальный менеджер транзакций координирует локальных менеджеров транзакций для завершения двухфазной фиксации.
Двухфазная фиксация (Two-Phase Commit, 2PC) разделяет транзакцию на две части:
Псевдокод
Выполнение кода {
// Фаза 1
aStatus = участник A.prepare()
bStatus = участник B.prepare()
// Фаза 2
if aStatus and bStatus:
участник A.commit()
участник B.commit()
else:
участник A.rollback()
участник B.rollback()
}
Проблемы
Этот протокол имеет две роли: узел A является координатором транзакции, узлы B и C являются участниками транзакции. Основной поток действий следующий:
Когда узел A получает подтверждения от узлов B и C, он выполняет следующие действия:
Трёхфазная фиксация (Three-Phase Commit, 3PC), основная цель которой — уменьшить время блокировки, вызванное сетевыми сбоями и другими проблемами.
Псевдокод
Выполнение кода {
// Фаза 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, и участники отвечают YES или NO.
На основе ответов участников координатор принимает решение, продолжать ли PreCommit:
Если координатор получает положительные ответы от участников, транзакция фиксируется:
Если координатор не получил ACK от участников в течение таймаута или получил отрицательные ответы, координатор отменяет транзакцию:
Основная идея заключается в регистрации операций подтверждения и компенсации для каждой операции.
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 зарезервированные бизнес-ресурсы освобождаются. Операция cancel также должна быть разработана с учетом идемпотентности, а в случае сбоя необходима повторная попытка.
Мы рассмотрим пример с использованием заказа и склада. Предположим, что наша распределённая система состоит из четырёх сервисов: сервис заказа, сервис склада, сервис баллов и сервис хранилища. Каждый сервис имеет свою собственную базу данных.
В нормальном процессе TCC всё ещё представляет собой двухэтапный протокол отправки. Однако при возникновении проблем он обладает определённой способностью к самовосстановлению. Если в любой транзакции участвует проблема, координатор может отменить предыдущую операцию, чтобы достичь окончательного согласованного состояния (например, отменить транзакцию или запросить транзакцию). Из процесса выполнения TCC также видно, что поставщик услуг должен предоставить дополнительную логику компенсации. Таким образом, один сервисный интерфейс после введения TCC может потребовать модификации в три типа логики:
Обратите внимание: при разработке транзакций TCC интерфейсы операций cancel и confirm должны соответствовать идемпотентной конструкции.
Этап try обычно используется для блокировки определённого ресурса, установки промежуточного состояния или замораживания части данных. Для каждого сервиса в примере этап try выполняет следующие действия:
В зависимости от результата этапа try, этап confirm делится на два случая:
Обратите внимание: на этапе confirm каждого сервиса могут возникнуть проблемы, и в этом случае обычно требуется TCC-фреймворк (например, ByteTCC, tcc-transaction, himly). TCC обычно записывает некоторые журналы активности распределённых транзакций, сохраняет информацию о каждом этапе и состоянии транзакции, обеспечивая окончательное согласование всей распределённой транзакции.
Если этап try завершается неудачно, будет выполнен этап cancel. Например, для сервиса заказа можно реализовать логику отмены следующим образом: установить состояние заказа как «отменено». Для сервиса склада логика отмены заключается в том, чтобы освободить поле для замораживания запасов и вернуть запасы в состояние, доступное для продажи.
Обратите внимание: многие компании, стремясь упростить использование TCC, обычно разделяют один сервис на несколько интерфейсов. Например, сервис склада разделяет интерфейс уменьшения запасов на два под-интерфейса: интерфейс уменьшения и интерфейс восстановления уменьшения запасов, позволяя TCC гарантировать выполнение соответствующего интерфейса восстановления при сбое интерфейса уменьшения.
Схема надёжных сообщений, также известная как схема окончательной согласованности, обычно применяется в асинхронных сценариях вызова сервисов и является основным методом реализации распределённых транзакций.
Принцип реализации:
Псевдокод:
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':
//ручное управление, потому что обычно все могут быть успешными
}
}
Преимущества и недостатки:
Локальная таблица сообщений основана на идее разделения распределённой транзакции на локальные транзакции. Эта идея исходит от eBay. Мы смотрим на следующую диаграмму, которая делит распределённую транзакцию на три части на основе локальной таблицы сообщений:
① Надёжная служба сообщений
Служба надёжных сообщений — это отдельный сервис со своей собственной базой данных, основная функция которого заключается в хранении сообщений (включая информацию об интерфейсе вызова и уникальный номер глобального сообщения), обычно включая следующие состояния:
Примечание: для предотвращения ситуации, когда локальная транзакция производителя успешно выполнена, но отправка подтверждающего/отменяющего сообщения задерживается. Надёжный сервис сообщений обычно предоставляет фоновое задание, которое непрерывно проверяет сообщения с состоянием «ожидает подтверждения» в таблице сообщений, а затем вызывает интерфейс производителя, чтобы подтвердить, следует ли отменить это сообщение или подтвердить и отправить его.
③ Потребитель
Поставщик услуг (потребитель сообщений) получает сообщения из MQ и выполняет локальную транзакцию. После успешного выполнения он уведомляет надёжный сервис сообщений о том, что обработка завершена, и сервис устанавливает состояние сообщения в таблице как «завершено». Здесь необходимо учитывать два случая:
Заключение
Преимущество этого решения заключается в простоте, но основным недостатком является сильная зависимость надёжного сервиса сообщений от базы данных, который управляет транзакциями через таблицы сообщений. Это не подходит для сценариев с высокой степенью параллелизма.
Многие открытые сервисы сообщений поддерживают распределённые транзакции, такие как RocketMQ и Kafka. Их концепция почти такая же, как у локальных таблиц сообщений/сервисов, за исключением того, что они объединяют функции надёжных сервисов сообщений и MQ для удобства использования. Этот подход иногда называют решением для обеспечения строгой согласованности надёжных сообщений. Рассмотрим RocketMQ в качестве примера, где отправка сообщений разделена на две фазы: подготовка и подтверждение.
① Подготовка
Обратите внимание: потребитель не может немедленно обработать HalfMsg, производитель может выполнить Commit или Rollback для завершения транзакции. Только после выполнения Commit для HalfMsg потребитель сможет обработать это сообщение.
② Подтверждение
Сервис сообщений периодически запрашивает производителя о возможности выполнения Commit или Rollback для тех HalfMsg, которые не были завершены из-за ошибки, чтобы завершить их жизненный цикл и достичь окончательной согласованности транзакции. Необходимость в этом механизме запроса обусловлена тем, что производитель может завершить локальную транзакцию и не успеть выполнить Commit или Rollback перед сбоем, оставляя HalfMsg в несогласованном состоянии.
③ Механизм ACK
После обработки сообщения потребителем, если по какой-либо причине возникает ошибка, которая приводит к неудачному выполнению бизнес-логики, необходимо обеспечить возможность повторной обработки сообщения. RocketMQ предоставляет механизм ACK (Acknowledgement), согласно которому RocketMQ считает обработку успешной только после получения подтверждения от потребителя. Таким образом, потребитель может отправить сообщение ACK в RocketMQ после успешного выполнения бизнес-логики для подтверждения успешной обработки.
Рассмотрим пример использования надёжной системы обмена сообщениями для обеспечения согласованности в критически важных транзакциях электронной коммерции.
Цепочка транзакций
Представим, что пользователь инициирует платёж при оформлении заказа. Сначала вызывается внешний интерфейс заказа, после чего начинается основная цепочка транзакций, проходящая через сервисы заказа, инвентаризации и накопления баллов. После успешной обработки всеми сервисами асинхронно вызывается хранилище через MQ.
На схеме выше показано, что сервисы заказа, инвентаря и накопления баллов являются синхронными вызовами, поскольку они представляют собой основную цепочку транзакций. Мы можем использовать TCC (Two-Phase Commit) для обеспечения распределённой согласованности. Вызов хранилища через MQ является асинхронным, поэтому мы полагаемся на RocketMQ для реализации распределённых транзакций.
Выполнение транзакции
Теперь рассмотрим, как внедрение 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-транзакциях.
Давайте рассмотрим, как добавить TCC в наш процесс оформления заказа и уменьшения количества товара на складе.
На этапе try система резервирует n единиц товара для этого заказа и создаёт два зарезервированных ресурса. На этапе confirm система использует зарезервированные ресурсы этапа try. В механизме транзакций TCC считается, что если ресурсы были успешно зарезервированы на этапе try, они будут полностью переданы на этапе confirm.
Если на этапе try одна из задач завершается неудачно, будет выполнена операция cancel, и зарезервированные на этапе try ресурсы будут освобождены.
Традиционный режим
Разделение на базы данных и таблицы
Seata — это промежуточное программное обеспечение, которое предлагает решение для распределённых транзакций. Существует несколько распространённых подходов к реализации распределённых транзакций, таких как двухфазная фиксация (2PC), основанная на протоколе XA, трёхфазная фиксация (3PC) и программирование на основе бизнес-слоя, такое как TCC. Также существуют решения, основанные на использовании очередей сообщений и таблиц для достижения окончательной согласованности.
Двухфазная фиксация основана на протоколе XA. Протокол XA состоит из двух частей: менеджера транзакций и локального менеджера ресурсов. Локальный менеджер ресурсов обычно реализуется базой данных, такой как Oracle или MySQL, которые поддерживают интерфейс XA. Менеджер транзакций выступает в роли глобального диспетчера.
2PC обеспечивает минимальную инвазивность для бизнеса. Его основным преимуществом является прозрачность использования, поскольку пользователи могут использовать распределённые транзакции на основе XA так же, как и локальные транзакции. Это позволяет обеспечить строгие гарантии свойств ACID для транзакций.
Однако 2PC представляет собой жёсткую синхронную блокирующую схему, где все необходимые ресурсы должны быть заблокированы во время выполнения транзакции. Поэтому он лучше подходит для коротких транзакций с определённым временем выполнения, но имеет низкую общую производительность.
В случае сбоя координатора транзакций или возникновения сетевых колебаний, участники могут оставаться заблокированными или только часть участников может успешно выполнить фиксацию, что приводит к несогласованности данных. Поэтому 2PC не является оптимальным выбором для сценариев с высокой степенью параллелизма.
Трёхфазная фиксация является улучшенной версией 2PC, решая проблему блокировки в 2PC. В 2PC только координатор транзакций имеет механизм тайм-аута. В 3PC оба координатора и участники имеют механизмы тайм-аута, предотвращая блокировку участников после сбоя координатора. Кроме того, между первой и второй фазами добавляется фаза подготовки, обеспечивая согласованное состояние всех участников перед окончательной фиксацией.
Хотя 3PC использует механизмы тайм-аутов для решения проблемы блокировки, это также увеличивает количество сетевых коммуникаций и снижает производительность, что не рекомендуется.
Программирование на основе TCC также является разновидностью двухфазной фиксации. TCC отличается тем, что он реализуется на уровне бизнес-логики и включает три этапа: try, confirm и cancel для каждой бизнес-операции.
Например, при оформлении заказа и уменьшении количества товара этап try резервирует товар, этап confirm фактически уменьшает количество товара, а этап cancel отменяет уменьшение количества товара и освобождает зарезервированный товар в случае неудачи.
TCC не имеет проблемы блокировки ресурсов, так как каждая операция сразу же фиксирует транзакцию. В случае ошибки выполняется операция cancel для отката и восстановления состояния. Это также называется компенсационной транзакцией.
Несмотря на отсутствие проблем с блокировкой, TCC требует трёх методов для поддержки одной операции, что значительно увеличивает сложность разработки и может привести к увеличению объёма кода. Необходимо учитывать возможные сетевые колебания и обеспечивать надёжную доставку запросов, поэтому требуется реализация механизма повторной попытки с учётом эквивалентности интерфейса.
Распределённые транзакции на базе очередей сообщений (окончательная согласованность) представляют собой двухэтапную фиксацию на основе очередей сообщений. Они объединяют локальную транзакцию и отправку сообщения в одну транзакцию, гарантируя успешное выполнение локальной операции и отправки сообщения.
Процесс оформления заказа и уменьшения количества товара выглядит следующим образом:
Подход с использованием очередей сообщений для обеспечения двухэтапной фиксации обычно используется в сценариях с высокой степенью параллелизма, жертвуя строгой согласованностью данных для значительного повышения производительности. Однако реализация такого подхода требует значительных усилий и ресурсов.
Seata также эволюционировала из двухфазной модели фиксации и предоставляет различные режимы транзакций, такие как AT, TCC, SAGA и XA. Здесь мы сосредоточимся на режиме AT. Поскольку Seata основана на двухфазной фиксации, давайте рассмотрим, что происходит на каждом этапе. Рассмотрим пример оформления заказа и уменьшения количества товара или денег на счёте.
Сначала рассмотрим роли, используемые в распределённых транзакциях Seata:
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, storage и account должны соответствовать ожидаемым результатам.
Если один из сервисов выдаст ошибку, данные в таблицах должны быть восстановлены до исходного состояния. Обнаружено, что все транзакции не были успешно выполнены, что указывает на то, что глобальная транзакция также была успешно откачена.
Рисунок 1. Таблица данных после отката глобальной транзакции.
ID | Имя таблицы |
---|---|
1 | Данные таблицы до отката |
2 | Данные таблицы после отката |
Примечание: таблица содержит данные, которые демонстрируют изменения в таблице после выполнения отката.
Давайте посмотрим на журнал отката undo_log
, поскольку Seata удаляет журнал отката очень быстро, нам нужно остановить процесс на одном из серверов, чтобы увидеть изменения в журнале отката.
Рисунок 2. Журнал отката.
Время | ID транзакции | Описание |
---|---|---|
время | ID транзакции | Описание |
Примечание: в таблице содержится информация о выполненных транзакциях и их результатах.
В целом, мы рассмотрели пять решений для распределённых транзакций: 2PC, 3PC, TCC, MQ и Seata. Мы подробно изучили решение с использованием промежуточного программного обеспечения Seata. Однако независимо от того, какое решение мы выберем, при реализации в проекте необходимо быть осторожным и тщательно продумать его применение. Поскольку, за исключением определённых сценариев с сильной согласованностью данных, лучше избегать использования распределённых транзакций, если это возможно, поскольку они могут значительно снизить общую эффективность проекта, особенно в условиях высокой параллельности.
С ростом бизнеса компании объём данных в базе данных увеличивается, а производительность доступа к данным снижается. Это связано с тем, что реляционные базы данных легко становятся узким местом системы, а ёмкость одного компьютера, количество подключений и вычислительная мощность ограничены. Когда объём данных одной таблицы достигает 1000 Вт или 100 ГБ, запросы становятся более сложными, даже если добавить вторичные таблицы и оптимизировать индексы, производительность всё равно может значительно снизиться. Для решения этой проблемы обычно используются два подхода:
Повышение вычислительной мощности сервера, например, увеличение ёмкости хранилища, процессора и т. д. Этот подход требует значительных затрат, и если узкое место находится в самой MySQL, то повышение аппаратного обеспечения также не всегда эффективно.
Распределение данных по различным базам данных для уменьшения объёма данных в одной базе данных и облегчения нагрузки на неё. Это помогает улучшить производительность базы данных.
Решения:
Вертикальное разделение таблиц: можно разделить широкую таблицу на несколько более узких таблиц, основываясь на частоте доступа и размере полей. Это может улучшить как производительность, так и ясность бизнес-логики, но следует избегать одновременного доступа к нескольким таблицам, иначе производительность может ухудшиться.
Вертикальное разделение баз данных: можно распределить различные таблицы по разным базам данных в зависимости от степени связи между ними. Эти базы данных могут быть размещены на разных серверах, что позволяет распределить нагрузку и повысить производительность. Также это улучшает ясность архитектуры и позволяет настраивать оптимизацию для каждой базы данных в соответствии с её потребностями. Однако это требует решения сложных проблем, связанных с взаимодействием между базами данных.
Горизонтальное разделение баз данных: можно разделить данные одной таблицы (по строкам) между несколькими базами данных, каждая из которых содержит только часть данных таблицы. Это позволяет распределить нагрузку между серверами и значительно повысить производительность. Однако этот подход также требует решения проблем с маршрутизацией данных и других сложных вопросов.
Горизонтальное разделение таблиц: можно разделить данные одной таблицы (построчно) между несколькими таблицами в одной базе данных. Это может немного улучшить производительность, но является дополнением к горизонтальному разделению баз данных.
Обычно вертикальное разделение баз данных следует выбирать на этапе проектирования системы, исходя из степени связанности данных. Если объём данных не слишком велик, сначала следует рассмотреть такие методы, как кэширование, разделение чтения и записи и оптимизация индексов. Если же объём данных очень большой и продолжает расти, можно рассмотреть горизонтальное разделение баз данных и таблиц.
Цели разделения:
Вертикальное разделение: разделение бизнес-данных.
Горизонтальное разделение: решение проблем с объёмом данных и производительностью.
Проблемы перед разделением:
Большой объём запросов: из-за ограниченных возможностей одного сервера по обработке транзакций в секунду (TPS), памяти и ввода-вывода. Решение: распределить запросы между несколькими серверами. Фактически, пользовательские запросы и выполнение SQL-запросов — это одно и то же, разница лишь в том, что пользовательские запросы проходят через шлюз, маршрутизатор и HTTP-сервер.
Слишком большая одна база данных: обработка данных ограничена одним сервером, дисковое пространство на сервере недостаточно, ввод-вывод становится узким местом. Решение: разделить базу данных на более мелкие части.
Одна таблица слишком большая: возникают проблемы с операциями CRUD, индексами и временем ожидания запросов. Решение: разделить таблицу на несколько меньших таблиц.
Вертикальное разделение
Пример вертикального разделения таблиц: разделение одной таблицы на несколько таблиц, каждая из которых хранит только часть полей исходной таблицы. Например, в интернет-магазине при просмотре товаров пользователи обычно интересуются только названием товара, изображением и ценой. Эти поля имеют высокую частоту доступа. При необходимости просмотра подробного описания товара пользователи делают это редко. Поэтому можно разделить исходную таблицу товаров на две таблицы: одну для основных данных о товарах, а другую для подробных описаний.
Преимущества оптимизации:
Принципы разделения:
Пример вертикального разделения баз данных: распределение таблиц по различным базам данных на основе бизнес-связей. После вертикального разделения таблиц производительность улучшается, но проблема с дисковым пространством остаётся. Можно разделить таблицы на две базы данных: одна для пользовательских данных, а другая для данных товаров.
Преимущества оптимизации:
Горизонтальное разделение
Пример горизонтального разделения баз данных: разделение данных одной таблицы между различными базами данных, размещёнными на разных серверах. После вертикального разделения баз данных проблемы с производительностью решаются, но объём данных продолжает увеличиваться, и один сервер уже не справляется. Необходимо перейти к горизонтальному разделению.
Например, в системе интернет-магазина у нас есть две базы данных: для пользователей и для товаров. Эти две базы данных независимы друг от друга, поэтому мы можем разделить их на два отдельных экземпляра.
Преимущества оптимизации:
Пример горизонтального разделения таблиц: разделение данных одной таблицы внутри одной базы данных между несколькими таблицами. После горизонтального разделения баз данных проблема с большими данными решается, но может возникнуть проблема с одной таблицей, содержащей слишком много данных. В этом случае можно применить горизонтальное разделение таблиц.
Правила разделения:
После применения хеш-функции по модулю данные, распределённые по базам данных и таблицам, становятся равномерно распределёнными. Это предотвращает проблемы с неравномерным распределением ресурсов. Однако, если объём данных резко возрастает, потребуется миграция данных, изменение маршрутизации и другие операции.
Это простой метод, который заключается в разделении данных на диапазоны и размещении каждого диапазона в отдельной таблице или базе данных. Преимущество этого метода заключается в отсутствии необходимости миграции данных, что упрощает управление. Недостатком является неравномерное распределение данных, которое может привести к перегрузке некоторых баз данных или таблиц.
Этот метод использует кольцевой алгоритм для равномерного распределения данных по узлам. Он обеспечивает более равномерное распределение и упрощает миграцию данных при расширении системы. Однако реализация алгоритма маршрутизации может быть сложной задачей.
Данные могут быть разделены на основе географических регионов, таких как Восточная Азия, Южная Азия и Северная Америка. Это часто используется в облачных сервисах.
Данные, которые не использовались в течение определённого периода времени, могут быть перемещены в отдельную таблицу или базу данных. Это называется «холодные» и «горячие» данные. %SHARDINGSPHERE_PROXY_HOME%/lib каталог.
Запуск сервиса:
Используя дефолтные настройки:
sh %SHARDINGSPHERE_PROXY_HOME%/bin/start.sh
По умолчанию порт запуска — 3307, а каталог конфигурационного файла — %SHARDINGSPHERE_PROXY_HOME%/conf/
.
Настройка порта и каталога конфигурационных файлов:
sh %SHARDINGSPHERE_PROXY_HOME%/bin/start.sh ${proxy_port} ${proxy_conf_directory}
Использование ShardingSphere-Proxy
Выполнять команды MySQL или PostgreSQL для клиентов напрямую с ShardingSphere-Proxy. На примере MySQL:
mysql -u${proxy_username} -p${proxy_password} -h${proxy_host} -P${proxy_port}
Определяется как Kubernetes-облачный нативный прокси для баз данных в форме Sidecar. Он предоставляет слой взаимодействия без централизации и без вторжения, известный как Database Mesh, или сетка баз данных.
Сетка баз данных фокусируется на том, как связать распределённые данные с базами данных, и больше внимания уделяет взаимодействию, эффективно организуя взаимодействие между приложениями и базами данных. Используя Database Mesh, приложения и базы данных образуют большую сетку, где они оба управляются слоем взаимодействия.
Редактируйте %SHARDINGSPHERE_SCALING_HOME%/conf/server.yaml
. Подробности см. в руководстве пользователя.
Если вы подключаетесь к базе данных PostgreSQL, дополнительные зависимости не требуются. Если вы подключаетесь к MySQL, загрузите mysql-connector-java-5.1.47.jar
и поместите его в %SHARDINGSPHERE_SCALING_HOME%/lib
каталог.
sh %SHARDINGSPHERE_SCALING_HOME%/bin/start.sh
Управляйте миграцией задач через соответствующие HTTP-интерфейсы. Подробности см. в руководстве пользователя.
ShardingSphere-JDBC использует децентрализованную архитектуру, подходящую для высокопроизводительных облегчённых OLTP-приложений на Java. ShardingSphere-Proxy обеспечивает статический вход и поддержку гетерогенных языков, подходит для OLAP-приложений и управления и обслуживания сегментированных баз данных.
Apache ShardingSphere представляет собой экосистему с несколькими точками входа. Комбинируя ShardingSphere-JDBC и ShardingSphere-Proxy, используя единый центр регистрации для настройки стратегии сегментирования, можно гибко создавать системы, подходящие для различных сценариев, позволяя архитекторам более свободно настраивать оптимальную архитектуру для текущих бизнес-потребностей.
Функции:
Сегментирование данных:
Распределённые транзакции:
Управление базами данных:
Dada предлагает следующую схему нагрузочного тестирования: создание точной копии производственной среды и проведение тестирования в ней. Эта схема имеет низкую техническую сложность и проста в реализации, но она также имеет очевидные недостатки: затраты на человеческие ресурсы и оборудование растут по мере увеличения масштаба производственной среды. Поэтому мы изучили основные методы нагрузочного тестирования, используемые в отрасли, которые называются «маркировка трафика». Этот метод заключается в маркировке трафика запросов (HTTP, RPC, MQ) и использовании этих меток для отслеживания потока трафика между сервисами. Это позволяет отделить тестовый трафик от производственного трафика и обеспечить изоляцию данных на уровне баз данных, кэша и очередей сообщений.
На уровне базы данных используются теневые базы данных или теневые таблицы для изоляции данных. На уровне кэша используется теневой кэш для изоляции данных. На уровне очередей сообщений используются теневые очереди для изоляции данных.
Однако этот метод подходит только для сред с унифицированными промежуточными программами. Dada использует различные типы промежуточных программ, такие как ORM (Mybatis, Hibernate, JPA), и существует множество разнородных программ, включая Java и Python. Реализация метода «маркировки трафика» потребует значительных изменений в бизнес-логике, поэтому мы отказались от этого подхода.
После анализа собственной архитектуры мы разработали решение для нагрузочного тестирования на основе метода «маркировки устройств» в первом квартале 2019 года. Решение основано на абстрагировании всех узлов баз данных, кешей и очередей сообщений в отдельные узлы и регистрации этой информации в центре регистрации. Все сервисы подключаются к «SDK управления трафиком», который может маршрутизировать запросы в соответствии с маршрутом трафика.
Процесс реализации этого решения включает в себя следующие шаги:
Наиболее важным компонентом этого решения является «SDK управления трафиком», задача которого состоит в том, чтобы направлять трафик в соответствии с типом маршрута. Как показано на рисунке ниже, он запускается следующим образом:
В результате в производственной среде формируется два маршрута: маршрут для обработки производственного трафика и маршрут для обработки тестового трафика.
Сравнение методов «маркировки трафика» и «маркировки устройств»: оба метода имеют свои преимущества и недостатки. Dada выбирает метод «маркировки устройств», исходя из соображений безопасности и затрат на изменения в системе.
Окончательный выбор: после сравнения двух методов было решено использовать метод «маркировки устройств».
Первоначально нагрузочное тестирование проводилось с использованием JMeter, который является классическим инструментом нагрузочного тестирования. JMeter обладает высокой стабильностью и поддерживает распределённое тестирование. Однако при тестировании сложных сценариев использование JMeter недостаточно гибкое. Поэтому Dada отказалась от первоначального решения и разработала собственную платформу нагрузочного тестирования на базе JMeter.
Платформа нагрузочного тестирования состоит из следующих основных компонентов:
Новая платформа нагрузочного тестирования имеет следующие преимущества: визуализация интерфейса операций, динамическое отображение результатов тестирования в реальном времени. Разработчики могут сразу же выполнить нагрузочные тесты, настроив параметры производительности и сценарии генерации данных.
Механизм нагрузочного теста выполняет нагрузочные тесты во время тестирования, отображая результаты TPS, время отклика и коэффициент ошибок в интерфейсе.
Результаты нагрузочного тестирования отображаются в виде графиков и таблиц. ДадА: подход к нагрузочному тестированию
Перед, во время и после нагрузочного тестирования
Перед нагрузочным тестированием:
Анализ связей: анализ связей очень важен для определения того, какие сервисы должны быть развёрнуты при нагрузочном тестировании. В прошлом ДадА анализировала связи вручную, но этот метод был неэффективным, неточным, трудоёмким, и не позволял оперативно реагировать на изменения в производственной среде. Позже был внедрён APM (PinPoint), который имеет функцию анализа связей.
Однако эта проблема всё ещё не решена: «оперативное восприятие изменений связей». Для этого мы разработали тестовую среду, которая периодически отправляет запросы для проверки наличия изменений в связях. Если есть изменения, мы можем немедленно узнать об этом.*
Подготовка плана оптимизации:
Из-за развития бизнеса в последние годы объём данных в одной таблице одной базы данных в системе логистики ДадА постоянно увеличивается. Система неоднократно сталкивалась с задержкой мастер-репликаций MySQL. Наиболее часто используемым методом оптимизации является оптимизация MySQL Binlog, которая в основном оптимизирует два параметра:
Однако у этой оптимизации есть и недостатки: после оптимизации время отклика интерфейса улучшается, поэтому необходимо учитывать, может ли бизнес выдержать увеличение времени отклика.
Во время нагрузочного тестирования:
Моделирование нагрузки: с развитием бизнеса модели нагрузочного тестирования также постоянно совершенствуются. От первоначального использования виртуальных рыцарей для моделирования нагрузки на основе целевых значений TPS до текущего моделирования активных рыцарей с учётом временных и пространственных факторов для создания более точных моделей. Модели делятся на две категории:
Данные модели:
Потоковые модели:
Поскольку ДадА имеет сложную бизнес-логику, в которой преобладают операции записи, прямое использование метода воспроизведения потока не подходит. Поэтому ДадА выбрала метод ручного создания потока. Основными факторами, влияющими на точность воспроизведения потока, являются время и пространство.
Время:
С точки зрения времени, в течение дня есть три пика: утренний пик, обеденный пик и вечерний пик. Каждый пиковый период имеет различное состояние для каждого интерфейса:
Поэтому при нагрузочном тестировании ДадА разрабатывает два сценария тестирования на основе характеристик трёх пиковых периодов интерфейса, чтобы провести тестирование.
Пространство:
Заказы и рыцари распределены неравномерно по всей стране, некоторые регионы имеют высокую плотность населения и небольшое количество заказов, в то время как другие регионы имеют низкую плотность населения и большое количество заказов. Однако наибольшее влияние на систему оказывают регионы с высокой плотностью населения и большим количеством заказов.
У ДадА есть интерфейс для просмотра заказов в пределах X километров, производительность которого тесно связана с количеством горячих точек и количеством заказов в каждой горячей точке. Чтобы понять эти аспекты, мы проанализировали поток во время больших акций, разделили всю страну на квадраты на основе geohash и подсчитали количество заказов и плотность рыцарей в каждом квадрате. Наконец, мы восстановили и увеличили масштаб в тестовой среде.
Затем наступает этап нагрузочного тестирования, основными элементами которого являются проверка интерфейса, предварительное нагревание, проведение тестирования, наблюдение за показателями производительности и запись проблем с производительностью. Предварительное нагревание особенно важно, поскольку оно определяет точность результатов тестирования.
После нагрузочного тестирования:
После нагрузочного теста генерируется отчёт о тестировании, определяются и оптимизируются проблемы с производительностью, оценивается ёмкость системы и проводится анализ тестирования. Ниже приводится анализ тестирования.
При каждом анализе после крупной акции всегда можно найти направление для оптимизации следующего нагрузочного тестирования; наиболее важным аспектом анализа является сравнение показателей производительности производственной среды и среды тестирования, а также основных показателей промежуточного программного обеспечения. Эти данные сравниваются для подтверждения и оптимизации модели тестирования.
Итоги и выгоды
Проект нагрузочного тестирования начался в первом квартале 2019 года и прошёл четыре испытания во время крупных акций. В процессе реализации нагрузочного тестирования мы считаем, что есть три ключевых момента:
Общая выгода проекта также очевидна, она проявляется в следующих двух аспектах:
Конечно, у этого проекта есть области для улучшения, такие как стоимость. В настоящее время из соображений безопасности мы используем теневые склады вместо теневых таблиц, что увеличивает стоимость, но обеспечивает безопасность данных. Как найти баланс между безопасностью и стоимостью — это вопрос оптимизации нагрузочного тестирования в будущем. Кроме того, помимо выявления проблем с производительностью системы, может ли нагрузочное тестирование дать больше рекомендаций по интеллектуальному планированию рабочей силы и разумному распределению рабочей силы? Это также то, над чем мы размышляем.
Функция | 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) |
Включает регистрацию служб, подписку на службы, уведомления об изменениях служб и отправку уведомлений о состоянии служб. Сервер будет регистрировать службы при инициализации системы через модуль регистрации, а клиент будет подписываться на список конкретных серверов, предоставляющих услуги, при инициализации системы также через модуль регистрации. Когда список серверов изменится, модуль регистрации уведомит клиента.
Автоматическое переключение при возникновении сбоя, повторная попытка использования других серверов.
Быстрое обнаружение сбоёв, отправка запроса только один раз, немедленное сообщение об ошибке при сбое. Обычно используется для операций, которые не являются идемпотентными, таких как добавление записей.
Безопасный сбой, игнорирование при возникновении ошибки. Обычно используется для таких операций, как запись в журнал аудита.
Автоматическое восстановление после сбоя, запись неудачных запросов в фоновом режиме и их повторная отправка по расписанию. Обычно используется для уведомлений о сообщениях.
Параллельный вызов нескольких серверов, возврат результата после успешного выполнения хотя бы одним сервером. Обычно используется для высокопроизводительных операций чтения, но требует больше ресурсов сервера.
Широковещательный вызов всем провайдерам, последовательный вызов каждого провайдера, сообщение об ошибке, если хотя бы один провайдер сообщает об ошибке. Обычно используется для уведомления всех провайдеров об обновлении локальных ресурсов, таких как кэш или журналы.
Используется на стороне провайдера для записи предупреждающего журнала для тайм-аута вызова службы.
На стороне потребителя и провайдера отправляет информацию о времени выполнения, количестве одновременных запросов и т.д. в службу мониторинга.
Адаптер совместимости, который может выполнять некоторые преобразования типов возвращаемых значений, такие как преобразование базовых типов в упакованные типы, преобразование сложных типов в сериализованные значения (в зависимости от настроенного типа сериализации) и т. д.
На стороне провайдера выборочно упаковывает исключения. Исключения, не прошедшие проверку, напрямую выбрасываются, исключения JDK напрямую выбрасываются, исключения классов и интерфейсов в одном jar-пакете напрямую выбрасываются, а исключения, объявленные методом интерфейса службы, должны быть выброшены после упаковки в RpcResult.
В таких областях, как электронная коммерция и платёж, часто возникает ситуация, когда пользователь размещает заказ, но затем отказывается от оплаты. В этом случае заказ должен быть закрыт по истечении определённого периода времени. Внимательные читатели, вероятно, заметили, что эта логика присутствует в таких платформах, как Taobao и JD.com, причём время закрытия очень точное, с погрешностью в пределах 1 секунды. Как они это реализуют?
Существует несколько общих подходов:
Вы можете оставить комментарий после Вход в систему
Неприемлемый контент может быть отображен здесь и не будет показан на странице. Вы можете проверить и изменить его с помощью соответствующей функции редактирования.
Если вы подтверждаете, что содержание не содержит непристойной лексики/перенаправления на рекламу/насилия/вульгарной порнографии/нарушений/пиратства/ложного/незначительного или незаконного контента, связанного с национальными законами и предписаниями, вы можете нажать «Отправить» для подачи апелляции, и мы обработаем ее как можно скорее.
Опубликовать ( 0 )