System Design для QA: о чём вас спросят на собеседовании (и как не сесть в лужу)

Всё, что нужно знать об архитектуре систем, чтобы перестать тестировать вслепую


System Design

Я написал эту статью для тех, кто с System Design особо не сталкивался. Никаких предварительных знаний не нужно - всё объясню с нуля. Если вы уже знаете, что такое load balancer - местами будет скучно, но, может, в секции про очереди или мониторинг найдёте что-то новое.


Окей, а зачем QA вообще в это лезть?

Честный ответ: потому что без понимания архитектуры вы тестируете вслепую.

Вот реальная ситуация. Тестировщик прогнал все тесты на staging, всё зелёное, деплой в production. Через час - сайт лежит. Оказалось, на staging один сервер, а в production за load balancer-ом стоят три, и при определённом стечении обстоятельств запросы пользователя попадают на разные серверы, а сессия хранится в памяти одного. Тестами это не покроешь, если не знаешь, что вообще бывают load balancer-ы и sticky sessions.

Или вот: QA тестирует API, всё работает. А потом оказывается, что ответы летят из кэша, и реальные данные из базы вообще не те. Если не знаешь, что между клиентом и базой сидит Redis - даже не поймёшь, куда копать.

В общем, System Design для QA - это не про то, чтобы проектировать системы. Это про то, чтобы понимать, где они могут сломаться.

Load Balancer

Начнём с простого. У вас есть веб-приложение, оно крутится на сервере. Один сервер обрабатывает, скажем, 500 запросов в секунду. Пришло 2000 пользователей одновременно - сервер захлебнулся, сайт лёг.

Решение: поставить несколько серверов и перед ними - штуку, которая раскидывает запросы. Это и есть load balancer. По сути, диспетчер. Примерно как в больнице регистратура - вы не сами выбираете, к какому врачу идти, за вас распределяют. Ну, аналогия кривая, потому что в больнице распределяют по специализации, а тут скорее по загруженности, но суть такая.

Есть несколько способов раскидывать запросы:

  • Round Robin - по очереди: первый запрос на сервер 1, второй на сервер 2, третий на сервер 3, четвёртый опять на 1. Тупо, но работает.
  • Least Connections - на тот сервер, где меньше всего активных соединений. Логичнее, но чуть сложнее.
  • IP Hash - один и тот же пользователь всегда попадает на один и тот же сервер. Пригодится, если сессия хранится локально.

Что тут тестировать? Много чего. Самое очевидное - убить один сервер и посмотреть, что будет. Пользователь увидит ошибку? Или балансер молча перекинет его на другой сервер? А если пользователь был залогинен на том сервере, который умер - сессия пропала? Его выкинуло из аккаунта?

Ещё момент: health checks. Балансер должен понимать, что сервер умер, и перестать слать ему запросы. Но как быстро? Если проверка раз в 30 секунд, то полминуты часть пользователей будет получать ошибки. На собеседовании если упомянете это - будет плюс.

Кэширование

Кэш - штука, которая ломает тестировщикам жизнь регулярно. Вы обновили данные, проверяете - а на странице старые. Ctrl+F5 - новые. Всё, вроде работает? Нет, не работает, потому что у обычного пользователя Ctrl+F5 нет в привычке.

Как это устроено: между клиентом и базой данных есть промежуточная «память». Обычно это Redis или Memcached. Когда приходит запрос, система сначала смотрит в кэш. Если данные там есть (cache hit) - отдаёт мгновенно, даже не трогая базу. Если нет (cache miss) - лезет в базу, получает данные, сохраняет копию в кэш на какое-то время, и отдаёт клиенту.

Запрос от клиента
       |
       v
  +----------+     cache hit      +----------+
  |   КЭШ    | ----------------> |  Ответ   |
  |  (Redis)  |                   | мгновенно|
  +----------+                    +----------+
       |
       | cache miss
       v
  +----------+     сохранить      +----------+
  |    БД    | ----------------> |   КЭШ    |
  | (Postgres)|    в кэш          |  (Redis)  |
  +----------+                    +----------+
       |
       v
  +----------+
  |  Ответ   |
  | клиенту  |
  +----------+

Кэш бывает на разных уровнях: в браузере (картинки, CSS), на CDN (об этом дальше), на сервере (Redis), в самой базе (query cache). И на каждом уровне он может подложить свинью.

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

На собеседовании часто спрашивают: «Пользователь обновил профиль, но видит старые данные - что проверить?» Ответ: CDN-кэш, браузерный кэш, Redis-кэш, заголовки Cache-Control и ETag. Открыть инкогнито-окно - если там данные новые, значит проблема в браузерном кэше. Если и там старые - копать серверный кэш.

Базы данных: репликация и шардирование

Тут две отдельные темы, которые часто путают.

Репликация - это когда у вас одна основная база (master) и несколько копий (replica). Записывают данные всегда в master, а читают - из реплик. Зачем? Потому что на чтение обычно приходится 90% нагрузки, и если все запросы идут в одну базу, она не вытянет.

  INSERT / UPDATE / DELETE          SELECT (чтение)
         |                          /          \
         v                         v            v
  +-----------+             +-----------+ +-----------+
  |  MASTER   | ---------->|  REPLICA  | |  REPLICA  |
  |  (запись) | репликация  | (чтение)  | | (чтение)  |
  +-----------+             +-----------+ +-----------+

Тут есть подвох, на который я сам напоролся на реальном проекте. Пользователь создаёт пост, его редиректит на страницу профиля. Пост не отображается. Обновляет страницу - появился. Что произошло? Запись пошла в master, а чтение - из реплики, которая ещё не успела синхронизироваться. Это называется replication lag, и это настоящий баг, который тестами в лоб поймать сложно.

Шардирование - другая история. Это когда одна база не вмещает все данные (или не тянет нагрузку), и вы разбиваете данные на куски. Пользователи с именами A-M идут в одну базу, N-Z - в другую. Каждый такой кусок - шард.

  Все пользователи (1 000 000)
         |
    Shard Key: первая буква имени
         |
   +-----+-----+
   |           |
   v           v
+---------+ +---------+
| SHARD 1 | | SHARD 2 |
|  A - M  | |  N - Z  |
| 520 000 | | 480 000 |
+---------+ +---------+

Для QA тут главная проблема - запросы, которые затрагивают оба шарда. Например, «покажи всех пользователей, отсортированных по дате регистрации». Если пользователи раскиданы по двум базам, кто-то должен собрать данные из обеих и отсортировать. Иногда это работает криво или медленно. Стоит проверять.

Очереди сообщений

Допустим, пользователь зарегистрировался. Нужно: сохранить его в базу, отправить welcome-email, создать профиль, начислить бонус, отправить push-уведомление. Если всё это делать синхронно - пользователь будет ждать секунд 10, пока загрузится страница. Или, что ещё хуже, если сервис email не отвечает - весь процесс регистрации зависнет.

Поэтому придумали очереди. Основной сервис кладёт задачу в очередь (RabbitMQ, Kafka, SQS - неважно), а отдельный worker забирает и обрабатывает в фоне. Пользователь видит «Регистрация успешна» за полсекунды, а email придёт через пару секунд.

+----------+      +-----------------------------------+      +----------+
| PRODUCER | ---> |         MESSAGE QUEUE             | ---> | CONSUMER |
| (сервис) |      |  [msg1] [msg2] [msg3] [msg4] ... |      | (worker) |
+----------+      |     RabbitMQ / Kafka / SQS        |      +----------+
                  +-----------------------------------+
                        |                     |
                  сообщения ждут         обработка
                  своей очереди          по одному

Что тут может пойти не так? О, много всего:

  • Сообщение обработалось дважды - пользователь получил два одинаковых email. Бывает, если worker упал в процессе обработки, и сообщение вернулось в очередь, а потом другой worker его тоже обработал.
  • Сообщение потерялось - worker забрал его, упал, а в очередь оно не вернулось. Email не ушёл вообще.
  • Очередь переполнилась - producer пихает задачи быстрее, чем consumer справляется. В какой-то момент всё встаёт колом.

Был на собеседовании вопрос: «Пользователь оплатил заказ, а подтверждение на email пришло через 2 часа. Где копать?» Тут как раз про очередь - либо consumer-ов мало и очередь забилась, либо были ошибки обработки и сообщение перекидывалось в retry много раз.

Микросервисы

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

Поэтому многие переходят на микросервисы: Auth отдельно, Users отдельно, Orders отдельно, Payments отдельно. Каждый сервис - свой код, своя база, свой деплой. Общаются по сети через API или очереди.

    МОНОЛИТ                          МИКРОСЕРВИСЫ

+----------------+          +------+    +-------+
|  Auth          |          | Auth | <--> | Users |
|  Users         |          +------+    +-------+
|  Orders        |              |            |
|  Payments      |              v            v
|  Notifications |          +--------+  +----------+
|                |          | Orders | <--> | Payments |
| один процесс  |          +--------+  +----------+
| один деплой   |              |
| одна БД       |              v
+----------------+         +---------+--------+
                           | Notifications    |
                           +------------------+

                           каждый сервис = свой
                           процесс, деплой, БД

Для QA это и хорошо, и плохо. Хорошо - потому что можно деплоить и тестировать один сервис, не трогая остальные. Плохо - потому что появляются баги на стыках. Сервис Orders ожидает поле user_id от Users, а разработчик Users переименовал его в userId. Юнит-тесты обоих сервисов зелёные. А в интеграции - 500 ошибка.

Ещё интересная штука - circuit breaker. Это когда один сервис перестаёт вызывать другой, если тот не отвечает. Как предохранитель в электрике - вырубается, чтобы не спалить всё остальное. Стоит проверить: если Payments лежит, Orders хотя бы показывает «Оплата временно недоступна», а не падает вместе с ним?

Масштабирование

Тут коротко, потому что идея простая. Два варианта:

Вертикальное - берём сервер помощнее. Было 8 ГБ RAM - поставили 64. Просто, но есть потолок. Самый мощный сервер в мире всё равно конечен.

Горизонтальное - ставим больше серверов. Было 2 - стало 20. Теоретически бесконечно, но нужен load balancer, нужно думать о сессиях, о данных, о синхронизации.

На собеседовании могут спросить: «API отвечает за 200мс при 100 пользователях. Что будет при 10 000?» Если не масштабировано - время ответа вырастет, начнутся таймауты, часть пользователей увидит ошибки. QA должен это проверять нагрузочным тестированием (k6, JMeter) ещё до того, как трафик вырастет.

Мониторинг

Вот это прям моя больная тема. Я видел проекты, где нагрузочные тесты, unit-тесты, интеграционные тесты - всё есть. А в production сервис лёг, и команда узнала через 40 минут из жалобы пользователя в Telegram-чате. 40 минут! Потому что мониторинга нет.

Google в своей SRE-книге описывает четыре сигнала, за которыми нужно следить:

СигналЧто этоКогда бить тревогу
LatencyВремя ответаAPI стал отвечать дольше 2 секунд
TrafficКол-во запросовТрафик внезапно упал на 80%
ErrorsПроцент ошибок5xx ошибки перевалили за 1%
SaturationНагрузка на ресурсыCPU 95%, диск забит на 90%

Мониторинг можно разделить на уровни. Снизу вверх:

  Уровень 4:  БИЗНЕС-МЕТРИКИ ........... конверсия, выручка, регистрации
              ________________________________________________
  Уровень 3:  APPLICATION MONITORING .... ошибки, latency, throughput
              ________________________________________________
  Уровень 2:  INFRASTRUCTURE ........... CPU, RAM, disk, network
              ________________________________________________
  Уровень 1:  UPTIME / AVAILABILITY .... сайт жив? SSL валиден?

Начинать нужно с первого уровня - хотя бы проверять, что сайт вообще жив. Я для своих проектов использую SiteGuard - он проверяет uptime каждые 10-15 минут и шлёт алерты в Telegram. Не email, который я проверяю раз в день, а Telegram, который у меня всегда открыт. Ещё он мониторит SSL-сертификаты и проверяет, что формы на сайте работают. Бесплатного плана хватает для pet-проектов.

Кстати, на собеседовании «я настроил мониторинг и поймал downtime раньше пользователей» - это прям сильный аргумент. Большинство QA вообще не думают про мониторинг, считают это задачей DevOps. А зря.

Что проверять по мониторингу на проекте: алерты вообще настроены? Убейте сервис на staging - пришло уведомление? За сколько секунд? Нет ли ложных срабатываний? Если алерты приходят по 50 штук в день - команда их игнорирует, и реальный инцидент пропустят.

Rate Limiting

Если ваш API не ограничивает количество запросов - любой скрипт может заддосить его. Или злоумышленник будет подбирать пароли перебором тысячу раз в секунду.

Rate limiting - это ограничение: например, 100 запросов в минуту с одного IP. Запрос 101 получит 429 Too Many Requests.

Что проверять:

  • Отправить больше запросов, чем лимит - приходит 429?
  • Заголовки X-RateLimit-Remaining и Retry-After - они вообще есть? Клиенту нужно понимать, когда можно повторить.
  • Лимит привязан к пользователю или к IP? Потому что за корпоративным NAT может сидеть тысяча человек с одним IP. Если лимит per IP - заблокируется весь офис из-за одного чрезмерно активного коллеги.
  • Разные endpoint - разные лимиты? Логин обычно лимитируют строже (5-10 попыток в минуту), а чтение каталога - мягче.

CDN

Если ваш сервер стоит в Нью-Йорке, а пользователь сидит в Ашхабаде - данные летят через полмира. Физику не обманешь, скорость света конечна, плюс маршрутизация, плюс потери пакетов. В итоге сайт грузится 3 секунды вместо 300мс.

CDN (Content Delivery Network) - это куча серверов по всему миру, на которых хранятся копии вашей статики (картинки, JS, CSS). Пользователь получает файлы с ближайшего сервера, а не с origin-а.

  БЕЗ CDN:

  Ашхабад ------[ 8 000 км ]-----> Нью-Йорк (origin)
                                    latency: ~200 мс


  С CDN:

  Ашхабад ---[ 2 000 км ]---> Стамбул (edge)    Нью-Йорк (origin)
                               latency: ~30 мс     |
                               контент уже          | первый раз
                               закэширован    <-----+ скопировал

Тестировать тут нужно в основном кэширование. Обновили картинку на сайте - CDN отдаёт старую? Как быстро обновится? Некоторые CDN кэшируют на 24 часа, и если вы загрузили кривой баннер - сутки он будет висеть на всех edge-серверах мира.

Отказоустойчивость (High Availability)

Доступность измеряют в «девятках»:

ДоступностьDowntime в годЭто как
99%3.6 дняНорм для домашнего блога
99.9%8.7 часаБольшинство SaaS-ов
99.99%52 минутыФинансы, e-commerce
99.999%5 минутБанки, телеком. Очень дорого

Каждая дополнительная «девятка» стоит кратно дороже. И QA тут играет важную роль: нужно проверять, что failover работает. Основной дата-центр отвалился - резервный подхватил? Данные не потерялись? Сколько времени прошло между сбоем и восстановлением?

И отдельная тема - бэкапы. Все делают бэкапы. Мало кто проверяет, что из них можно восстановиться. Я серьёзно. Проведите на своём проекте учение - попробуйте поднять сервис из бэкапа. Есть ненулевой шанс, что бэкап битый или процедура восстановления не задокументирована. Лучше узнать об этом на учениях, чем в 3 часа ночи во время реального инцидента.

Задача с собеседования: «Спроектируйте систему уведомлений»

Эту задачу дают часто. Не пугайтесь - от QA не ждут идеального решения. Ждут правильных вопросов.

Первое, что стоит спросить:

  • Какие каналы? Email, SMS, Push, Telegram - или что-то одно?
  • Сколько уведомлений в день? Тысяча - это одна история, десять миллионов - совсем другая архитектура.
  • Насколько критична доставка? OTP-код для логина - должен дойти за 10 секунд. Маркетинговая рассылка - ну, дойдёт через час, ничего страшного.

Примерная архитектура:

  +------------+
  | API Server |  <-- "Отправь уведомление user #42"
  +-----+------+
        |
        v
  +---------------+
  | MESSAGE QUEUE |  <-- буфер, чтобы не перегрузить провайдеров
  +--+---+---+----+
     |   |   |   |
     v   v   v   v
  +----+ +---+ +----+ +--------+
  |Email| |SMS| |Push| |Telegram|   <-- workers (обработчики)
  +--+--+ +-+-+ +-+--+ +---+----+
     |      |     |         |
     v      v     v         v
  SendGrid Twilio Firebase  Bot API   <-- внешние провайдеры
     \      |     |         /
      v     v     v        v
  +--------------------------+
  |    NOTIFICATION DB       |   <-- лог: кому, что, когда, статус
  +--------------------------+

Что бы я тестировал:

  • Уведомление реально доходит по каждому каналу. Не только в логе «отправлено», а проверить, что получено.
  • Worker упал - сообщение не потерялось? Ушло в retry? Сколько попыток перед тем, как сдаться?
  • Дупликаты - пользователь не получит два одинаковых SMS?
  • Приоритеты - OTP-код проходит вне очереди? Или стоит за 50 000 маркетинговых рассылок?
  • Пользователь отписался - уведомления прекратились? (Это ещё и юридическое требование в некоторых странах.)

Частые ошибки на собеседовании

За двадцать собеседований я насобирал типичные грабли. Вот пять самых частых:

  1. Сразу рисовать архитектуру. Не уточнив требования, вы спроектируете что-то не то. Первые 5 минут задачи - только вопросы. Интервьюер это ценит.
  2. Забывать про нагрузку. «Работает» - это мало. Работает при 10 000 запросов/сек? При 100 000? Сколько данных в базе - тысяча записей или миллиард?
  3. Не спрашивать «а что если это упадёт?» Для QA это должен быть рефлекс. Каждый компонент на схеме - потенциальная точка отказа.
  4. Молчать про мониторинг. Если вы сами добавите «и ещё нужен мониторинг - алерты при росте ошибок, дашборд с latency» - это выделяет вас среди остальных кандидатов.
  5. Говорить «это не моя зона ответственности». Формально - да, System Design делают разработчики. Но Senior QA, который не понимает архитектуру, тестирует только формочки. А баги уходят в production.

Чек-лист: что выучить перед собеседованием

  • Load Balancer - что это, алгоритмы распределения, как тестировать failover
  • Кэш - Redis, уровни кэширования, cache invalidation
  • Репликация - master/replica, replication lag
  • Шардирование - зачем, как, проблемы cross-shard запросов
  • Очереди - RabbitMQ/Kafka, retry, дупликаты, dead letter queue
  • Микросервисы - contract testing, circuit breaker
  • Масштабирование - вертикальное vs горизонтальное
  • Мониторинг - 4 золотых сигнала, инструменты
  • Rate Limiting - 429, per user vs per IP
  • HA/DR - девятки, failover, бэкапы

Не обязательно знать всё это в деталях. Главное - понимать, что существуют эти компоненты, зачем они нужны, и что там может пойти не так. Этого хватит, чтобы на собеседовании не молчать.


Курс по тестированию с практическими заданиями - бесплатно на annayev.com (English, Русский, Türkçe).

Если ваш проект уже в production и мониторинга нет - начните с SiteGuard. Uptime-проверки + SSL-мониторинг + Telegram-алерты. Бесплатно, без карты, настраивается за минуту.