
Привет, Хабр! Меня зовут Виктор Лучиц, я архитектурный лид в отделе инфраструктурной разработки рекламных технологий VK. Расскажу, как наша команда осуществила конвергенцию двух наших core-технологий, как справлялись с инцидентами и что в итоге получили.
Это не столько рассказ о самих технологиях, сколько попытка частичной систематизации нашего опыта работы со сложными системами. Этим опытом нам хотелось бы поделиться с читателями Хабра, и надеемся, что он покажется вам полезным.
О том, как работает хранилище данных, о котором далее пойдёт речь, вы можете более подробно почитать в ранее выходившей на Хабре статье. Внутри компании это высоконагруженное распределённое key-value хранилище называется «Tarantella», оно построено на основе Tarantool. По сути это надстройка над большим количеством экземпляров Tarantool, которая позволяет распределённо хранить данные по ключам, получать их как отдельными записями, так и списками. Система отслеживает доступность данных, позволяет соблюдать факторы репликации для них и поддерживает распределённость по data-центрам для обеспечения отказоустойчивости. Можно для разных целей запускать несколько не связанных между собой кластеров. Например, один кластер может хранить данные для таргетингов рекламы, а другой — данные с аналитикой. Самый большой кластер на момент переезда имел объём 150 ТБ, при этом на экземпляры с данными приходилось порядка 30 миллионов запросов на чтение в секунду. Показатели надёжности работы этого кластера влияют на выручку рекламного департамента, что называется, в режиме реального времени.
One-Cloud — внутреннее облако VK, о котором коллеги уже тоже рассказывали.
О конвергенции этих двух технологий дальше и пойдёт речь.
Что мы выигрываем от переезда в облако
Удобство масштабирования. Запуски новых инсталляций Tarantool на bare metal исторически сопряжены с определёнными сложностями. Например, если закончились ресурсы на железной машине, то вам нужно докупить новую, и нужно ещё найти под неё место в стойке, дождаться установки и так далее. Или обратная ситуация: у вас освободилось место, и вы хотите им с кем-то поделиться. Когда у вас система управляется централизованно на bare metal-хостах, всё это может оказаться достаточно сложно.
Уплотнение ресурсов. Появляется возможность делиться ресурсами в периоды, когда кластер менее загружен. Например, ночью, когда загрузка кластера минимальна, вы можете предоставлять свои вычислительные ресурсы для аналитических задач, которые планировщик One-Cloud посчитает возможным у вас разместить.
Автоматизация и управляемость. В прошлой stateful-парадигме мы полагались на то, что на локальном диске есть и wal-логи Tarantool, и его снапшоты, чтобы в случае аварии восстановить данные в полуручном режиме. В новой реальности мы автоматизируем все процессы резервного копирования и восстановления, исходя из худших предпосылок. Если наихудшие предпосылки не случаются, то всё работает как раньше.
История одного инцидента
Начну рассказ с небольшого инцидента, который случился ещё на этапе планирования. Рассказать о нём хочется по двум причинам: во-первых, все любят истории про то, как кому-то другому было больно. А во-вторых, инцидент повлиял на то, как мы в итоге сформировали свой подход к миграции. Стоит напомнить читателям, что в системе Tarantella в том числе есть три важных глобальных сущности:
Мастер, который хранит топологию — отдельно запущенный кластер Tarantool, в котором хранится список экземпляров с данными и их состоянием, а также разметка данных по сегментам: какой сегмент какому экземпляру с данными соответствует.
Экземпляры с данными, которых может быть очень много, они обрабатывают запросы клиентов.
Клиенты — потребители, запрашивающие данные. Для этого они при старте подключаются к мастеру, получают топологию и отслеживают её изменения с мастера.

Алгоритм доставки топологии клиентам
Вначале клиент получает сериализованную резервную копию топологии из внешнего хранилища, стартует с ним, а дальше с этой резервной копии, зная его позицию в мастере, получает только обновления. Если резервная копия недоступна, либо произошла ошибка чтения с мастера, либо изменился идентификатор мастера (или, например, он поменял IP-адрес), то чтение всей топологии начинается с нуля с мастера, минуя резервную копию. Это была заглушка на случай каких-то непредвиденных аварийных состояний.
Надо сказать, что в облако переезжали не только кластеры в составе мастера и данных, но также параллельно и наши клиенты. При этом они переезжали в One-Cloud постепенно и, как правило, предусматривалась ситуация, что может потребоваться откат. Например, у одной из команд рекламы уже было запущено 100 клиентов. Она, мигрируя в One-Cloud, запускала дополнительно 100 клиентов уже там, и таким образом для мастера Tarantella количество клиентов увеличивалось ещё на 100. Такая ситуация продолжалась некоторое время и с разными командами, и на метрики производительности самого крупного кластера не влияла из-за имевшегося запаса прочности и относительно мелкого шага изменений. Вместе с тем количество соединений к мастеру постепенно увеличилось более чем вдвое.
И это привело к инциденту: оказался недоступен мастер самого крупного кластера рекламы, который циклично съедал всё отведённое ему процессорное время и память, а затем убивался OOM-killer’ом. После рестарта ситуация мгновенно и циклично повторялась. Недоступность мастера приводила к тому, что топология кластера перешла в состояние read-only, а клиенты не могли получать обновления.

Как оказалось, причина взрыва по памяти была в том, что один из клиентов массово сделал рестарт, и описанный выше алгоритм получения топологии дал сбой. Постепенно росло количество клиентов, получавших timeout в ответ на запрос к мастеру и дальше пытавшихся получить топологию полностью, минуя резервную копию. Пока таких клиентов было немного — всё работало, но когда произошёл массовый рестарт, всё резко стало плохо, так как отдача всей топологии требовала много памяти на каждый запрос.
Вот так казавшийся надёжным предохранитель в алгоритме при определённых обстоятельствах оказался тем самым ружьём, которым мы выстрелили себе в ногу. Не помогла даже заложенная в запросы топологии пагинация, доставка чанками и экспоненциальные backoff’ы на клиентах.
Риски из-за недоступности мастера
Главным образом они заключаются в невозможности инсталляциям Tarantool сообщать об изменении своего состояния и корректировке топологии, что является базовой необходимостью. Мы могли так жить какое-то время благодаря тому, что у нас есть резервная копия во внешнем хранилище, но срок хранения резервной копии был ограничен, и рано или поздно это привело бы к тому, что клиенты не смогли бы подключаться к кластеру.
Как мы устраняли инцидент
Временно уменьшили количество клиентов в One-Cloud, благо, оно позволяет легко это сделать. И стали срочно вносить изменения в код:
Добавили клиенту возможность подключаться к случайной реплике мастера: у него как минимум две реплики в режиме hot standby в разных data-центрах, и мы добавили возможность подключаться к ним для получения топологии, что снизило нагрузку на чтение на мастере в несколько раз.
При ошибке мы больше не пытаемся получить всё с нулевой точки, а всё равно сначала идём за резервной копией. Казавшийся ранее разумным алгоритм дал сбой и стал выстрелом себе в ногу.
Добавили пагинацию в доставке списка экземпляров с данными, который отдаёт мастер. До этого пагинация была предусмотрена только для топологии. На мелких кластерах это абсолютно лишнее, но в супербольших уменьшает время блокировки.
И наконец, увеличили срок хранения бэкапа топологии с 24 часов до недели. Ранее казавшийся разумным лимит оказался проблематичным: он вызывал ощущение цейтнота у команды, которая занималась устранением инцидента (разумеется, поздно ночью), и в итоге оказался дополнительным фактором, который нужно было учитывать.
Возвращаясь к миграции в One-Cloud
Любое масштабное изменение несёт в себе риски. В том числе во время миграции базы — это потеря уникальных данных и их согласованности, простой для клиентов. У этих рисков могут быть причины, которые мы решили категоризировать для того, чтобы лучше понимать, как с ними работать. Есть разные способы категоризации рисков, в том числе и тот, который любит применять экс-министр экс-министр обороны США Дональд Рамсфелд и который он упомянул в одном из своих выступлений:
Reports that say that something hasn't happened are always interesting to me, because as we know, there are known knowns; there are things we know we know. We also know there are known unknowns; that is to say we know there are some things we do not know. But there are also unknown unknowns — the ones we don't know we don't know.
Я всегда с большим интересом читаю отчёты о том, что могло случиться, но было предотвращено, поскольку мы знаем, что существуют известные известности: вещи, о которых мы знаем, что мы их знаем. Мы также знаем, что существуют известные неизвестности, то есть что есть определённые вещи, о которых мы ничего не знаем. Но есть также и неизвестные неизвестности — те вещи, о которых мы не знаем, что мы их не знаем.
Таким образом, поделить риски в процессе нашей миграции можно на следующие категории:
Известные известные: отражены в существующем мониторинге, ведь он появился в ходе эксплуатации.
Неизвестные известные: риски, о которых мы предполагаем, но не знаем их масштаб: например, legacy-код, который давно не менялся.
Известные неизвестные: то, о чём мы знаем, что не знаем: в основном человеческий фактор, от которого надо защищаться.
Неизвестные неизвестные: слепые зоны — то, что, скорее всего, неизбежно всплывёт только в процессе и не относится к предыдущим категориям. Тут вы ничего уже не можете сделать — только минимизировать ущерб и получить новый опыт.
Как мы управляли рисками
Постоянно актуализировали уже существующий мониторинг под новые реалии. Любое изменение должно было быть отражено в новых правилах мониторинга.
Из всех главных категорий риска приоритет мы отдали главным образом потере данных. То есть можно было допустить некий разумный простой, но важно было не потерять никаких данных. Временную потерю согласованности также можно преодолеть.
Использовали подходы из Defensive Programming. Общее правило гласит, что на каждом этапе, где что-то может пойти не так, нужно считать, что это обязательно случится. Ниже я приведу конкретные примеры этих подходов.
Исходя из этого мы перешли к проектированию алгоритмов миграции.
Адаптация системы к жизни в One-Cloud
Для отказоустойчивости каждый кластер должен размещаться в нескольких data-центрах, а в пределах одного data-центра — в нескольких контейнерах. Контейнеры живут в условных пространствах имён, называемых очередями. Мы решили, что каждый кластер будет размещаться в своей отдельной очереди — это позволит установить однозначное соответствие экземпляра Tarantool кортежу (data-центр, очередь, ID контейнера внутри очереди). Это позволит нам хранить и находить резервные копии снапшотов Tarantool во внешнем хранилище, из которого мы будем в полуавтоматическом режиме получать снапшоты и закачивать их обратно. Мы выделили мастеры в отдельную подочередь, чтобы они не мешались с экземплярами с данными, и решили, что переезжать они будут первыми, памятуя о том, что нам нужен больший запас прочности из-за инцидента, описанного в начале статьи. Поэтому мы дополнительно решили, что сделаем для мастера пять синхронных, разнесённых по разным data-центрам реплик.
В общих чертах скрипт запуска контейнера с Tarantool в One-Cloud выглядит так:
В самом начале проверяем по кортежу (data-центр, очередь, ID контейнера внутри очереди), есть ли для него резервная копия инсталляции Tarantool во внешнем хранилище. Элементы кортежа берутся из окружения.
Если есть, то выкачивает её, если же нет, то запускает новую инсталляцию.
По возможности запускаемся на persist-дисках, которые переживают перезапуск контейнера, чтобы не скачивать каждый раз снапшот из внешнего хранилища (также в One-Cloud помимо persist-дисков есть ещё cache-диски, живучесть которых не гарантируется при перезапуске контейнера).
Приведу некоторые приёмы из defensive programming, которые мы использовали при написании наших скриптов, на примере работы с внешним хранилищем снапшотов:
При загрузке снапшота во внешнее хранилище недостаточно полагаться на HTTP-код ответа 200, нужно проверять размер файла отдельным запросом и считать загрузку неуспешной. Аналогично при загрузке снапшота из хранилища.
Не полагаться на то, что список удалённых файлов может вернуться в отсортированном порядке, хоть это и предусмотрено контрактом, а сортировать заново на клиенте, особенно, если мы собираемся удалять файлы.
Если попадаем в ветку запуска инсталляции с нуля, то мы должны быть железобетонно уверены, что снапшота во внешнем хранилище нет — лучше не запуститься совсем, чем ошибочно создать новую инсталляцию Tarantool, потому что на него сразу же польются данные с других инсталляций из-за начавшейся перебалансировки данных и попытки системы выровнять долю занятости экземпляров.
Первый вариант алгоритма миграции
Так выглядел наш первый подход к алгоритму переноса:
Пишем новую обвязку в виде скриптов и мониторинга, по возможности перенося всё как есть.
Создаем в One-Cloud нужное количество экземпляров, покрывающих по объёму данные на железе: один на каждую инсталляцию Tarantool.
Отмечаем инсталляции на железе как выведенные из эксплуатации.
Система переносит все данные в фоне. О том, как это происходит, рассказано в статье про Tarantella.
Profit!
Преимущества такого подхода:
Можно отдельно мигрировать мастеры и инсталляции с данными.
Если нагрузка будет увеличиваться, то будем просто поднимать новые реплики, что One-Cloud позволяет легко сделать.
Пользуемся тем, что система легко адаптируется к изменению топологии: легко добавлять и выводить из эксплуатации инсталляции.
Сохраняем устойчивость к отказам отдельных data-центров.
Недостатки такого подхода:
Требуется заранее создать ресурсы для переезда в облако, то есть в моменте мы можем потреблять вдвое больше ресурсов: bare metal + облачные.
Также в процессе переноса первых более мелких кластеров мы выяснили, что перебалансировка при переезде всего кластера происходит медленно.
Такой вариант подходит только для кластеров небольшого и среднего размера, и нам потребовался альтернативный подход для больших кластеров.
Альтернативный алгоритм миграции для больших кластеров
Помним о том, что у каждой инсталляции с данными есть уникальный идентификатор (UUID), который генерируется при первом запуске Tarantool, считаем объёмы по data-центрам, размечаем по ним UUID’ы и присваиваем каждому экземпляру ID в One-Cloud. Выбираем жертвы из числа bare metal, а далее:
Тушим их и загружаем их снапшоты во внешние хранилища в соответствии с кортежем (data-центр, очередь, ID).
Запускаем экземпляры в One-Cloud, рассчитываем, что они при запуске скачают снапшоты и запустятся. Для клиентов кластера это будет выглядеть как простая смена IP и порта у инсталляции с данными, что Tarantella поддерживает из коробки.
Определяем приоритеты ресурса: в каком порядке переносим кластеры, какие ресурсы и в каком объёме нужно выделить.
Этот алгоритм мы успешно применили на самых крупных кластерах, и первое время всё выглядело просто замечательно!
И всё же что-то пошло не так
Сыграло то самое известное неизвестное имени Рамсфелда. Прошло какое-то время после миграции первого большого и важного кластера, и вроде бы всё работало. Только иногда на некоторых экземплярах наблюдались пики потребления CPU, которые были распределены по времени, загадочно выглядели и изредка приводили к таймаутам у клиентов. С течением времени пики стали учащаться и становиться продолжительнее, на некоторых экземплярах наблюдался рост потребляемой памяти, а далее — смерть контейнера после её исчерпания. Дальше следовал автоматический рестарт, и всё вроде как продолжало работать нормально.

В какой-то момент клиенты стали жаловаться на работу кластера, поскольку уходы экземпляров участились и стали накладываться по времени. Причины были неочевидны, и мы потратили на анализ ситуации долгие часы. В итоге выяснилось, что это был баг в фоновом скрипте мониторинга, который ожидал увидеть конкретную строчку в генерируемом конфигурационном файле, а содержание этого файла незначительно поменялось во время переезда: буквально добавилась «невинная» точка с запятой. При этом провинившийся cron-скрипт был написан много лет назад по итогам другого инцидента, после чего вскоре про него все забыли. Скрипт не проверял данные, которые он получал от конфига, а после того как делал своё чёрное дело, за миллисекунды загружая Tarantool через запрос вида SELECT * прекращал потреблять процессорное время. Распределённость же падений по времени на разных экземплярах объяснялась тем, что в начале скрипта стоял sleep со случайным аргументом.
Исправление оказалось технически довольно простым, хоть и потребовало обновления всех экземпляров.
Прочие доработки
Везде включили режим, при котором долго отсутствующий экземпляр помечается как offline, после чего начинается авторепликация данных с оставшихся инсталляций для сегментов, которые на ней хранятся — это нужно для поддержания режима, когда контейнеры могут появляться и исчезать без участия человека, что нормально для облачной среды.
Интегрировали интересную фичу One-Cloud, которая гарантирует, что планировщик не будет тушить контейнер по служебным нуждам, если он рапортует свой статус как
PREFAIL— таким статусом мы стали отмечать, если контейнер хостит экземпляр с последней копией хотя бы одного сегмента данных.Для факторов репликации > 2 или там, где потеря актуальных данных не сильно страшна, допустили работу с cache-дисками. В таком случае экземпляр с данными всегда запускается со снапшота из внешнего хранилища, который не всегда может быть суперсвежим.
Реализовали режим «Повреждённый сегмент»: для такого сегмента выполняется принудительная внеочередная синхронизация данных между экземплярами. Режим автоматически включается для всех сегментов на экземпляре, которые поднимаются со «старого» снапшота, а «старым» снапшотом мы считаем тот, который устарел на 6 и более часов. Таким образом кластер приоритетно лечит сам себя.
Итоги
Весь процесс занял у нас около полугода с перерывами на другие задачи. Подводя итоги, численные показатели выглядят следующим образом:
Мигрировано в One-Cloud 8 кластеров.
Общий объём перенесённых данных: > 300 ТБ.
Передано в общее пользование в облако около 200 единиц серверного оборудования.
За счёт уплотнения и оптимизации ресурсов сэкономлено 8000 виртуальных ядер.
Количество зарегистрированных инцидентов — 1.
Помимо того, что мы натерпелись много страху, мы смогли существенно автоматизировать и унифицировать многие части системы, ранее управлявшиеся вручную. Теперь выпадение конкретного экземпляра с данными не является чем-то экстраординарным, и в подавляющем числе случаев решается автоматически.
В то время в команде оказалось много новичков, для которых этот проект был первым опытом работы со сложными системами подобного масштаба. И тем не менее, команде удалось сплотиться, выработать и реализовать несколько рабочих планов под разные сценарии, а за всё это время случился только один инцидент среднего приоритета, что говорит о высоком профессионализме команды. Спасибо вам всем!
Ввиду обзорного формата статьи многие детали и кровавые подробности пришлось опустить, но надеюсь, что описанный опыт показался вам полезным.