В monday.com нам, инженерам, часто приходится искать баланс между скоростью унаследованного монолита и требованиями к масштабируемости быстрорастущей платформы. Долгое время монолит отлично справлялся со своей задачей: создавал элементы доски и сохранял их в единой основной базе MySQL. Простота была важнее всего, и обычная колонка с последовательными ID полностью решала вопрос идентификации.
Но по мере взрывного роста нагрузки основной экземпляр MySQL превратился в узкое место и единую точку отказа при создании элементов доски. Любой сбой в работе базы мешал пользователям взаимодействовать со своими досками.
Нам нужно было вынести генерацию элементов из монолита, чтобы продолжить переход к распределённой и отказоустойчивой микросервисной архитектуре.
На первый взгляд это обычный рефакторинг в сторону микросервисов, если бы не одно серьёзное ограничение: идентификаторы элементов доски должны были оставаться целыми числами.
Слишком много зависимых сервисов и унаследованной логики рассчитывали на то, что ID будет числом. Перейти на UUID без переписывания половины кодовой базы было невозможно.
В этой статье расскажу, как мы решили проблему целочисленных ID в распределённой системе.
Решение: централизованный генератор идентификаторов
Мы решили создать отдельный сервис генерации идентификаторов с DynamoDB в качестве хранилища.
Основная идея была простой: вместо того чтобы заставлять базу данных увеличивать счётчик при каждой вставке строки, создавая блокировки и узкое место, мы сделали сервис, который выдаёт клиентам диапазоны ID.
Архитектурный выбор: SDK или центральный сервис
На раннем этапе мы обсуждали, стоит ли клиентским SDK обращаться к DynamoDB напрямую или лучше поставить между ними отдельный сервис.
Вариант A: SDK обращаются к DynamoDB напрямую
Плюсы: ниже задержка за счёт одного исключённого сетевого перехода, меньше компонентов и проще архитектура.
Минусы:
Кошмар с сопровождением: затраты на поддержку были бы огромными. Любое изменение логики генерации, ограничения частоты запросов или стратегии кеширования потребовало бы обновлений по всей monday.com. Нам пришлось бы координировать обновление SDK в сотнях микросервисов и ждать, пока каждая команда развернёт новую версию.
Хаос с подключениями и троттлингом: при сотнях тысяч подов прямое подключение даже не рассматривалось. Тысячи клиентов, одновременно обращающихся к одному и тому же ключу раздела (boards.item), почти сразу превратили бы его в горячий раздел DynamoDB и вызвали троттлинг. Кроме того, поддерживать тысячи одновременных HTTP-соединений с AWS неэффективно: лимиты API быстро оказались бы исчерпаны.
Вариант B: централизованный сервис генерации идентификаторов
Плюсы: гибкость и инкапсуляция. Сосредоточив всю логику в одном месте, мы могли менять размеры буферов, переключаться на другую систему хранения или исправлять ошибки одним развёртыванием, не затрагивая клиентские сервисы. Кроме того, сервис работал как буфер для DynamoDB, сглаживая всплески нагрузки.
Минусы: небольшая дополнительная сетевая задержка. Впрочем, позже мы полностью от неё избавились.
Мы выбрали вариант B – отдельный сервис. Решающим аргументом стала возможность развивать логику генерации ID без миграции в масштабах всей компании.
Переход от монолита к микросервисам почти всегда связан с архитектурными выборами: где провести границы сервисов, что оставить общим, а что вынести отдельно. Проверить свои знания по этим вопросам можно с помощью вступительного теста по микросервисной архитектуре.
Почему DynamoDB?
Мы выбрали DynamoDB в качестве слоя постоянного хранения, потому что для источника истины идентификаторов надёжность важнее всего. DynamoDB дала нам:
-
Высокую доступность: SLA 99,999% для глобальных таблиц DynamoDB (Global Tables).
-
Мультирегиональную репликацию: она критически важна для аварийного восстановления и наших будущих планов по работе в нескольких регионах.
-
Низкую задержку: единицы миллисекунд внутри нашего VPC.
Модель данных
Мы сделали схему предельно простой. В ней хранится только последний ID, выделенный для конкретного типа сущности.
-
Ключ раздела (
entityType): строка в формате{domain}.{entityType}, напримерboards.item. -
Атрибут (
lastID): число – последний выделенный ID для этой сущности.
Атомарные обновления
Это основа всей системы. Когда сервису требовалось пополнить буфер, он выполнял в DynamoDB одну атомарную операцию:
SET lastID = if_not_exists(lastID, :min) + :count
SET lastID = if_not_exists(lastID, :min) + :count
Эта операция увеличивала счётчик в базе на значение :count и возвращала новый результат. Допустим, база вернула 1050, а мы запросили диапазон из 50 идентификаторов. Значит, диапазон 1001–1050 однозначно принадлежал нам. Благодаря этому два сервера не могли получить один и тот же диапазон ID, даже если отправляли запросы в одну и ту же микросекунду.
Оптимизация производительности
Главная проблема при переходе от локальной базы данных к сетевому сервису – задержка. В высоконагруженной системе дополнительные 50 мс на критическом пути – это не просто заметное замедление, а неприемлемый результат. Чтобы устранить этот риск, мы реализовали стратегию двойной буферизации, которая фактически убирает задержку во время обработки запросов.
-
Буферизация в памяти и предварительная загрузка
Важно было не только получать ID пакетами, но и делать это в нужный момент. Мы использовали упреждающую загрузку: идентификаторы выделялись заранее и уже находились в памяти к моменту поступления запроса.
На стороне сервиса: сервис поддерживал в памяти буфер готовых к выдаче ID. Когда запас опускался ниже 80%, фоновый поток запрашивал новый диапазон и пополнял буфер.
На стороне клиентского SDK: при запуске сервис сразу запрашивал диапазон ID, например 1000 идентификаторов, и сохранял их в оперативной памяти. Когда пользователь создавал элемент доски, SDK мгновенно забирал ID из локального пула. На критическом пути создания элемента не было ни одного сетевого запроса. SDK асинхронно пополнял пул в фоновом режиме.
2. Что делать с «потерянными» ID
Неизбежная обратная сторона предварительного выделения – часть идентификаторов остаётся неиспользованной. Каждый раз, когда Kubernetes-под получает 1000 ID, а затем перезапускается, эти идентификаторы теряются навсегда. Пространство ID у нас было огромным, но всё же конечным: оно ограничивалось Number.MAX_SAFE_INTEGER в JavaScript. Поэтому внезапный цикл аварийных перезапусков подов мог незаметно и очень быстро съесть весь оставшийся запас.
Чтобы снизить этот риск, мы ввели строгие ограничения:
-
Ежедневные лимиты потребления: мы задали защитный лимит на количество ID, которое конкретная сущность может использовать за сутки.
-
Наблюдаемость: мы настроили алерты на скорость расходования идентификаторов (burn rate). Если сервис начинает потреблять ID на 50% быстрее обычного, что может указывать на цикл аварийных перезапусков, при котором поды запускаются, забирают диапазон и завершаются, дежурному немедленно приходит оповещение.
Внедрение без простоя
Генерация элементов доски – один из наших базовых и наиболее нагруженных сценариев. Нам нужно было убедиться, что все возможные риски учтены, снижены и находятся под наблюдением.
1. Стресс- и хаос-тестирование
Прежде чем что-либо менять в продакшене, мы использовали k6, чтобы создать серьёзную нагрузку на staging-окружение. Мы проверяли не только штатный сценарий (happy path), но и нештатные ситуации:
-
Сбои DynamoDB: мы имитировали потерю соединения, чтобы убедиться, что локальные буферы позволяют приложению переживать кратковременные перебои.
-
Частое пересоздание подов (pod churn): мы быстро запускали и останавливали поды, чтобы оценить влияние потерянных ID.
-
Исчерпание буфера: мы создавали высокую нагрузку на небольшое число подов и проверяли, успевают ли фоновые механизмы пополнения поддерживать нужный запас.
2. Теневой режим с «фейковыми» сущностями
Мы развернули генератор идентификаторов в продакшене в теневом режиме. Для каждого создаваемого элемента сервис в фоне генерировал ID, но мы отбрасывали их и продолжали использовать идентификаторы из MySQL.
Важно, что на этом этапе мы использовали тестовый ключ сущности, например shadow.boards.item. Так мы могли проводить стресс-тестирование инфраструктуры, не увеличивая счётчик настоящего ключа boards.item. Благодаря этому к моменту полноценного запуска мы начинали с чистого начального состояния и правильной последовательности ID.
3. Поэтапное внедрение и проблема верхней отметки
Самый опасный риск при внедрении был связан с внутренним поведением MySQL: вставка строки с более высоким ID мгновенно продвигает вперёд счётчик AUTO_INCREMENT таблицы. Если бы генератор обогнал последовательность MySQL, база незаметно «догнала» бы его, что почти гарантированно привело бы к катастрофическим коллизиям идентификаторов между монолитом и новым сервисом.
Чтобы избежать этого, мы создали безопасный диапазон в пять миллиардов ID:
-
Вручную установили счётчик
AUTO_INCREMENTв MySQL на пять миллиардов выше текущего максимального значения. -
Настроили генератор идентификаторов так, чтобы он заполнял промежуток строго ниже этой новой верхней границы.
Поскольку новые ID оставались меньше верхней отметки базы, MySQL не сдвигала свой указатель. Это позволило безопасно заполнять свободный диапазон, пока мы постепенно увеличивали долю трафика с 1 до 100%, отслеживая задержку и скорость расходования идентификаторов. После этого монолит официально вывели из эксплуатации.
Если бы мы начинали сегодня…
Если вы читаете эту статью и проектируете новую архитектуру с нуля, не повторяйте наш путь.
Если у вас нет жёсткого ограничения со стороны унаследованной системы, как было у нас, UUID будут лучшим выбором. Они позволяют генерировать идентификаторы полностью автономно, без центрального координатора и единой точки отказа.
В первую очередь стоит обратить внимание на UUIDv7.
Стандартные UUID версии 4 генерируются полностью случайным образом. Это отлично обеспечивает уникальность, но плохо сказывается на производительности базы данных: вставка случайных ключей приводит к фрагментации индекса.
UUIDv7 решает эту проблему, добавляя в UUID временную метку. Благодаря этому такие идентификаторы:
-
Сортируются: их можно упорядочивать по времени так же, как целые числа.
-
Удобны для базы данных: близкие по времени значения оказываются рядом, поэтому индексы на основе B-дерева сохраняют хорошую производительность.
Но если вы вынуждены использовать целые числа и при этом хотите масштабироваться, буферизованный генератор идентификаторов на базе DynamoDB – проверенный в боевых условиях способ освободиться от ограничений монолита.

Когда система растёт, выбор между монолитом и распределённой архитектурой становится вопросом не только удобства разработки, но и устойчивости продукта. Разобраться в принципах построения таких систем и ключевых инструментах помогут открытые занятия по архитектуре и обмену сообщениями.
-
3 августа в 20:00. «Использование брокера сообщений Apache Kafka в распределенных очередях». Записаться
-
19 августа в 20:00. «Монолит или микросервисы? Руководство для архитекторов, которые ценят свои нервы». Записаться
ссылка на оригинал статьи https://habr.com/ru/articles/1064930/