> Как обеспечить идемпотентность запросов на создание виртуальных машин (Go)

Уровень: senior · Роль: backend · Язык: Go · Категория: Технические вопросы

Компании: Альфа-банк

Стек: Go

> Пример ответа

Короткий ответ

Идемпотентность создания VM обеспечивается через клиентский идемпотентный ключ (Idempotency-Key), который сервер сохраняет вместе с результатом запроса. При повторном запросе с тем же ключом возвращается сохранённый ответ, а не создаётся новая VM. Ключ должен быть уникальным для каждой логической операции, генерироваться клиентом и передаваться в заголовке. Сервер хранит пару ключ-ответ в распределённом хранилище с TTL и блокировкой на время выполнения.

Подробное объяснение

Идемпотентность - это свойство операции, при котором многократное выполнение с одинаковыми параметрами даёт тот же результат, что и однократное. Для создания VM это критично: повторный POST без идемпотентности создаст лишние машины.

Основные подходы:

  1. Идемпотентный ключ в заголовке - клиент генерирует UUID для каждой логической операции создания. Сервер проверяет: если ключ уже есть в хранилище - возвращает сохранённый ответ, иначе выполняет создание и сохраняет результат.

  2. Естественная идемпотентность через уникальные поля - например, если у VM есть уникальное имя или идентификатор в рамках проекта, повторный запрос с тем же именем вернёт ошибку конфликта или существующую VM. Но это не покрывает все сценарии (например, разные имена для одной логической операции).

  3. Оптимистичная блокировка - при создании VM используется версия ресурса или conditional request (If-Match). Если версия изменилась - запрос отклоняется.

  4. Компенсирующие операции - если создание не идемпотентно, можно отслеживать состояние и при повторе откатывать предыдущее создание.

Для Go-сервиса с высоким RPS ключевые моменты:

  • Хранилище ключей должно быть распределённым (Redis, etcd, DynamoDB) с TTL, обычно 24 часа.
  • Нужна атомарная операция "вставить если отсутствует" (SETNX в Redis) для защиты от гонок.
  • Ответ должен содержать статус операции (создана или уже существовала) и идентификатор VM.
  • Если создание VM асинхронное (через очередь), ключ хранится до завершения операции, а клиент получает ссылку на статус.

На практике

В реальном API (например, облачный провайдер) схема такая:

  1. Клиент генерирует UUID и передаёт его в заголовке Idempotency-Key.
  2. Сервер проверяет ключ в Redis: если есть - возвращает сохранённый ответ (200 или 201 с телом).
  3. Если нет - создаёт запись в Redis со статусом processing, затем запускает создание VM.
  4. После завершения обновляет запись: статус done, тело ответа, идентификатор VM.
  5. Если клиент повторит запрос - получит тот же ответ.
  6. Если создание упало - запись удаляется или помечается failed, клиент может повторить с новым ключом.

Важно: ключ должен быть привязан к конкретному телу запроса. Если клиент шлёт тот же ключ, но другое тело - это ошибка клиента, сервер должен вернуть 422.

Пример кода

GO
type CreateVMRequest struct {
Name string `json:"name"`
CPU int `json:"cpu"`
Memory int `json:"memory"`
}
type VMResponse struct {
ID string `json:"id"`
Status string `json:"status"`
}
type IdempotencyStore interface {
Get(key string) (*VMResponse, error)
SetIfAbsent(key string, resp *VMResponse) (bool, error)
Update(key string, resp *VMResponse) error
}
func CreateVMHandler(store IdempotencyStore, vmService VMService) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
idemKey := r.Header.Get("Idempotency-Key")
if idemKey == "" {
http.Error(w, "missing Idempotency-Key", http.StatusBadRequest)
return
}
// проверяем, есть ли уже результат
if resp, err := store.Get(idemKey); err == nil && resp != nil {
writeJSON(w, http.StatusOK, resp)
return
}
var req CreateVMRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "invalid body", http.StatusBadRequest)
return
}
// атомарно резервируем ключ
placeholder := &VMResponse{Status: "processing"}
inserted, err := store.SetIfAbsent(idemKey, placeholder)
if err != nil {
http.Error(w, "internal error", http.StatusInternalServerError)
return
}
if !inserted {
// другой запрос уже выполняется
resp, _ := store.Get(idemKey)
writeJSON(w, http.StatusAccepted, resp)
return
}
// создаём VM
vm, err := vmService.Create(r.Context(), req)
if err != nil {
store.Update(idemKey, &VMResponse{Status: "failed"})
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
resp := &VMResponse{ID: vm.ID, Status: "created"}
store.Update(idemKey, resp)
writeJSON(w, http.StatusCreated, resp)
}
}

Как отвечать на собеседовании

Начни с определения идемпотентности и почему это важно для создания ресурсов. Затем опиши основной механизм - идемпотентный ключ. Упомяни, что ключ хранится в распределённом хранилище с TTL, и что нужна атомарная операция вставки. Приведи пример из реального API (Stripe, AWS). Обязательно скажи про обработку гонок и про то, что ключ должен быть привязан к телу запроса. Если спросят про асинхронность - объясни, что ключ хранится до завершения операции, а клиент получает статус.

Что проверяет интервьюер

  • Понимание проблемы идемпотентности и её важности для POST-запросов.
  • Знание механизма идемпотентного ключа и его реализации.
  • Умение проектировать распределённые системы: хранилище, TTL, атомарность.
  • Понимание гонок и способов их предотвращения (SETNX, conditional writes).
  • Умение объяснить trade-off между синхронным и асинхронным созданием VM.
  • Знание HTTP-статусов и заголовков.

Типичные ошибки

  • Предложение использовать только уникальное имя VM - это не покрывает все сценарии.
  • Игнорирование гонок: два параллельных запроса с одним ключом создадут две VM.
  • Хранение ключа в памяти одного инстанса - при рестарте или нескольких репликах всё сломается.
  • Не привязывать ключ к телу запроса - клиент может случайно переиспользовать ключ с другими параметрами.
  • Не обрабатывать случай, когда операция упала после создания VM, но до сохранения ответа - нужна компенсация или повторная проверка.
  • Возвращать 500 при повторном запросе с тем же ключом, но другим телом - должен быть 422.

> ГОТОВЫ К СЛЕДУЮЩЕМУ СОБЕСЕДОВАНИЮ?

Запустите тренировочную сессию с ИИ и получите детальную обратную связь, чтобы увереннее проходить реальные интервью