Прежде чем перейти к статье, хочу вам представить, экономическую онлайн игру Brave Knights, в которой вы можете играть и зарабатывать. Регистируйтесь, играйте и зарабатывайте!
Меня зовут Александр, я руковожу направлением больших данных в Битрикс24. Клиенты нашего сервиса хранят миллиарды файлов: от документов до фотографий, — а моя команда предоставляет возможность строить бизнес‑аналитику на основе этого множества данных. И нам важно позаботиться об их сохранности.
Более 10 лет назад мы продумали необходимую нам схему репликации объектного хранилища в облаке. Затем файлы клиентов потребовалось перенести в другое облако, и нам очень хотелось также перенести все наши наработки в режиме «Ctrl+C, Сtrl+V».
В статье расскажу, как мы организовали резервирование данных в парадигме слабого связывания и как перенесли эту схему в Yandex Cloud без потери важных нам деталей.
Какие данные и как мы резервировали
Битрикс24 задуман как сервис, где собраны все нужные инструменты для ведения бизнеса: от CRM и почты до корпоративных чатов и видеозвонков. Со временем он пополнился новыми инструментами наподобие AI‑помощника CoPilot, который облегчает выполнение рутинных задач. А любые ИИ‑модели лучше всего работают, когда есть качественные датасеты, так что мы помогаем клиентам повышать качество данных.
Сейчас количество созданных клиентами порталов исчисляется миллионами. Их данные хранятся в десятках дата‑центров.
Моя команда помогает клиентам «доставать» из этих данных BI‑сущности и создавать BI‑аналитику с помощью разных инструментов по выбору клиента. Например, PowerBI, Google Looker Studio, Yandex DataLens или нашего собственного BI‑конструктора. Для этого мы используем множество bigdata‑инструментов, в основном это опенсорс: Apache Spark™, Apache Lucene™, Elastic и другие.
Сам продукт развёрнут на базе MySQL. В БД хранятся данные клиента: задачи, календари, CRM с лидами, сделками и контактами. На одного клиента приходится больше 1000 таблиц БД.
Также Битрикс24 оперирует множеством файлов, которые нужны в работе бизнес‑инструментов, например, онлайн‑документов. Для их размещения у нас есть объектное хранилище — на один портал приходится от тысяч до миллионов файлов. В 2012 году для этих файлов мы использовали AWS S3.
![](https://habrastorage.org/r/w1560/getpro/habr/upload_files/359/90a/4af/35990a4afe1befdc4d6ff7917e2259f2.png)
Данные объектного хранилища нужно бэкапить, но как? В начале 2010-х годов у Amazon ещё не было функциональности Replicating Objects. Так что в то время нам пришлось изобретать своё решение. Мы решили начать с самых очевидных вариантов реализации и постепенно добавлять необходимую функциональность.
Копирование «в лоб». Первый пришедший нам в голову способ решения: получить список файлов в виде листинга и скопировать их в резервное хранилище построчно.
Для начала мы решили получить список файлов из одного бакета. Вернулся список из миллиардов строк — получалась уже не самая простая задачка. Для её решения мы задали каждую строку как задание в кластер MapReduce и подняли десяток машин.
![Первая итерация операции «резервирование» схематично выглядела так. Первая итерация операции «резервирование» схематично выглядела так.](https://habrastorage.org/r/w1560/getpro/habr/upload_files/9d1/611/0c7/9d16110c7fefe49b8d4682e1134ed154.png)
В результате данные переливались в другой бакет почти неделю. Окей, бакет мы скопировали, данные перенесли. Но с миллиардами задач получается дорого, а делать это нужно регулярно. Кажется, что можно сделать всё оптимальнее.
Инкрементальный бэкап. Когда мы копнули глубже, оказалось, что при получении данных из исходного и резервного бакета в листинге указывается хэш файла. Конечно, не всех порадовал MD5, но всё равно это хоть какой‑то хэш.
Алгоритм получился такой:
Получаем список папок верхнего уровня в бакете.
Благодаря магии Java запускаем многопоточность: пул воркеров конкурентно работает с HashMap и кладёт туда имя файла и хэш.
Получаем список следующего уровня директорий: имя файла и хэш. И так далее рекурсивно.
Сравниваем и принимаем решение: если хэши не совпадают или нужного файла в резервном хранилище просто нет — файл надо переслать.
На первом шаге мы получили не список из миллиардов строк, а список папок верхнего уровня: от сотен тысяч до миллионов строк. Это уже относительно небольшой список, который можно прорабатывать в несколько потоков. А значит, мы можем обойтись без MapReduce на десяти машинах и всё решить в памяти одного среднего сервера с помощью воркеров‑копировщиков Java/Netty.
![Схема для второй итерации с инкрементальным бэкапом. Схема для второй итерации с инкрементальным бэкапом.](https://habrastorage.org/r/w1560/getpro/habr/upload_files/85b/e29/f42/85be29f42c1b7bbd99c63d922c24df28.png)
Таким образом, нам хватило одной машины, чтобы решить эти задачи многопоточно.
Понадобилось только добавить больше оперативной памяти одной ВМ: для того чтобы сравнить разницу файлов в HashMap в Java и потом в неблокирующем асинхронном режиме их найти. Инкрементальный бэкап взлетел, мы стали его делать раз в неделю.
Но что делать на случай аварии, которая может произойти в окне между еженедельными бэкапами?
Realtime‑репликация. К тому времени Amazon добавил события по добавлению‑удалению файлов в бакет. Мы стали брать эти события и запускать AWS Lambda — по сути, это бессерверные вычисления по модели «функция как услуга».
Платишь только за время и за память, очень удобно.
Как только файл появляется, функция ставит задачу на копирование. Если файл маленький, она копирует его быстро и дёшево. Но в остальных случаях получится дорого: если копировать файл минуту, мы уже заплатим много денег.
Поэтому задачи на копирование больших файлов стали попадать в отложенную обработку в очередь Amazon SQS, которую разгребают те же воркеры‑копировщики Java/Netty. Если сообщение не доставилось в Lambda, оно попадает в dead letter queue, эту очередь тоже обрабатывают воркеры.
Получилась такая схема realtime‑репликации из одного бакета в резервный:
![](https://habrastorage.org/r/w1560/getpro/habr/upload_files/4a0/a89/c9c/4a0a89c9c1b082b2a94c7729447d32fd.png)
Восстановление уже удалённого. Появился новый вопрос: что будет, если мы вдруг удалим файлы в основном бакете, а они понадобятся?
Нередкая ситуация, когда специалист клиента может случайно удалить на клиентском портале какой‑то файл или запись, которые затем нужно восстановить. На такие случаи нужно отложенное удаление: после удаления в основном бакете мы ждём два месяца и только после этого удаляем в резервном бакете.
Чтобы это реализовать в текущей схеме, мы стали перехватывать события на удаление файлов с помощью Lambda и кидать их в DynamoDB — распределённое key‑value хранилище. Для создания такого задания использовалось всего два поля: ставилась метка по времени запуска задания, и выбирались рандомные шарды от 0 до 100 тысяч.
После этого воркеры обрабатывали DynamoDB случайным образом: с учётом неконсистентной выборки запросов они в дешёвом режиме сканировали хранилище в поиске заданий, которым уже исполнилось 2 месяца, — и только тогда удаляли файл в резервном хранилище. В таком режиме поиск может не вернуть недавно добавленные данные или, наоборот, вернуть устаревшие данные. Но для этого сценария это было некритично.
![](https://habrastorage.org/r/w1560/getpro/habr/upload_files/d74/b45/f5f/d74b45f5f9b7beb13877e924285b9cd6.png)
Итоговая схема. По результатам всех итераций получилась архитектура, которая решила наши проблемы.
Мы отлавливаем события на создание файла и принимаем по ним решение: маленькие копируем сразу, большие файлы прогоняем через очередь.
В случае запроса на удаление файла отправляем задание в DynamoDB, пулами воркеров стохастически сканируем базу, ищем задания на файлы, которым уже исполнилось два месяца, и затем удаляем файлы из резервного бакета.
Помимо этого делаем еженедельную инкрементальную проверку, не было ли какой‑то аварии: вдруг какие‑то файлы не доставлены, вдруг где‑то порушилась оперативная память. На этот случай делаем сверку: всех миллиардов файлов в основном хранилище с миллиардами резервных.
![](https://habrastorage.org/r/w1560/getpro/habr/upload_files/251/132/7e3/2511327e385ebd20b55cbe1d86719869.png)
Какие ещё тонкости здесь учтены: в таких задачах нужно было научиться асинхронно работать с файлами, потому что синхронные копирования тратят большое количество вычислительных ресурсов. Асинхронность может быть реализована в Java/Netty, в Rust/Tokio и даже в Python/PHP есть свои асинки. Такой код получается плохо читаемый, с монадическими конструкциями, но, к сожалению, без этого никуда. В нашем примере мы имеем в виду реализацию с Java/Netty.
В качестве маст‑хэва у нас предусмотрено:
асинхронная не блокирующая работа с сокетами: чтение событий из SQS‑очереди;
async‑работа с загрузкой файлов;
заливка из сетевого сокета в сетевой сокет без записи на диск.
Эта система простая, надёжная, проверенная годами. Но теперь эту же схему нужно было перенести в Россию.
Как мы переходили на резервирование между провайдерами
Итак, несколько лет назад мы научились резервировать данные в Amazon. Но поскольку данные российских клиентов важно хранить и резервировать в РФ, то вскоре нам понадобился переезд. И раз мы уходили из Amazon, то стали искать аналогичные услуги на отечественном рынке.
Вдобавок мы понимали, что хотим избежать возможного вендор‑лока. По этой причине рассматривалось сразу несколько российских облачных провайдеров, у которых были похожие решения: объектное хранилище и облачные функции. Yandex Cloud приглянулся в качестве решения для резервирования данных. В качестве основного хранилища было выбрано ещё одно облако.
В выбранном резервном хранилище хотелось реализовать уже проверенную годами схему.
Начало миграции в Yandex Cloud. Мы подключили стандартные клиенты Amazon к Yandex Cloud: поменяли ключи, эндпоинты, поменяли регион.
Стандартные амазоновские SDK сложны: там реализована многопоточность, асинхронная работа с сокетами — так что их решили взять уже готовыми и развернуть в Yandex Cloud. Всё завелось без проблем, и получилась такая схема:
![](https://habrastorage.org/r/w1560/getpro/habr/upload_files/aff/f38/adb/afff38adb3adf6f5a273d9655b65b5c8.png)
В этой архитектуре используются реализованные на первом этапе паттерны:
Все события сначала ставятся в очередь.
Очередь разбирается воркерами в асинхронном режиме, не блокирующем сокеты. Для этого можно использовать Golang, Python, у нас это по‑прежнему Java/Netty.
Если в процессе обнаружено задание на создание файла, мы его копируем в таком же асинхронном не блокирующем режиме в объектное хранилище.
На случай отложенного удаления работает уже распределённая СУБД YDB в режиме, совместимом с DynamoDB.
Дальше пул воркеров чистит YDB и удаляет устаревшие данные: в режиме нестрогой консистентности воркеры последовательно сканируют шарды (random init positions), запрашивая строки «старше» временной метки.
Также настроен еженедельный инкрементальный бэкап для сверки файлов.
Почти все элементы прежней системы переехали в новое облако в неизменном виде. Но в схеме появился новая деталь — YDB в режиме совместимости с DynamoDB. Так что стоит остановиться на том, какую роль она выполняет и в чём особенности её работы.
Миграция на YDB. Наш продукт изначально развёрнут на базе MySQL, а это порой задаёт ограничения масштабируемости. YDB помог заменить MySQL в задачах, где нужно «резиновое» масштабирование.
Если в первоначальной схеме при работе с DynamoDB для решения этой проблемы нам понадобилось писать отдельный автоскейлер, то после переезда в Yandex Cloud это было больше не нужно. YDB автоматически масштабируется и при этом совместим с DynamoDB (сначала мы не поверили, что для этого нужно нажать буквально одну кнопку).
В случае с отложенным удалением мы отправляем задание на удаление в YDB, и нам снова достаточно пары полей:
Шард (hash key, 0–100 000, число, случайное)
Время постановки задания (range key, миллисекунды UTC)
Путь к удаляемому файлу в Object Storage (строка, a/b/c/d.txt)
Уже проверенная схема отложенного удаления продолжает работать, при этом для YDB работает более выгодный режим serverless: СУБД подстраивается под нагрузку и сама управляет своими ресурсами. Нам не нужно заранее поднимать виртуалки с N ядрами, и M ГБ оперативной памяти, и L дисками, а можно по запросу тратить только те ресурсы, которые нужны, и не платить за «запас» вперёд.
Сейчас в таблице хранится несколько миллиардов записей, туда пишется несколько сот событий в секунду.
![](https://habrastorage.org/r/w1560/getpro/habr/upload_files/b9d/f7a/d71/b9df7ad715ffbfb3a4e18f427fd790a8.png)
В самой YDB включён автоматический бэкап, это тоже было для нас важной деталью.
Позаботились о резервировании, позаботимся о мониторинге. Первое, что мы сделали после развёртывания — настроили алерты.
![](https://habrastorage.org/r/w1560/getpro/habr/upload_files/dd7/33b/8b8/dd733b8b817d59dd55fc9c2d9441460b.png)
Вот что мониторили:
Отслеживали нагрузку на базу по сообщениям в полёте — мы не хотели обрабатывать больше 120 тысяч одновременно.
Отдельно смотрели число сообщений в очереди — если они не разгребаются, значит, у нас проблемы с обработкой сообщений из очереди.
Также было важно количество принятых сообщений в очередь, измерять нагрузку и трафик.
На основе этих данных создали дашборд. Оцените нагрузку:
![](https://habrastorage.org/r/w1560/getpro/habr/upload_files/260/09b/af8/26009baf8b9f9242040ce63f2ffd3355.png)
В процессе миграции также пригодился мониторинг бакета.
![Перенесли несколько петабайт, ни одного инцидента. Перенесли несколько петабайт, ни одного инцидента.](https://habrastorage.org/r/w1560/getpro/habr/upload_files/a2c/5e7/b7c/a2c5e7b7c801d57991d19c7349a8e562.png)
Что ещё можно добавить в схему
У нас получилась готовая работающая система, но в любой схеме всегда есть что‑то, что хочется добавить и улучшить. Мы наметили несколько векторов возможного развития:
Сейчас репликация у нас работает только в одну сторону, но мы можем добавить всей системе больше устойчивости, если сможем развернуть процесс обратно. Для этого нам нужно будет создать в резервном объектном хранилище точно такие же триггеры на события, как создание или удаление файлов. Это можно сделать с помощью аналога AWS Lambda — Cloud Functions.
Также было бы полезно на лету анализировать некоторые потоки больших данных от клиентов: логи, трафик, бизнес‑события. В текущей реализации с классическими Message Queues это сложно сделать из‑за разных архитектур, но можно попробовать такой инструмент, как Data Streams.
В перспективе также хотим разместить несколько виртуальных машин в Compute Cloud для обслуживания клиентов Битрикс24. Эти ВМ могут пригодиться как серверы для подготовки и обработки данных.