Восемь смертных грехов распределённых систем

Warning

В распределённой системе всё, что может пойти не так, пойдёт не так, причём в самый неподходящий момент и с вероятностью, о которой вы даже не подозревали

Многим, кто только начинает работать с распределёнными системами, кажется, что он почти бог :). Бесконечные серверы, кластеры, асинхронные очереди - всё это выглядит для него как магия, которая решает любые проблемы. Многие уверены: если монолит тормозит, просто распили его на микросервисы, раскидай по контейнерам, и мир станет прекрасен.

Но первый год работы с продом выбьет эту дурь.

Система не просто падают - они распадаются на части каким-то изощрённым способом. Сервисы не находят друг друга, хотя работают в соседних подах, запросы висят минутами, хотя вроде должны выполняться за миллисекунды. Логи врут, метрики расходятся, а пользователи просто уходят к конкурентам.

Примерно в такой свой первый момент я впервые услышал фразу: "Ты просто столкнулся с реальностью распределённых систем. Добро пожаловать во взрослую жизнь".

Только спустя некоторое время практики я понял: проблема не в коде, не в серверах и даже не в людях. Проблема в том, что все мы верили в мифы о распределённых вычислениях. В эти мифы свято верят все те, кто никогда не строил по настоящему большие распределённые системы. Эти мифы просты, как три копейки: сеть надёжна, задержки нулевые, пропускная способность бесконечна.

В 1994 году Питер Дойч, работавший в Sun Microsystems, сформулировал 8 заблуждений, которые разрушаются при переходе от монолита к распределённым системам. Спустя почти 30 лет эти заблуждения по-прежнему актуальны. И для SRE они стали не просто теорией, а ежедневной реальностью, определяющей наши SLO, error budget и ночные звонки.

В этой главе мы разберём каждое заблуждение не абстрактно, а через призму SRE-практик: как оно проявляется, почему опасно и что с этим делать инженеру, отвечающему за надёжность.

Заблуждение 1: Сеть надёжна

Миф

Сеть работает как электрическая розетка и всегда доступна, всегда стабильна. Пакеты доходят мгновенно и в правильном порядке.

Реальность

Сеть это самый ненадёжный компонент любой распределённой системы. За годы работы я видел:

  • Network partitions: когда два сервиса в одном дата-центре вдруг перестают видеть друг друга из-за сбоя коммутатора.
  • Перегрузки: когда один "шумный" сервис забивает всю доступную полосу.
  • Packet loss: особенно в облачных средах с виртуализированной сетью.
  • MTU проблемы: когда пакеты больше допустимого размера просто исчезают.
  • DNS сбои: когда сервис имён перестаёт отвечать.
  • и многое другое :)

История из практики

Мы переносили критический сервис в облако. Всё настроили по бестпрактисам, провели нагрузочное тестирование в staging (причем, что редкость - в точно такой же конфигурации, как production, только с меньшими мощностями). Метрики летят, все довольны, релиз выкатили.

Через пару дней после переезда начались жалобы от партнёров: их запросы иногда доходят с огромной задержкой, а иногда не доходят вообще. Мы смотрим в дашборды облачного провайдера - а там, как всегда, всё зелененькое. Смотрим свои метрики - тоже зеленькое. А запросы падают.

Оказалось, что в production у нас был включён забытый сетевой security group rule, который маркировал часть трафика как "подозрительный" и отправлял на дополнительную проверку через облачную DDoS-защиту. В staging эта опция была отключена, потому что "там же не настоящий трафик, зачем платить". Защита работала хорошо, 99.9% трафика проезжало, а 0,1% зависал на дополнительных проверках.

Для нас эти 0,1% стали причиной почти двухнедельного расследования и испорченных отношений с ключевыми клиентами. Мы искали проблему в коде, в базе, в очередях, а она сидела в забытом флажке в консоли AWS.

SRE-практики

Ретраи с exponential backoff и jitter

Никогда не повторяйте неуспешный запрос мгновенно. Если первый запрос упал, подождите 100мс, потом 200мс, потом 400мс. И обязательно добавьте случайный разброс (jitter), чтобы все клиенты не начали стучать одновременно.

def call_with_retry(max_retries=5):
    for attempt in range(max_retries):
        try:
            return make_request()
        except NetworkError:
            if attempt == max_retries - 1:
                raise
            wait_time = (2 ** attempt) * 0.1  # exponential backoff
            wait_time += random.uniform(0, 0.1)  # jitter
            time.sleep(wait_time)

Circuit breakers

Не долбите в закрытую дверь. Если сервис упал, - не шлите запросы какое-то время.

Graceful degradation

Если зависимость недоступна, падайте красиво. Отдайте кеш, покажите заглушку, но не падайте сами.

Мониторинг сетевых метрик

Вы должны видеть: - Packet loss между всеми критическими сервисами - Latency (не только среднюю, но и p95, p99) - TCP retransmits - Connection failures

Заблуждение 2: задержка равна нулю

Миф

Вызов удалённого сервиса происходит практически мгновенно, как вызов локальной функции.

Реальность

Физика жестока. Даже в одном дата-центре свет проходит расстояние со скоростью ~200000 км/с (в оптоволокне). Добавьте коммутаторы, очереди, обработку и вы получите задержки, которые убивают производительность. Средняя задержка может быть 10мс, но p99 (худший 1% запросов) может быть и 500мс. И именно этот 1% пользователей будут названивать и написывать в поддержку.

История из практики

Однажды мы оптимизировали запрос, который ходил в 5 разных сервисов последовательно. Среднее время было 150мс, что вроде нормально. Но p99 достигал 3 секунд. Оказалось, что при нагрузке один из сервисов иногда тупил на секунду. Добавили параллельные вызовы и кеширование и p99 упал до 400мс.

SRE-практики

Не усредняйте метрики. Живите перцентилями (значение, ниже которого находится заданный процент наблюдений в наборе данных). p50, p95, p99, p99.9. Стройте SLO вокруг них.

Знайте, сколько времени занимает каждый внешний вызов. Используйте распределённую трассировку.

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

Не ждите синхронно, если можно ответить "принято" и обработать потом. Очереди, события, webhooks.

Кеш это единственное лекарство от задержек. Но помните про инвалидацию (удаление или помечания устаревших данных в кеше как недействительных).

Заблуждение 3: пропускная способность бесконечна

Миф

Можно передавать столько данных, сколько нужно, и это ничего не стоит.

Реальность

Каналы связи имеют пределы. И эти пределы ближе, чем кажется.

История из практики

Мы гордились своей микросервисной архитектурой, пока не заметили, что примерно 70% времени запроса тратится на JSON-сериализацию. Мы передавали огромные вложенные структуры, потому что "пропускная способность же бесконечна". Переход на Protobuf и оптимизация форматов данных сократили время почти в 3 раза.

SRE-практики

Batch processing

Вместо 1000 маленьких запросов один большой.

Сжатие

gzip на HTTP, сжатие в брокерах сообщений.

gRPC вместо REST

Protobuf компактнее JSON, HTTP/2 эффективнее.

Rate limiting

Не давайте одному клиенту съесть весь канал.

Мониторинг трафика

Знайте, какие сервисы сколько генерируют трафика и куда.

Заблуждение 4: сеть безопасна

Миф

Наша сеть надёжно защищена всякими накупленными штуками с громкими и известными названиями, можно не беспокоиться, они же известные, дорогие и значит надежные.

Реальность

Внутренние угрозы, инсайдеры, скомпрометированные сервисы, MITM-атаки и т.д. и т.п.

История из практики

Одни крупные ребята почему-то считали, что их внутренняя сеть полностью безопасна, потому что "в нее доступа снаружи вообще нет". Пока из-под учетки уволившегося сотрудника не натворили дел, про которые писали во всех новостях.

SRE-практики

mTLS везде

Взаимная аутентификация и шифрование между сервисами. Не доверяйте сети, доверяйте криптографии. И вообще никому не доверяйте.

Service mesh

Istio, Consul и т.п. - берите и внедряйте. Они дают безопасность "из коробки".

Сетевые политики

Кто с кем может общаться? Если сервис А не должен вызывать сервис В - запретите это на уровне сети.

Регулярные пентесты

Приглашайте этичных хакеров ломать вашу систему. Регулярно.

Заблуждение 5: топология не меняется

Миф

Сервисы всегда там же, где и вчера. Можно сохранить IP-адреса в конфигах и забыть.

Реальность

В облачном мире всё течёт и меняется: - Автоскейлинг создаёт и убивает инстансы. - Переезды между зонами доступности. - Canary deployments. - Аварийное переключение.

История из практики

Разработчики захардкодили IP адреса брокера сообщений прямо в коде. Это работало на проде почти год. Потом обновили кластер Kafka и IP изменились, все встало колом. Деплой занял сутки, потому что пришлось пересобирать полсотни сервисов.

SRE-практики

Service discovery

Consul, etcd, ZooKeeper, DNS SRV записи - используйте что-то динамическое.

Идемпотентность

Клиенты должны переживать потерю соединения и переподключаться.

Graceful shutdown

При остановке сервис должен: 1. Перестать принимать новые запросы; 2. закончить обрабатывать текущие; 3. сообщить discovery, что он уходит; 4. подождать, пока трафик на него перестанет идти; 5. и только сейчас умереть.

Не полагаться на статические IP

Даже для баз данных. Используйте DNS имена, которые можно поменять.

Заблуждение 6: администратор один

Миф

Систему администрирует одна команда с едиными целями и видением и эта команда все знает про сервис и все умеет.

Реальность

Распределённые системы обслуживают десятки команд с разными приоритетами, бюджетами, навыками, графиками дужурств, представлениями о "правильном"...

История из практики

Две команды в компании использовали один и тот же Kafka-кластер. Команда А обновила конфиг, увеличив задержки для команды Б. Команда Б узнала об этом только когда упали их SLO. Месяц выясняли отношения.

SRE-практики

Чёткие границы ответственности

"Этот сервис принадлежит команде А, и только она может менять его конфигурацию". Зоны ответственности должны быть документированы.

Observability

Все сервисы должны отдавать метрики в одном формате, логи в одном формате в единое хранилище, трейсы в общую систему.

Runbooks и документация

Любая команда должна иметь инструкцию: "Если упал наш сервис, делай раз, два, три".

Заблуждение 7: транспорт бесплатен

Миф

Передача данных не требует затрат, ни денежных, ни вычислительных.

Реальность

Всё имеет цену: - Деньги: облачные провайдеры берут за трафик между зонами и наружу - CPU: сериализация/десериализация жрет процессор - Память: буферы для отправки и приёма - Время: чем больше данных, тем дольше ждать

История из практики

Одни товарищи построили аналитику на потоках событий, каждое событие весило примерно 50Kb (JSON с кучей метаданных). Миллиард событий в месяц и 50Tb трафика. Счета от GCP превысили зарплаты разработчиков. Оптимизация формата до 5KB сэкономила 90% бюджета.

SRE-практики

Оптимизация форматов

Protobuf, Avro, Thrift - выбирайте бинарные форматы, не JSON.

Минимизация

Один запрос с 10 полями лучше, чем 10 запросов по одному полю.

Batch

Копить события и отправлять пачками эффективнее по трафику и CPU.

Мониторинг стоимости

Сделайте дашборд "Стоимость трафика по сервисам". Удивитесь, кто съедает бюджет.

Заблуждение 8: cеть однородна

Миф

Все узлы сети работают одинаково, все протоколы ведут себя предсказуемо.

Реальность

Гетерогенность правит миром: - Разные версии Linux. - Разные сетевые карты. - Разные облака (AWS, GCP, on-prem). - Разные реализации TCP/IP. - Разные версии протоколов.

История из практики

Мы тестировали на ноутбуках разработчиков (macOS) и в тестовом окружении (Ubuntu). Production был на CentOS с другими сетевыми настройками. В production таймауты случались, потому что sysctl-параметры TCP отличались от тестовых.

SRE-практики

Production-like окружения

Тестовая среда должна максимально возможно повторять production. Различия в сетевых стеках это источник проблем.

Chaos Engineering

Целенаправленно ломайте сеть, вносите задержки, теряйте пакеты. Netflix Chaos Monkey - ваш друг.

Абстракции

Скрывайте гетерогенность за библиотеками, которые гарантируют поведение независимо от платформы.

Постепенное внедрение

Никаких "пятничных наскоряк" деплоев. Canary, feature flags, A/B тесты.

Как это всё влияет на SRE-практики

Эти восемь заблуждений не просто интересная теория. Они напрямую влияют на наши SLO и Error Budget.

Допустим, у нас SLO 99,9% доступности в месяц. Это ~43 минуты простоя в месяц.

Теперь представьте: - Fallacy #1 (сеть ненадёжна): 10 минут на потерю пакетов. - Fallacy #2 (задержки не нулевые): 15 минут на таймауты. - Fallacy #5 (топология меняется): 10 минут на проблемы discovery. - Fallacy #8 (неоднородность): 8 минут на несовместимость.

43 минуты закончились. А у нас ещё 4 заблуждения не учтены.

Планируйте с запасом.

SRE-подход к заблуждениям

  1. Принять: да, сеть ненадёжна, да, задержки есть. Смиритесь.
  2. Проектировать: каждая архитектурная встреча должна начинаться с вопроса: "Что если сеть упадёт прямо сейчас?"
  3. Измерять: метрики, метрики, метрики. Всё, что движется по сети, должно быть измерено.
  4. Тестировать: Game Days, Chaos Engineering, Load Testing, DRT.
  5. Автоматизировать: восстановление должно быть c с минимальным (а лучше вообще без) участием человека.

Распределённые системы сложны не потому, что мы плохие инженеры. Они сложны потому, что мир, в котором они работают, сложен и непредсказуем. Сеть будет падать, задержки будут расти, топология будет меняться. Наша задача не предотвратить это (это невозможно), а продумать и построить систему, которая выживет, когда это случится.