Дисконт Spot инстансов достигает девяноста процентов от цены обычной машины, и именно поэтому вычислительные фермы рендеринга, пакетные обработчики и распределённые сборки охотно живут на них. Взамен облако оставляет за собой право забрать машину, когда свободной ёмкости в регионе становится мало, и предупреждает об этом всего за две минуты. Две минуты это очень мало для человека и вполне достаточно для автоматики, если наблюдение за уведомлением и процедура корректного завершения построены заранее. Разберём, как устроено само уведомление, как построить наблюдателя на systemd и что успеть сделать в отведённое окно.
Почему Spot дешевле и когда облако забирает машину
Экономика Spot строится на продаже свободных мощностей: облако сдаёт незанятые машины с большим дисконтом, но оставляет за собой право востребовать их в любой момент, когда спрос со стороны обычных инстансов вырастает. Цены при этом плавают вслед за предложением, и долгосрочное планирование бюджета строится на среднем значении, а не на фиксированной ставке. Отсюда правило выбора нагрузки: Spot подходят всему, что разбивается на независимые порции работы и переживает потерю отдельной машины без последствий.
Кандидаты на изъятие выбираются не по злому умыслу, а по внутренней логике распределения ёмкости, поэтому предсказать конкретную машину заранее невозможно. Возможна и обратная ситуация, когда цену подняли и держать ферму стало невыгодно: механизм уведомления в этом сценарии не нужен, ведь владелец останавливает машины сам. Именно поэтому процедура завершения важна в обе стороны: и при изъятии облаком, и при плановом уменьшении фермы владельцем. Устойчивость фермы повышает разнообразие: если партия машин собрана на разных семействах железа и распределена по зонам доступности, изъятие затрагивает их неравномерно, и хозяйство продолжает работать, пока процедура завершения разбирается с пострадавшей частью. Ставка на один тип машины в одной зоне превращает любое неудачное стечение спроса в остановку целой службы.
Уведомление о прерывании в сервисе метаданных
Канал оповещения работает через сервис метаданных самой машины. За две минуты до изъятия в метаданных по специальному адресу появляется запись о планируемом действии и времени события. До этого момента обращение к этому адресу возвращает пустой ответ, и именно по переходу из пустого состояния в заполненное наблюдатель и ловит сигнал. Тот же механизм публикует события и в облачной шине событий, откуда их можно направить в очередь или функцию, но вариант с чтением метаданных на самой машине универсальнее, потому что не требует дополнительной инфраструктуры вокруг.
Начиная со второго поколения сервиса метаданных, обращения защищены сеансовым токеном: сначала машина получает короткоживущий токен, и уже с ним читает интересующие поля. Такая схема закрывает целый класс ошибок конфигурации, когда посторонний код на машине незаметно собирал чувствительные данные из открытого ранее сервиса. Наблюдателю нужно выполнить два обращения: получить токен, затем с ним опросить адрес уведомления.
TOKEN=$(curl -s -X PUT "http://169.254.169.254/latest/api/token" \
-H "X-aws-ec2-metadata-token-ttl-seconds: 21600")
ACTION=$(curl -s -H "X-aws-ec2-metadata-token: $TOKEN" \
http://169.254.169.254/latest/meta-data/spot/instance-action)
Пустой ответ означает спокойную жизнь, заполненный означает, что до изъятия осталось меньше двух минут, и пора действовать.
Наблюдатель на systemd с опросом метаданных
Самый надёжный наблюдатель это маленькая служба на самой машине, которая опрашивает метаданные каждые несколько секунд. Частота опроса выбирается из простого расчёта: окно в две минуты при пятисекундном интервале даёт около двух десятков проверок до наступления события, и даже несколько пропущенных циклов не приводят к пропуску сигнала. Служба оформляется юнитом, который стартует вместе с машиной и перезапускается при сбое.
[Unit]
Description=Spot interruption watcher
After=network-online.target
Wants=network-online.target
[Service]
ExecStart=/usr/local/bin/spot-watch.sh
Restart=always
RestartSec=3
[Install]
WantedBy=multi-user.target
Внутри сценария наблюдателя три части: цикл опроса, реакция на сигнал и страховка. Реакция это вызов процедуры корректного завершения, о которой речь ниже, а страховка это ограничение времени всей процедуры, чтобы даже зависший слив соединений не съел отведённое окно целиком.
#!/bin/bash
while true; do
TOKEN=$(curl -s -X PUT "http://169.254.169.254/latest/api/token" \
-H "X-aws-ec2-metadata-token-ttl-seconds: 300")
ACTION=$(curl -s -H "X-aws-ec2-metadata-token: $TOKEN" \
http://169.254.169.254/latest/meta-data/spot/instance-action)
if [ -n "$ACTION" ]; then
logger "Spot interruption: $ACTION"
/usr/local/bin/graceful-drain.sh
systemctl poweroff
fi
sleep 5
done
После отправки машины в остановку цикл уже не имеет значения, поэтому финальной командой становится выключение. Область ответственности службы на этом заканчивается, и дальше работает сама процедура слива.
Слив соединений и сохранение состояния перед остановкой
Сама процедура завершения сильно зависит от роли машины, но каркас одинаков, и на него легко набрать конкретику:
- наблюдатель получает уведомление и помечает машину как уходящую в очереди задач или в реестре служб;
- балансировщик выводит узел из ротации, и новые запросы перестают на него попадать;
- worker завершает текущую порцию работы и подтверждает обработку сообщения;
- накопленное состояние выгружается в постоянное хранилище: база, объектное хранилище, общий том;
- журналы закрываются и отправляются в централизованное хранилище логов;
- машина выключается штатной командой, и облачный учёт прекращает начисление.
Ключевое здесь пункт про подтверждение обработки: сообщество очередей прощает потерю машины ровно до тех пор, пока сообщение, взятое в работу, не считается обработанным. Отсюда практическое правило для воркера: брать новую задачу только если окно позволяет её закончить, а длинные порции дробить на чекпоинты. Для отрисовки кадра, статистической выкладки или сборки модуля это означает сохранение промежуточного результата каждые несколько минут, чтобы потеря машины откатывала работу на минуты, а не на часы.
Состояние удобно выгружать по принципу одного артефакта: один архив с рабочими данными и манифестом, записанный в постоянное хранилище под предсказуемым именем. Такой артефакт затем подхватывается либо новой машиной фермы, либо плановым процессом обработки, и никакая ручная сборка результатов не требуется. Для веб служб к этому добавляется корректное обращение с сигналами: приложение, получившее сигнал завершения, перестаёт принимать новые соединения, дорабатывает существующие в пределах таймаута и только затем закрывается. Если приложение этого не умеет, его оборачивают прослойкой, которая умеет, потому что резкое обрывание активных запросов пользователи замечают раньше любых других огрехов.
Состав фермы из базовой и пиковой ёмкости
Разумное хозяйство редко состоит из одних Spot машин. Устойчивая картина складывается из базовой ёмкости на обычных тарифах, которая держит минимально допустимый уровень службы при любых раскладах, и пиковой ёмкости на Spot, которая обрабатывает всплески и пакетную работу. Пропорция подбирается по характеру нагрузки: для очередей задач доля Spot бывает высокой, для службы с жёсткими требованиями к доступности скромной. Такой состав превращает прерывание из происшествия в штатное событие, на которое заведомо рассчитана ёмкостная модель.
Отдельный резерв это несколько семейств машин в списке замены: если один тип внезапно дорожает или исчезает из свободных мощностей, запрос автоматически переключается на соседний. Диверсификация по типам и зонам стоит несколько строк в шаблоне запуска, а эффект даёт несопоставимый: ферма переживает локальные колебания ёмкости без единого ручного действия. Планировщику нагрузки при этом достаточно видеть машины как взаимозаменяемых исполнителей очереди, и тип железа остаётся для него деталью, а не ограничением.
Различие сценариев terminate и instance stop
Уведомление различается по типу действия, и от него зависит, что происходит с дисками. При полном изъятии машина и её временные тома исчезают безвозвратно, и спасать нужно только то, что успели выгрузить наружу. В варианте с остановкой машины постоянные тома сохраняются и подключатся к следующему запуску, поэтому часть состояния можно оставить на диске. Но полагаться на это стоит с оговоркой: стратегия размещения фермы, построенная на постоянных локальных данных, превращает каждую потерю машины в связывание с конкретным томом, и гибкость, ради которой всё затевалось, уходит.
Практический вывод для проектировщика: состояние live в постоянном хранилище снаружи, тома для кэша и временных файлов, а дисковая привязка только там, где без неё действительно не обойтись. Тогда оба сценария уведомления обрабатываются одной и той же процедурой без ветвлений.
Помимо метаданных, событие о прерывании публикуется в облачной шине событий и может быть доставлено в очередь, функцию или на тему рассылки. Такой путь удобен для централизованной картины: один обработчик видит все машины фермы и принимает решения на уровне хозяйствования. Но у централизованного канала есть слабое место: доставка идёт через внешние службы, и сбой в этой цепочке оставит машину в неведении. Поэтому надёжные схемы используют оба пути сразу: событие из шины для быстрой реакции на уровне фермы и локальный наблюдатель на каждой машине как страховка, работающая без внешних зависимостей.
Существует и режим с плавной заменой ёмкости, когда облако заранее подсказывает, что машину выгоднее пересоздать на другой категории железа, и даёт на решение заметно больше двух минут. Этот канал полезен для планового обновления парка, но не отменяет короткого уведомления: резервный сценарий должен работать при любом раскладе. Частоту изъятий стоит считать: если каждая машина пула хранит счётчик пережитых прерываний, а ферма суммирует их в отчёте, картина ёмкости конкретного семейства и зоны становится видимой в цифрах, а не на ощущениях. Рост частоты подсказывает, что пора перераспределить нагрузку или пересмотреть состав парка, снижение наоборот позволяет поднять долю дешёвой ёмкости. Такой учёт превращает отношения с прерываниями в управляемый параметр эксплуатации, а не в серию неожиданностей, и даёт аргументы для разговора о бюджете на языке чисел.
Тестирование процедуры без ожидания реального прерывания
Самая частая ошибка со Spot это проверенная на бумаге процедура, которую ни разу не прогоняли вживую. Проверка строится просто: вместо чтения реальных метаданных наблюдателю подставляют заглушку, возвращающую заполненный ответ, и прогоняют весь путь от сигнала до выключения на тестовой машине. В заглушке удобно настраивать и скорость ответа, чтобы посмотреть, как процедура ведёт себя при почти истёкшем окне.
spot-watch.sh --test-mode http://127.0.0.1:9999/instance-action
В тестовом прогоне проверяют три вещи: попадает ли машина в состояние уходящей в реестре служб, успевает ли воркер закрыть текущую задачу и выгрузить артефакт, и выключается ли машина по таймеру даже при зависшем сливе. Полезно также измерить фактическую длительность процедуры и записать её рядом с сценарием: знание того, что слив занимает сорок секунд, превращает две минуты из абстракции в рабочее окно с запасом.
Последний штрих это мониторинг самих наблюдателей. Служба опроса должна рапортовать о живости в систему мониторинга, и молчание наблюдателя на машине фермы должно быть заметно не позже, чем через несколько минут после сбоя. Тогда схема прерываний перестаёт быть обещанием и становится проверенной частью повседневной эксплуатации. Одна такая репетиция на квартал держит всю ферму в тонусе лучше, чем страница с инструкцией, которую никто не открывал со дня написания.