Дисконт 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

После отправки машины в остановку цикл уже не имеет значения, поэтому финальной командой становится выключение. Область ответственности службы на этом заканчивается, и дальше работает сама процедура слива.

Слив соединений и сохранение состояния перед остановкой

Сама процедура завершения сильно зависит от роли машины, но каркас одинаков, и на него легко набрать конкретику:

  1. наблюдатель получает уведомление и помечает машину как уходящую в очереди задач или в реестре служб;
  2. балансировщик выводит узел из ротации, и новые запросы перестают на него попадать;
  3. worker завершает текущую порцию работы и подтверждает обработку сообщения;
  4. накопленное состояние выгружается в постоянное хранилище: база, объектное хранилище, общий том;
  5. журналы закрываются и отправляются в централизованное хранилище логов;
  6. машина выключается штатной командой, и облачный учёт прекращает начисление.

Ключевое здесь пункт про подтверждение обработки: сообщество очередей прощает потерю машины ровно до тех пор, пока сообщение, взятое в работу, не считается обработанным. Отсюда практическое правило для воркера: брать новую задачу только если окно позволяет её закончить, а длинные порции дробить на чекпоинты. Для отрисовки кадра, статистической выкладки или сборки модуля это означает сохранение промежуточного результата каждые несколько минут, чтобы потеря машины откатывала работу на минуты, а не на часы.

Состояние удобно выгружать по принципу одного артефакта: один архив с рабочими данными и манифестом, записанный в постоянное хранилище под предсказуемым именем. Такой артефакт затем подхватывается либо новой машиной фермы, либо плановым процессом обработки, и никакая ручная сборка результатов не требуется. Для веб служб к этому добавляется корректное обращение с сигналами: приложение, получившее сигнал завершения, перестаёт принимать новые соединения, дорабатывает существующие в пределах таймаута и только затем закрывается. Если приложение этого не умеет, его оборачивают прослойкой, которая умеет, потому что резкое обрывание активных запросов пользователи замечают раньше любых других огрехов.

Состав фермы из базовой и пиковой ёмкости

Разумное хозяйство редко состоит из одних Spot машин. Устойчивая картина складывается из базовой ёмкости на обычных тарифах, которая держит минимально допустимый уровень службы при любых раскладах, и пиковой ёмкости на Spot, которая обрабатывает всплески и пакетную работу. Пропорция подбирается по характеру нагрузки: для очередей задач доля Spot бывает высокой, для службы с жёсткими требованиями к доступности скромной. Такой состав превращает прерывание из происшествия в штатное событие, на которое заведомо рассчитана ёмкостная модель.

Отдельный резерв это несколько семейств машин в списке замены: если один тип внезапно дорожает или исчезает из свободных мощностей, запрос автоматически переключается на соседний. Диверсификация по типам и зонам стоит несколько строк в шаблоне запуска, а эффект даёт несопоставимый: ферма переживает локальные колебания ёмкости без единого ручного действия. Планировщику нагрузки при этом достаточно видеть машины как взаимозаменяемых исполнителей очереди, и тип железа остаётся для него деталью, а не ограничением.

Различие сценариев terminate и instance stop

Уведомление различается по типу действия, и от него зависит, что происходит с дисками. При полном изъятии машина и её временные тома исчезают безвозвратно, и спасать нужно только то, что успели выгрузить наружу. В варианте с остановкой машины постоянные тома сохраняются и подключатся к следующему запуску, поэтому часть состояния можно оставить на диске. Но полагаться на это стоит с оговоркой: стратегия размещения фермы, построенная на постоянных локальных данных, превращает каждую потерю машины в связывание с конкретным томом, и гибкость, ради которой всё затевалось, уходит.

Практический вывод для проектировщика: состояние live в постоянном хранилище снаружи, тома для кэша и временных файлов, а дисковая привязка только там, где без неё действительно не обойтись. Тогда оба сценария уведомления обрабатываются одной и той же процедурой без ветвлений.

Помимо метаданных, событие о прерывании публикуется в облачной шине событий и может быть доставлено в очередь, функцию или на тему рассылки. Такой путь удобен для централизованной картины: один обработчик видит все машины фермы и принимает решения на уровне хозяйствования. Но у централизованного канала есть слабое место: доставка идёт через внешние службы, и сбой в этой цепочке оставит машину в неведении. Поэтому надёжные схемы используют оба пути сразу: событие из шины для быстрой реакции на уровне фермы и локальный наблюдатель на каждой машине как страховка, работающая без внешних зависимостей.

Существует и режим с плавной заменой ёмкости, когда облако заранее подсказывает, что машину выгоднее пересоздать на другой категории железа, и даёт на решение заметно больше двух минут. Этот канал полезен для планового обновления парка, но не отменяет короткого уведомления: резервный сценарий должен работать при любом раскладе. Частоту изъятий стоит считать: если каждая машина пула хранит счётчик пережитых прерываний, а ферма суммирует их в отчёте, картина ёмкости конкретного семейства и зоны становится видимой в цифрах, а не на ощущениях. Рост частоты подсказывает, что пора перераспределить нагрузку или пересмотреть состав парка, снижение наоборот позволяет поднять долю дешёвой ёмкости. Такой учёт превращает отношения с прерываниями в управляемый параметр эксплуатации, а не в серию неожиданностей, и даёт аргументы для разговора о бюджете на языке чисел.

Тестирование процедуры без ожидания реального прерывания

Самая частая ошибка со Spot это проверенная на бумаге процедура, которую ни разу не прогоняли вживую. Проверка строится просто: вместо чтения реальных метаданных наблюдателю подставляют заглушку, возвращающую заполненный ответ, и прогоняют весь путь от сигнала до выключения на тестовой машине. В заглушке удобно настраивать и скорость ответа, чтобы посмотреть, как процедура ведёт себя при почти истёкшем окне.

spot-watch.sh --test-mode http://127.0.0.1:9999/instance-action

В тестовом прогоне проверяют три вещи: попадает ли машина в состояние уходящей в реестре служб, успевает ли воркер закрыть текущую задачу и выгрузить артефакт, и выключается ли машина по таймеру даже при зависшем сливе. Полезно также измерить фактическую длительность процедуры и записать её рядом с сценарием: знание того, что слив занимает сорок секунд, превращает две минуты из абстракции в рабочее окно с запасом.

Последний штрих это мониторинг самих наблюдателей. Служба опроса должна рапортовать о живости в систему мониторинга, и молчание наблюдателя на машине фермы должно быть заметно не позже, чем через несколько минут после сбоя. Тогда схема прерываний перестаёт быть обещанием и становится проверенной частью повседневной эксплуатации. Одна такая репетиция на квартал держит всю ферму в тонусе лучше, чем страница с инструкцией, которую никто не открывал со дня написания.