Меня зовут Александр, я руковожу направлением больших данных в Битрикс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.
Данные объектного хранилища нужно бэкапить, но как? В начале 2010-х годов у Amazon ещё не было функциональности Replicating Objects. Так что в то время нам пришлось изобретать своё решение. Мы решили начать с самых очевидных вариантов реализации и постепенно добавлять необходимую функциональность.
Копирование «в лоб». Первый пришедший нам в голову способ решения: получить список файлов в виде листинга и скопировать их в резервное хранилище построчно.
Для начала мы решили получить список файлов из одного бакета. Вернулся список из миллиардов строк — получалась уже не самая простая задачка. Для её решения мы задали каждую строку как задание в кластер MapReduce и подняли десяток машин.
В результате данные переливались в другой бакет почти неделю. Окей, бакет мы скопировали, данные перенесли. Но с миллиардами задач получается дорого, а делать это нужно регулярно. Кажется, что можно сделать всё оптимальнее.
Инкрементальный бэкап. Когда мы копнули глубже, оказалось, что при получении данных из исходного и резервного бакета в листинге указывается хэш файла. Конечно, не всех порадовал MD5, но всё равно это хоть какой‑то хэш.
Алгоритм получился такой:
Получаем список папок верхнего уровня в бакете.
Благодаря магии Java запускаем многопоточность: пул воркеров конкурентно работает с HashMap и кладёт туда имя файла и хэш.
Получаем список следующего уровня директорий: имя файла и хэш. И так далее рекурсивно.
Сравниваем и принимаем решение: если хэши не совпадают или нужного файла в резервном хранилище просто нет — файл надо переслать.
На первом шаге мы получили не список из миллиардов строк, а список папок верхнего уровня: от сотен тысяч до миллионов строк. Это уже относительно небольшой список, который можно прорабатывать в несколько потоков. А значит, мы можем обойтись без MapReduce на десяти машинах и всё решить в памяти одного среднего сервера с помощью воркеров‑копировщиков Java/Netty.
Таким образом, нам хватило одной машины, чтобы решить эти задачи многопоточно.
Понадобилось только добавить больше оперативной памяти одной ВМ: для того чтобы сравнить разницу файлов в HashMap в Java и потом в неблокирующем асинхронном режиме их найти. Инкрементальный бэкап взлетел, мы стали его делать раз в неделю.
Но что делать на случай аварии, которая может произойти в окне между еженедельными бэкапами?
Realtime‑репликация. К тому времени Amazon добавил события по добавлению‑удалению файлов в бакет. Мы стали брать эти события и запускать AWS Lambda — по сути, это бессерверные вычисления по модели «функция как услуга».
Платишь только за время и за память, очень удобно.
Как только файл появляется, функция ставит задачу на копирование. Если файл маленький, она копирует его быстро и дёшево. Но в остальных случаях получится дорого: если копировать файл минуту, мы уже заплатим много денег.
Поэтому задачи на копирование больших файлов стали попадать в отложенную обработку в очередь Amazon SQS, которую разгребают те же воркеры‑копировщики Java/Netty. Если сообщение не доставилось в Lambda, оно попадает в dead letter queue, эту очередь тоже обрабатывают воркеры.
Получилась такая схема realtime‑репликации из одного бакета в резервный:
Восстановление уже удалённого. Появился новый вопрос: что будет, если мы вдруг удалим файлы в основном бакете, а они понадобятся?
Нередкая ситуация, когда специалист клиента может случайно удалить на клиентском портале какой‑то файл или запись, которые затем нужно восстановить. На такие случаи нужно отложенное удаление: после удаления в основном бакете мы ждём два месяца и только после этого удаляем в резервном бакете.
Чтобы это реализовать в текущей схеме, мы стали перехватывать события на удаление файлов с помощью Lambda и кидать их в DynamoDB — распределённое key‑value хранилище. Для создания такого задания использовалось всего два поля: ставилась метка по времени запуска задания, и выбирались рандомные шарды от 0 до 100 тысяч.
После этого воркеры обрабатывали DynamoDB случайным образом: с учётом неконсистентной выборки запросов они в дешёвом режиме сканировали хранилище в поиске заданий, которым уже исполнилось 2 месяца, — и только тогда удаляли файл в резервном хранилище. В таком режиме поиск может не вернуть недавно добавленные данные или, наоборот, вернуть устаревшие данные. Но для этого сценария это было некритично.
Итоговая схема. По результатам всех итераций получилась архитектура, которая решила наши проблемы.
Мы отлавливаем события на создание файла и принимаем по ним решение: маленькие копируем сразу, большие файлы прогоняем через очередь.
В случае запроса на удаление файла отправляем задание в DynamoDB, пулами воркеров стохастически сканируем базу, ищем задания на файлы, которым уже исполнилось два месяца, и затем удаляем файлы из резервного бакета.
Помимо этого делаем еженедельную инкрементальную проверку, не было ли какой‑то аварии: вдруг какие‑то файлы не доставлены, вдруг где‑то порушилась оперативная память. На этот случай делаем сверку: всех миллиардов файлов в основном хранилище с миллиардами резервных.
Какие ещё тонкости здесь учтены: в таких задачах нужно было научиться асинхронно работать с файлами, потому что синхронные копирования тратят большое количество вычислительных ресурсов. Асинхронность может быть реализована в Java/Netty, в Rust/Tokio и даже в Python/PHP есть свои асинки. Такой код получается плохо читаемый, с монадическими конструкциями, но, к сожалению, без этого никуда. В нашем примере мы имеем в виду реализацию с Java/Netty.
В качестве маст‑хэва у нас предусмотрено:
асинхронная не блокирующая работа с сокетами: чтение событий из SQS‑очереди;
async‑работа с загрузкой файлов;
заливка из сетевого сокета в сетевой сокет без записи на диск.
Эта система простая, надёжная, проверенная годами. Но теперь эту же схему нужно было перенести в Россию.
Как мы переходили на резервирование между провайдерами
Итак, несколько лет назад мы научились резервировать данные в Amazon. Но поскольку данные российских клиентов важно хранить и резервировать в РФ, то вскоре нам понадобился переезд. И раз мы уходили из Amazon, то стали искать аналогичные услуги на отечественном рынке.
Вдобавок мы понимали, что хотим избежать возможного вендор‑лока. По этой причине рассматривалось сразу несколько российских облачных провайдеров, у которых были похожие решения: объектное хранилище и облачные функции. Yandex Cloud приглянулся в качестве решения для резервирования данных. В качестве основного хранилища было выбрано ещё одно облако.
В выбранном резервном хранилище хотелось реализовать уже проверенную годами схему.
Начало миграции в Yandex Cloud. Мы подключили стандартные клиенты Amazon к Yandex Cloud: поменяли ключи, эндпоинты, поменяли регион.
Стандартные амазоновские SDK сложны: там реализована многопоточность, асинхронная работа с сокетами — так что их решили взять уже готовыми и развернуть в Yandex Cloud. Всё завелось без проблем, и получилась такая схема:
В этой архитектуре используются реализованные на первом этапе паттерны:
Все события сначала ставятся в очередь.
Очередь разбирается воркерами в асинхронном режиме, не блокирующем сокеты. Для этого можно использовать 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 дисками, а можно по запросу тратить только те ресурсы, которые нужны, и не платить за «запас» вперёд.
Сейчас в таблице хранится несколько миллиардов записей, туда пишется несколько сот событий в секунду.
В самой YDB включён автоматический бэкап, это тоже было для нас важной деталью.
Позаботились о резервировании, позаботимся о мониторинге. Первое, что мы сделали после развёртывания — настроили алерты.
Вот что мониторили:
Отслеживали нагрузку на базу по сообщениям в полёте — мы не хотели обрабатывать больше 120 тысяч одновременно.
Отдельно смотрели число сообщений в очереди — если они не разгребаются, значит, у нас проблемы с обработкой сообщений из очереди.
Также было важно количество принятых сообщений в очередь, измерять нагрузку и трафик.
На основе этих данных создали дашборд. Оцените нагрузку:
В процессе миграции также пригодился мониторинг бакета.
Что ещё можно добавить в схему
У нас получилась готовая работающая система, но в любой схеме всегда есть что‑то, что хочется добавить и улучшить. Мы наметили несколько векторов возможного развития:
Сейчас репликация у нас работает только в одну сторону, но мы можем добавить всей системе больше устойчивости, если сможем развернуть процесс обратно. Для этого нам нужно будет создать в резервном объектном хранилище точно такие же триггеры на события, как создание или удаление файлов. Это можно сделать с помощью аналога AWS Lambda — Cloud Functions.
Также было бы полезно на лету анализировать некоторые потоки больших данных от клиентов: логи, трафик, бизнес‑события. В текущей реализации с классическими Message Queues это сложно сделать из‑за разных архитектур, но можно попробовать такой инструмент, как Data Streams.
В перспективе также хотим разместить несколько виртуальных машин в Compute Cloud для обслуживания клиентов Битрикс24. Эти ВМ могут пригодиться как серверы для подготовки и обработки данных.