Системный дизайн инфраструктуры
Senior-сценарии: HA stateless-сервисы, выкат и миграции схемы без простоя, централизованное логирование, планирование ёмкости, мультирегион, секреты и rate limiting.
9 вопросов
SeniorДизайнОчень частоТрёхслойное веб-приложение — web/UI-слой, stateless-слой приложения и одна реляционная БД — сегодня работает по одному серверу на слой, поэтому любой единичный отказ кладёт весь продукт. Перепроектируйте его под высокую доступность без единой точки отказа. Опишите, как сделать слой приложения горизонтально избыточным и по-настоящему stateless (куда уходит session и загруженные файлы), как трафик доходит только до здоровых инстансов, как убрать БД как точку отказа (primary с репликами и автоматическим failover) и как система деградирует, когда один слой частично лежит. Назовите устранённые точки отказа на каждом слое и те, что осознанно приняли.
Трёхслойное веб-приложение — web/UI-слой, stateless-слой приложения и одна реляционная БД — сегодня работает по одному серверу на слой, поэтому любой единичный отказ кладёт весь продукт. Перепроектируйте его под высокую доступность без единой точки отказа. Опишите, как сделать слой приложения горизонтально избыточным и по-настоящему stateless (куда уходит session и загруженные файлы), как трафик доходит только до здоровых инстансов, как убрать БД как точку отказа (primary с репликами и автоматическим failover) и как система деградирует, когда один слой частично лежит. Назовите устранённые точки отказа на каждом слое и те, что осознанно приняли.
Держите каждый слой избыточным: несколько stateless-инстансов за балансировщиком, который health-проверяет цели и шлёт трафик только на здоровые, а session и загруженные файлы вынесены в общее хранилище, чтобы любой инстанс обслуживал любой запрос. Сделайте БД primary с репликами и автоматическим failover, разгружая чтения на реплики. Добавьте graceful degradation, чтобы падающий слой сбрасывал нагрузку, а не каскадил отказ.
Типичные ошибки
- ✗Считать высокой доступностью большее число инстансов при одной БД
- ✗Держать session в памяти инстанса, из-за чего инстансы не взаимозаменяемы
- ✗Сделать веб-слой избыточным, оставив БД единой точкой отказа
Уточняющие вопросы
- →Как вынос session в общее хранилище делает инстансы взаимозаменяемыми?
- →Что ломается при отказе primary БД без автоматического failover?
SeniorДизайнЧастоRead-heavy веб-приложение показывает одинаковый контент многим пользователям, но бьёт в БД почти на каждом запросе, и задержка чтения растёт. Спроектируйте многоуровневую стратегию кеширования. Опишите CDN или edge-кеш для статики и кешируемых ответов (через HTTP cache-заголовки), кеш уровня приложения (например, Redis) для горячих вычисленных данных и результатов запросов и read-реплики БД. Сложное — инвалидация и консистентность: как задаёте TTL, как инвалидируете или обновляете записи, когда исходные данные меняются, и как избежать cache stampede, когда горячий ключ протухает. Объясните компромисс между дешёвой отдачей слегка устаревших данных и сложностью держать каждый слой кеша корректным.
Read-heavy веб-приложение показывает одинаковый контент многим пользователям, но бьёт в БД почти на каждом запросе, и задержка чтения растёт. Спроектируйте многоуровневую стратегию кеширования. Опишите CDN или edge-кеш для статики и кешируемых ответов (через HTTP cache-заголовки), кеш уровня приложения (например, Redis) для горячих вычисленных данных и результатов запросов и read-реплики БД. Сложное — инвалидация и консистентность: как задаёте TTL, как инвалидируете или обновляете записи, когда исходные данные меняются, и как избежать cache stampede, когда горячий ключ протухает. Объясните компромисс между дешёвой отдачей слегка устаревших данных и сложностью держать каждый слой кеша корректным.
Кешируйте слоями: CDN/edge-кеш для статики и кешируемых ответов через HTTP cache-заголовки, кеш приложения вроде Redis для горячих данных и результатов запросов и read-реплики БД. Задавайте TTL по терпимости данных к устареванию, инвалидируйте или обновляйте записи при изменении источника и защищайте горячие ключи от stampede при протухании. Компромисс — дешёвая отдача слегка устаревших данных против сложности держать каждый слой корректным.
Типичные ошибки
- ✗Кешировать без инвалидации, из-за чего записи устаревают после изменения источника
- ✗Дать горячему ключу протухнуть и завалить БД стадом на перестройку
- ✗Считать CDN, кеш приложения и реплики одним слоем без отдельного рассуждения
Уточняющие вопросы
- →Как инвалидировать запись кеша в момент изменения её исходных данных?
- →Что такое cache stampede и как не дать протуханию горячего ключа его вызвать?
SeniorДизайнЧастоОколо 500 хостов шлют логи приложений и системы, а инженеры сейчас заходят по SSH на отдельные машины, чтобы их читать. Спроектируйте централизованный конвейер логирования. Опишите сбор (агент на каждом хосте), транспорт с буферизацией, чтобы замедление ниже по потоку не теряло логи и не блокировало приложения, хранилище и его индекс для быстрых запросов и — ведь именно тут логирование разоряет бюджет — уровни хранения и контроль объёма/кардинальности (что держим горячим, что в архив, что отбрасываем). Объясните компромисс между индексированием всего ради мгновенного поиска и ценой хранения, которую это влечёт, и как единообразно раскатить агент на 500 хостов. Отметьте, почему безграничные high-cardinality поля делают индекс и медленным, и дорогим в содержании.
Около 500 хостов шлют логи приложений и системы, а инженеры сейчас заходят по SSH на отдельные машины, чтобы их читать. Спроектируйте централизованный конвейер логирования. Опишите сбор (агент на каждом хосте), транспорт с буферизацией, чтобы замедление ниже по потоку не теряло логи и не блокировало приложения, хранилище и его индекс для быстрых запросов и — ведь именно тут логирование разоряет бюджет — уровни хранения и контроль объёма/кардинальности (что держим горячим, что в архив, что отбрасываем). Объясните компромисс между индексированием всего ради мгновенного поиска и ценой хранения, которую это влечёт, и как единообразно раскатить агент на 500 хостов. Отметьте, почему безграничные high-cardinality поля делают индекс и медленным, и дорогим в содержании.
Поставьте на каждый хост агент сбора, шлющий в буферизованный транспорт (очередь или брокер), чтобы замедление ниже по потоку не теряло логи и не блокировало приложение. Складывайте их в индексированное хранилище, но разбейте хранение на уровни — горячее для свежих данных, дешёвый архив для старых, шум отбрасывать, — ведь индексировать всё разоряет бюджет. Ограничьте high-cardinality поля и раскатайте агент через config management, чтобы все 500 хостов были едины.
Типичные ошибки
- ✗Писать логи в хранилище синхронно, из-за чего медленное хранилище блокирует приложение
- ✗Индексировать и держать всё горячим вечно вместо уровней хранения
- ✗Добавлять безграничные high-cardinality поля, раздувающие и тормозящие индекс
Уточняющие вопросы
- →Почему ставить буфер между агентами и хранилищем, а не писать напрямую?
- →Как уровни хранения режут цену, не теряя возможности разбирать старые инциденты?
SeniorДизайнЧастоРуководство просит план аварийного восстановления для stateful-сервиса после близкого промаха, когда плохая миграция повредила прод-данные. Спроектируйте DR-план. Определите RPO и RTO и что задаёт каждый со стороны бизнеса, затем выберите механизмы под них: частота и тип бэкапов задают RPO, скорость restore и failover задаёт RTO. Опишите, как снимаются бэкапы и — что критично — как регулярно тестируется восстановление (непроверенный бэкап — не бэкап), где живут бэкапы, чтобы один отказ не уничтожил и данные, и их бэкапы, и письменный, отрепетированный runbook failover. Объясните разницу между RPO (сколько данных можно позволить себе потерять) и RTO (сколько можно быть недоступным) и почему они требуют разных вложений.
Руководство просит план аварийного восстановления для stateful-сервиса после близкого промаха, когда плохая миграция повредила прод-данные. Спроектируйте DR-план. Определите RPO и RTO и что задаёт каждый со стороны бизнеса, затем выберите механизмы под них: частота и тип бэкапов задают RPO, скорость restore и failover задаёт RTO. Опишите, как снимаются бэкапы и — что критично — как регулярно тестируется восстановление (непроверенный бэкап — не бэкап), где живут бэкапы, чтобы один отказ не уничтожил и данные, и их бэкапы, и письменный, отрепетированный runbook failover. Объясните разницу между RPO (сколько данных можно позволить себе потерять) и RTO (сколько можно быть недоступным) и почему они требуют разных вложений.
Задайте RPO (допустимую потерю данных) и RTO (допустимый простой) из терпимости бизнеса, затем подберите механизмы: частота и тип бэкапов задают RPO; скорость restore и failover задаёт RTO. Снимайте бэкапы с этой частотой, храните их изолированно от основного, чтобы один отказ не уничтожил оба, и — критично — тестируйте восстановление, ведь непроверенный бэкап — не бэкап. Держите отрепетированный runbook failover, чтобы восстановление не импровизировали.
Типичные ошибки
- ✗Путать RPO (потеря данных) и RTO (простой) как одно число
- ✗Хранить бэкапы рядом с основным, чтобы один отказ уничтожил оба
- ✗Никогда не тестировать restore, из-за чего восстановление падает в нужный момент
Уточняющие вопросы
- →Как частота бэкапов и скорость restore ложатся на RPO против RTO?
- →Почему бэкап, который вы ни разу не восстанавливали, — не бэкап?
SeniorДизайнЧастоВы — платформенная команда системы из примерно 40 микросервисов, и разбор инцидента сегодня — это заход по SSH на машины и grep логов. Спроектируйте observability-стек, общий для всей платформы. Опишите три столпа — метрики, логи и трейсы — что каждый умеет лучше всего и чего стоит; как единообразно инструментировать сервисы (общая библиотека или sidecar), чтобы один запрос прослеживался через все переходы между сервисами; на какие сигналы вы алертите (симптомные — burn SLO и сигналы RED/USE), а какие держите только для отладки; и как не дать кардинальности метрик взорвать вашу time-series БД. Объясните, почему алерты на каждую низкоуровневую причину вместо пользовательских симптомов рождают усталость от пейджера и замедляют реальные инциденты.
Вы — платформенная команда системы из примерно 40 микросервисов, и разбор инцидента сегодня — это заход по SSH на машины и grep логов. Спроектируйте observability-стек, общий для всей платформы. Опишите три столпа — метрики, логи и трейсы — что каждый умеет лучше всего и чего стоит; как единообразно инструментировать сервисы (общая библиотека или sidecar), чтобы один запрос прослеживался через все переходы между сервисами; на какие сигналы вы алертите (симптомные — burn SLO и сигналы RED/USE), а какие держите только для отладки; и как не дать кардинальности метрик взорвать вашу time-series БД. Объясните, почему алерты на каждую низкоуровневую причину вместо пользовательских симптомов рождают усталость от пейджера и замедляют реальные инциденты.
Используйте все три столпа по их сильной стороне: метрики — для дешёвых агрегатных трендов и алертов, логи — для контекста по событию, трейсы — чтобы вести запрос через переходы между сервисами. Инструментируйте каждый сервис одинаково с проброшенными trace ID. Алертите на пользовательские симптомы — burn SLO и сигналы RED/USE, а не на каждую причину, — остальное держите для отладки. Ограничьте кардинальность лейблов, чтобы time-series БД не взорвалась.
Типичные ошибки
- ✗Считать метрики, логи и трейсы взаимозаменяемыми, а не дополняющими
- ✗Алертить на низкоуровневые причины вместо симптомов, плодя усталость от пейджера
- ✗Игнорировать кардинальность метрик, пока time-series БД не ляжет
Уточняющие вопросы
- →Почему алертить на burn SLO, а не на всплеск CPU одного сервиса?
- →Как high-cardinality лейбл вроде user ID ломает time-series БД?
SeniorДизайнЧастоПо всему флоту сервисов пароли БД и API-ключи сейчас вставлены в env-файлы и закоммиченный конфиг, поэтому один утёкший репозиторий или хост раскрывает всё. Спроектируйте архитектуру управления секретами на весь флот. Опишите центральное хранилище секретов как единый источник истины, как сервисы аутентифицируются в нём и получают секреты в рантайме вместо запекания в образы, как ротировать доступы почти без простоя, как ограничить радиус поражения, чтобы один скомпрометированный сервис не читал все секреты (scoped-доступ по least privilege), и как держать секреты в открытом виде вне кода, логов и CI. Объясните, почему короткоживущие динамически выданные доступы уменьшают ущерб от утечки против долгоживущих общих.
По всему флоту сервисов пароли БД и API-ключи сейчас вставлены в env-файлы и закоммиченный конфиг, поэтому один утёкший репозиторий или хост раскрывает всё. Спроектируйте архитектуру управления секретами на весь флот. Опишите центральное хранилище секретов как единый источник истины, как сервисы аутентифицируются в нём и получают секреты в рантайме вместо запекания в образы, как ротировать доступы почти без простоя, как ограничить радиус поражения, чтобы один скомпрометированный сервис не читал все секреты (scoped-доступ по least privilege), и как держать секреты в открытом виде вне кода, логов и CI. Объясните, почему короткоживущие динамически выданные доступы уменьшают ущерб от утечки против долгоживущих общих.
Держите каждый секрет в центральном хранилище как единый источник истины; каждый сервис аутентифицируется собой и получает секреты в рантайме, а не запечёнными в образ. Ограничьте доступ по сервису по least privilege, чтобы одна компрометация не читала всё, и предпочитайте короткоживущие динамические доступы с авто-истечением, что ужимает радиус поражения и делает ротацию рутиной, а не передеплоем. Держите открытый текст вне кода, логов и CI.
Типичные ошибки
- ✗Коммитить секреты открытым текстом, полагаясь на шифрование репозитория
- ✗Давать всем сервисам один общий доступ вместо scoped по least privilege
- ✗Держать долгоживущие статические секреты и считать ротацию редким авралом
Уточняющие вопросы
- →Почему короткоживущий доступ ограничивает ущерб от утёкшего секрета?
- →Как scoping по сервисам ограничивает радиус поражения от одной компрометации?
SeniorДизайнИногдаПотребительский сервис видит непредсказуемый рваный трафик — часами тихо, затем всплеск в 10 раз за секунды, когда пост становится вирусным, — и нужно спроектировать стратегию автоскейлинга, балансирующую стоимость и достаточный headroom, чтобы поглощать всплески. Опишите, по какому сигналу масштабируете (CPU против метрики запросов или глубины очереди ближе к реальной нагрузке), почему чисто реактивный автоскейлинг отстаёт от мгновенного всплеска (время загрузки и прогрева инстанса) и как это компенсировать (тёплый буфер, предварительное масштабирование по расписанию или прогнозу), как задать min/max и защиту от scale-in против флаппинга и сам компромисс стоимость-против-headroom. Объясните, почему работа у 100% утилизации ради экономии не оставляет запаса под те самые всплески, которыми этот сервис и определяется.
Потребительский сервис видит непредсказуемый рваный трафик — часами тихо, затем всплеск в 10 раз за секунды, когда пост становится вирусным, — и нужно спроектировать стратегию автоскейлинга, балансирующую стоимость и достаточный headroom, чтобы поглощать всплески. Опишите, по какому сигналу масштабируете (CPU против метрики запросов или глубины очереди ближе к реальной нагрузке), почему чисто реактивный автоскейлинг отстаёт от мгновенного всплеска (время загрузки и прогрева инстанса) и как это компенсировать (тёплый буфер, предварительное масштабирование по расписанию или прогнозу), как задать min/max и защиту от scale-in против флаппинга и сам компромисс стоимость-против-headroom. Объясните, почему работа у 100% утилизации ради экономии не оставляет запаса под те самые всплески, которыми этот сервис и определяется.
Масштабируйте по сигналу ближе к реальной нагрузке — запросы или глубина очереди, а не только CPU. Реактивный автоскейлинг отстаёт от всплеска на время загрузки и прогрева, поэтому держите тёплый буфер headroom и предмасштабируйте по расписанию или прогнозу. Задайте min/max и защиту от scale-in, чтобы не флаппило. Компромисс — стоимость против headroom: работа у 100% утилизации экономит деньги, но не оставляет запаса под всплески.
Типичные ошибки
- ✗Считать, что реактивный автоскейлинг реагирует мгновенно, игнорируя лаг загрузки
- ✗Работать у 100% утилизации без headroom под определяющие всплески
- ✗Масштабировать только по CPU, когда запросы или очередь точнее отражают нагрузку
Уточняющие вопросы
- →Почему один реактивный автоскейлинг может не спасти при мгновенном всплеске в 10 раз?
- →Как тёплый буфер или предмасштабирование по расписанию меняет баланс стоимость-headroom?
SeniorДизайнИногдаЗа вашим API-gateway стоит флот stateless-инстансов gateway, и нужно применять глобальный лимит на клиента — скажем, 1000 запросов в минуту на API-ключ — который держится независимо от того, на какой инстанс попал запрос. Спроектируйте rate limiter. Опишите алгоритм (например, token bucket против fixed/sliding window) и что он разрешает для всплесков, почему счётчик в памяти на инстанс не может обеспечить глобальный лимит, как инстансы делят состояние через быстрое центральное хранилище атомарными операциями, как не дать задержке и доступности самого лимитера бить по пути запроса и что делать, когда общее хранилище недоступно (fail-open против fail-closed). Объясните компромисс между строгой глобальной точностью и лишней задержкой обращения к общему счётчику на каждом запросе.
За вашим API-gateway стоит флот stateless-инстансов gateway, и нужно применять глобальный лимит на клиента — скажем, 1000 запросов в минуту на API-ключ — который держится независимо от того, на какой инстанс попал запрос. Спроектируйте rate limiter. Опишите алгоритм (например, token bucket против fixed/sliding window) и что он разрешает для всплесков, почему счётчик в памяти на инстанс не может обеспечить глобальный лимит, как инстансы делят состояние через быстрое центральное хранилище атомарными операциями, как не дать задержке и доступности самого лимитера бить по пути запроса и что делать, когда общее хранилище недоступно (fail-open против fail-closed). Объясните компромисс между строгой глобальной точностью и лишней задержкой обращения к общему счётчику на каждом запросе.
Держите счётчики в быстром общем хранилище вроде Redis, которое все stateless-инстансы обновляют атомарно, ведь счётчик в памяти на инстанс лимитирует только этот инстанс, а не глобальный ключ. Обеспечьте средний темп, позволяя контролируемые всплески (token bucket). Ограничьте задержку проверки, а при недоступности хранилища осознанно выбирайте fail-open или fail-closed. Более строгая глобальная точность стоит общего обращения на каждом запросе.
Типичные ошибки
- ✗Счётчики на инстанс, дающие клиенту лимит в N раз при N инстансах
- ✗Делать read-modify-write общего счётчика без атомарных операций
- ✗Не решить заранее fail-open или fail-closed при недоступности хранилища
Уточняющие вопросы
- →Почему счётчик на инстанс позволяет клиенту превысить задуманный глобальный лимит?
- →Когда выбрать fail-open вместо fail-closed при падении хранилища счётчиков?
SeniorДизайнИногдаФлот серверов приложения за балансировщиком нужно выкатить на новую версию, которая ещё и меняет схему реляционной БД — переименовывает и разбивает колонку — без простоя и с безопасным откатом. Спроектируйте релиз. Опишите, как упорядочить изменение схемы относительно выката кода, чтобы старая и новая версии приложения работали одновременно с одной БД (паттерн expand/contract, обратно совместимая миграция), как rolling-выкат сливает и заменяет инстансы, не теряя запросов, что делает каждый шаг миграции прямо- и обратно-совместимым и как откатиться, если новая версия ведёт себя плохо после того, как часть данных уже записана. Объясните, почему один разрушительный ALTER в одном релизе с кодом вызвал бы простой.
Флот серверов приложения за балансировщиком нужно выкатить на новую версию, которая ещё и меняет схему реляционной БД — переименовывает и разбивает колонку — без простоя и с безопасным откатом. Спроектируйте релиз. Опишите, как упорядочить изменение схемы относительно выката кода, чтобы старая и новая версии приложения работали одновременно с одной БД (паттерн expand/contract, обратно совместимая миграция), как rolling-выкат сливает и заменяет инстансы, не теряя запросов, что делает каждый шаг миграции прямо- и обратно-совместимым и как откатиться, если новая версия ведёт себя плохо после того, как часть данных уже записана. Объясните, почему один разрушительный ALTER в одном релизе с кодом вызвал бы простой.
Применяйте expand/contract: добавьте колонку аддитивно и заполните её, выкатите код, который пишет в обе и читает новую с откатом к старой, затем удалите старую колонку только когда весь флот на новом коде. Каждый шаг остаётся обратно совместимым, поэтому старая и новая версии делят одну схему во время rolling-выката, сливающего и заменяющего инстансы. Один разрушительный ALTER сломал бы ещё работающий старый код и вызвал простой.
Типичные ошибки
- ✗Слать разрушительный ALTER в одном релизе с кодом, читающим колонку
- ✗Считать, что старая и новая версии не могут делить одну схему при выкате
- ✗Удалять старую колонку до того, как весь флот на новом коде
Уточняющие вопросы
- →Почему шаг contract (удаление старой колонки) ждёт, пока её никто не читает?
- →Как connection draining балансировщика не роняет текущие запросы?