Перейти к содержимому

Микросервисы и распределённые системы

Микросервисная архитектура — это способ разрезать систему на независимо развёртываемые процессы, каждый со своим жизненным циклом, своей командой и, главное, со своим хранилищем. Ключевое слово здесь не «микро», а «независимо развёртываемые»: если для выката сервиса A нужно синхронно выкатить сервис B, у вас распределённый монолит — худший из миров, где к сложности сети добавлена связность монолита. Второй критерий — независимое хранилище: как только два сервиса пишут в одни и те же таблицы, схема БД становится общим публичным API, и менять её нельзя без согласования, то есть независимость теряется.

Главная модель в голове: при переходе к сети вы обмениваете простоту на масштабируемость организации. В монолите вызов метода — это переход по указателю, он либо выполнился, либо запаниковал, и транзакция БД даёт вам ACID бесплатно. В распределённой системе вызов — это сетевой запрос, у которого три исхода вместо двух: успех, отказ и неизвестность (таймаут — вы не знаете, применилась операция или нет). Именно третий исход порождает всю остальную повестку: ретраи → дубликаты → идемпотентность; отсутствие общей транзакции → сага/Outbox/2PC; отсутствие стектрейса через процессы → distributed tracing; частичные отказы → таймауты, circuit breaker, bulkhead.

Из невозможности атомарно «записать в БД и отправить сообщение в брокер» вырастает почти вся практика согласованности. Два ресурса без общей транзакции нельзя изменить атомарно, поэтому либо вводят двухфазный коммит (дорого, блокирующе, плохо переживает отказ координатора), либо соглашаются на eventual consistency и делают запись в БД единственным атомарным актом: сообщение кладётся в таблицу outbox той же транзакцией, а отдельный процесс дочитывает её в брокер (at-least-once). Симметрично на приёме работает Inbox — таблица обработанных message_id, превращающая at-least-once в эффект «ровно один раз». Отсюда практический вывод: exactly-once delivery в сети недостижима, достижима exactly-once processing = at-least-once + идемпотентность.

Третий блок — эксплуатация. Распределённая система не имеет глобального состояния, которое можно посмотреть в отладчике, поэтому наблюдаемость — не «приятный бонус», а функциональное требование. Стандартный набор: метрики (RED/USE + перцентили, никогда не средние), структурные логи с trace_id, распределённый трейсинг (OpenTelemetry, W3C traceparent), профилирование. Тот же принцип касается развёртывания: rolling/blue-green/canary, health-пробы, graceful shutdown, expand–contract-миграции БД — потому что в любой момент в проде живут одновременно две версии кода.

Коротко. Ключ идемпотентности (Idempotency-Key) — уникальный идентификатор попытки операции, который генерирует клиент и присылает вместе с запросом; сервер запоминает пару «ключ → результат» и на повторный запрос с тем же ключом не выполняет работу заново, а возвращает сохранённый результат. Это стандартный способ сделать неидемпотентный по природе POST (создать платёж, списать деньги) безопасным для ретраев.

Глубже. Ключ должен генерироваться на бизнес-попытку, а не на HTTP-запрос: если клиент нажал «Оплатить» один раз, все сетевые ретраи этой попытки идут с одним ключом, а повторное нажатие — с новым. Хранят ключи в таблице с уникальным индексом, и вставка ключа выполняется той же транзакцией, что и сама операция, иначе между «проверил» и «выполнил» пролезет второй параллельный запрос. Классическая схема — вставить строку со статусом in_progress и полагаться на конфликт уникального индекса:

// PostgreSQL: idempotency_keys(key TEXT PRIMARY KEY, request_hash BYTEA,
// status TEXT, response JSONB, created_at TIMESTAMPTZ)
tx, err := db.BeginTx(ctx, nil)
if err != nil {
return err
}
defer tx.Rollback()
var stored []byte
err = tx.QueryRowContext(ctx, `
INSERT INTO idempotency_keys (key, request_hash, status)
VALUES ($1, $2, 'in_progress')
ON CONFLICT (key) DO UPDATE SET key = excluded.key
RETURNING response`, key, hash).Scan(&stored)
if err != nil {
return err
}
if stored != nil {
return writeStoredResponse(stored) // повтор: отдаём прежний ответ
}
// ... основная бизнес-операция в этой же транзакции ...
_, err = tx.ExecContext(ctx, `
UPDATE idempotency_keys SET status='done', response=$2 WHERE key=$1`,
key, respJSON)
if err != nil {
return err
}
return tx.Commit()

Важные детали: (1) храните хеш тела запроса и отвечайте 422, если по тому же ключу пришли другие параметры — иначе клиент случайно получит чужой ответ; (2) ключам нужен TTL (обычно 24 ч – 7 дней) и фоновая чистка; (3) параллельный повтор, попавший в in_progress, корректнее отбить 409 Conflict, чтобы клиент повторил позже, чем ждать блокировку. Так это устроено в Stripe API, который и популяризовал заголовок Idempotency-Key.

Как в распределенной системе с чистой архитектурой обеспечить атомарность операции, затрагивающей несколько сервисов? Какие паттерны распределенных транзакций использовали для гарантии согласованности?

Заголовок раздела «Как в распределенной системе с чистой архитектурой обеспечить атомарность операции, затрагивающей несколько сервисов? Какие паттерны распределенных транзакций использовали для гарантии согласованности?»

Коротко. Настоящей атомарности через несколько сервисов не бывает без 2PC, а 2PC в микросервисах почти никогда не применяют. Практический ответ: свести операцию к цепочке локальных ACID-транзакций и связать их сагой, где каждый шаг публикует событие транзакционно через Outbox, а получатель дедуплицирует через Inbox; на ошибке выполняются компенсирующие действия. Гарантия при этом не «атомарность», а eventual consistency с бизнес-компенсациями.

Глубже. С точки зрения чистой архитектуры это раскладывается так: доменный слой описывает состояния агрегата (OrderPending → OrderPaid → OrderConfirmed) и компенсации как явные доменные операции (ReleaseReservation, RefundPayment); слой приложения содержит оркестратор саги как use case; инфраструктура даёт репозиторий и Outbox-таблицу. Ключевое правило — use case коммитит ровно одну транзакцию, включающую и изменение агрегата, и запись события в outbox; отправка в брокер вынесена наружу и никогда не делается внутри доменного слоя.

Порядок выбора на собеседовании стоит проговорить явно: сначала попытаться не делить операцию — если два шага обязаны быть атомарными и не переживают промежуточное состояние, скорее всего, граница сервисов проведена неверно и это один агрегат. Если делить всё-таки надо: сага (оркестрация или хореография) как основной инструмент, Outbox/Inbox как транспортная гарантия, идемпотентность на всех обработчиках, и уже в крайнем случае — 2PC (обычно только там, где есть XA-совместимые ресурсы или единый движок вроде Postgres prepared transactions).

Что такое идемпотентность в запросах к сервисам?

Заголовок раздела «Что такое идемпотентность в запросах к сервисам?»

Коротко. Запрос идемпотентен, если его повторное выполнение с теми же параметрами приводит систему в то же состояние, что и однократное: f(f(x)) == f(x). Это свойство эффекта на состояние, а не совпадение тел ответов — счётчик попыток или updated_at могут отличаться.

Глубже. В межсервисном взаимодействии идемпотентность обязательна, потому что сеть даёт неопределённый исход: клиент получил таймаут, но сервер, возможно, всё выполнил. Единственная безопасная реакция на таймаут — ретрай, а безопасный ретрай возможен только для идемпотентной операции. Способы получить идемпотентность: (1) естественная идемпотентность — операция «установить значение» (SET status='paid') вместо «прибавить» (balance += 100); (2) ключ идемпотентности (см. выше); (3) дедупликация по бизнес-идентификатору с уникальным индексом (UNIQUE(payment_id)); (4) условное обновление по версии/состоянию — UPDATE ... WHERE id=$1 AND status='pending', тогда второй апдейт затронет 0 строк; (5) для событий — таблица Inbox с message_id.

Вопросы по поиску проблемы в распределенной системе;

Заголовок раздела «Вопросы по поиску проблемы в распределенной системе;»

Коротко. Формулировка обрывочная — это тема «как дебажить распределённую систему». Ответ-каркас: идти сверху вниз от симптома к причине — сначала подтвердить проблему по метрикам «золотого сигнала» на границе (latency/errors/traffic/saturation), затем локализовать сервис по трассам, затем найти конкретную причину по логам и профилю этого сервиса, и только потом лезть в код.

Глубже. Практический порядок, который стоит проговорить: (1) симптом — что именно деградирует: p99 latency, доля 5xx, лаг консьюмера, длина очереди; (2) скоуп — все пользователи или один сегмент, все поды или один, все ЦОД или один; сравнение по лейблам в Prometheus обычно сразу режет пространство поиска пополам; (3) корреляция во времени — что изменилось: деплой, миграция, рост трафика, ротация сертификата, соседний сервис; (4) трассы — взять несколько медленных/ошибочных трасс с trace_id из логов и посмотреть, на каком спане уходит время; (5) ресурсы — CPU throttling из-за limits, память/OOMKilled, исчерпание пула соединений к БД, GC-паузы; (6) зависимости — БД (медленные запросы, блокировки, autovacuum), брокер, внешние API. Типовые классы причин в распределёнке: retry storm и метастабильные отказы, каскад из-за отсутствия таймаутов, исчерпание пула соединений, «шумный сосед», рассинхронизация времени, забытый индекс после роста данных, hot partition. Полезно назвать инструменты: Prometheus/Grafana, Jaeger/Tempo, Loki/ELK, pprof (в Go — net/http/pprof, continuous profiling через Pyroscope), pg_stat_statements, kubectl describe/logs --previous.

Что такое микросервисы и монолиты? Их преимущества и недостатки, когда стоит использовать микросервисы а когда - монолиты?

Заголовок раздела «Что такое микросервисы и монолиты? Их преимущества и недостатки, когда стоит использовать микросервисы а когда - монолиты?»

Коротко. Монолит — одно приложение, разворачиваемое как единая единица, с одной кодовой базой и, как правило, одной БД. Микросервисы — набор небольших независимо развёртываемых сервисов, каждый со своим хранилищем, общающихся по сети. Монолит выигрывает в простоте разработки, отладки и транзакционной целостности; микросервисы — в независимости команд, изоляции отказов и точечном масштабировании. По умолчанию начинают с (модульного) монолита и выделяют сервисы там, где появляется реальная причина.

Глубже. Критерии «пора делить»: (1) организационный — команд стало больше 2–3 и релизы блокируют друг друга; (2) нагрузочный — один компонент требует принципиально иного масштабирования или железа (GPU, много памяти); (3) надёжностный — падение второстепенной функции роняет ядро; (4) технологический — компоненту объективно нужен другой стек; (5) регуляторный — часть данных обязана жить в изолированном контуре. Признаки «рано»: маленькая команда, неясная предметная область, отсутствие CI/CD, отсутствие наблюдаемости, стремление «делать как в Netflix». Промежуточный и часто оптимальный ответ — модульный монолит: чёткие модули с явными интерфейсами и раздельными схемами в одной БД, что позволяет позже вырезать модуль в сервис почти механически. Стоит помнить закон Конвея: архитектура повторяет структуру коммуникаций в организации, поэтому дробление сервисов без дробления команд обычно проваливается.

Коротко. Service mesh — инфраструктурный слой, который выносит межсервисное сетевое взаимодействие (mTLS, ретраи, таймауты, балансировку, circuit breaking, маршрутизацию для canary, сбор метрик и трасс) из кода приложения в сеть — обычно через sidecar-прокси рядом с каждым подом, управляемый централизованным control plane. Примеры: Istio, Linkerd, Consul Connect; в Cilium — вариант без sidecar, на eBPF.

Глубже. Архитектурно mesh делится на data plane (прокси, чаще всего Envoy, перехватывающий весь входящий/исходящий трафик пода через iptables или eBPF) и control plane (istiod — раздаёт прокси конфигурацию и сертификаты). Что это даёт: mTLS и identity-based авторизация между сервисами без единой строки в коде; единообразные политики повторов и таймаутов; L7-маршрутизация по заголовкам, которая делает canary и A/B без изменения деплойментов; единые RED-метрики и спаны для всех сервисов независимо от языка. Что это стоит: дополнительный хоп и латентность (обычно единицы миллисекунд), потребление CPU/памяти на sidecar в каждом поде, заметная сложность отладки (сетевые проблемы теперь могут быть в Envoy), высокая планка эксплуатации. С Istio 1.22+ появился ambient mode (ztunnel на ноде + waypoint-прокси), снижающий накладные расходы. Разумная позиция на собеседовании: mesh оправдан, когда сервисов десятки, языков несколько и нужен единый mTLS/политики; для 5–10 однородных Go-сервисов дешевле библиотеки (gRPC-балансировка, go-resiliency/gobreaker, OpenTelemetry SDK).

Коротко. Основные группы: декомпозиция (по бизнес-возможностям/DDD-поддоменам, Strangler Fig для миграции), интеграция (API Gateway, BFF, Aggregator, синхронный RPC vs событийный обмен), данные (Database per Service, Saga, Outbox/Inbox, CQRS, Event Sourcing, Materialized View, API Composition), надёжность (Circuit Breaker, Retry с backoff+jitter, Timeout, Bulkhead, Rate Limiting, Fallback), эксплуатация (Service Discovery, Sidecar/Service Mesh, Health Check API, Log Aggregation, Distributed Tracing, Externalized Configuration).

Глубже. На собеседовании ценнее не перечислить список, а связать паттерны с проблемами: Database per Service порождает проблему «как сделать запрос по данным двух сервисов» → API Composition или CQRS с материализованным представлением; она же порождает «как менять данные в двух сервисах» → Saga; Saga требует надёжной публикации событий → Outbox; Outbox даёт at-least-once → Inbox/идемпотентность; синхронные вызовы дают каскадные отказы → Circuit Breaker + Timeout + Bulkhead; множество адресов → Service Discovery; отсутствие стектрейса → Tracing. Отдельно стоит назвать Strangler Fig как основной паттерн распила монолита: перед монолитом ставится прокси, который постепенно перенаправляет отдельные маршруты на новые сервисы, пока старый код не окажется полностью «задушен».

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

Заголовок раздела «Представьте, что у вас есть настроенный дашборд в Grafana для микросервиса. Активных алертов нет, инциденты не зафиксированы, но вам нужно провести плановую проверку состояния сервиса. Как вы определите, что сервис действительно работает стабильно и нет скрытых проблем?»

Коротко. Смотрю не на «горит/не горит», а на тренды и хвосты распределений: RED-метрики (Rate, Errors, Duration) с p95/p99 в сравнении с прошлой неделей, USE по ресурсам (utilization/saturation/errors: CPU throttling, RSS против limit, рестарты подов, пул соединений), состояние зависимостей (лаг консьюмера, latency БД, ошибки внешних API) и «тихие» индикаторы — рост ретраев, рост доли 4xx, приближение к квотам. Отсутствие алертов означает лишь то, что не сработали заданные пороги.

Глубже. Конкретный чек-лист планового ревью: (1) насыщение против лимитовcontainer_cpu_cfs_throttled_seconds_total (троттлинг есть даже при «нормальном» CPU usage), память относительно limits (пила без OOM — предвестник), заполненность пулов db.Stats().InUse/WaitCount, длина очередей; (2) хвосты — p99 и максимум по эндпоинтам отдельно, а не общий средний; средняя латентность прячет проблему, поэтому строить по гистограммам и histogram_quantile, а не по среднему; (3) error budget — сколько SLO съедено за 30 дней; если бюджет тает равномерно, алертов может не быть, но деградация есть; (4) скрытые ошибки — 4xx (клиенты уже страдают, но 5xx нет), ошибки в логах без метрики, panic/recover-счётчики, DLQ и число ретраев, лаг консьюмеров; (5) рестарты и evictionskube_pod_container_status_restarts_total, OOMKilled в kubectl describe; (6) зависимости — рост latency БД, число медленных запросов из pg_stat_statements, размер таблиц и bloat, приближение xid к wraparound; (7) утечки — растущий на длинной дистанции RSS, число горутин (go_goroutines), число открытых FD, число соединений; в Go это классические индикаторы утечки горутин; (8) сравнение с прошлым периодом — те же графики с offset 1w, иначе медленная деградация незаметна. Итог формулирую как: проверил хвосты, насыщение, бюджет ошибок и тренды неделя-к-неделе, нашёл/не нашёл узкие места, завёл задачи и, если чего-то не хватало в наблюдаемости, добавил метрику и алерт — плановая проверка обычно должна заканчиваться новым алертом, а не только «всё ок».

Представьте, что вы наблюдаете за работой микросервиса и замечаете периодические «волны» в его поведении. Как можно обнаружить эти волны и что вы предпримете?

Заголовок раздела «Представьте, что вы наблюдаете за работой микросервиса и замечаете периодические «волны» в его поведении. Как можно обнаружить эти волны и что вы предпримете?»

Коротко. «Волны» — периодические всплески латентности/нагрузки/ошибок. Обнаруживаю их через графики с достаточным окном и разрешением, сравнение с offset 1d/1w для поиска периодичности, а дальше сопоставляю период с кандидатами: cron/шедулеры, батчи и отчёты, GC, autovacuum, ротация кеша с одинаковым TTL, синхронные ретраи, health-check-штормы, скейлинг HPA. Дальше — устраняю синхронизацию: джиттер в расписаниях и TTL, backoff с jitter, вынос батчей на отдельные реплики, сглаживание HPA.

Глубже. Обнаружение: смотреть p99, а не среднее, и брать шаг графика меньше периода волны — при rate(...[5m]) всплеск в 30 секунд размажется; для коротких пиков помогают max_over_time и «сырые» гистограммы. Периодичность легко подтвердить наложением того же ряда со сдвигом на сутки/неделю. Типичные причины и лечение: cron/batch (ночные выгрузки, ретраи «раз в час», обход всех клиентов) — размазать по времени, добавить случайную задержку старта; кеш — массово одинаковый TTL порождает синхронное протухание и «thundering herd» на БД, лечится джиттером TTL и single-flight (в Go — golang.org/x/sync/singleflight); retry storm — все клиенты повторяют одновременно, лечится экспоненциальным backoff с jitter и circuit breaker; БД — autovacuum/checkpoint в Postgres, лечится настройкой checkpoint_completion_target, autovacuum_* и мониторингом; GC/аллокации — в Go всплески пауз и CPU при пиковых аллокациях, видно по go_gc_duration_seconds и GODEBUG=gctrace=1, лечится снижением аллокаций, GOGC/GOMEMLIMIT; HPA-осцилляция — скейл вверх-вниз по запаздывающей метрике, лечится стабилизационным окном и правильной целевой метрикой; внешние партнёры с собственными окнами обработки. Отдельно проверить, не совпадает ли период с интервалом самих метрик — иногда «волна» является артефактом скрейпа или несовпадения окна rate с интервалом сбора.

У вас есть микросервис, работающий с PostgreSQL, и вам нужно изменить схему: добавить колонку или таблицу. Как вы это реализуете?

Заголовок раздела «У вас есть микросервис, работающий с PostgreSQL, и вам нужно изменить схему: добавить колонку или таблицу. Как вы это реализуете?»

Коротко. Версионированными миграциями в репозитории сервиса (goose/golang-migrate/atlas), применяемыми автоматически в пайплайне, и обязательно по схеме expand–migrate–contract: сначала обратно совместимое расширение схемы, затем выкат кода, затем бэкфилл данных, и только потом удаление старого. Во время rolling update в проде одновременно работают старая и новая версии кода, поэтому любая миграция должна быть совместима с обеими.

Глубже. Добавление таблицы или nullable-колонки в Postgres безопасно: ALTER TABLE ... ADD COLUMN x int берёт ACCESS EXCLUSIVE на очень короткое время и не переписывает таблицу; начиная с PostgreSQL 11 ADD COLUMN ... NOT NULL DEFAULT <константа> тоже не переписывает таблицу (значение хранится в каталоге), но DEFAULT с волатильным выражением (now(), gen_random_uuid()) — переписывает. Опасные операции, которые надо делать иначе: индексы — только CREATE INDEX CONCURRENTLY (не в транзакции; при сбое остаётся INVALID-индекс, который надо дропнуть и пересоздать); NOT NULL на существующей колонке — через ADD CONSTRAINT ... CHECK (x IS NOT NULL) NOT VALID, затем VALIDATE CONSTRAINT, затем SET NOT NULL (Postgres 12+ умеет опираться на валидный CHECK); FK — ADD CONSTRAINT ... NOT VALID + VALIDATE; переименование колонки — никогда напрямую, а через новую колонку и двойную запись. Всегда ставить SET lock_timeout и statement_timeout перед DDL, чтобы миграция не встала в очередь блокировок и не заморозила все запросы за собой (очередь блокировок в Postgres — FIFO, один ждущий ALTER блокирует всех, кто за ним). Бэкфилл больших таблиц — батчами по несколько тысяч строк с паузами, не одним UPDATE. Полный цикл на примере добавления обязательной колонки: (1) expand — добавить nullable-колонку; (2) выкатить код, который пишет и в старое, и в новое поле; (3) бэкфилл батчами; (4) выкатить код, читающий только новое; (5) contract — навесить NOT NULL, удалить старую колонку следующим релизом. Миграции запускать отдельным шагом (в Kubernetes — initContainer или Job с хуком, а не в каждой реплике), либо полагаться на advisory-lock самой библиотеки миграций, чтобы N реплик не запустили их параллельно.

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

Заголовок раздела «Есть микросервис, развернутый в Kubernetes с несколькими репликами за балансировщиком нагрузки. Внешняя система отправляет вебхук для обновления данных. Поскольку балансировщик направляет запрос только на одну случайную реплику, как обеспечить актуальность данных на всех экземплярах сервиса? Как организовать синхронизацию между репликами, чтобы минимизировать рассогласование?»

Коротко. Правильный ответ — не синхронизировать реплики, а сделать их stateless: вебхук пишет данные в общее хранилище (Postgres/Redis), и все реплики читают оттуда, тогда рассогласования нет по построению. Локальный кеш в памяти — это оптимизация, и если он нужен, его инвалидация делается через широковещательный канал (Redis Pub/Sub, Kafka-топик, где каждая реплика — отдельная consumer group, NATS) плюс короткий TTL и версия записи как страховка.

Глубже. Разложение вариантов по стоимости и гарантиям: (1) без кеша — реплика при каждом запросе идёт в БД; проще всего, задержка нулевая, цена — нагрузка на БД; (2) общий кеш (Redis) — вебхук инвалидирует/обновляет ключ, все реплики видят одно и то же; остаётся один сетевой хоп, но рассогласования нет; (3) локальный in-memory кеш + инвалидация по шине — самый быстрый на чтение, но появляется окно рассогласования на время доставки события, нужна обработка «реплика была недоступна/только что стартовала» (обязательный TTL и прогрев при старте); важная деталь: при Kafka каждая реплика должна иметь уникальный group.id, иначе событие получит только одна из них — это типичная ошибка; в Redis Pub/Sub сообщения не персистентны, пропустивший их под останется со старым кешем, поэтому TTL обязателен; (4) версионирование — хранить version/updated_at рядом с данными и при критичных операциях проверять актуальность в БД (read-through по версии), что превращает рассогласование из «неправильно» в «чуть медленнее». Дополнительно про сам вебхук: он должен быть идемпотентным (внешняя система будет ретраить), с проверкой подписи (HMAC) и защитой от reordering — сравнивать порядковый номер/updated_at события и игнорировать устаревшие. Если обработка тяжёлая — вебхук только валидирует, кладёт задачу в очередь и отвечает 200, иначе отправитель отвалится по таймауту и начнёт ретраить.

Расскажите о масштабировании в распределенных системах. Какие есть виды масштабирования?

Заголовок раздела «Расскажите о масштабировании в распределенных системах. Какие есть виды масштабирования?»

Коротко. Вертикальное (scale up) — увеличить ресурсы одного узла: просто, но упирается в потолок железа и не даёт отказоустойчивости. Горизонтальное (scale out) — добавить узлы: практически безгранично, но требует stateless-сервисов, балансировки и решения проблем согласованности. Отдельно выделяют функциональное масштабирование (разделение по функциям — собственно микросервисы) и шардирование данных (разделение по ключу).

Глубже. Полезная рамка — «куб масштабирования» (AKF): ось X — клонирование (реплики за балансировщиком), ось Y — декомпозиция по функциям/сервисам, ось Z — шардирование по данным (по пользователю, тенанту, региону). Практика: сервисы делают stateless, состояние выносят в БД/кеш/очередь, дальше по оси X масштабируют бездумно (HPA по CPU/RPS/лагу очереди, KEDA для событийных нагрузок). Узкое место при этом почти всегда переезжает в БД, где доступны: read-replicas для чтения (ценой репликационного лага и read-your-writes проблем), партиционирование, шардирование (ключ шардирования — самое важное решение: он определяет, будут ли hot partition и возможны ли кросс-шардовые запросы), CQRS с отдельной моделью для чтения, кеширование. Ограничения проговорить честно: закон Амдала (последовательная часть ограничивает выигрыш), USL (при росте узлов растёт стоимость когерентности, кривая может пойти вниз), CAP/PACELC (при сетевом разделении выбираем между доступностью и согласованностью, а вне разделения — между латентностью и согласованностью). Эластичность и autoscaling — отдельная тема: важно, чтобы под стартовал быстрее, чем растёт очередь, отсюда прогрев, readinessProbe, «pod disruption budget» и запас реплик.

Работал ли с микросервисами и монолитом? В чем основное отличие микросервисной архитектуры и монолитной?

Заголовок раздела «Работал ли с микросервисами и монолитом? В чем основное отличие микросервисной архитектуры и монолитной?»

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

Глубже. Каркас хорошего ответа: (1) контекст — «система обработки заказов, ~15 сервисов на Go, Kafka как шина, Postgres на сервис, Kubernetes»; (2) моя зона — «отвечал за сервис платежей: gRPC-API, сага с сервисом склада, Outbox в Kafka»; (3) конкретная сложность и как решили — «после перехода получили дубликаты платежей на ретраях, ввели ключи идемпотентности и Inbox»; (4) честный вывод — «два сервиса потом слили обратно, потому что они всегда менялись вместе». Интервьюер проверяет, отличаете ли вы «работал с микросервисами» от «писал HTTP-хендлеры в одном из сервисов»: упоминание границ сервисов, согласованности, версионирования контрактов и наблюдаемости сразу отличает опыт от знакомства. Типичные ошибки: рассказывать теорию вместо опыта; говорить «микросервисы всегда лучше»; не уметь назвать ни одной проблемы, возникшей после распила.

Расскажи как у вас общались микросервисы между собой?

Заголовок раздела «Расскажи как у вас общались микросервисы между собой?»

Коротко. Вопрос про опыт. Хороший ответ описывает две плоскости: синхронную (gRPC/REST для запрос-ответ, где нужен немедленный результат) и асинхронную (Kafka/RabbitMQ для событий и фоновой обработки), с обоснованием, почему что применялось, и с упоминанием сквозных вещей: контракты (protobuf, схема-реестр), таймауты и ретраи, идемпотентность, трассировка через traceparent.

Глубже. Структура рассказа: «команды — синхронно, факты — асинхронно». Синхронно — gRPC между внутренними сервисами (строгий контракт, HTTP/2, стриминг, кодогенерация), REST/JSON — наружу и там, где важна отладка руками. Асинхронно — Kafka для доменных событий (OrderCreated, PaymentCaptured), партиционирование по aggregate_id для сохранения порядка внутри сущности, Outbox для транзакционной публикации, consumer group на сервис, DLQ для битых сообщений. Обязательно назвать защиту: таймауты на каждый вызов и общий дедлайн запроса (в Go — context.WithTimeout, у gRPC дедлайн передаётся по проводу), ретраи только идемпотентных вызовов с backoff+jitter, circuit breaker, ограничение конкурентности. Хорошо, если добавите, чего избегали: цепочек синхронных вызовов длиной 4+ (латентность перемножается, доступность падает мультипликативно) и общей БД между сервисами.

Какие плюсы и минусы микросервисной архитектуры?

Заголовок раздела «Какие плюсы и минусы микросервисной архитектуры?»

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

Глубже. Полезно проговорить, что почти каждый плюс имеет цену. Независимый деплой требует обратной совместимости контрактов и feature flags. Изоляция отказов работает только при таймаутах, circuit breaker и graceful degradation — иначе получается наоборот, каскад. Точечное масштабирование выгодно, если профиль нагрузки у компонентов реально разный. Свобода стека быстро становится минусом: пять языков — это пять наборов библиотек, метрик и практик, поэтому на практике стек ограничивают. И главный неочевидный минус: доступность цепочки синхронных вызовов мультипликативна — пять сервисов по 99.9% дают 99.5%, поэтому асинхронность и кеширование становятся требованием надёжности, а не оптимизацией.

Расскажите, что знаете про распределенные транзакции.

Заголовок раздела «Расскажите, что знаете про распределенные транзакции.»

Коротко. Распределённая транзакция — изменение состояния в нескольких независимых ресурсах (БД, сервисы, брокер) как одно логическое целое. Классический способ — двухфазный коммит (2PC/XA) с координатором: он даёт атомарность, но блокирует ресурсы и не переживает отказ координатора, поэтому в микросервисах обычно применяют сагу: цепочку локальных транзакций с компенсациями и eventual consistency, а надёжность доставки обеспечивают Outbox/Inbox и идемпотентность.

Глубже. Стоит показать спектр: 2PC/XA — атомарность, блокирующий протокол, uncertainty period у участников после PREPARE; 3PC — теоретическая небло­кирующая версия, на практике не используется, так как некорректна при сетевых разделениях; консенсус (Paxos/Raft) — атомарность через реплицированный лог, база современных распределённых БД; распределённые БД с транзакциями (Spanner с TrueTime, CockroachDB, YDB) — 2PC поверх Raft-групп, доступен нормальный ACID, но это внутри одной СУБД, а не между сервисами; saga — компенсации вместо отката, нет изоляции (промежуточные состояния видимы), нужны семантические блокировки и статусы; TCC (Try-Confirm-Cancel) — резервирование ресурсов на первом шаге и подтверждение на втором, компромисс между сагой и 2PC. Ключевая мысль для ответа: в микросервисах отказываются не от согласованности, а от немедленной согласованности; система обязана быть корректной в конечном итоге, а промежуточные состояния должны быть явно смоделированы в домене (pending, reserved, failed).

Какой есть другой способ взаимодействия микросервисов кроме оркестрации?

Заголовок раздела «Какой есть другой способ взаимодействия микросервисов кроме оркестрации?»

Коротко. Хореография: сервисы не управляются центральным координатором, а реагируют на события друг друга через брокер. Заказ публикует OrderCreated, платежи сами подписаны на него и публикуют PaymentCaptured, склад слушает уже это событие. Ни один сервис не знает всей цепочки.

Глубже. Сравнение: оркестрация — есть явный процесс (saga orchestrator, workflow-движок вроде Temporal/Cadence), который знает шаги и компенсации; плюсы — процесс виден в одном месте, легко менять и наблюдать, проще компенсации; минусы — координатор становится узлом связности и должен быть отказоустойчивым (персистентный стейт-машина, а не переменная в памяти). Хореография — слабая связность, легко добавлять новых подписчиков без изменения существующих; минусы — бизнес-процесс «размазан», сложно понять текущее состояние и отладить, легко получить циклы событий и неявные зависимости. Практическое правило: 2–3 шага — хореография, 5+ шагов с компенсациями и таймаутами — оркестрация. Кроме этой оси есть и другие способы взаимодействия: синхронный RPC, обмен через общий шардированный кеш/хранилище (антипаттерн при записи), файловые/батчевые выгрузки, и «pull» вместо «push» — когда потребитель сам периодически вычитывает изменения (feed/CDC).

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

Глубже. Часть минусов лечится не распилом, а модульностью: явные границы модулей, запрет обращаться к чужим таблицам напрямую (в Postgres — раздельные схемы и права), внутренние интерфейсы, архитектурные тесты на запрещённые импорты (в Go — отдельные пакеты и internal/, проверка через go list/depguard). Что распил действительно даёт, а модульный монолит нет: независимый деплой, независимое масштабирование и изоляцию отказов на уровне процесса. Отдельная реальная проблема больших монолитов — время сборки и прогонки тестов, оно напрямую бьёт по скорости команды, и это часто более сильный аргумент за распил, чем «архитектурная чистота».

Коротко. См. выше — ответ тот же, что про плюсы и минусы микросервисной архитектуры. Кратко: выигрыш в независимости деплоя, масштабирования и владения; плата — сетевая сложность, eventual consistency, дороже отладка, тестирование и инфраструктура.

Глубже. Здесь уместно добавить количественный акцент, который редко звучит: микросервисы переносят сложность из кода в эксплуатацию. Нужны CI/CD на каждый сервис, единый способ логирования, метрик и трассировки, шаблоны сервисов, каталог сервисов и владельцев, управление секретами, схема-реестр контрактов, среды для контрактных тестов. Если этой платформы нет, распил ухудшит всё, что можно измерить: и time-to-market, и MTTR.

Удавалось ли реализовывать exactly once на уровне микросервисов? Какие паттерны использовали? Какие еще можно предложить способы реализации?

Заголовок раздела «Удавалось ли реализовывать exactly once на уровне микросервисов? Какие паттерны использовали? Какие еще можно предложить способы реализации?»

Коротко. Exactly-once доставки в распределённой системе невозможна (это следствие проблемы двух генералов); достижима exactly-once обработка = at-least-once доставка + идемпотентный потребитель. На практике это Outbox на стороне отправителя, Inbox/дедупликация по message_id на стороне получателя, идемпотентные операции над данными и, где применимо, транзакционные возможности брокера (Kafka transactions + read_committed для сценария consume-transform-produce внутри Kafka).

Глубже. Способы дедупликации: (1) Inbox-таблицаINSERT INTO inbox(message_id) ON CONFLICT DO NOTHING в той же транзакции, что и бизнес-эффект; если вставка не добавила строку — сообщение уже обработано; (2) естественная идемпотентность через уникальный бизнес-ключ (UNIQUE(order_id) в таблице платежей); (3) условные обновления по версии/статусу — оптимистическая блокировка UPDATE ... WHERE version = $1; (4) дедуп в брокере — идемпотентный продьюсер Kafka (enable.idempotence=true) снимает дубли при ретраях продьюсера в пределах сессии, а транзакции Kafka дают атомарность «прочитал-обработал-записал» только если и вход, и выход — Kafka; как только в цепочке появляется внешняя БД или HTTP-вызов, гарантия рушится и снова нужна идемпотентность; (5) дедуп-кеш (Redis SETNX с TTL) — дёшево и быстро, но не атомарно с БД и теряет данные при сбросе, поэтому подходит как оптимизация перед основной проверкой, а не как единственная защита. Важная деталь для ответа: дедуп-хранилище требует политики TTL и должно быть в том же ресурсе, что и бизнес-данные, иначе между «записали дедуп» и «записали данные» появится окно несогласованности.

// Inbox: идемпотентная обработка сообщения в одной транзакции
func handle(ctx context.Context, db *sql.DB, msg Message) error {
tx, err := db.BeginTx(ctx, nil)
if err != nil {
return err
}
defer tx.Rollback()
res, err := tx.ExecContext(ctx,
`INSERT INTO inbox (message_id, handled_at) VALUES ($1, now())
ON CONFLICT (message_id) DO NOTHING`, msg.ID)
if err != nil {
return err
}
if n, _ := res.RowsAffected(); n == 0 {
return tx.Commit() // уже обрабатывали — просто подтверждаем offset
}
if err := applyBusinessEffect(ctx, tx, msg); err != nil {
return err
}
return tx.Commit()
}

Коротко. Transactional Outbox решает задачу «атомарно изменить данные в БД и отправить сообщение в брокер». Сообщение записывается в таблицу outbox той же транзакцией, что и бизнес-изменение, а отдельный процесс (poller или CDC-коннектор) читает таблицу и публикует сообщения в брокер, помечая отправленные. Так исчезает состояние «в БД записали, а событие потеряли» и обратное.

Глубже. Реализации доставки две. Polling publisher: воркер каждые N миллисекунд делает SELECT ... WHERE published_at IS NULL ORDER BY id LIMIT k FOR UPDATE SKIP LOCKED, публикует и помечает; SKIP LOCKED (Postgres 9.5+) позволяет запускать несколько воркеров без конфликтов, но тогда теряется глобальный порядок — если порядок нужен, публикует один воркер (лидер через advisory lock) или порядок гарантируется только внутри aggregate_id за счёт ключа партиции в Kafka. Transaction log tailing / CDC: Debezium читает WAL Postgres и публикует изменения таблицы outbox в Kafka — нет нагрузки поллинга и нет лишней задержки, но добавляется Debezium+Kafka Connect в эксплуатацию. Гарантия в обоих случаях — at-least-once: между публикацией и пометкой строки процесс может упасть, и сообщение уйдёт повторно, поэтому получателю нужна идемпотентность. Практические детали: класть в outbox aggregate_id (ключ партиционирования), event_type, payload, trace_id (чтобы трасса не рвалась), версию схемы события; чистить таблицу по расписанию (partition drop или удаление опубликованных старше суток), иначе она распухает и убивает автовакуум; не публиковать «сырую» строку таблицы как событие — событие является публичным контрактом и должно версионироваться отдельно от внутренней схемы.

CREATE TABLE outbox (
id BIGSERIAL PRIMARY KEY,
aggregate_id UUID NOT NULL,
event_type TEXT NOT NULL,
payload JSONB NOT NULL,
trace_id TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
published_at TIMESTAMPTZ
);
CREATE INDEX ON outbox (id) WHERE published_at IS NULL;

Допустим, приходит тимлид и говорит: «Нужна такая фича», но я понимаю, что для нее нужен новый микросервис, новое взаимодействие между микросервисами или новая технология. Что делали?

Заголовок раздела «Допустим, приходит тимлид и говорит: «Нужна такая фича», но я понимаю, что для нее нужен новый микросервис, новое взаимодействие между микросервисами или новая технология. Что делали?»

Коротко. Вопрос про процесс и зрелость, а не про технологию. Ожидают услышать: сначала уточнить требования и ограничения, потом сравнить минимум два варианта (в том числе «сделать в существующем сервисе»), оценить стоимость владения новой технологией, оформить решение как короткий RFC/ADR, согласовать с владельцем и командой эксплуатации, выкатывать итеративно за feature flag.

Глубже. Каркас ответа. (1) Требования: какую проблему решаем, кто пользователь, какие нефункциональные ограничения (RPS, latency, объём данных, SLA, требования безопасности и хранения данных) и когда нужно. Половина «новых сервисов» отпадает уже здесь. (2) Варианты: минимум три — сделать в существующем сервисе; сделать новый сервис; взять готовое решение/managed-сервис. Для каждого — стоимость реализации, эксплуатации и отката. (3) Критерий выделения сервиса: другая скорость изменений, другой профиль нагрузки, другой владелец, другие требования к доступности или изоляции данных. Если ни одного нет — это модуль, а не сервис. (4) Новая технология: кто будет её эксплуатировать 3 года, есть ли экспертиза, есть ли managed-вариант, как мониторится, как бэкапится, что при инциденте ночью. Хороший приём — предложить пилот с явными критериями успеха и точкой отказа от решения. (5) Оформление: ADR на страницу (контекст, варианты, решение, последствия) — это то, что отличает инженера уровня Senior/Staff. (6) Реализация: тонкий вертикальный срез за флагом, метрики и алерты сразу, потом расширение. Типичные ошибки в ответе: «я бы просто сделал новый сервис на Kafka» без обоснования; отсутствие разговора о стоимости эксплуатации; неготовность спорить с тимлидом фактами и, наоборот, готовность спорить эмоциями.

У компании есть распределенное «облако», состоящее из множества физических серверов, сгруппированных в N (N > 3) разных дата-центров (ЦОД). На этих серверах развертываются кластеры баз данных (Scylla/Cassandra) по следующим правилам:

Заголовок раздела «У компании есть распределенное «облако», состоящее из множества физических серверов, сгруппированных в N (N > 3) разных дата-центров (ЦОД). На этих серверах развертываются кластеры баз данных (Scylla/Cassandra) по следующим правилам:»

Коротко. Обрывок исходника, условия задачи нет — сформулировать точный ответ невозможно. Ниже — краткая база по мультидатацентровым кластерам Cassandra/Scylla, которой обычно достаточно, чтобы начать разбирать такую задачу вслух.

Глубже. Ключевые понятия: данные распределяются по кольцу токенов, реплики размещаются согласно стратегии NetworkTopologyStrategy, где фактор репликации задаётся отдельно на каждый ЦОД ({'dc1': 3, 'dc2': 3}), а Ec2Snitch/GossipingPropertyFileSnitch сообщает кластеру топологию «датацентр/стойка», чтобы реплики раскладывались по разным стойкам. Уровни согласованности задаются на запрос: LOCAL_QUORUM (кворум в своём ЦОД — обычный продовый выбор, устойчив к падению целого ЦОД и не платит межрегиональной латентностью), QUORUM/EACH_QUORUM (глобальный кворум — сильнее, но дороже и уязвимее к разделению), ONE/LOCAL_ONE (быстро, слабые гарантии). Правило «сильной» согласованности: R + W > RF. Между ЦОД репликация асинхронная, поэтому запись с LOCAL_QUORUM доезжает в другой ЦОД с задержкой; для восстановления пропущенного есть hinted handoff, read repair и regular repair (nodetool repair, в Scylla — repair-based node operations). Практический расчёт: при RF=3 в каждом из N ЦОД полный объём данных равен data × 3 × N, и это обычно главный фактор стоимости. Если задача про размещение — обсуждать надо равномерность токенов (vnodes), доменные зоны отказа (стойка/ЦОД), и то, что кворум по всем ЦОД при N>3 делает запись зависимой от самого медленного региона.

Что такое идемпотентность и как ее реализовать?

Заголовок раздела «Что такое идемпотентность и как ее реализовать?»

Коротко. См. выше: идемпотентность — свойство операции давать тот же итог состояния при любом числе повторов. Реализуется четырьмя основными способами: уникальный ключ идемпотентности с сохранением результата, дедупликация по естественному бизнес-ключу (уникальный индекс), условные обновления (WHERE status='pending' / по версии) и переформулировка операции в «установить значение» вместо «изменить на дельту».

Глубже. Самое важное на практике — атомарность проверки и эффекта. Схема «сначала SELECT, есть ли такой ключ, потом INSERT» гонится: два параллельных ретрая пройдут проверку одновременно. Правильно полагаться на СУБД: INSERT ... ON CONFLICT DO NOTHING и анализ RowsAffected, либо уникальный индекс и обработка ошибки нарушения ограничения (в Go с pgx — проверка pgconn.PgError.Code == "23505"). Для операций с деньгами формулируйте эффект через журнал: не «увеличить баланс на 100», а «вставить проводку с уникальным operation_id», баланс же считается или обновляется той же транзакцией — тогда дубликат просто не вставится. Про распределённые блокировки (Redis/etcd) стоит сказать честно: они помогают от конкуренции, но не заменяют идемпотентность, потому что аренда может истечь у живого держателя, и корректность в итоге всё равно обеспечивается уникальным ключом в БД.

В чем преимущества и недостатки микросервисного подхода?

Заголовок раздела «В чем преимущества и недостатки микросервисного подхода?»

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

Глубже. Отличие этой формулировки от предыдущих — акцент на «подход», то есть на процесс. Стоит добавить организационный слой: микросервисы дают выигрыш только при выполнении предпосылок — автономные команды с полным владением сервисом (build it, run it), автоматизированный CI/CD, платформенная инфраструктура, культура наблюдаемости и постмортемов. Без них подход приносит только издержки, что и подтверждают массовые истории обратной консолидации сервисов.

Почему микросервисный подход усложняет отладку транзакций?

Заголовок раздела «Почему микросервисный подход усложняет отладку транзакций?»

Коротко. Потому что «транзакция» превращается в бизнес-процесс из нескольких локальных транзакций в разных БД, разнесённых по процессам и во времени: нет единого стектрейса, нет общего снапшота состояния, нет атомарного отката, а часть шагов асинхронна и может выполниться через минуты. Найти, где именно процесс встал, можно только по корреляционным идентификаторам и трассировке.

Глубже. Конкретные усложняющие факторы: (1) частичные и неопределённые исходы — шаг мог выполниться, но ответ потерялся, и по логам вызывающего это неотличимо от «не выполнился»; (2) компенсации — откат делается бизнес-действиями, которые сами могут упасть, поэтому нужны ретраи и dead-letter для компенсаций; (3) отсутствие изоляции — сага видна снаружи в промежуточном состоянии, возможны аномалии вроде dirty read и lost update на уровне бизнес-логики, для которых в базе аналогов нет; (4) порядок и время — события могут прийти не в том порядке и дважды, поэтому воспроизвести баг локально трудно; (5) разные часы и логи — без общего trace_id события в разных сервисах не сшиваются, а по времени сшивать нельзя из-за расхождения часов. Что делают, чтобы отладка была возможной: сквозной trace_id/correlation_id во всех логах и сообщениях, распределённая трассировка со спанами вокруг каждого шага саги, персистентное состояние саги в БД (или workflow-движок вроде Temporal, где история выполнения хранится и её можно просмотреть), метрики по стадиям процесса (сколько заказов «зависло» в pending дольше 5 минут — это лучший алерт на сломанную сагу), DLQ и админ-инструменты для ручного дожатия/компенсации.

Как организуешь микросервис, который делает долгие вычисления;

Заголовок раздела «Как организуешь микросервис, который делает долгие вычисления;»

Коротко. Асинхронно: HTTP-хендлер только валидирует запрос, создаёт задачу со статусом в БД, кладёт её в очередь и сразу отвечает 202 Accepted с job_id и ссылкой на статус; пул воркеров (отдельный деплоймент, масштабируемый независимо) выполняет задачу, пишет прогресс и результат, клиент опрашивает статус или получает вебхук/SSE. Синхронно долгие вычисления держать нельзя — таймауты балансировщика, ретраи и рестарты подов гарантированно всё сломают.

Глубже. Что должно быть в такой системе: (1) идемпотентность постановки — по ключу запроса, чтобы повтор не создал вторую задачу; (2) at-least-once обработка — воркер может упасть в середине, значит задача должна возвращаться в очередь по видимости/lease и быть перезапускаемой, а вычисление — идемпотентным или разбитым на чекпоинты; (3) отмена и дедлайны — в Go задача выполняется под context, отменяемым по требованию пользователя или по лимиту времени, и код обязан регулярно проверять ctx.Done(), иначе отмена ничего не даст; (4) ограничение конкурентности — семафор/пул воркеров, иначе один под съест CPU и его вытеснят по throttling; для CPU-bound задач имеет смысл GOMAXPROCS, согласованный с CPU limit (в Kubernetes помогает automaxprocs), и вынос в отдельный nodepool; (5) прогресс и наблюдаемость — метрики длительности (гистограмма), длины очереди, числа падений, возраст самой старой необработанной задачи; (6) бэкпрешер — ограничение размера очереди и 429 при перегрузке; (7) graceful shutdown — при SIGTERM перестать брать новые задачи, дожать текущую в пределах terminationGracePeriodSeconds или честно вернуть её в очередь; (8) результат — хранить в БД/объектном хранилище с TTL, а не отдавать через память процесса. Если процесс многошаговый и долгоживущий (часы-дни), уместно назвать workflow-движок (Temporal), который даёт персистентное состояние, ретраи и таймеры из коробки. Отдельная развилка — разбиение задачи на подзадачи и параллельная обработка (map-reduce поверх очереди), если она делится.

Микросервис перевода денег на счет мобильного оператора.

Заголовок раздела «Микросервис перевода денег на счет мобильного оператора.»

Коротко. Это задача на дизайн: главное — деньги и внешний партнёр, значит ключевые требования — идемпотентность, отсутствие потерянных и задвоенных платежей, корректная обработка «неизвестного» исхода у оператора и сверка. Архитектурно: списание в своей БД и обращение к оператору — разные шаги; связываются сагой с состояниями created → funds_reserved → sent_to_provider → succeeded/failed(+refunded), публикация событий через Outbox, вызовы оператора с ключом идемпотентности и обязательной последующей сверкой по его API статуса.

Глубже. Разбор по частям. Модель данных: payments(id, idempotency_key UNIQUE, user_id, msisdn, amount, currency, status, provider_ref, created_at, updated_at) плюс журнал ledger_entries с двойной записью (дебет счёта пользователя, кредит транзитного счёта) и уникальным operation_id; баланс никогда не «плюсуем в поле» без проводки. Списание — локальная транзакция: проверка баланса, вставка проводок, перевод платежа в funds_reserved — всё в одной транзакции Postgres, с блокировкой строки счёта (SELECT ... FOR UPDATE) или условным UPDATE accounts SET balance = balance - $1 WHERE id=$2 AND balance >= $1 с проверкой RowsAffected. Вызов оператора: обязательно с нашим уникальным idempotency_key/external_id, с таймаутом и ограниченным числом ретраев; критично корректно обработать три исхода — успех, явный отказ, таймаут/5xx. При таймауте нельзя ни считать платёж успешным, ни откатывать: платёж остаётся в sent_to_provider, и его судьбу выясняет фоновый reconciler, опрашивающий статус у оператора по external_id с backoff, а также ежедневная сверка реестров. Компенсация: при подтверждённом отказе — возврат средств отдельной проводкой (не DELETE прежних записей, финансовые данные append-only). Защита от повторов: ключ идемпотентности от клиента, уникальный индекс на нём, плюс лимиты (rate limit на пользователя, лимит суммы, антифрод-проверка). Надёжность: circuit breaker на оператора, очередь на повторную отправку, DLQ, алерты на «платежи, висящие в sent_to_provider дольше N минут» — это главный операционный алерт такой системы. Наблюдаемость и аудит: неизменяемый журнал всех переходов статуса, trace_id во всех логах, метрики по конверсии и по кодам ответа оператора. Хорошо упомянуть, что деньги хранятся в целых минимальных единицах (копейках) в numeric/int64, а не в float.

Какие можете назвать преимущества и недостатки микросервисной архитектуры?

Заголовок раздела «Какие можете назвать преимущества и недостатки микросервисной архитектуры?»

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

Глубже. При третьем повторе вопроса на собеседовании стоит добавить нюанс, который выделяет ответ: назвать условия, при которых плюсы реализуются (автономные команды, CI/CD, платформа, наблюдаемость) и признаки, что архитектура выродилась в распределённый монолит (синхронные цепочки, общая БД, согласованные релизы, изменение фичи требует правок в 5 репозиториях).

Что можете рассказать о реализации распределенных транзакций?

Заголовок раздела «Что можете рассказать о реализации распределенных транзакций?»

Коротко. См. выше про распределённые транзакции. Реализация на практике: локальная транзакция + Outbox для публикации, сага (оркестрованная или хореографическая) для последовательности шагов, компенсации вместо отката, Inbox/идемпотентность на потребителях, персистентное состояние саги и таймауты на каждый шаг. 2PC применяется точечно, там где есть XA-ресурсы или один движок БД.

Глубже. Практический скелет оркестратора: состояние саги хранится в таблице (saga_id, step, status, payload, attempts, next_retry_at), переходы выполняются идемпотентно, а «двигает» сагу тот же механизм, что и Outbox — воркер, который выбирает саги, готовые к следующему шагу (FOR UPDATE SKIP LOCKED). Такой дизайн переживает рестарт процесса: состояние в БД, а не в памяти. Обязательны таймауты шага (иначе сага повиснет навсегда) и явная терминальная ветка failed с алертом. Отдельно проговорить отсутствие изоляции и меры против неё: семантические блокировки (статус pending блокирует конкурирующие операции над агрегатом), коммутативные операции, повторное чтение перед подтверждением. Если в компании есть Temporal/Cadence — сказать, что они реализуют ровно это (durable execution: код процесса + персистентная история), избавляя от ручного стейт-машина.

Как вы определяли границы сервисов при переходе на микросервисы?

Заголовок раздела «Как вы определяли границы сервисов при переходе на микросервисы?»

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

Глубже. Рабочий метод: (1) собрать сценарии/события домена (event storming), сгруппировать по агрегатам и найти места, где меняется язык («заказ» в продажах и «заказ» в логистике — разные сущности) — это и есть границы контекстов; (2) построить матрицу «сценарий × данные» и посмотреть, какие таблицы всегда меняются вместе — они должны остаться в одном сервисе; операция, требующая атомарности, — сильный сигнал, что граница проведена неверно; (3) проверить связность релизов: если два кандидата всегда меняются одним PR, их не надо разделять; (4) учесть закон Конвея и организационную структуру — сервис без владельца обречён; (5) выделять итеративно через Strangler Fig, начиная с наименее связанного и наиболее ценного куска (обычно это что-то на периферии: уведомления, поиск, отчёты), при этом обязательно разрезая данные, а не только код. Полезно назвать анти-паттерны: деление по техническим слоям (api-service, db-service), «наносервисы» на одну функцию, общий сервис-хранилище для всех, и разделение кода при сохранении общей БД — оно даёт всю боль и ни одного плюса.

Как технически проверяется подпись и защита JWT между микросервисами?

Заголовок раздела «Как технически проверяется подпись и защита JWT между микросервисами?»

Коротко. Токен подписывается издателем; получатель проверяет подпись публичным ключом (асимметрично, RS256/ES256/EdDSA) или общим секретом (HS256), после чего валидирует стандартные claims: exp, nbf, iat, iss, aud, а также alg строго из белого списка и kid для выбора ключа из JWKS. Между сервисами предпочтителен асимметричный алгоритм: приватный ключ есть только у эмитента, сервисы-получатели проверяют публичным ключом, скачанным по /.well-known/jwks.json и закешированным.

Глубже. Что именно проверяется по шагам: разобрать header и убедиться, что alg — ожидаемый (иначе классические атаки alg: none и подмена RS256 на HS256, где публичный ключ используется как HMAC-секрет; хорошие библиотеки этого не допускают, но проверять белый список обязательно); по kid найти ключ в кеше JWKS (с ротацией и ограничением частоты обновления, чтобы неизвестный kid не превратился в DoS на эндпоинт JWKS); проверить подпись над base64url(header) + "." + base64url(payload); проверить exp/nbf с небольшим допуском на расхождение часов; проверить iss и aud — без проверки aud токен для одного сервиса примут в другом; при необходимости — scope/roles и jti против списка отозванных. Защита в целом: только TLS (лучше mTLS между сервисами), короткое время жизни access-токена (минуты) и refresh-токен для продления, отсутствие чувствительных данных в payload (он лишь base64, а не шифр — при необходимости JWE), явная стратегия отзыва (JWT по природе не отзывается, поэтому либо короткий TTL, либо denylist по jti, либо интроспекция). Для service-to-service часто разумнее не пробрасывать пользовательский токен вглубь, а использовать отдельные машинные токены (client credentials) или mTLS-идентичность (SPIFFE/SPIRE в mesh), а данные пользователя передавать отдельным подписанным заголовком, чтобы внутренние сервисы не могли действовать от имени пользователя вовне. В Go — github.com/golang-jwt/jwt/v5 с явным jwt.WithValidMethods([]string{"RS256"}), jwt.WithAudience, jwt.WithIssuer, и github.com/MicahParks/keyfunc для JWKS.

Настраивали ли вы Nginx как Reverse Proxy или API Gateway?

Заголовок раздела «Настраивали ли вы Nginx как Reverse Proxy или API Gateway?»

Коротко. Вопрос про опыт. Каркас: сказать, что именно настраивали — upstream-пулы и балансировку, TLS-терминацию, таймауты и буферизацию, ретраи через proxy_next_upstream, ограничение скорости limit_req, маршрутизацию по путям/хостам, проброс заголовков X-Forwarded-For/X-Request-Id, gzip и кеширование, — и честно разграничить: Nginx как reverse proxy делает L7-маршрутизацию и защиту, но полноценный API Gateway (аутентификация, квоты по ключам, агрегация ответов, трансформация схем) обычно требует Kong/APISIX/Envoy или Ingress-контроллера с плагинами.

Глубже. Что стоит уметь назвать конкретно: proxy_pass и различие поведения с завершающим слешем в location; таймауты proxy_connect_timeout/proxy_send_timeout/proxy_read_timeout (значения по умолчанию 60s почти всегда надо уменьшать); proxy_next_upstream — по умолчанию ретраит error timeout, и включать в него неидемпотентные non_idempotent опасно; keepalive в блоке upstream (без него на каждый запрос открывается новое соединение к бэкенду, и порты кончаются); балансировка round-robin/least_conn/ip_hash, пассивные проверки max_fails/fail_timeout (активные health-check только в Nginx Plus); limit_req_zone (алгоритм leaky bucket) и limit_conn; proxy_buffering и его влияние на SSE/стриминг (для стриминга буферизацию отключают); поддержка gRPC через grpc_pass и обязательный HTTP/2; TLS-терминация, HSTS, OCSP stapling. В Kubernetes всё это чаще живёт как ingress-nginx с аннотациями или как Gateway API. Типичная ошибка на собеседовании — путать reverse proxy и load balancer L4: Nginx работает на L7 (видит заголовки и пути), L4-балансировщик — только адреса и порты.

Объясните паттерны Inbox и Outbox. Для решения каких проблем они предназначены? Как они помогают обеспечивать согласованность данных в распределенных системах?

Заголовок раздела «Объясните паттерны Inbox и Outbox. Для решения каких проблем они предназначены? Как они помогают обеспечивать согласованность данных в распределенных системах?»

Коротко. Outbox решает проблему атомарности «изменить данные + опубликовать событие»: событие пишется в таблицу той же транзакцией, отправляется отдельным процессом. Inbox решает зеркальную проблему на приёме: обработать сообщение ровно один раз, несмотря на дубликаты at-least-once — идентификатор сообщения вставляется в таблицу inbox той же транзакцией, что и бизнес-эффект, и повтор отсекается уникальным индексом. Вместе они дают надёжный обмен без потерь и без задвоений, то есть eventual consistency с корректным итогом.

Глубже. Проблема, которую они закрывают, называется dual write: два независимых ресурса (БД и брокер) нельзя записать атомарно, и любой порядок ломается — «сначала брокер, потом БД» даёт событие о несуществующем факте при падении, «сначала БД, потом брокер» теряет событие. Outbox сводит запись к одному ресурсу, поэтому корректность обеспечивается обычной ACID-транзакцией. Цена — задержка (poller работает с интервалом) и at-least-once, что и компенсирует Inbox.

Практические замечания: message_id для Inbox должен быть стабильным идентификатором события (генерируется отправителем и сохраняется в outbox), а не смещением в брокере; Inbox надо чистить по TTL, при этом TTL должен превышать максимально возможную задержку доставки/ретраев; offset в Kafka коммитится после коммита транзакции БД, иначе появится потеря; если бизнес-эффект и так защищён уникальным бизнес-ключом, отдельная Inbox-таблица не нужна — важна не таблица, а свойство идемпотентности.

Коротко. OpenTelemetry: SDK в каждом сервисе, автоматическая инструментация HTTP/gRPC/SQL, распространение контекста через заголовок W3C traceparent, экспорт спанов через OTLP в коллектор и дальше в Jaeger/Tempo; trace_id пишется в каждую строку лога, чтобы из графика в Grafana можно было провалиться в конкретную трассу и в логи по ней.

Глубже. Ключевые детали реализации в Go: контекст трассы живёт в context.Context, поэтому обязательное условие — прокидывать ctx через все слои; для HTTP-сервера — otelhttp.NewHandler, для клиента — otelhttp.NewTransport, для gRPC — otelgrpc статистический хендлер, для БД — otelsql/обёртки драйвера. Асинхронные границы требуют ручной работы: при отправке в Kafka контекст инжектится в заголовки сообщения (propagation.TraceContext{}.Inject в otelsarama/otelkafka), при чтении — извлекается, иначе трасса рвётся на брокере; связь между producer- и consumer-спанами оформляется через span link или обычную родительскую связь. Сэмплирование: полный сбор дорог, обычно ставят parent-based + ratio (например, 1–10%) или tail-based сэмплирование в коллекторе, чтобы гарантированно сохранять трассы с ошибками и высокой латентностью. Полезно назвать, что даёт трассировка на практике: карта сервисных зависимостей, поиск N+1-вызовов, точная атрибуция латентности по спанам, и связка «метрика → экземпляры (exemplars) → трасса → логи», которая сокращает MTTR сильнее всего. Отдельно упомянуть, что в спаны стоит класть бизнес-атрибуты (order_id, tenant), но не персональные данные и не токены.

Как организовать работу трех сервисов с одной базой данных без конфликтов? Какие стратегии и паттерны для этого существуют?

Заголовок раздела «Как организовать работу трех сервисов с одной базой данных без конфликтов? Какие стратегии и паттерны для этого существуют?»

Коротко. Сначала честно сказать, что общая БД — антипаттерн для микросервисов, и правильная цель — раздельные схемы с одним владельцем на таблицу. Если общая БД неизбежна (переходный период, легаси), конфликты снимают разграничением владения: у каждой таблицы ровно один сервис-писатель, остальные читают через API этого сервиса либо через отдельную схему/представление с правами только на чтение; конкурентная запись защищается оптимистической блокировкой по версии, короткими транзакциями и правильным уровнем изоляции.

Глубже. Набор стратегий по слоям. Организационный: schema per service в одном инстансе Postgres + GRANT так, чтобы физически нельзя было писать в чужие таблицы; миграции чужой схемы запрещены. Конкурентность на данных: оптимистическая блокировка (UPDATE ... SET version = version + 1 WHERE id = $1 AND version = $2, при 0 затронутых строк — конфликт и повтор) — предпочтительна, потому что не держит блокировок; пессимистическая (SELECT ... FOR UPDATE, при желании NOWAIT/SKIP LOCKED) — когда конфликты часты и повтор дорог; advisory locks для координации фоновых задач (например, чтобы миграцию или крон запускал один экземпляр). Уровни изоляции: по умолчанию READ COMMITTED (в Postgres) допускает non-repeatable read; REPEATABLE READ/SERIALIZABLE в Postgres реализованы через снапшоты и SSI, то есть транзакция может упасть с 40001 serialization_failure, и приложение обязано уметь повторять — это надо проговорить явно. Очереди задач в БД: SELECT ... FOR UPDATE SKIP LOCKED — стандартный способ дать нескольким воркерам разбирать задачи без конфликтов. Дедлоки: возникают при разном порядке захвата строк, лечатся единым порядком обновления (например, всегда по возрастанию id) и коротким deadlock_timeout/ретраями. Ресурсы: три сервиса делят один пул соединений сервера — обязательно ограничить max_open_conns в каждом (иначе too many connections), поставить PgBouncer в transaction pooling и помнить, что в этом режиме недоступны session-level фичи (prepared statements без statement_cache_mode=describe, advisory-локи на сессию, LISTEN/NOTIFY). Целевое состояние: развязать через события — сервис-владелец публикует изменения, остальные держат свои реплики нужных данных (CQRS/materialized view), после чего конфликтов записи не остаётся вовсе.

Какие паттерны микросервисной архитектуры вы знаете? Можете привести примеры?

Заголовок раздела «Какие паттерны микросервисной архитектуры вы знаете? Можете привести примеры?»

Коротко. См. выше перечень паттернов. С примерами: API Gateway — единая точка входа с аутентификацией и rate limit (Kong, ingress-nginx); BFF — отдельный агрегатор под мобильное приложение; Database per Service — у платежей своя БД, у заказов своя; Saga — оформление заказа (резерв склада → оплата → отгрузка) с компенсациями; Outbox — публикация OrderCreated из транзакции заказа; CQRS — отдельная денормализованная витрина для поиска заказов; Circuit Breaker — обрыв вызовов к упавшему рекомендательному сервису с фолбэком на топ-товары; Strangler Fig — постепенный вынос модуля из монолита за прокси; Sidecar — Envoy рядом с подом для mTLS и метрик.

Глубже. Хорошая практика ответа — сгруппировать паттерны и для каждой группы дать один живой пример из своего опыта, чтобы это не звучало как список из книги. Дополнительно можно упомянуть менее очевидные, но ценные: Health Check API и разделение liveness/readiness/startup-проб; Externalized Configuration (конфиг из env/ConfigMap/Vault, а не из файла в образе); Service Template / Chassis — общий каркас сервиса с логированием, метриками, трассировкой, graceful shutdown, чтобы каждый новый сервис не изобретал их заново; Consumer-Driven Contract Testing (Pact) — тесты контрактов вместо дорогих сквозных; Backpressure/Rate Limiting и Bulkhead — изоляция пулов, чтобы медленная зависимость не выела все воркеры; Materialized View — локальная копия чужих данных, обновляемая событиями.

В чем недостатки микросервисной архитектуры по сравнению с монолитами?

Заголовок раздела «В чем недостатки микросервисной архитектуры по сравнению с монолитами?»

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

Глубже. Стоит добавить количественные и организационные аспекты, которых нет в монолите: рост объёма «служебного» кода (сериализация, ретраи, клиенты) — часто 20–30% усилий; необходимость поддерживать N пайплайнов, N наборов дашбордов и алертов; сложность локальной разработки (поднять 15 сервисов на ноутбуке невозможно — нужны моки, contract-тесты, dev-окружения или telepresence-подходы); удорожание изменений, пересекающих границы сервисов (одна фича — 4 PR в 4 репозиториях и согласованный порядок выката); и когнитивная нагрузка на дежурного, которому нужно понимать всю топологию.

Что такое идемпотентность в REST? Какие рестовые методы идемпотентны a какие нет?

Заголовок раздела «Что такое идемпотентность в REST? Какие рестовые методы идемпотентны a какие нет?»

Коротко. По RFC 9110 метод идемпотентен, если несколько одинаковых запросов имеют тот же эффект на сервере, что и один. Идемпотентны: GET, HEAD, PUT, DELETE, OPTIONS, TRACE. Не идемпотентен POSTPATCH — в общем случае, хотя конкретный PATCH может быть идемпотентным). Отдельно: GET, HEAD, OPTIONS, TRACE ещё и безопасны (safe) — не должны менять состояние; безопасность влечёт идемпотентность, обратное неверно.

Глубже. Тонкости, которые проверяют на собеседовании: (1) идемпотентность — про состояние сервера, а не про одинаковость ответов; повторный DELETE /orders/1 вернёт 404 вместо 204, но метод всё равно идемпотентен, так как состояние не меняется; (2) идемпотентность — это свойство контракта, которое обязан обеспечить разработчик, а не гарантия протокола: PUT, который делает counter++, нарушает спецификацию; (3) PATCH идемпотентен, если тело описывает целевое состояние ({"status":"paid"}), и не идемпотентен, если описывает дельту ({"op":"increment","value":1}); (4) POST можно сделать безопасным для повторов ключом идемпотентности или PUT-семантикой с клиентски сгенерированным идентификатором (PUT /orders/{uuid}); (5) практическое следствие: HTTP-клиенты и прокси по умолчанию ретраят только идемпотентные методы — так устроен и net/http (повтор при разрыве переиспользуемого keep-alive соединения выполняется только для идемпотентных/без тела запросов), и proxy_next_upstream в Nginx.

Коротко. Вопрос про опыт. Типовой ответ: каждый сервис — свой репозиторий и свой пайплайн (тесты, линтеры, сборка контейнера, скан уязвимостей, публикация в registry с тегом = git sha), выкат в Kubernetes через Helm/Kustomize и GitOps (Argo CD/Flux), стратегия — rolling update по умолчанию, canary или blue-green для рискованных изменений, миграции БД отдельным шагом по схеме expand–contract, откат — переключением на предыдущий образ/ревизию.

Глубже. Что стоит назвать, чтобы ответ звучал зрело: иммутабельные артефакты (тег образа — коммит, а не latest), разделение деплоя и релиза через feature flags — код выкатывается выключенным, включается флагом, что делает откат мгновенным без передеплоя; пробы: readinessProbe (когда под можно ставить в балансировку), livenessProbe (когда перезапустить — часто вреден, если настроен агрессивно), startupProbe для медленного старта; graceful shutdown: обработка SIGTERM, preStop-hook с небольшой паузой, чтобы endpoint успел уйти из iptables/endpoints, terminationGracePeriodSeconds больше самой долгой обработки — в Go это http.Server.Shutdown(ctx); PodDisruptionBudget и maxUnavailable/maxSurge для контролируемого rolling; совместимость версий: во время выката работают обе версии, поэтому контракты и схема БД должны быть совместимы в обе стороны; наблюдаемость релиза: аннотация версии в метриках, автоматический откат канарейки по SLO (Argo Rollouts/Flagger); секреты из Vault/External Secrets, а не в манифестах.

Как вы отслеживаете распределенную трассировку запросов через несколько сервисов?

Заголовок раздела «Как вы отслеживаете распределенную трассировку запросов через несколько сервисов?»

Коротко. См. выше про трейсинг. Механика: на входе генерируется/принимается trace_id, каждый сервис создаёт спаны и передаёт контекст дальше в заголовке traceparent (W3C Trace Context) или b3 (Zipkin); спаны экспортируются по OTLP в коллектор и сшиваются бэкендом (Jaeger/Tempo) в одно дерево по trace_id/parent_span_id.

Глубже. Практика «как именно отслеживаю»: в логах каждой записи есть trace_id и span_id (в Go со slog это добавляется хендлером, читающим trace.SpanContextFromContext(ctx)), в Grafana настроены exemplars и корреляция Loki↔Tempo, поэтому путь всегда одинаковый: заметил всплеск p99 на графике → кликнул в exemplar → открыл трассу → увидел долгий спан → перешёл в логи этого сервиса по trace_id. Для асинхронных цепочек контекст переносится в заголовках сообщений Kafka/RabbitMQ. Что важно не забыть: единый формат распространения во всех сервисах (иначе трассы рвутся на границе несовместимых форматов), сэмплирование, согласованное между сервисами (parent-based, иначе получите обрывки), ограничение кардинальности атрибутов, и обязательная пометка спана статусом ошибки (span.RecordError, span.SetStatus(codes.Error, ...)) — иначе tail-based сэмплирование не сохранит нужные трассы.

В чем разница между монолитом и микросервисами?

Заголовок раздела «В чем разница между монолитом и микросервисами?»

Коротко. См. выше. Одной фразой: монолит — одна единица развёртывания с внутрипроцессными вызовами и общей транзакционной БД; микросервисы — множество независимо развёртываемых процессов со своими БД и сетевым взаимодействием. Всё остальное (сложность, надёжность, скорость команд) — следствия этих двух различий.

Глубже. Полезно проговорить, что «монолит vs микросервисы» — не бинарный выбор, а спектр: модульный монолит, монолит с вынесенными воркерами, несколько крупных сервисов («макросервисы»), полноценные микросервисы. Различать также стоит логическую и физическую архитектуру: монолит может быть внутри отлично модульным, а набор микросервисов — сильно связанным. Критерий, который расставляет всё по местам: можете ли вы выкатить один компонент в прод, не выкатывая остальные, и не сломав их.

Коротко. См. выше: свойство метода, при котором N одинаковых запросов оставляют сервер в том же состоянии, что и один. Идемпотентны GET, HEAD, PUT, DELETE, OPTIONS, TRACE; POST — нет.

Глубже. Дополнение к предыдущему разбору — зачем это нужно в REST именно как в протоколе: идемпотентность разрешает промежуточным узлам (прокси, балансировщикам, клиентским библиотекам) автоматически повторять запрос при сетевой ошибке, а безопасные методы разрешает кешировать. Поэтому нарушение контракта («GET меняет состояние») ломает не только вашу логику, но и инфраструктуру: префетч браузера или скан монитора могут выполнить операцию за пользователя.

Что использовал для межсервисного взаимодействия? (grpc/rest)

Заголовок раздела «Что использовал для межсервисного взаимодействия? (grpc/rest)»

Коротко. Вопрос про опыт: внутри — gRPC (строгий контракт в protobuf, кодогенерация, HTTP/2 с мультиплексированием, бинарный формат, встроенные дедлайны и стриминг), наружу и для интеграций — REST/JSON (простота, кешируемость, отладка curl’ом, поддержка любым клиентом). Асинхронные события — Kafka/RabbitMQ.

Глубже. Аргументы для выбора, если попросят сравнить: gRPC даёт меньше трафика и меньшую латентность на плотных внутренних вызовах, генерирует клиенты и серверные заглушки, из коробки распространяет deadline и метаданные, умеет двунаправленный стриминг и клиентскую балансировку по спискам адресов; минусы — нужен HTTP/2 end-to-end (не все прокси и балансировщики дружелюбны, для L7-балансировки долгоживущих соединений нужен mesh или grpc-go resolver), сложнее отлаживать руками (нужен grpcurl), браузеру нужен gRPC-Web. REST проще для внешних потребителей, лучше кешируется, но контракт слабее — его дисциплинируют OpenAPI и генерацией клиентов. Отдельный практический пункт — эволюция контрактов: в protobuf правила совместимости жёсткие и понятные (не менять номера полей, не переиспользовать удалённые — reserved, добавлять только опциональные поля), в JSON приходится договариваться (tolerant reader: игнорировать неизвестные поля, не считать отсутствие поля ошибкой). Если в системе есть Kafka, стоит добавить, что синхронный вызов используется там, где нужен немедленный ответ, а всё, что может подождать, уходит в события — это главный рычаг снижения связности и повышения доступности.

Какие тулзы использовал для построения микросервисной архитектуры?

Заголовок раздела «Какие тулзы использовал для построения микросервисной архитектуры?»

Коротко. Вопрос про опыт; отвечать по слоям. Рантайм и оркестрация: Docker, Kubernetes, Helm/Kustomize, Argo CD. Взаимодействие: gRPC/protobuf с buf, REST с OpenAPI, Kafka (или RabbitMQ/NATS), schema registry. Данные: PostgreSQL, Redis, миграции goose/golang-migrate, Debezium для CDC. Наблюдаемость: OpenTelemetry, Prometheus, Grafana, Loki, Jaeger/Tempo, Alertmanager, Sentry. Безопасность и конфигурация: Vault/External Secrets, Keycloak/OIDC, mTLS или service mesh. CI/CD: GitLab CI/GitHub Actions, Trivy, golangci-lint, тесты на testcontainers.

Глубже. Ценится не список, а обоснование и понимание границ: почему Kafka, а не RabbitMQ (лог с сохранением и переигрыванием, партиционирование и порядок по ключу против гибкой маршрутизации и per-message ack), почему Argo CD, а не kubectl apply из пайплайна (декларативное состояние в git, дрейф виден, откат — revert), почему testcontainers, а не моки БД (тестируем реальный Postgres, включая миграции и SQL-диалект). Полезно упомянуть внутренние инструменты, которые обычно приходится делать самим: шаблон сервиса (chassis), общая библиотека логирования/метрик/трассировки, генератор клиентов из proto, каталог сервисов с владельцами и рунбуками.

Коротко. См. выше — вопрос повторяется. Плюсы: независимые релизы, автономия команд, изоляция отказов, точечное масштабирование, свобода стека. Минусы: сеть, eventual consistency, дороже отладка, тесты и эксплуатация, эволюция контрактов, риск распределённого монолита.

Глубже. Отличие от прошлых формулировок можно закрыть одним практическим тезисом: плюсы микросервисов почти все организационные, а минусы — почти все технические. Поэтому решение о распиле принимается по состоянию команды и продукта, а не по «модности» архитектуры, и почти всегда правильный порядок — сначала навести порядок в модульности и в CI/CD, потом выносить сервисы.

Коротко. Стиль построения системы как набора небольших автономных сервисов, каждый из которых реализует одну бизнес-возможность, владеет своими данными, разворачивается и масштабируется независимо и общается с остальными по сети через явные контракты (RPC или события).

Глубже. Ключевые характеристики, отличающие микросервисы от «просто нескольких приложений»: независимость развёртывания; децентрализация данных (database per service); децентрализация управления (команда владеет сервисом от кода до дежурства); умные конечные точки и «глупые» каналы (логика в сервисах, а не в ESB); проектирование под отказ; автоматизация инфраструктуры как предпосылка; эволюционный дизайн — сервисы должны быть заменяемыми. Каноническое описание — статья Джеймса Льюиса и Мартина Фаулера 2014 года; из неё же — рекомендация «monolith first» и «размер сервиса определяется не строками кода, а границей бизнес-возможности».

Назовите недостатки микросервисной архитектуры.

Заголовок раздела «Назовите недостатки микросервисной архитектуры.»

Коротко. См. выше. Сжатый список: сетевые отказы и латентность, отсутствие распределённых транзакций и жизнь с eventual consistency, сложность отладки и сквозного тестирования, эксплуатационные затраты (CI/CD, наблюдаемость, платформа), версионирование контрактов, распределённые данные и трудные кросс-сервисные запросы, повышенные требования к квалификации команды, риск распределённого монолита.

Глубже. К этому стоит добавить два часто забываемых пункта: стоимость инфраструктуры — сервис-минимум в Kubernetes с двумя репликами, сайдкарами и своей БД стоит денег, и на 30 сервисах это заметная строка бюджета; сложность обеспечения безопасности — периметр перестаёт быть периметром, требуется аутентификация «сервис-сервис» (mTLS/токены), управление секретами и аудит доступа к данным в каждом сервисе отдельно.

Коротко. См. выше. Свойство операции: f(f(x)) = f(x) — повторное применение не меняет результат по сравнению с однократным. В распределённых системах это то, что делает безопасными ретраи при неопределённых исходах.

Глубже. Термин пришёл из математики (идемпотентный оператор), и полезно уметь привести примеры за пределами HTTP: SET key value в Redis идемпотентна, INCR — нет; UPDATE ... SET status='paid' идемпотентна, UPDATE ... SET balance = balance - 100 — нет; kubectl apply идемпотентен (декларативность), kubectl create — нет; операция «добавить элемент в множество» идемпотентна, «добавить в список» — нет (на этом свойстве, коммутативности и идемпотентности, построены CRDT).

С какими способами взаимодействия между сервисами вы сталкивались?

Заголовок раздела «С какими способами взаимодействия между сервисами вы сталкивались?»

Коротко. Синхронный запрос-ответ (gRPC, REST/HTTP, реже GraphQL); асинхронные сообщения через брокер (Kafka, RabbitMQ, NATS, SQS) в вариантах «событие» (публикация факта) и «команда» (адресная задача); а также обмен через общее хранилище/CDC и батчевые выгрузки. Ортогонально: один-к-одному против публикация-подписка, и оркестрация против хореографии.

Глубже. Классификация, удобная для ответа: по синхронности (блокирующий вызов vs сообщение), по числу получателей (point-to-point vs pub/sub), по семантике (команда, запрос, событие). Осевые различия: синхронный вызов даёт немедленный результат и простую модель ошибок, но создаёт временну́ю связность — вызываемый обязан быть жив прямо сейчас; асинхронный обмен снимает эту связность, добавляет буферизацию и естественный бэкпрешер, но делает согласованность отложенной и требует идемпотентности и работы с порядком сообщений. Стоит упомянуть гибриды: асинхронный запрос-ответ через reply-to очередь и correlation_id; server-sent events / websocket для доставки результата клиенту; long polling; и CDC как способ интеграции с легаси, который нельзя изменить.

Как можем построить распределенную транзакцию в рамках сервиса с распределенным вычислением?

Заголовок раздела «Как можем построить распределенную транзакцию в рамках сервиса с распределенным вычислением?»

Коротко. Тем же набором: разложить вычисление на шаги, каждый из которых атомарен локально и идемпотентен, хранить состояние процесса персистентно (таблица саги/воркфлоу), двигать шаги воркерами с ретраями и таймаутами, а на неуспехе выполнять компенсации. Если вычисление действительно распределено по узлам, добавляется координация: единый источник истины о состоянии задачи, аренда (lease) на выполнение части работы и фиксация результата по принципу «победитель один» (условный UPDATE по версии).

Глубже. Практические механики: разбиение на детерминированные шаги с чекпоинтами, чтобы после падения узла продолжать, а не начинать заново; распределение частей задачи через очередь с at-least-once и защитой от двойной фиксации результата (уникальный ключ (job_id, chunk_id)); аренда с TTL вместо вечных блокировок, при этом корректность обеспечивается не арендой, а уникальностью записи результата — истёкшая аренда не должна приводить к порче данных; агрегация частичных результатов транзакционно (счётчик завершённых кусков в той же БД, финализация при достижении полного числа). Если нужна именно ACID-семантика на распределённых данных — это уровень СУБД: Raft-репликация плюс 2PC между шардами, как в CockroachDB/Spanner/YDB, и правильный ответ «взять СУБД, которая это умеет», а не реализовывать самим. Для оркестрации долгих распределённых вычислений в проде обычно берут Temporal или Airflow/Dagster (для батчей), а не пишут свой координатор.

Коротко. Нужен координатор и участники. Фаза 1 (prepare/voting): координатор просит всех подготовиться — участник выполняет работу, записывает её в свой журнал так, чтобы гарантированно суметь закоммитить, и отвечает yes/no, после чего держит блокировки. Фаза 2 (commit/abort): если все ответили yes, координатор записывает в свой durable-лог решение commit и рассылает его; иначе рассылает abort. Участник обязан подчиниться решению и не может передумать.

Глубже. В PostgreSQL это доступно напрямую: PREPARE TRANSACTION 'gid' фиксирует транзакцию в подготовленном состоянии (переживает рестарт, видна в pg_prepared_xacts), затем COMMIT PREPARED 'gid' или ROLLBACK PREPARED 'gid'; требуется max_prepared_transactions > 0. Ключевые проблемы, которые обязательно надо назвать: (1) блокирующий протокол — если координатор упал после PREPARE, участники сидят с удерживаемыми блокировками в состоянии неопределённости неограниченно долго, а в Postgres забытая prepared-транзакция блокирует автовакуум и приводит к разрастанию и в пределе к wraparound-угрозе; (2) координатор — единая точка отказа, лечится репликацией его лога через консенсус (Raft), то есть 2PC поверх Raft, как в распределённых СУБД; (3) производительность — два раунда сети и fsync, блокировки удерживаются весь протокол, пропускная способность падает, латентность растёт; (4) требования к участникам — нужны XA/prepared-транзакции, а HTTP-сервис или Kafka их не предоставляют, поэтому «2PC между микросервисами» обычно невозможен физически. Отсюда стандартный вывод: 2PC уместен внутри одного доверенного контура с XA-ресурсами (несколько БД одной команды, БД + JMS в Java-мире), а между сервисами применяют сагу или TCC — последняя, по сути, «2PC на прикладном уровне» с явными Try/Confirm/Cancel, но без удержания блокировок СУБД. Обязательно нужен и «resolver» — фоновый процесс, который дожимает зависшие prepared-транзакции по журналу координатора.

Коротко. Сага — способ выполнить бизнес-операцию, охватывающую несколько сервисов, как последовательность локальных транзакций: каждый шаг коммитится в своей БД и публикует событие, запускающее следующий; если шаг падает, выполняются компенсирующие транзакции для уже выполненных шагов в обратном порядке. Атомарности нет, есть atomicity-подобная гарантия «либо все шаги, либо все компенсированы» и eventual consistency.

Глубже. Два способа координации: оркестрация (центральный процесс-стейт-машина знает шаги и компенсации, вызывает участников командами) и хореография (каждый сервис подписан на события других). Свойства и требования: шаги и компенсации должны быть идемпотентны (сообщения дублируются) и по возможности коммутативны; компенсация — это не откат, а обратное бизнес-действие (не «удалить платёж», а «сделать возврат»), причём некоторые шаги некомпенсируемы (отправленное письмо не вернуть) — такие шаги ставят в конец; шаги делят на компенсируемые, «поворотные» (pivot — после него откат невозможен, дальше только идти вперёд) и повторяемые (retriable — должны в итоге завершиться успешно). Изоляции нет, поэтому применяют контрмеры: семантические блокировки (статус pending, который видят конкурирующие операции), commutative updates, пессимистический порядок шагов (сначала то, что легче компенсировать), повторное чтение перед подтверждением, версионные записи. На эксплуатации саге нужны таймауты на каждый шаг, персистентное состояние, метрика «зависших» саг и ручные инструменты для дожатия.

  • Определяют микросервисы через размер («сервис должен быть на 200 строк») вместо независимости развёртывания и владения данными; отсюда же миф про «наносервисы».
  • Говорят, что микросервисы всегда лучше монолита, и не могут назвать ни одного случая, когда распил вреден, ни одной проблемы, полученной после перехода.
  • Обещают «exactly once» доставку через Kafka. Гарантия доставки в сети принципиально at-least-once (или at-most-once); exactly-once достигается только как идемпотентная обработка, а транзакции Kafka работают лишь внутри Kafka.
  • Путают идемпотентность с безопасностью (safe) методов и утверждают, что DELETE неидемпотентен, потому что второй раз вернёт 404. Идемпотентность — про состояние сервера, а не про код ответа.
  • Считают, что Outbox сам по себе даёт «ровно один раз». Он даёт только «не потеряем», дубликаты остаются, и без Inbox/идемпотентности решение неполно.
  • Реализуют идемпотентность как «сначала SELECT, потом INSERT» — гонка между параллельными ретраями; нужна атомарная вставка с уникальным индексом в той же транзакции, что и бизнес-эффект.
  • Предлагают 2PC между HTTP-микросервисами, не понимая, что участники должны поддерживать prepared-транзакции, и не упоминая блокировку ресурсов и отказ координатора.
  • Оставляют общую БД на несколько сервисов и называют это микросервисами; при этом схема становится публичным контрактом, и независимость деплоя теряется.
  • Не упоминают таймауты, backoff с jitter и circuit breaker, из-за чего в дизайне возникает каскадный отказ и retry storm; либо, наоборот, предлагают ретраить любой запрос, включая неидемпотентные.
  • Забывают про наблюдаемость: нет trace_id, нет перцентилей (только средние), нет метрик по стадиям саги — в результате «зависшие» процессы обнаруживаются от пользователей, а не от алертов.
  • Делают миграции БД несовместимыми с работающей старой версией кода, забывая, что при rolling update обе версии живут одновременно; не знают про CREATE INDEX CONCURRENTLY и lock_timeout.