Как я удалил clickstream, но его восстановили из небытия
Всем привет! Я Дмитрий Немчин из Т-Банка. Расскажу не очень успешную историю о том, как я удалил данные и что из этого вышло.
В ИТ я больше 12 лет, начинал DBA и разработчиком в кровавом энтерпрайзе с Oracle. В 2015 году познакомился с Greenplum в Т, да так тут и остался. С 2017 года стал лидить команду, потом все чуть усложнилось и команда стала не одна. Возможно, вы могли видеть меня как организатора Greenplum-митапов в России.
Но команда командой, менеджмент менеджментом, а руки чешутся…
Как я все удалил
Одним погожим зимним днем, 11 января 2024 года, в первый рабочий день года, произошла замечательная история. У нас есть кластер Hadoop, не очень большой (по меркам Bigdata), порядка пяти петабайт (общей capacity), и в нем в очередной раз закончилось место.
Исторически у нас в Т Hadoop используется не как DataLake, куда стекаются все данные компании, а скорее как «дача для данных». Там лежит то, что мы не хотим или не можем по разным причинам сложить в Greenplum. Таких данных не очень много, но на них бывают и важные процессы. И еще там лежат архивы, то есть иногда данные приходят сюда доживать свой век и умирать.
Конечно, я очень хотел помочь инженерам что-то с этим сделать.
С Data Governance в этом кластере, конечно, все плохо, мы не понимали, что чистить, и нашли большую папочку, которая называлась Amplitude. По названию можно догадаться, что это были наши кликстримы, собираемые когда-то одноименным сервисом.
Началось обсуждение в нескольких тредах. В одном говорили, что в этой папке лежит то, что никому не нужно, и можно легко удалить 200 терабайт. В другом треде говорили, что там лежат самые важные данные компании и ни в коем случае удалять и даже дышать на них нельзя.
Естественно, я посмотрел не туда и все удалил.
Через 20 минут признался коллегам, что снес 200 терабайт кликстрима, и позвал всех на мозговой штурм, как теперь исправить ситуацию.
Решили, что данные терять нельзя, мы все-таки дата-платформа. Будем делать что можем. Мы поняли, что восстановить данные неоткуда физически: я провел удаление мимо корзины, бэкапов нет.
Не все могут себе позволить бэкапить мультипетабайтные кластеры (привет и некоторая зависть компании «Одноклассники»). А источник (Amplitude) по некоторым независящим от нас причинам недоступен.
Прошел час беготни по индустрии со странными вопросами типа «А как бы нам бы поднять бы данные из ниоткуда?». Все смотрели на нас как на дурачков альтернативно одаренных и сочувственно говорили: «Ну все, ребят, попрощайтесь с ними, это конец».
И тут мы поняли, что у нас львиная доля процессов, работающих на HDFS, могут подождать, но при этом работающий HDFS-кластер — это точно потеря данных.
Да, мы решили остановить продакшен. Подняли в стороне небольшой кластерок из восьми нод общим объемом меньше петабайта и туда переключили все самое критичное. После этого начали искать способ восстановить данные.
Закономерный вопрос: если у вас места нет, чего раньше эти ноды не подняли? Они нужны были для другого, все было расписано.
Задача — восстановить данные
Опишем задачу восстановления. У нас есть набор parquet-файлов, parquet-файлы лежат на HDFS, HDFS кластерный. Как это выглядит: есть много серверов, на этих серверах есть куча дисков с локальными ФС, в нашем случае это XFS — и это нам пригодится.
Файл, который мы пишем в HDFS, может влезать в один блок файловой системы, а может в несколько. Каждый блок — это файл на диске, который можно увидеть через ls в консоли сервера. А если файл занимает больше одного блока HDFS, он просто пилится где-то посередине и раскладывается на несколько блоков (которые попадают на несколько серверов).
У HDFS есть репликейшен-фактор. У нас репликейшен-фактор был два. Дети, не делайте так в большом продакшене никогда!
Репликейшен-фактор должен быть три либо какой-нибудь erasure coding и вот эти вот всякие штуки. Но что было, то было. В итоге нам RF2 даже помог.
Чтобы восстановить данные, нам нужно было поднять сначала нижний слой. Файлы были удалены мимо корзины, мониторинг сообщал, что 200 терабайт освободилось.
Но фактически в современных файловых системах, как и в современных СУБД, если вы что-то удалили — никто ничего не удаляет. Прямо как ремонт: если вы его закончили, это не значит, что кто-то что-то закончил делать.
Место помечается как очищенное, его можно переиспользовать, пока никто ничего не записал. Соответственно, в теории можно успеть поднять то, что осталось на дисках в блоках, помеченных как удаленные.
Мы прошерстили интернеты и нашли на Гитхабе скриптик под названием xfs_undelete. Скрипт на TCL, на вид не очень сложный. Решили его внимательно изучить.
Он поднимает по некоторым введенным параметрам удаленные с дисков айноды: сканирует файловую систему, пытается найти айноды по заданным параметрам, если получилось — поднимает их в память, если они нормально прочитались — записывает их на диск. У него даже есть отдельный сабреддит с поддержкой — практически уровень энтерпрайзного ПО.
Мы попробовали позапускать скрипт на наших дисках — все работало. Но был ряд дополнительных сложностей (куда же без них).
Проблема: как поднять данные с 800 дисков
Нужно поднять данные не с одного диска, а с 800. И записать эти данные в тучу S3-бакетов (больше некуда было).
Мы написали сложную bash-магию, которая вылилась в три скрипта: подготовка, запуск и watchdog.
Скрипт подготовки из нашего инвентори собирает информацию о серверах и примонтированных дисках, создает S3-бакеты, разливает пакеты и монтирует S3-бакеты через S3-fuse. Это не лучший метод, но какой был: быстро, просто, а на скорость записи тогда смотрели в последнюю очередь.
Скрипт запуска собирал из inventory все точки монтирования для всех серверов, рассчитывал временные метки (он знал, когда я удалил данные, и отсчитывал от текущего момента разницу), собирал shell-однострочники для запуска xfs_undetele с нужными параметрами, раскладывал это на серверы и запускал. Ну и да, еще логировал свои действия.
Третий скрипт делал до четырех попыток перезапуска xfs_undelete и писал понятный лог, с которым можно было работать.
В итоге мы восстановили некое количество бинарных блоков и не особо выбирали, что можем восстановить. У нас был таймфрейм с 16:00 до 17:00 11 января. Мы пытались восстановить данные, но в это время (в указанный таймфрейм, пока мы бегали и думали, что делать) работала еще куча процессов — и в итоге мы подняли 1,1 петабайта вместо 200 терабайт.
Мы рассчитывали на 400 терабайт, потому что RF2: 200 удалили + 200 копия — 400 подняли. В итоге притащили кучу мусора и потеряли длину исходных файлов, потому что восстанавливались не начальные файлы, а блоки файловой системы. Блоки устроены так, что если записал чуть меньше блока, то в конце будут нули или вообще какой-то мусор.
Теперь у нас был фарш и, кажется, можно было делать котлеты. Но задача была, наоборот, провернуть фарш назад и найти мясо.
Кажется, это был хороший момент поднять продакшен, ведь мы подняли данные. Но это не так, потому что мы подняли не паркеты и вообще не доверяли тем блокам. В тот момент мы пока не знали, как восстанавливать паркеты. Если бы мы включили кластер, то гарантированно отрезали бы себе путь к перевыгрузке удаленных блоков или к возможности еще что-то сделать с данными, потому что их могли перетереть.
Расскажу, что у нас произошло из-за выключенного кластера. Пострадала аналитика звонков, охлаждение данных из нашего основного движка Greenplum, встала поставка данных из внешних источников NiFi и работа с А/В-тестами. Но главное, январь на дворе, а логистика — суровая штука. И у нас проблемы с подарками детям и сотрудникам из-за того, что доступа к нужным данным нет (они в том самом выключенном проде).
Проблема: как спасти production
Маленького HDFS на все не хватало, и мы подняли еще один HDFS на 5 петабайт.
И тут опять вопрос: отчего вы его раньше-то не подняли? А это были не наши серверы, нам их по доброте душевной дали коллеги, которые должны были там поднять для нас кое-что другое.
Подняли большой кластер, благо у нас был довольно свежий дамп метаданных HDFS. При помощи Питона, напильника и простого советского мата научились выгружать данные из выключенного кластера HDFS во включенный.
Мы перелили все самое критичное, что просили пользователи, на новый кластер и пошли дальше заниматься восстановлением.
У нас было 800+ бакетов S3, и нужно было найти там что-нибудь полезное. Поднялись на уровень HDFS и взяли бэкап меты. Из него мы нашли список исходных файлов и для части файлов нашли оригинальное расположение, которое нам пригодилось позже.
Проблема: как восстановить блоки HDFS и сделать из них parquet-файлы
Требовалось поднять нужные блоки HDFS из фарша и очистить их от мусора. А потом разобраться, где там паркеты, и восстановить их.
Сверху — parquet одноблочный, который влез в блок HDFS. В нашем случае это 512 мегабайт, и нам очень повезло, что это parquet.
Parquet — структурированный файл, у него есть четкая метка начала и метка конца: PAR1. При этом в конце файла есть метаданные, в которых указана структура файла, row-группы и их расположение и еще много чего полезного.
Нюанс вот в чем: если файлы небольшие, они влезают в блоки HDFS, они неразрывные, есть начало и конец. Все счастливы, все классно.
Но есть история, когда паркет не влез в один блок файловой системы и он занимает больше: два блока или триста — сколько угодно. Parquet где-то пилится, причем он может пилиться посередине, а может пилиться непонятно как.
Мы попытались все это восстановить. Для начала нам надо было хотя бы отфильтровать и разметить данные.
Алгоритм для одноблочных файлов:
Проверяем PAR1 в начале файла.
Проверяем PAR1 в конце файла.
Восстанавливаем исходную длину файла и обрезаем нули в конце.
Пытаемся прочитать.
Если в конце PAR1, по смещению с конца достаем мету паркета.
Считаем hash от файла для поиска дублей (RF2).
Алгоритм восстановления примерно такой же:
Проверяем PAR1 в начале файла.
Проверяем PAR1 в конце файла.
В метаданных находим правильное имя таблицы.
Проверяем правильную схему паркета.
Файл полностью читается (нет битых блоков внутри).
Алгоритмы для одноблочных файлов выглядят довольно просто. А вот дальше началось еще одно веселье — многоблочные паркеты. Там оказалось немного черной магии.
Конец паркета несложно найти, у него в конце PAR1 — читаем метку, это будет хвост.
Мы нашли интересную закономерность: внутри parquet-файла, в той части, где данные, есть magic numbers, но они нефиксированные и зависят от того, чем и как мы записываем эти паркеты.
Алгоритм восстановления:
Взять хвост паркета, там есть карта офсетов — расстояний между началами row groups.
Найти magic numbers.
Отсканировать все-все байты.
Получить для всех файлов, похожих на то, что нам надо, карту офсетов внутри этих файлов.
Наложить одно на другое и так собрать большие пакеты.
Попытаться прочитать то, что получилось.
Задача была не самая простая и не самая быстрая, но там, где это получилось, мы считали, что данные восстановились.
У нас были собранные паркеты в S3, теперь очередь данных в HDFS. Вроде ничего сложного, но с нюансом: нужно было восстановить структуру партиций.
Нужно было восстановить партиции, потому что код обработки заточен под структуру папок. Иначе обработка была бы неоптимальной. Ну и в целом — откуда данные удалил, туда и верни.
В данных обычно можно найти поле, по которому сделано партиционирование, но это не всегда так. Иногда внутри данных не остается полей, по которым разложены партиции, и это как раз наш случай.
Довольно много времени занял разбор данных, чтобы понять, как синтезировать те поля, по которым все было разложено, или хотя бы близкие к ним. Получилась некая аналитическая работа.
Например, в одной из таблиц были партиции по времени загрузки данных на сервер. Для большинства случаев это время очень близко к времени отправки данных клиентом, а оно у нас сохранилось внутри файлов.
По таймлайну мы начали в феврале — и сделали только к концу апреля. Пусть долго, но хотя бы разложили.
Проблема: нет половины данных
Мы проверили итоги — и вроде бы 90% минимум было по объему. Но аналитики сказали: «Ребята, извините, 45% восстановлено». Нам показалось, что совпадение какое-то странное.
Оказалось, что в эксельке, по которой ребята сверялись, всплыли неочевидные дубли. Строка Total подло закралась в середину файла, а не сидела себе в конце, как все остальные Total. И когда считали сумму нужных строк, данные фактически задвоились.
Что у нас получилось
Сначала о позитивном: у нас получилось поднять фарш и из фарша восстановить 90%. Какое-то время мы были счастливы: продакшен-процессы вернули, подарки детям и сотрудникам доехали — праздник не пострадал.
В процессе мы подняли пару кластеров HDFS. Казалось бы, нужно что-то удалить и сломать — и у нас тут же появятся мощности. Не надо так делать.
Но было много не очень хорошего:
Считали систему неважной. Hadoop где-то там далеко и заброшен, эдакая дача для данных — кладите и не будем разбираться.
Не было Governance и квотирования, поэтому у нас был RF2, и это нас спасло. Если бы был RF3, нам бы потребовалось 1,5 петабайта — и их точно некуда было девать.
Удаление мимо корзины и без уведомления — плохое решение.
Какие выводы сделали
Теперь поговорим о том, как этого можно было избежать. Тут я надеваю погоны капитана Очевидность:
Удалять данные только через корзину. Да, это чуть дороже, но нервы всем еще пригодятся.
Не делать на проде ничего без явных апрувов (лучше в git). Если есть важная система хранения, нужно стараться использовать инструменты управления данными, в которых нужны вторые руки для серьезных действий. Если нет готовых, то напишите. Это не панацея, но неплохой аварийный клапан.
Что мы сделали у себя нового для избежания подобных ситуаций
TTL для данных из коробки. Есть репозиторий, в который нам приносят yaml, в yaml указан путь владельца или группа, потому что владелец может уволиться или еще что-то с ним может произойти.
Три важных параметра:
Наличие подпапок — это важно, потому что у нас вся эта история заточена на определенную структуру папок или подпапок.
TTL — сколько дней хранить данные.
Откуда считать TTL — суперважный флаг, который может выстрелить в ногу, если его не сделать.
Почему это важно? Потому что вполне может быть картина, что грузили, грузили, грузили данные и потом перестали их грузить. И вот они год лежат без дела.
Если считать TTL год от сегодня, когда уже прошел год — мы все удалим. Если надо хранить последний год, а это вполне бывает по регуляторным соображениям, то вот этот флаг вас может как раз спасти.
Если yaml валидный, он проверяется на корректность того, что этот yaml правильный, yaml на схему. Прогоняется dry run при MR и пишет что-то вроде: «Я бы удалил такое, проверь, корректно или нет». Если корректно, выставляется в cron и по указанным правилам данные чистятся.
Но на семь дней данные всегда помещаются в корзину и там маринуются. Если семь дней прошло и никто не пришел — можно удалять. Если больше семи дней — извините. Можно, конечно, держать данные дольше, но не всегда это оправданно. Нам пока вроде семи дней хватает.
Квотирование — важная штука, которой у нас не было и которую мы очень хотели. В HDFS квотирование есть из коробки, можно по папочкам настроить квоты.
Квоты работают изолированно: если кончилась квота на запись в папку A, тому, кто в нее пишет, придет ошибка. При этом в папку B все продолжит писаться.
Но есть нюанс. Нам хотелось к этой штуке какого-то внятного управления (а не руками по консолям бегать), и мы написали gitops c yaml: путь, владелец, квота и какое-то описание.
Напоследок скажу еще пару слов по поводу «как можно было бы избежать», но это уже про процессы и про подумать.
Данные в дата-платформах растут, никаких исключений. Если данные в платформе не растут, что-то не так с платформой или с бизнесом.
Если ваша система не под EOL, то озаботьтесь ее расширением. Или хотя бы такой возможностью. Если вы считаете ее EOL / legacy / как хотите называйте, то убедитесь, что все вокруг считают так же. Желательно, чтобы была какая-то замена, но это отдельный вопрос.