-Спаситель, обезвредивший бомбу расходов на БД: знакомство с многоуровневым хранением данных на базе S3-
Больше сервисов — и незаметно подкрадывающаяся бомба расходов на БД
В наших недавних проектах количество сервисов росло, и мы добавляли множество новых функций. В результате нам пришлось столкнуться с экспоненциальным ростом объёма данных и пропорциональным увеличением расходов на инфраструктуру.
Особенно стремительно рос объём логов из-за того, что мы использовали архитектуру микросервисов (MSA). Даже один пользовательский запрос запускал через шлюз цепочку внутренних распределённых вызовов между многочисленными микросервисами. Чтобы отслеживать транзакции и контролировать их во всей системе, нам приходилось тщательно записывать распределённые логи для каждого независимого сервиса. В результате одновременно генерировались огромные объёмы логов — несравнимые с тем, что возникало бы при монолитной архитектуре.
Изначально для хранения этих распределённых логов мы выбрали MongoDB. В среде MSA каждый сервис может использовать немного отличающийся формат логов, а гибкая структура документов MongoDB избавляла нас от необходимости беспокоиться об изменениях схемы. Ещё одним важным преимуществом была высокая скорость записи, позволявшая обрабатывать огромные объёмы логов, одновременно поступавших из множества микросервисов, а также удобная интеграция с панелью мониторинга.
Однако по мере роста числа микросервисов и быстрого расширения сервисов объём логов, составлявший сотни ГБ в месяц, вскоре вырос до нескольких ТБ и быстро исчерпал ёмкость базы данных. Ещё серьёзнее была проблема производительности. Чтобы поддерживать её на должном уровне, MongoDB использует механизм кеширования WiredTiger, который хранит индексы и часто используемые данные в памяти (RAM). По мере увеличения объёма логов росли и индексы. Когда их размер превысил ограниченный объём RAM, последовала разрушительная цепная реакция.-
-
Нехватка RAM и узкие места дискового ввода-вывода: когда памяти стало не хватать, базе данных пришлось гораздо чаще читать и записывать страницы на диск, что сразу же привело к задержкам запросов в рабочих сервисах.
-
Набор реплик утроил расходы: для обеспечения доступности мы настроили кластер из трёх инстансов EC2 с высокопроизводительными SSD. Это означало, что для хранения 1 ТБ логов нам фактически приходилось платить за 3 ТБ дорогостоящего хранилища.
Анализ нашей работы показал, что большинство запросов к логам ограничивались данными за последние три дня. К более старым логам обращались лишь изредка — например, для разбора инцидентов постфактум или проверок на соответствие нормативным требованиям. Мы решили отделить старые данные, занимавшие дорогостоящее пространство в БД и RAM, чтобы значительно снизить расходы на инфраструктуру. Нашим решением стало внедрение многоуровневого хранения данных — архитектуры, сочетающей высокую производительность и экономию.
Проектирование архитектуры многоуровневого хранения данных
Главное в многоуровневом хранении данных — выбирать место хранения с учётом частоты доступа к данным и требований бизнеса, а не помещать всё в одну систему хранения. Чтобы максимально снизить расходы и при этом свести к минимуму операционные издержки, мы упростили архитектуру и разделили хранилище всего на два уровня — горячий и холодный, — вместо сложной многоуровневой структуры:
- Горячий уровень (последние 30 дней): высокопроизводительный уровень для отладки в реальном времени
-
Хранилище: MongoDB
-
Назначение: мониторинг ошибок в реальном времени, отслеживание проблем в недавно развёрнутых функциях и распределённая трассировка сервисов MSA
-
Подход к проектированию: данные за последний месяц — это «горячие» данные, к которым команды разработки и эксплуатации обращаются чаще всего, чтобы реагировать на инциденты и отлаживать проблемы. Поэтому производительность для нас — главный приоритет.
- Холодный уровень (от 31 дня до 1 года): экономичный уровень для соблюдения нормативных требований и архивирования
-
Хранилище: AWS S3 Standard
-
Назначение: отслеживание редких ошибок, обнаруженных несколько месяцев назад, долгосрочный статистический анализ и прохождение проверок безопасности и юридических аудитов
-
Подход к проектированию: через месяц логи становятся «холодными», поскольку частота запросов к ним резко снижается. Мы удаляем все индексы для этого уровня и храним данные в сжатых файлах в S3 Standard. В S3 Standard нет отдельных расходов на индексацию данных, а стоимость единицы хранилища в 10–25 раз и более ниже, чем в MongoDB.
Главное инженерное преимущество этой двухуровневой архитектуры — полное разделение хранения и вычислений. В прежней архитектуре увеличение объёма логов означало необходимость наращивать не только дисковую ёмкость, но и ресурсы самого сервера MongoDB, чтобы справляться с постоянно растущими индексами. Иными словами, мы платили за дорогие вычислительные ресурсы и память в комплекте просто ради «хранения» данных. Переместив данные старше 30 дней в S3 — бессерверное объектное хранилище, — мы получили возможность масштабировать эти два ресурса независимо друг от друга. Теперь MongoDB хранит только предсказуемый объём данных за последние 30 дней, обеспечивая максимальную производительность при минимальных фиксированных расходах. А для нескольких терабайт исторических данных мы можем использовать недорогое хранилище S3 практически неограниченного объёма, максимально повышая масштабируемость нашей инфраструктуры.
Создание конвейера автоматизации
Даже самая идеальная архитектура не будет устойчивой, если инженерам придётся вручную перемещать и удалять данные. Мы создали автоматизированный конвейер обработки данных, который безопасно удаляет из MongoDB логи старше 30 дней и архивирует их в S3 Standard. Конвейер состоит из следующих основных этапов:
-
Найти в MongoDB логи старше 30 дней и обработать данные
-
Создать резервную копию данных в S3 и настроить её автоматическое удаление через год
-
Удалить данные, для которых создана резервная копия, из MongoDB
В реальной рабочей среде за один день могут поступить десятки миллионов записей логов. Если загрузить все эти данные в память сразу, это немедленно приведёт к сбою OOM (Out Of Memory). Чтобы снизить нагрузку на память, мы применили следующие стратегии:
-
Постраничная выборка на основе курсора: безопасно получать данные пакетами по несколько тысяч записей (2 000 записей).
-
Потоковая запись на диск: вместо того чтобы хранить считанные из БД данные в памяти, сразу передавать их в файловую систему и записывать во временный локальный файл (Temp File).
-
Пакетное удаление: собирать идентификаторы перенесённых записей и безопасно удалять их пакетами по 10 000, чтобы не создавать нагрузку на диск.
Ниже приведён код конвейера миграции, реализующего этот основной рабочий процесс.
private <T> void executeLogMigrationPipeline(...) {
List<String> logIds = new ArrayList<>();
// 1. 메모리 오버헤드를 방지하기 위한 임시(Temp) 파일 생성
Path tempFile = Files.createTempFile(filePrefix, ".json");
// 2. Jackson SequenceWriter를 활용해 디스크로 즉시 스트리밍 (JSON Array 구조)
try (var fileOut = Files.newOutputStream(tempFile);
var seqWriter = objectMapper.writer().writeValuesAsArray(fileOut)) {
int pageCounter = 0;
int pageSize = 2000;
while (true) {
// DB에서 정해진 배치 사이즈만큼 순차 조회
Pageable pageable = PageRequest.of(pageCounter++, pageSize, Sort.by("registrationTime"));
List<T> logs = logFinder.apply(targetEpochMillisToMove, pageable);
if (logs.isEmpty()) break;
// ID 수집 및 파일에 즉시 기록 (메모리 점유 방지)
logIds.addAll(logs.stream().map(idExtractor).toList());
LogHelper.appendLogsToJsonStream(logs, seqWriter);
if (logs.size() < pageSize) break;
}
}
// 3. S3 스토리지(Depot)로 안전하게 파일 업로드
if (!logIds.isEmpty() && Files.exists(tempFile)) {
depotProxy.uploadDTSPFiles(remoteFilePath, new FileSystemResource(tempFile.toFile()), targetEpochMillisToRemoval);
// 4. 대량의 DB 쓰기 부하를 방지하기 위해 10,000건씩 쪼개서 삭제(Bulk Delete)
int deleteBatchSize = 10000;
for (int i = 0; i < logIds.size(); i += deleteBatchSize) {
List<String> subBatchIds = logIds.subList(i, Math.min(i + deleteBatchSize, logIds.size()));
batchRemover.accept(subBatchIds);
}
}
// 5. 작업이 끝나면 로컬 임시 파일 자원 해제 (Retry 로직 포함)
cleanUpTempFile(tempFile);
}
Благодаря использованию обобщённых типов (T) и функциональных интерфейсов (BiFunction, Consumer) нам также удалось сделать логику того же конвейера миграции пригодной для повторного использования с разными типами и форматами логов. Теперь MongoDB хранит только логи за последние 30 дней, поэтому разросшиеся ранее индексы легко помещаются в доступный объём RAM. Это полностью устранило узкие места дискового ввода-вывода и улучшило среднее время отклика запросов (Latency) в рабочей среде. Кроме того, мы сократили расходы на хранение данных в БД, которые прежде были значительной статьёй затрат. Если сравнить стоимость за ТБ, S3 Standard оказался несравнимо дешевле нашего кластера MongoDB, позволив существенно сократить общие расходы на инфраструктуру логирования.
Заключение
Когда многие сервисы быстро развиваются, команды часто прибегают к быстрому решению: масштабируют базу данных по мере роста объёма данных. Однако ценность данных для бизнеса и частота обращений к ним неизбежно меняются со временем. Внедрение многоуровневого хранения данных на базе S3 было не просто способом немного сократить расходы — это был опыт архитектурного устранения несоответствия между требованиями бизнеса и техническими издержками. Самое дорогое и мощное хранилище не всегда оказывается правильным выбором. Понять природу и «температуру» данных в системе и разместить их там, где им место, — возможно, именно в этом заключается настоящая роль инженера, которому предстоит обезвредить бомбу расходов на инфраструктуру. Надеюсь, эта статья будет полезна всем, кто сталкивается с ростом объёма данных и расходов на инфраструктуру.
Hustle Paul