Эй, товарищи!

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

Высокоуровневая архитектура геомессенджера представлена ​​ниже:

Описание изображения

Экземпляры микросервисов прикладного уровня разбросаны по всему миру в выбранных облачных регионах. Уровень API на базе Kong Gateway позволяет микросервисам взаимодействовать друг с другом через простые конечные точки REST.

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

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

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

Так что, если вы еще со мной в этом путешествии, то, как говаривали пираты, «Снять якорь и поднять бизань!» что означает: «Поднимите якорь и отправьте корабль в плавание!»

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

ЮгабайтДБ, мой распределенная база данных SQL на выбор, поддерживает четыре варианты развертывания базы данных в нескольких регионах которые используются геораспределенными приложениями:

  1. Одиночный растянутый кластер: Кластер базы данных «растянут» на несколько регионов. Эти регионы обычно расположены относительно близко друг к другу (например, Средний Запад и Восточный регион США).

  2. Одиночный растянутый кластер с репликами чтения: Как и в предыдущем варианте, кластер развертывается в одном географическом местоположении (например, в Северной Америке) в облачных регионах в относительно непосредственной близости (например, в регионах Среднего Запада и Востока США). Но с помощью этого варианта вы можете добавить узлы реплик чтения в удаленные географические местоположения (например, в Европу и Азию), чтобы повысить производительность рабочих нагрузок чтения.

  3. Единый кластер с географическим разделением: Кластер базы данных распределен по нескольким удаленным географическим точкам (например, в Северной Америке, Европе и Азии). В каждом географическом местоположении есть собственная группа узлов, развернутых в одном или нескольких регионах в непосредственной близости (например, в регионах Среднего Запада и Востока США). Данные автоматически прикрепляются к определенной группе узлов на основе значения столбца географического разделения. Этот вариант развертывания обеспечивает низкую задержку для операций чтения и записи по всему миру.

  4. Несколько кластеров с асинхронной репликацией: несколько автономных кластеров развернуты в разных регионах. Регионы могут располагаться относительно близко друг к другу (например, регионы Среднего Запада и Востока США) или в удаленных местах (например, регионы Восток США и Юг Азии). Изменения реплицируются асинхронно между кластерами. Вы добьетесь низкой задержки для рабочих нагрузок чтения и записи по всему миру, как и в предыдущем варианте, но вы будете иметь дело с несколькими автономными кластерами, которые асинхронно обмениваются изменениями.

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

Первый кластер охватывает три региона США — Запад, Центральную часть и Восток США.

Описание изображения

Экземпляры приложений/микрослужб и серверы API (не показаны на рисунке) работают в тех же местах, а также в регионах Европа-Запад и Азия-Восток.


Задержка чтения/записи базы данных для трафика в США

Предположим, мисс Блю работает на этой неделе из Айовы, США. Она открывает гео-мессенджер, чтобы отправить заметку в корпоративный канал. Ее трафик будет обрабатываться экземпляром микросервиса, развернутым в Центральном регионе США.

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

В моем случае Центральный регион США настроен как предпочтительный для YugabyteDB это означает, что узел базы данных из этого региона будет обрабатывать все запросы на чтение с прикладного уровня. Занимает 10-15 мс для загрузки истории сообщений канала из этого узла базы данных US Central в экземпляр приложения из того же региона. Вот вывод моего журнала Spring Boot с последней строкой, показывающей время выполнения запроса:

Hibernate: select message0_.country_code as country_1_1_, message0_.id as id2_1_, message0_.attachment as attachme3_1_, message0_.channel_id as channel_4_1_, message0_.message as message5_1_, message0_.sender_country_code as sender_c6_1_, message0_.sender_id as sender_i7_1_, message0_.sent_at as sent_at8_1_ from message message0_ where message0_.channel_id=? order by message0_.id asc

INFO 11744 --- [-nio-80-exec-10] i.StatisticalLoggingSessionEventListener : Session Metrics {
1337790 nanoseconds spent acquiring 1 JDBC connections;
0 nanoseconds spent releasing 0 JDBC connections;
1413081 nanoseconds spent preparing 1 JDBC statements;
14788369 nanoseconds spent executing 1 JDBC statements; (14 ms!)
Войти в полноэкранный режим

Выйти из полноэкранного режима

Далее, когда г-жа Блю отправит сообщение в канал, задержка между микросервисом и базой данных будет около 90 мс. Это занимает больше времени, чем предыдущая операция, потому что Hibernate генерирует несколько SQL-запросов для моего JpaRepository.save(message) вызов метода (который, безусловно, можно оптимизировать), а затем YugabyteDB необходимо сохранить копию сообщения на всех узлах, расположенных на западе, в центре и на востоке США. Вот как выглядят выходные данные и задержка:

Hibernate: select message0_.country_code as country_1_1_0_, message0_.id as id2_1_0_, message0_.attachment as attachme3_1_0_, message0_.channel_id as channel_4_1_0_, message0_.message as message5_1_0_, message0_.sender_country_code as sender_c6_1_0_, message0_.sender_id as sender_i7_1_0_, message0_.sent_at as sent_at8_1_0_ from message message0_ where message0_.country_code=? and message0_.id=?
Hibernate: select nextval ('message_id_seq')
Hibernate: insert into message (attachment, channel_id, message, sender_country_code, sender_id, sent_at, country_code, id) values (?, ?, ?, ?, ?, ?, ?, ?)

31908 nanoseconds spent acquiring 1 JDBC connections;
0 nanoseconds spent releasing 0 JDBC connections;
461058 nanoseconds spent preparing 3 JDBC statements;
91272173 nanoseconds spent executing 3 JDBC statements; (90 ms)
Войти в полноэкранный режим

Выйти из полноэкранного режима


Задержка чтения/записи базы данных для трафика APAC

Помните, что при каждом развертывании базы данных в нескольких регионах будут определенные компромиссы. При текущем развертывании базы данных задержка чтения/записи невелика для трафика из США, но высока для трафика из других, более удаленных местоположений. Давайте посмотрим на пример.

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

Однако на Тайване или рядом с ним нет развернутого узла базы данных, поэтому экземпляр микрослужбы должен запрашивать узлы базы данных, работающие в США. Вот почему это занимает 165 мс в среднем, чтобы загрузить всю историю канала до того, как г-н Рэд увидит сообщение г-жи Блю:

Hibernate: select message0_.country_code as country_1_1_, message0_.id as id2_1_, message0_.attachment as attachme3_1_, message0_.channel_id as channel_4_1_, message0_.message as message5_1_, message0_.sender_country_code as sender_c6_1_, message0_.sender_id as sender_i7_1_, message0_.sent_at as sent_at8_1_ from message message0_ where message0_.channel_id=? order by message0_.id asc

[p-nio-80-exec-8] i.StatisticalLoggingSessionEventListener : Session Metrics {
153152267 nanoseconds spent acquiring 1 JDBC connections;
0 nanoseconds spent releasing 0 JDBC connections;
153217915 nanoseconds spent preparing 1 JDBC statements;
165798894 nanoseconds spent executing 1 JDBC statements; (165 ms)
Войти в полноэкранный режим

Выйти из полноэкранного режима

Когда г-н Красный отвечает г-же Блю в том же канале, это занимает около 450 мс для Hibernate для подготовки, отправки и сохранения сообщения в базе данных. Что ж, по законам физики пакет(ы) с сообщением должен пройти через Тихий океан из Тайваня, а затем копия сообщения должна храниться на западе, в центре и на востоке США:

Hibernate: select message0_.country_code as country_1_1_0_, message0_.id as id2_1_0_, message0_.attachment as attachme3_1_0_, message0_.channel_id as channel_4_1_0_, message0_.message as message5_1_0_, message0_.sender_country_code as sender_c6_1_0_, message0_.sender_id as sender_i7_1_0_, message0_.sent_at as sent_at8_1_0_ from message message0_ where message0_.country_code=? and message0_.id=?
select nextval ('message_id_seq')
insert into message (attachment, channel_id, message, sender_country_code, sender_id, sent_at, country_code, id) values (?, ?, ?, ?, ?, ?, ?, ?)

 i.StatisticalLoggingSessionEventListener : Session Metrics 
    23488 nanoseconds spent acquiring 1 JDBC connections;
    0 nanoseconds spent releasing 0 JDBC connections;
    137247 nanoseconds spent preparing 3 JDBC statements;
    454281135 nanoseconds spent executing 3 JDBC statements (450 ms);
Войти в полноэкранный режим

Выйти из полноэкранного режима

Теперь высокая задержка для трафика на основе APAC может не иметь большого значения для приложений, которые не ориентированы на пользователей из региона, но в моем случае это не так. Мой гео-мессенджер должен бесперебойно функционировать по всему миру. Давайте бороться с этой высокой задержкой, приятель! Начнем с чтения!

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

Итак, я «прикрепил» узел реплики к моему работающему кластеру баз данных в США, и эта реплика была размещена в регионе Восток Азии рядом с экземпляром микросервиса, который обслуживает запросы для Mr.Red.

Описание изображения

Затем я запросил экземпляр приложения из Тайваня, чтобы использовать этот узел-реплику для запросов к базе данных. Время ожидания предзагрузки истории канала для Mr.Red сократилось со 165 мс до 10-15 мс! Это так же быстро, как и у г-жи Блю, которая живет в США.

Hibernate: select message0_.country_code as country_1_1_, message0_.id as id2_1_, message0_.attachment as attachme3_1_, message0_.channel_id as channel_4_1_, message0_.message as message5_1_, message0_.sender_country_code as sender_c6_1_, message0_.sender_id as sender_i7_1_, message0_.sent_at as sent_at8_1_ from message message0_ where message0_.channel_id=? order by message0_.id asc

 i.StatisticalLoggingSessionEventListener : Session Metrics 
    1210615 nanoseconds spent acquiring 1 JDBC connections;
    0 nanoseconds spent releasing 0 JDBC connections;
    1255989 nanoseconds spent preparing 1 JDBC statements;
    12772870 nanoseconds spent executing 1 JDBC statements; (12 mseconds)
Войти в полноэкранный режим

Выйти из полноэкранного режима

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

Но для праздника еще рано. Запись по-прежнему слишком медленная.

Представьте, что Mr.Red отправляет в канал очередное сообщение. Экземпляр микросервиса из Тайваня попросит узел реплики выполнить запрос. И реплика будет перенаправлять почти все запросы, сгенерированные Hibernate, на узлы в США, где хранятся первичные копии записей. Таким образом, задержка все еще может достигать 640 мс для трафика APAC:

Hibernate: set transaction read write;
Hibernate: select message0_.country_code as country_1_1_0_, message0_.id as id2_1_0_, message0_.attachment as attachme3_1_0_, message0_.channel_id as channel_4_1_0_, message0_.message as message5_1_0_, message0_.sender_country_code as sender_c6_1_0_, message0_.sender_id as sender_i7_1_0_, message0_.sent_at as sent_at8_1_0_ from message message0_ where message0_.country_code=? and message0_.id=?
Hibernate: select nextval ('message_id_seq')
Hibernate: insert into message (attachment, channel_id, message, sender_country_code, sender_id, sent_at, country_code, id) values (?, ?, ?, ?, ?, ?, ?, ?)

 i.StatisticalLoggingSessionEventListener : Session Metrics 
    23215 nanoseconds spent acquiring 1 JDBC connections;
    0 nanoseconds spent releasing 0 JDBC connections;
    141888 nanoseconds spent preparing 4 JDBC statements;
    640199316 nanoseconds spent executing 4 JDBC statements; (640 ms)
Войти в полноэкранный режим

Выйти из полноэкранного режима

Наконец-то, дружище, решим этот вопрос раз и навсегда!

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


Изменения схемы базы данных

В двух словах, мои таблицы, такие как Message, определяют country_code столбец:

CREATE TABLE Message(
    id integer NOT NULL DEFAULT nextval('message_id_seq'),
    channel_id integer,
    sender_id integer NOT NULL,
    message text NOT NULL,
    attachment boolean NOT NULL DEFAULT FALSE,
    sent_at TIMESTAMP NOT NULL DEFAULT NOW(),
    country_code varchar(3) NOT NULL
) PARTITION BY LIST (country_code);
Войти в полноэкранный режим

Выйти из полноэкранного режима

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

CREATE TABLE Message_USA
    PARTITION OF Message
    (id, channel_id, sender_id, message, sent_at, country_code, sender_country_code,
    PRIMARY KEY(id, country_code))
    FOR VALUES IN ('USA') TABLESPACE us_central1_ts;

CREATE TABLE Message_EU
    PARTITION OF Message
    (id, channel_id, sender_id, message, sent_at, country_code, sender_country_code,
    PRIMARY KEY(id, country_code))
    FOR VALUES IN ('DEU') TABLESPACE europe_west3_ts;

CREATE TABLE Message_APAC
    PARTITION OF Message
    (id, channel_id, sender_id, message, sent_at, country_code, sender_country_code,
    PRIMARY KEY(id, country_code))
    FOR VALUES IN ('TWN') TABLESPACE asia_east1_ts;
Войти в полноэкранный режим

Выйти из полноэкранного режима

Каждый раздел сопоставляется с одним из табличных пространств:

CREATE TABLESPACE us_central1_ts WITH (
  replica_placement='{"num_replicas": 1, "placement_blocks":
  [{"cloud":"gcp","region":"us-central1","zone":"us-central1-b","min_num_replicas":1}]}'
);

CREATE TABLESPACE europe_west3_ts WITH (
  replica_placement='{"num_replicas": 1, "placement_blocks":
  [{"cloud":"gcp","region":"europe-west3","zone":"europe-west3-b","min_num_replicas":1}]}'
);

CREATE TABLESPACE asia_east1_ts WITH (
  replica_placement='{"num_replicas": 1, "placement_blocks":
  [{"cloud":"gcp","region":"asia-east1","zone":"asia-east1-b","min_num_replicas":1}]}'
);
Войти в полноэкранный режим

Выйти из полноэкранного режима

И каждое табличное пространство принадлежит группе узлов базы данных из определенной географии. Например, все записи с кодом страны Тайвань (country_code=TWN) будет храниться на узлах кластера из облачного региона Восточная Азия, поскольку эти узлы содержат раздел и табличное пространство для данных APAC. Проверьте следующая статья если вы хотите узнать подробности георазбивки.


Низкая задержка чтения/записи на разных континентах

Итак, я развернул гео-разделенный кластер из трех узлов в регионах Центральная часть США, Западная Европа и Восточная Азия.

Описание изображения

Теперь давайте удостоверимся, что задержка чтения для запросов Mr. Red остается неизменной. Как и в случае с развертыванием реплики для чтения, это все еще занимает 5-15 мс чтобы загрузить историю сообщений канала (для каналов и сообщений, принадлежащих к региону APAC):

Hibernate: select message0_.country_code as country_1_1_, message0_.id as id2_1_, message0_.attachment as attachme3_1_, message0_.channel_id as channel_4_1_, message0_.message as message5_1_, message0_.sender_country_code as sender_c6_1_, message0_.sender_id as sender_i7_1_, message0_.sent_at as sent_at8_1_ from message message0_ where message0_.channel_id=? and message0_.country_code=? order by message0_.id asc

 i.StatisticalLoggingSessionEventListener : Session Metrics 
1516450 nanoseconds spent acquiring 1 JDBC connections;
  0 nanoseconds spent releasing 0 JDBC connections;
  1640860 nanoseconds spent preparing 1 JDBC statements;
  7495719 nanoseconds spent executing 1 JDBC statements; (7 ms)
Войти в полноэкранный режим

Выйти из полноэкранного режима

И… барабанная дробь, пожалуйста… когда г-н Ред отправляет сообщение на канал APAC, задержка записи снижается с 400–650 мс до 6 мс в среднем!

Hibernate: select message0_.country_code as country_1_1_0_, message0_.id as id2_1_0_, message0_.attachment as attachme3_1_0_, message0_.channel_id as channel_4_1_0_, message0_.message as message5_1_0_, message0_.sender_country_code as sender_c6_1_0_, message0_.sender_id as sender_i7_1_0_, message0_.sent_at as sent_at8_1_0_ from message message0_ where message0_.country_code=? and message0_.id=?
Hibernate: select nextval ('message_id_seq')
Hibernate: insert into message (attachment, channel_id, message, sender_country_code, sender_id, sent_at, country_code, id) values (?, ?, ?, ?, ?, ?, ?, ?)

1123280 nanoseconds spent acquiring 1 JDBC connections;
  0 nanoseconds spent releasing 0 JDBC connections;
  123249 nanoseconds spent preparing 3 JDBC statements;
  6597471 nanoseconds spent executing 3 JDBC statements; (6 ms)
Войти в полноэкранный режим

Выйти из полноэкранного режима

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


Случай межконтинентальных запросов

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

Мне было важно иметь единую базу данных для мессенджера, чтобы:

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

Например, если Mr. Red присоединяется к каналу обсуждения, принадлежащему узлам США (country_code=’USA') и отправляет туда сообщение, экземпляр микрослужбы из Тайваня отправит запрос на узел базы данных в Тайване, а этот узел перенаправит запрос на аналог в США. Задержка для этой операции состоит из трех запросов SQL и будет около 165 мс:

Hibernate: select message0_.country_code as country_1_1_0_, message0_.id as id2_1_0_, message0_.attachment as attachme3_1_0_, message0_.channel_id as channel_4_1_0_, message0_.message as message5_1_0_, message0_.sender_country_code as sender_c6_1_0_, message0_.sender_id as sender_i7_1_0_, message0_.sent_at as sent_at8_1_0_ from message message0_ where message0_.country_code=? and message0_.id=?
Hibernate: select nextval ('message_id_seq')
Hibernate: insert into message (attachment, channel_id, message, sender_country_code, sender_id, sent_at, country_code, id) values (?, ?, ?, ?, ?, ?, ?, ?)

 i.StatisticalLoggingSessionEventListener : Session Metrics 
1310550 nanoseconds spent acquiring 1 JDBC connections;
  0 nanoseconds spent releasing 0 JDBC connections;
  159080 nanoseconds spent preparing 3 JDBC statements;
  164660288 nanoseconds spent executing 3 JDBC statements; (164 ms)
Войти в полноэкранный режим

Выйти из полноэкранного режима

165 мс, несомненно, больше, чем 6 мс (задержка, когда Mr. Red отправляет сообщение в локальный канал на основе APAC), но здесь важна возможность делать межконтинентальные запросы через одно соединение с базой данных, когда это необходимо. Кроме того, как показывает план выполнения, на уровне Hibernate есть много возможностей для оптимизации. В настоящее время Hibernate переводит мой JpaRepository.save(message) вызовите 3 оператора JDBC. Это то, что можно дополнительно оптимизировать, чтобы снизить задержку межконтинентальных запросов со 165 мс до гораздо более низкого значения.

Хорошо, приятель!

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

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

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