Пока продукт один, с ошибкой всё просто: нажали, упало, вот лог. Но большинство систем, которые вы будете проверять, собраны из десятка сервисов, которые ходят друг к другу по сети и общаются через очереди сообщений. Тогда «нажали, упало» превращается в вопрос «в каком из восьми сервисов», а «сообщение не дошло» в разбор, застряло оно в очереди, потерялось или пришло дважды. Разберём, как такая система устроена и какие проверки она добавляет.
Монолит и микросервисы
Монолит это одно приложение: один репозиторий, одна сборка, одна база, и заказ, оплата и уведомления живут внутри одного процесса. Микросервисы разрезают его на независимые сервисы, у каждого своя команда, своя база и свой выпуск: сервис заказов, сервис оплаты, сервис уведомлений. Говорят они друг с другом по сети: синхронно через HTTP или gRPC, когда нужен ответ сейчас, и асинхронно через брокер сообщений, когда достаточно сообщить «заказ оплачен», а кто и когда это обработает, отправителю неважно.
Что это меняет для проверки. Плюсы: сервисы выпускают по отдельности, и регресс одного сервиса меньше, чем регресс всего монолита. Минусы: появляются стыки, которых в монолите не было. Сеть между сервисами может подвести, версии сервисов на стенде могут разойтись, данные о заказе лежат в трёх базах и могут не совпасть, а один пользовательский сценарий проходит через пять сервисов. Поэтому тестировать микросервисы не проще, а иначе: больше проверок на стыках, больше внимания к логам и меньше возможностей увидеть всё на одном экране.
Где ломаются стыки
Типичные дефекты распределённой системы не видны ни в одном сервисе по отдельности. Сервис заказов отправил запрос в оплату и не дождался ответа за таймаут, а оплата при этом прошла: на экране ошибка, деньги списаны. Сервис уведомлений обновился раньше сервиса заказов и ждёт поле, которого ещё нет. Два сервиса считают сумму по-разному, потому что один округляет, другой нет. Сообщение обработано дважды, потому что брокер доставил его повторно после сбоя.
Отсюда набор проверок, который в монолите не нужен. Таймаут и недоступность соседа: что видит пользователь, если сервис оплаты не отвечает, и откатится ли заказ. Повтор запроса: что будет, если нажать «оплатить» дважды или сеть повторит запрос сама. Порядок событий: что, если «заказ отменён» придёт раньше, чем «заказ создан». Разные версии: работает ли старый сервис с новым. И согласованность данных: сходится ли заказ в сервисе заказов, оплата в сервисе оплаты и письмо в сервисе уведомлений.
Локализовать ошибку 500
Самый частый вопрос на практике: положительный сценарий, а в ответ ошибка 500. В микросервисах ответ пользователю формирует один сервис, а упасть мог любой из тех, к кому он сходил. Порядок разбора такой.
Сначала запрос и ответ в DevTools или Postman: адрес, тело, заголовки, и среди заголовков сквозной идентификатор запроса, обычно X-Request-Id или traceparent. Этот идентификатор сервисы передают друг другу и пишут в каждую строку лога, и по нему в Kibana видна вся цепочка: шлюз принял запрос, сервис заказов вызвал оплату, оплата ответила 500. Дальше открывают лог именно того сервиса, где впервые появилась ошибка, и читают трассировку снизу вверх: что случилось и на какой строке. Если ошибка в самом нижнем сервисе, баг его, если верхний сервис не пережил нормальный отказ нижнего, например не обработал пустой ответ, баг верхнего. В отчёт кладут идентификатор запроса, имя сервиса и строку ошибки, тогда разработчик найдёт место за минуту.
Когда стенд живёт в Kubernetes, логи сервиса достают командой kubectl logs имя-пода, а список подов и их состояние показывает kubectl get pods: под, который перезапускается по кругу, это и есть ответ на вопрос «почему 500 через раз».
Брокер сообщений: что это и зачем
Брокер это отдельный сервис-почта между сервисами. Отправитель кладёт сообщение в очередь или топик и продолжает работу, получатель забирает, когда готов. Если получатель упал, сообщения ждут в брокере, а не теряются, и отправитель об этом даже не знает. Так развязывают сервисы: оплата не должна ждать, пока уведомления отправят письмо.
Два брокера, о которых спрашивают всегда. RabbitMQ работает как очередь: сообщение лежит, пока получатель его не заберёт и не подтвердит, после чего исчезает; умно маршрутизирует сообщения по правилам и хорош для задач «сделай это один раз». Kafka работает как журнал: сообщения пишутся в топик, разбитый на партиции, хранятся заданный срок, retention, независимо от того, прочитаны ли, и читать их могут несколько групп получателей, consumer group, каждая со своей позицией, offset. Kafka берут для потоков событий, которые нужны многим и которые можно перечитать, RabbitMQ для очередей задач. Для тестировщика разница в проверках: в Kafka можно перечитать событие заново и посмотреть, что лежало в топике неделю назад, в RabbitMQ прочитанное сообщение уже ушло.
Что проверять в очередях
Три гарантии доставки задают три набора проверок. «Хотя бы один раз», самый частый режим: сообщение может прийти повторно после сбоя, и получатель обязан быть идемпотентным, то есть обработать повтор без второго списания или второго письма. Проверка: отправить одно и то же сообщение дважды и убедиться, что результат один. «Не более одного раза»: сообщение может потеряться, и проверяют, что потеря замечена. «Ровно один раз» на практике это первый режим плюс идемпотентность.
Дальше порядок: внутри одной партиции Kafka порядок сохраняется, между партициями нет, и события одного заказа должны попадать в одну партицию по ключу; проверка, что отмена не обгоняет создание. Задержка: сколько времени сообщение идёт от отправителя до результата, и что происходит при отставании получателя, lag. Ошибка обработки: сообщение, которое получатель не может обработать, не должно блокировать остальные и не должно исчезнуть; обычно оно уезжает в отдельную очередь ошибок, dead letter queue, которую тоже нужно уметь найти.
Идемпотентность, гонки и контракт
Идемпотентность это свойство операции давать тот же результат при повторе: PUT и DELETE по определению такие, POST нет, и для оплаты вводят ключ идемпотентности, который клиент шлёт с запросом, а сервер по нему отвергает дубль. Проверка: повторить запрос с тем же ключом, ожидать тот же ответ и одно списание.
Гонка, race condition, это когда два действия происходят одновременно и результат зависит от порядка: два пользователя покупают последний товар, два окна списывают бонусы. В монолите это редкий дефект, в микросервисах обычный, и проверяют его двумя одновременными запросами, например из двух вкладок Postman или коллекцией с параллельным запуском, а в автотестах двумя потоками.
Контракт API это договорённость о форме запроса и ответа между сервисами, записанная в OpenAPI или в схеме сообщения. Контрактные тесты проверяют, что сервис отдаёт то, что обещал, и что потребитель ждёт именно этого, ещё до выката, и ловят ломающее изменение поля. Чего они не ловят: ошибок в данных и логике, таймаутов и поведения под нагрузкой, это остаётся интеграционным и сквозным проверкам.
Коротко
- Монолит это один процесс и одна база, микросервисы это независимые сервисы со своими базами, которые говорят по HTTP и через брокер; тестировать их не проще, а иначе: добавляются стыки.
- Дефекты стыков: таймаут соседа при прошедшей операции, разъехавшиеся версии, рассинхрон данных, повтор и порядок сообщений.
- Ошибку 500 локализуют по сквозному идентификатору запроса в логах всей цепочки, виноват сервис, где ошибка появилась впервые, или верхний, если не пережил нормальный отказ;
kubectl logsдля стендов в Kubernetes. - Брокер развязывает сервисы; RabbitMQ это очередь с подтверждением, Kafka это журнал с партициями, retention, consumer group и offset, который можно перечитать.
- Очереди проверяют на повтор (идемпотентность получателя), потерю, порядок внутри ключа, отставание и очередь ошибок.
- Идемпотентность: тот же результат при повторе, ключ идемпотентности для оплаты; гонку проверяют двумя одновременными запросами; контракт API ловит ломающие изменения формы, но не логику.
Что почитать дальше
- Логи и наблюдение за системой — сквозной идентификатор и поиск в Kibana, с которых начинается разбор.
- Клиент, сервер и HTTP — методы, статусы и повтор запроса, на которых держится идемпотентность.
- Стили API — где лежит контракт и как читать OpenAPI.
- Что такое Docker — как сервисы упакованы и запущены на стенде.