Тестирование микросервисов
Замер: сервис A исправен, но сосед завис - принимает соединение и молчит. У A восемь рабочих потоков и нет таймаута на вызов соседа. Двадцать четыре запроса заняли 9 секунд, и всё это время A не отвечал никому. С таймаутом в полсекунды те же двадцать четыре запроса уложились в 1,5 секунды - все с ошибкой, но сервис остался живым и отвечающим.
Стержень: сервис тестируют в изоляции с дублёрами соседей плюс договорами между парами, а полный стенд всех сервисов сразу хрупок и почти всегда красен не по вашей вине.
// Формулировки: «как тестировать микросервисы?», «что такое контрактные тесты?», «как проверить устойчивость к сбою соседа?».
Каскад и предохранитель: замер
Каскадное падение - это когда отказ одного сервиса роняет тех, кто сам исправен. Механика видна из замера. Каждый вызов зависшего соседа занимает поток обслуживания на всё время ожидания. Потоков конечное число, они кончаются, и новые запросы просто некому взять - снаружи это выглядит как «лежит и A тоже».
Таймаут проблему не решает, но меняет её характер: вы начинаете быстро отвечать ошибкой вместо того, чтобы молча копить очередь. Это и есть цель - деградировать, а не умирать.
// Следующий уровень - предохранитель: после нескольких неудач подряд он перестаёт ходить к соседу вовсе и отказывает сразу, а через паузу пробует снова. Замер: тридцать запросов к зависшему соседу без предохранителя заняли 12,05 секунды, с предохранителем - 1,20 секунды, потому что реально сходили только три раза, а 27 раз отказали мгновенно. Заодно это даёт лежащему соседу шанс подняться, а не добивает его потоком запросов.
- каскадное падение
- отказ соседа занимает все потоки и роняет исправный сервис
- предохранитель
- после серии неудач отказывает сразу, не ходя к лежащему соседу
Три слоя вместо общего стенда
Профиль дефектов в микросервисах смещён: логика внутри одного сервиса обычно несложная, а беды концентрируются на стыках - несовпадение версий договора, сетевые сбои, рассогласованность данных. Поэтому и стратегия другая.
Основа - тесты сервиса в изоляции, где соседи заменены дублёрами: подставными сервисами, которые отвечают ровно так, как их научили. Быстро, стабильно, и только так можно заказать сбой: таймаут, пятисотую ошибку, битый ответ. Второй слой - контрактные тесты: потребитель записывает свои ожидания к API поставщика, а поставщик проверяет их у себя при каждой сборке. Так несовместимое изменение ловится до выкладки, без общего стенда вообще. Третий слой - несколько сквозных тестов на критичные бизнес-потоки целиком, вроде оформления и оплаты заказа.
// А вот на полный стенд всех сервисов ставку не делают. Он дорогой, медленный и почти всегда сломан не вашим кодом: чтобы прогон был зелёным, должны одновременно работать все сервисы, и вероятности перемножаются. В итоге красный прогон перестают читать, что хуже, чем его отсутствие.
- контрактный тест
- ожидания потребителя, которые поставщик проверяет у себя при сборке
- тонкий сквозной слой
- E2E (end-to-end): считанные сценарии целиком, а не весь набор
Согласованность, дубли, версии
Общей транзакции на несколько сервисов не бывает, поэтому длинные операции собирают цепочкой: списали деньги, зарезервировали товар, создали доставку - и на каждый шаг заготовлено обратное действие на случай отказа следующего. Тестируют именно обратные действия (заказ отменился - деньги вернулись?) и промежуточные состояния, которые успевает увидеть пользователь: на секунду «оплачено», а заказа ещё нет.
Обмен через очередь приносит свои сценарии. Доставка обещана «хотя бы раз», значит дубли штатны: я это мерил - шесть сообщений при трёх уникальных списали 870 рублей вместо 420, пока получатель не начал пропускать повторы по идентификатору. Порядок сообщений тоже не гарантирован в общем случае, и обработчик обязан это переживать.
// И версии. Сервисы выкладываются независимо, то есть новую версию раскатывают по машинам постепенно, и какое-то время часть из них уже с новой версией, а часть ещё со старой. Значит, проверяют совместимость соседних версий в обе стороны: новый сервис обязан работать со старым соседом, и наоборот. Плюс сквозная метка запроса через все сервисы - без неё дефект в цепочке из восьми участников не локализуется вообще.
- цепочка с откатом
- шаги с заготовленными обратными действиями вместо общей транзакции
- сквозная метка запроса
- общий идентификатор, по которому виден путь через все сервисы
Как отвечать: «Как тестировать микросервисы, если полный стенд хрупок?»
Именно потому, что полный стенд всех сервисов хрупок и почти всегда красен не по моей вине, на него не опираются. Стратегия трёхслойная. Основа - тесты каждого сервиса в изоляции, где соседи заменены управляемыми дублёрами: это быстро, стабильно и, главное, позволяет заказать сбой. Я так и проверяю устойчивость: сосед завис - что делает мой сервис. Мерил специально: без таймаута двадцать четыре запроса заняли девять секунд, и всё это время сервис не отвечал никому, хотя сам был исправен; с таймаутом уложились в полторы секунды, все с ошибкой, но сервис живой. А с предохранителем тридцать запросов к зависшему соседу заняли секунду вместо двенадцати. Второй слой - контрактные тесты: потребитель фиксирует ожидания к API поставщика, поставщик проверяет их у себя при сборке, и несовместимое изменение ловится до выкладки без всякого общего стенда. Третий, тонкий слой - несколько сквозных сценариев на критичные потоки. Отдельно проверяю обратные действия в цепочках, дубли из очереди, потому что доставка обещана хотя бы раз, и совместимость соседних версий: при выкладке машины какое-то время работают в разных версиях.
Почему это сильный ответ: трёхслойная стратегия обоснована профилем дефектов, устойчивость подкреплена измерениями каскада и предохранителя, и добавлены откаты, дубли и совместимость версий.
На чём валят
- −Ставить всё на полный стенд: он красен не по вашей вине, и его перестают читать.
- −Ходить к соседу без таймаута: 24 запроса заняли 9 секунд, и сервис молчал всем.
- −Проверять только живых соседей - первый же зависший роняет исправный сервис.
- −Не защищаться от повторов из очереди: доставка обещана хотя бы раз, дубли штатны.
- −Забыть про смешанные версии при выкладке - новый сервис обязан работать со старым соседом.
Проверьте себя
Пять вопросов из банка по этой подтеме. Всего их 13, остальные разбираются в тренажёре.
- Что проверяет контрактный тест (consumer-driven, напр. Pact) между сервисом-потребителем и провайдером?A)Скорость ответа провайдера под нагрузкой и его способность держать пиковый трафикB)Что оба сервиса подняты в одном окружении и успешно проходят сквозной сценарийC)Что провайдер зашифровал ответ и потребитель может его расшифровать своим ключомD)Что провайдер отдаёт ровно те поля и форматы, на которые рассчитывает потребитель
показать ответ и разбор
+D)Что провайдер отдаёт ровно те поля и форматы, на которые рассчитывает потребитель// разбор: Контрактный тест по модели consumer-driven: потребитель описывает свои ожидания к API провайдера (какие поля, типы, коды он использует) — это «пакт»; провайдер прогоняет его у себя и краснеет, если несовместимо изменил ответ. Ценность — ловит поломку интеграции рано и дёшево, не поднимая всю систему в общий стенд: каждый сервис проверяется против контракта отдельно, в своём CI. Так закрывают риск «провайдер выкатил изменение → тихо сломал потребителей», не гоняя медленные сквозные e2e на каждый коммит.
- Событие кладётся в очередь (Kafka), консьюмер обрабатывает его асинхронно. Как это тестировать?A)Проверить, что продюсер получил успешный ответ на отправку события в топик, — дальнейшая обработка обеспеченаB)Отправить событие и с поллингом/ретраями дождаться результата обработки на выходе или в БДC)Добавить фиксированную паузу sleep(5) и один раз проверить результат — этого достаточноD)Читать результат сразу после отправки: очередь доставляет и обрабатывает событие синхронно
показать ответ и разбор
+B)Отправить событие и с поллингом/ретраями дождаться результата обработки на выходе или в БД// разбор: Асинхронность значит, что отправка события и его обработка разнесены во времени: успешная запись в топик не доказывает, что консьюмер обработал сообщение верно (или вообще). Синхронно «сразу прочитать» нельзя, а фиксированный sleep делает тест хрупким (то рано, то долго). Правильный приём — poll-until: после отправки периодически опрашивать выход (БД, ответ другого сервиса, новый топик) с таймаутом и условием готовности. Отдельно проверяют идемпотентность консьюмера (повтор сообщения не задваивает эффект) и порядок, если он важен.
- POST вернул 201, но GET к другому сервису тут же не показывает запись. Почему это может быть не баг?A)Данные расходятся между сервисами асинхронно — это eventual consistency, нужен повтор с ожиданиемB)Код 201 подтверждает лишь постановку в очередь, а до самой базы запись, как правило, не доходитC)Свежую запись обязаны видеть все сервисы сразу, поэтому расхождение — это дефект синхронизацииD)Второй сервис держит собственную независимую копию данных, которая с первым никак не синхронизируется
показать ответ и разбор
+A)Данные расходятся между сервисами асинхронно — это eventual consistency, нужен повтор с ожиданием// разбор: В распределённой системе разные сервисы часто держат свои представления данных, а изменения расходятся между ними асинхронно (через события/репликацию). Поэтому сразу после успешной записи чтение из другого сервиса может ещё не увидеть её — это eventual consistency, штатное свойство, а не баг: согласованность наступает спустя короткое время. Тест, читающий немедленно, будет флакать. Правильно — читать с повтором и таймаутом (poll-until), а багом считать лишь ситуацию, когда данные не сходятся и после разумного ожидания.
- В микросервисах нет общей транзакции на все сервисы. Как держат согласованность, если оплата прошла, а резерв склада упал?A)Оборачивают все вызовы сервисов в одну распределённую ACID-транзакцию с общим коммитом в самом концеB)Просто ретраят упавший шаг до успеха — рано или поздно резерв склада пройдётC)Откатывают всю систему к резервной копии базы, снятой перед началом операцииD)Паттерн Saga: цепочка локальных транзакций, а на сбой шага — компенсирующее действие
показать ответ и разбор
+D)Паттерн Saga: цепочка локальных транзакций, а на сбой шага — компенсирующее действие// разбор: Одной ACID-транзакции на несколько сервисов с их отдельными БД на практике нет. Согласованность обеспечивают паттерном Saga: бизнес-операцию разбивают на последовательность локальных транзакций, каждая в своём сервисе; если очередной шаг падает, запускают компенсирующие действия, отменяющие уже сделанные шаги (вернуть оплату, снять бронь). Итог — не мгновенная атомарность, а согласованность через компенсации. Тестировщику важно проверять именно сбой в середине: что компенсации отработали, деньги не зависли, склад не остался ошибочно зарезервированным, повторный запуск не задвоил эффект.
- Провайдер добавляет поле в ответ API. Как выкатить изменение, не сломав действующих потребителей?A)Переименовать заодно пару старых полей под новый стиль — потребители подстроятся самиB)Делать изменения аддитивными: добавлять поля, не убирая и не переименовывая существующиеC)Выкатить как есть и разослать потребителям письмо с просьбой срочно обновить клиентовD)Удалить устаревшие поля сразу же, чтобы ответ не разрастался и не тянул лишний трафик
показать ответ и разбор
+B)Делать изменения аддитивными: добавлять поля, не убирая и не переименовывая существующие// разбор: Потребители завязаны на текущую форму ответа, поэтому безопасны только обратно-совместимые (аддитивные) изменения: добавить новое поле можно — старые клиенты его просто игнорируют. А убрать, переименовать поле или сменить его тип — ломающее изменение: потребители получат null/ошибку. Если ломающее изменение неизбежно — вводят версионирование (/v2) или приём expand-contract: сначала добавить новое рядом со старым, дать потребителям мигрировать, потом убрать старое. Контрактные тесты стерегут границу и краснеют на несовместимое изменение до релиза.
дальше
Теорию прочитали. Навык ставится повторением
В Сеньорчике эта подтема идёт в ежедневных сессиях: движок возвращает её, пока ответы не станут уверенными, и ведёт прогресс отдельно по каждой подтеме. Теория внутри тоже бесплатна, лимит только на количество вопросов в день.