Пока программа работает на одном компьютере, всё предсказуемо: одна и та же операция даёт один и тот же результат, а если что-то ломается — обычно ломается всё сразу и целиком. Но как только процессы начинают общаться по сети, эта определённость исчезает. Появляется частичный отказ: одни узлы работают, другие нет, а третьи вроде работают, но точно никто не знает. Это и есть главная особенность распределённых систем.
Почти вся эта статья — про то, чему в такой системе нельзя доверять: сети, часам и даже тому, что ваш собственный процесс не поставили на паузу. Дальше прямая дорога к алгоритмам консенсуса, но сначала надо понять, от чего именно они защищают.
Самая наглядная иллюстрация — распределённая блокировка: одна пауза, которая оказалась длиннее аренды лока, превращает одного владельца в двух.
Пауза в 40 с длиннее аренды лока в 30 с — и владельцев становится два, причём каждый уверен, что он один. Отсечь опоздавшего может только сам ресурс: хранилище видело маркер 34 и отклоняет запись с маркером 33. Клиент, которому «всё в порядке», сделать этого не может.
Частичный отказ: тайм-аут — единственный инструментспросят на собеседовании
Узел отправил запрос и не получил ответа. Что случилось? Вариантов много: потерялся сам запрос; узел упал; узел жив, но отвечает медленно; узел всё обработал, а потерялся уже ответ. И вот главная неприятность — различить эти случаи невозможно. У отправителя есть только один факт: «ответа нет». Единственный способ вообще принять хоть какое-то решение — тайм-аут: подождать сколько-то времени и, если ответа так и нет, объявить узел неработающим.
Четыре разные причины дают отправителю один и тот же вид, поэтому решение принимают не по причине, а по тайм-ауту.
Но какой тайм-аут выбрать? Короткий — быстро замечаешь сбои, но рискуешь объявить мёртвым узел, который просто притормозил под нагрузкой. Тогда его работу передадут другим — и добавят нагрузки уже перегруженной системе, а это прямой путь к каскадному отказу. Длинный тайм-аут — наоборот, долго ждёшь, прежде чем среагировать. «Правильного» значения тут нет, потому что в обычной сети задержки ничем не ограничены: пакет может застрять в очереди перегруженного коммутатора, у занятого процессора, у гипервизора, который приостановил виртуальную машину. (Сеть с гарантированной задержкой в принципе возможна — так работает телефония с выделенным каналом на звонок, — но интернет и сети дата-центров оптимизированы под пиковый трафик через очереди, и платят за это предсказуемостью.)
Но это не значит, что число берут из ощущений. Отправная точка — сколько времени вообще есть у ответа: из бюджета всей операции вычитают то, что уходит на остальные шаги, а остаток сверяют с тем, как долго зависимость отвечает в жизни — берут её p99 и добавляют запас. Получилось меньше p99 — вы будете обрывать нормальные ответы; получилось намного больше бюджета — тайм-аут бесполезен, пользователь уйдёт раньше. Дальше значение уточняют по измерениям, а самые аккуратные системы считают разброс задержек и подстраивают тайм-аут на ходу (так делают Cassandra и Akka).
Тайм-аут сработал — что дальше
Тайм-аут отвечает на вопрос «ждать ли дальше» и не отвечает на главный: что теперь делать. А исходов у сетевого вызова не два, а три, и третий — самый неудобный.
«Получилось», «не получилось» и «не знаю». Первые два очевидны. Третий — это как раз тайм-аут: возможно, сосед всё сделал, а ответ потерялся; возможно, не получил даже запроса. Ошибка начинается там, где «не знаю» молча приравнивают к «не получилось»: тогда повтор списывает деньги второй раз. Поэтому третий исход кодируют явно — отдельным состоянием операции (UNKNOWN, PENDING), а не исключением, которое где-то поймают и решат за вас.
Дальше из «не знаю» есть три выхода, и выбирают по смыслу операции.
Повторить — но только если операция идемпотентна. Это обычный выход для чтения и для записи с ключом операции: клиент придумывает идентификатор один раз и присылает его при каждой попытке, а получатель по нему узнаёт повтор и возвращает результат первой попытки. Без такого ключа повтор — это не «попробуем снова», а «сделаем дважды». Повторять стоит с растущей паузой и со случайным разбросом, иначе все клиенты синхронно ударят по поднимающемуся соседу.
Спросить. Если операция не идемпотентна, а ключа у соседа нет, остаётся запросить у него состояние: «что с платежом 42?». Для этого у вызова должен быть собственный идентификатор, известный обеим сторонам до начала, — иначе спрашивать нечем. Так работает страховка от потерянных уведомлений у платёжных провайдеров.
Отказать и оставить след. Если ни повторить, ни спросить нельзя, операция переводится в состояние «требует разбора» и попадает к человеку или в сверку. Это не поражение, а честный ответ: лучше одна запись в очереди разбора, чем двойное списание.
И то, что относится к любому из трёх: у повторов должен быть предел. Бесконечные повторы при недоступном соседе — самый простой способ превратить его частичный отказ в полный отказ всей системы, о чём следующий раздел.
Три вещи, которым нельзя доверятьспросят на собеседовании
Сеть. Пакеты теряются и задерживаются на непредсказуемое время; соединение может работать в одну сторону и молчать в обратную; сетевые сбои случаются даже в аккуратно управляемых дата-центрах чаще, чем кажется. Вывод простой: любой обмен по сети может сорваться, и обработку этих сбоев надо специально проектировать и тестировать (это называют инженерией хаоса: у Netflix Chaos Monkey прямо в проде гасит случайные экземпляры сервисов, а соседние инструменты семейства добавляют задержки и обрывают связи — и смотрят, выживет ли система).
Часы. У каждой машины свои внутренние часы, и они потихоньку расходятся друг с другом (это называют clock drift). Важно: часов бывает два вида, и путать их опасно.
- Часы «настенного времени» (
System.currentTimeMillis()) синхронизируются по сети (по протоколу NTP) и могут прыгнуть назад при коррекции, спотыкаются о «секунды координации», зависят от неизвестной точности сервера времени. Для измерения интервалов они не годятся. - Монотонные часы (
System.nanoTime()) гарантированно идут только вперёд, но их абсолютное значение само по себе бессмысленно (это просто «сколько-то от какого-то момента»). Именно и только ими меряют длительности.
Из «часам нельзя доверять» напрашивается вопрос, чем тогда пользоваться. Ответ по случаям.
Длительности — монотонными часами, всегда. Разность двух currentTimeMillis() может оказаться отрицательной после коррекции времени, и код «сколько выполнялся запрос» начнёт печатать минус.
Время события — из источника события, а не из места его обработки. Когда сервис получает сообщение, «сейчас» у него и «когда это случилось» — два разных момента; в модели нужны оба, и их держат отдельными полями (occurred_at от отправителя и received_at у получателя). Иначе после разбора очереди у всех событий окажется одно и то же время — время разгребания.
Порядок — не временем, а версиями. Номер версии записи, счётчик у агрегата, смещение в журнале: любой монотонно растущий признак, который выдаёт один и тот же источник, надёжнее любых часов. Логические часы (те же метки Лампорта) — обобщение этой же идеи для случая, когда источников несколько.
Колонка времени в базе — всегда с зоной (timestamptz), и «сейчас» берётся у базы, а не у приложения: тогда у всех экземпляров сервиса одно понятие времени, а не десять своих. Разбор того, как это работает в PostgreSQL, — в статье про время.
Отсюда главная ловушка — упорядочивать события по меткам настенного времени. Ровно так работает разрешение конфликтов «выигрывает последний» (LWW): у двух конкурирующих записей сравнивают время, побеждает та, что «позже». Но если часы двух узлов разошлись на 100 мс, то «более поздняя» по метке запись могла на самом деле произойти раньше — и та запись, которую клиент реально сделал последней, молча пропадёт. Метки времени не гарантируют причинно-следственный порядок; для него нужны логические часы и версии. (Google Spanner ставит GPS-приёмники и атомные часы в каждый дата-центр как раз для того, чтобы сжать погрешность до пары миллисекунд.)
Смотрите на второй ряд: запись Y сделана позже, а метка у неё меньше, и правило «побеждает последний» молча оставляет X.
Паузы процессов. Твой поток может быть остановлен в любой момент на непредсказуемое время. Причин масса: всеобъемлющая пауза сборщика мусора (stop-the-world, иногда на минуты), приостановка виртуальной машины при переезде на другой хост, «украденное» время процессора, подкачка страницы памяти с диска, даже Ctrl+Z. И самое коварное — узел этого не замечает: для него между двумя соседними строками кода прошло «мгновение», а на самом деле — минута, и его уже давно все считают мёртвым.
Обнаружение отказов на практике: пробы
Теория говорит «тайм-аут и кворум», а в контейнерной среде это выражено двумя настройками, которые часто путают, и от их значений зависит, поможет ли платформа или сделает хуже.
Проба готовности отвечает на вопрос «можно ли давать этому экземпляру трафик». Пока она отрицательна, балансировщик убирает экземпляр из ротации, но процесс не трогает. Это правильное место для проверки зависимостей: не прогрелся кэш, не поднялся пул соединений — трафик пока не нужен.
Проба живости отвечает на вопрос «жив ли процесс вообще». Отрицательный ответ означает перезапуск. И тут главная ловушка: проверять в ней зависимости нельзя. Если проба живости ходит в базу, то недоступность базы перезапустит все экземпляры сервиса разом — и вместо одной проблемы станет две, причём вторая ваша. Проба живости проверяет, что процесс отвечает и не завис, и ничего больше.
Почему короткий тайм-аут пробы опасен. Под нагрузкой сервис отвечает медленнее — и проба тоже. Тайм-аут в секунду при обычном ответе за 300 миллисекунд выглядит запасом, пока не случится пик: пробы начинают не успевать, платформа перезапускает экземпляры, оставшиеся получают ещё больше нагрузки, их пробы тоже перестают успевать. Это тот же каскадный отказ, только запущенный собственной системой обнаружения. Поэтому у пробы живости тайм-аут и порог числа неудач берут с большим запасом, а отдельный поток или отдельный путь обработки для неё — не роскошь, а способ не мерить здоровье процесса длиной общей очереди.
И то, что пробы не заменяют. Платформа проверяет экземпляры, а не связи между сервисами: разделение сети, при котором два экземпляра живы и не видят друг друга, пробы не обнаружат — для этого и нужен кворум из следующего раздела. Отсюда практическое правило: пробы решают задачу «убрать мёртвое», а не задачу «выбрать, кто прав».
Истину определяет большинство, а не сам узелспросят на собеседовании
Из паузы вырастает один из самых коварных багов. Представьте узел, который держит распределённую блокировку (лок) или считает себя ведущим. Он проверяет: «лок ещё мой?» — да, — и идёт писать. Но между проверкой и записью его заморозила пауза сборщика мусора. За эту минуту лок протух, его перехватил другой узел, а очнувшийся первый — всё ещё уверен, что владеет им — и пишет, портя данные. Два владельца одновременно.
Мораль: узел не может доверять собственному мнению о своём статусе. В распределённой системе истину определяет кворум — решение большинства узлов (обычно больше половины). Если большинство объявило узел мёртвым, он считается мёртвым, даже если на самом деле прекрасно работает. Решение по большинству безопасно из-за простого счёта: в системе из пяти узлов большинство — это минимум три, а два таких большинства дают уже шесть голосов на пять узлов. Значит, хотя бы один узел попал в оба — и он не даст принять два противоречащих решения. Именно так узлы выбирают ведущего и не допускают split brain (когда ведущих сразу два).
Но кворум решает, кто должен писать, — и не мешает опоздавшему всё испортить. Практическая защита — ограждающий маркер (fencing token). Сервис блокировок при каждой выдаче лока возвращает постоянно растущий номер; клиент прикладывает этот номер к каждой своей записи; а хранилище отклоняет запись, если её номер меньше уже виденного. Узел, очнувшийся после паузы, приходит со старым номером — и его запись отвергают. Ключевой момент: проверять маркер должен сам ресурс (хранилище), а не клиент — ведь клиент, который считает себя «в порядке», как раз и есть источник проблемы. Подробный разбор с Redis и кодом — в задаче про лок без fencing.
Остаётся вопрос, откуда берётся сам номер. Готовые источники такие.
Счётчик службы согласования. ZooKeeper выдаёт при создании узла последовательности монотонно растущий номер (zxid или номер последовательного узла), etcd — номер ревизии хранилища. Обе службы и созданы для того, чтобы такой номер был один на всех и не повторялся; если у вас уже есть одна из них, номер брать оттуда.
Версия строки в базе. Самый частый и самый дешёвый вариант, доступный любому сервису: колонка version, которую увеличивает каждое изменение, плюс запись через UPDATE ... WHERE id = ? AND version = ?. Опоздавший придёт со старой версией, обновит ноль строк и узнает об этом. Это обычная оптимистичная блокировка — и она же fencing, только внутри одной базы.
Последовательность или монотонный идентификатор. Если ресурс умеет сравнивать, годится и значение из последовательности базы, и упорядоченный по времени идентификатор (UUID версии 7): главное, чтобы он выдавался одним источником и только возрастал.
А если хранилище проверять номер не умеет? Так бывает с файловым хранилищем, внешним API или очередью. Тогда честный ответ: полной защиты нет, и её отсутствие надо компенсировать в другом месте. Варианты по убыванию надёжности: сделать саму операцию идемпотентной по ключу, чтобы повтор опоздавшего ничего не менял; вести промежуточную запись в своей базе (где версия есть) и переносить её наружу одним владельцем; выбрать срок жизни блокировки заведомо больше самой долгой возможной паузы и принять остаточный риск. Последнее — не защита, а осознанная ставка, и записывать её надо в тех же словах.
Каскадный отказ и как его останавливаютспросят на собеседовании
Каскадный отказ упоминался дважды, и это тот случай, где механизм важнее названия: именно так небольшая неприятность превращается в недоступность всей системы.
Как это разворачивается. Сосед начинает отвечать медленнее (база тормозит, сборщик мусора, лишний запрос в цикле). Ваши запросы копятся в ожидании: каждый занимает поток и соединение из пула. Пул кончается — и сервис перестаёт отвечать на все запросы, включая те, которым сосед не нужен вовсе. Ваши вызывающие видят таймауты и начинают повторять, добавляя нагрузки. Теперь неисправен не один сервис, а цепочка.
Три вещи, которые разрывают эту цепочку.
Повторы под контролем. Наивный повтор умножает нагрузку: три попытки на каждый запрос — это троекратный трафик в момент, когда соседу и так плохо. Поэтому повторы делают с растущей паузой и разбросом, ограничивают общий бюджет повторов на запрос и вообще не повторяют то, что не станет лучше от повтора (4xx, отказ по правам, неверный формат).
Ограничение одновременных вызовов. У каждого внешнего вызова свой предел числа одновременных запросов и своя изолированная часть пула — тогда медленный сосед выедает только свою квоту, а остальные пути сервиса продолжают работать. Это и называют перегородками: отсек затопило, корабль плывёт.
Сброс нагрузки. Когда запросов больше, чем сервис способен обработать, честнее отказать сразу, чем поставить в очередь ожидания. Быстрый 503 с заголовком «попробуйте позже» сохраняет систему живой; очередь на минуту не сохраняет ничего — ответ всё равно придёт после того, как пользователь ушёл. Сюда же относится и предохранитель: после серии отказов вызовы к соседу перестают уходить вовсе, а одиночные пробные запросы проверяют, ожил ли он.
Разбор этих приёмов с кодом и настройками — в статье про устойчивость. Здесь важно другое: все три относятся к вызывающей стороне. Сосед не может защитить вас от собственной медлительности — это делаете вы.
Византийские сбои
До сих пор мы считали узлы «честными»: они могут молчать, тормозить, отдавать устаревшее — но если уж отвечают, то не врут. Если же узел способен врать — слать произвольные или поддельные сообщения (из-за порчи памяти, бага или злого умысла), — это называют византийским сбоем, а согласование в такой среде — задачей о византийских генералах. Защита от неё дорогая и нужна там, где нет доверия: авиакосмос (радиация портит память), блокчейны (участники не доверяют друг другу). Внутри собственного дата-центра византийских сбоев обычно нет, и защищаться от них не окупается — но проверять данные, приходящие от внешних клиентов (валидация, защита от инъекций), нужно всегда.
Модель системы: безопасность и живучесть
Чтобы рассуждать о корректности, алгоритмы описывают через модель системы — набор допущений о времени и отказах. Реалистичный дефолт — частично синхронная модель: сеть и узлы обычно ведут себя хорошо, но иногда нет, и алгоритм обязан это пережить. И два свойства, которые от алгоритма требуют: безопасность (safety — «ничего плохого не случится»; нарушение имеет конкретный момент и необратимо) и живучесть (liveness — «со временем случится что-то хорошее»; пример — та самая конечная согласованность). Хорошие алгоритмы держат безопасность всегда, а живучесть — при разумных допущениях.
CAP без мифов — и что добавляет PACELCспросят на собеседовании
Популярная формулировка CAP-теоремы «выбери два из трёх: согласованность, доступность, устойчивость к разделению» — неточна и приводит к неверным выводам. Разделение сети не выбирают — оно случается: сеть ненадёжна по природе. Точная формулировка такая: в момент разделения сети система вынуждена выбрать между согласованностью (все узлы отвечают одинаково) и доступностью (каждый запрос получает ответ). В нормальном режиме, без разделения, доступны обе.
Из-за этой оговорки CAP отвечает только на вопрос «что будет при сбое сети» и молчит о повседневной работе. Пробел закрывает PACELC: при разделении (P) — выбор между A и C, иначе (Else) — выбор между задержкой (Latency) и согласованностью (Consistency). Даже в идеально здоровой сети синхронная репликация покупает согласованность ценой задержки на каждую запись, асинхронная — наоборот. Примеры для ориентира: PostgreSQL с синхронной репликой — PC/EC, с асинхронной — PC/EL. Cassandra с настройками по умолчанию — PA/EL: при разделении отвечает всеми узлами, а в обычной жизни предпочитает быстрый ответ свежему. Кворумы на чтение и запись (когда прочитанных и записанных копий вместе больше, чем всех копий) двигают её в обратную сторону, к PC/EC, — и платит она за это задержкой каждой операции.
PACELC это два вопроса, а не один: что делать при разрыве сети и что предпочитать, когда с сетью всё хорошо; внизу три настройки баз и их пара букв.
Где это применяется
Как только у вас больше одного сервиса и они ходят друг к другу по сети — вы уже в распределённой системе, даже если не планировали. Любой сетевой вызов может потеряться, зависнуть или упереться в тайм-аут; любой узел может замолчать посреди операции. Практическая рамка: не тяните распределённость раньше времени (три условия, когда она реально нужна), но если она уже есть — проектируйте под частичный отказ: тайм-ауты и повторы (обязательно идемпотентные!), никакого упорядочивания по настенным часам, решения через кворум, fencing на общих ресурсах.
Где спотыкаются начинающие:
- Считают, что «нет ответа» значит «узел упал». Это неотличимо от потери ответа или медленного узла. Так появляются двойные списания и потерянные данные — без единой ошибки в логах.
- Упорядочивают события по
currentTimeMillis(). Часы узлов разошлись — и LWW молча теряет запись, которую клиент считал сохранённой. - Проверяют лок и сразу пишут. Пауза сборщика мусора между проверкой и записью даёт двух владельцев. Нужен fencing token, который проверяет сам ресурс.
- Узел верит собственному «я ещё ведущий». А за время его паузы кворум уже выбрал другого. Истину определяет большинство.
- Тянут микросервисы «ради масштаба». И получают все проблемы распределённых систем там, где хватило бы одного узла.
Глубже: что видно при разборе: сквозной идентификатор, трасса и корреляция логоврасширенное
Всё выше объясняет, почему в распределённой системе нельзя доверять ответу, часам и себе. При инциденте к этому добавляется четвёртое: нельзя понять, что произошло, потому что следы разбросаны по десяти сервисам. Три вещи, без которых разбор превращается в гадание.
Сквозной идентификатор. Первый сервис на пути запроса выдаёт идентификатор (в HTTP это заголовок X-Request-Id, или traceparent по стандарту W3C) и передаёт его в каждый исходящий вызов, каждое событие и каждую запись в журнале. Тогда «покажи всё, что случилось с запросом abc123» это один поисковый запрос по журналам всех сервисов, а не десять. Идентификатор генерирует граница (шлюз), а не клиент, чтобы его нельзя было подделать; клиентский идентификатор, если пришёл, сохраняют отдельно.
Трасса. Журнал отвечает «что», трасса отвечает «сколько и где». Каждый сервис пишет спан: начало, конец, кого вызвал, с каким результатом; спаны склеиваются по идентификатору трассы, и на одной картинке видно, что запрос жил две секунды, из них полторы ждал сервис доставки, который ждал базу. В Spring Boot 3 трассы идут через Micrometer Tracing без кода, в Go их даёт OpenTelemetry с обёртками otelhttp и otelgrpc, в Node автоинструментирование @opentelemetry/auto-instrumentations-node, в Python запуск через opentelemetry-instrument; передача контекста через брокер везде отдельная забота, о чём статьи про Kafka и RabbitMQ. Трассы хранят выборочно (доля от всех запросов и все ошибки), журналы полностью.
Корреляция журналов. Записи журнала структурированы (JSON с полями), содержат идентификатор запроса и трассы, имя сервиса, экземпляр и время в UTC с миллисекундами. Без общего времени порядок событий между сервисами не восстановить; без структуры не отфильтровать. Ошибка пишется один раз там, где её обработали, с причиной, а не на каждом уровне вызова, иначе одна авария даёт десять тысяч записей.
Что смотрят при разборе, по порядку. Панель с ошибками и задержкой по сервисам, чтобы найти, где началось (первый сервис, у которого выросли ошибки, обычно и есть причина, остальные жертвы). Трассу одного неудачного запроса из этого сервиса, чтобы увидеть, на чём он ждал. Журнал по идентификатору трассы, чтобы прочитать причину словами. И состояние инфраструктуры вокруг (выкаты, изменения настроек, нагрузка) за полчаса до начала, потому что большинство инцидентов начинается с изменения.
Глубже: разбор аварии: время обнаружения, оповещение на симптом и хронология без виноватыхрасширенное
Авария случилась, починили, и дальше два пути: забыть до следующей или разобрать так, чтобы следующая была короче. Разбор это не поиск виноватого, а несколько чисел и один документ.
Два времени. Время от начала до обнаружения и время от обнаружения до починки. Первое почти всегда больше и почти всегда дешевле сократить: авария, которую заметили по жалобам пользователей через сорок минут, чинилась пять. Сокращает его оповещение, и оно должно срабатывать на симптом, а не на причину: «доля ошибок оформления выше одного процента» и «p99 ответа выше двух секунд», а не «процессор выше 80 %» и «очередь длиннее тысячи». Причин бесконечно много, симптомов у сервиса три-четыре, и они известны заранее.
Хронология. Первое, что пишут после аварии, это хронология с отметками времени: когда началось (по графикам, не по ощущениям), когда оповестило, когда дежурный увидел, что делал, когда починилось. Из неё видно, где потеряли время: оповещение не сработало, дежурный не понял панель, откат занял полчаса из-за миграции. Каждая такая потеря это конкретное улучшение с исполнителем и сроком.
Без виноватых. Разбор, в котором ищут, кто выкатил, приводит к тому, что в следующий раз хронологию не восстановить: люди не пишут, что делали. Вопрос «почему система позволила этому случиться и почему не поймала раньше» даёт улучшения; вопрос «кто» даёт тишину. Это правило работает, только если его соблюдает руководство, а не пишут в шаблоне.
Что в документе. Влияние (сколько пользователей, сколько времени, сколько денег), хронология, причина словами и цепочка причин под ней (выкат без ревью, потому что спешили, потому что…), что сработало, что нет, и список действий. Действий немного и они проверяемые: «добавить оповещение на долю ошибок», «сделать откат без миграции», а не «быть внимательнее». Документ читают все, кто держит похожие сервисы, потому что та же авария у соседей ещё впереди.
Оповещения при этом тоже разбирают: сработавшие зря выключают, а не привыкают к ним, потому что дежурный, который получает сорок ложных тревог в день, не увидит настоящую.
Коротко
- «Нет ответа» неотличимо от «медленно» и «потерялся ответ»: единственный инструмент это тайм-аут, а повтор безопасен только для идемпотентного.
- Сети, часам и паузам процесса не доверяют: события не упорядочивают по настенным часам, монотонные часы только для интервалов. Вместо часов: длительности монотонными, время события от источника отдельным полем, порядок по версиям и счётчикам, колонка времени с зоной и «сейчас» от базы.
- Истину определяет кворум, а не сам узел; проверил лок и пишешь это два владельца, нужен ограждающий маркер, который проверяет ресурс. Ограждающий маркер берут из счётчика службы согласования, из версии строки (
UPDATE ... WHERE version = ?) или из монотонного идентификатора; если ресурс проверять его не умеет, остаётся идемпотентность по ключу или осознанный остаточный риск. - CAP говорит только о моменте разрыва сети, PACELC добавляет выбор между задержкой и согласованностью в обычной работе.
- Разбор возможен при сквозном идентификаторе в журналах, событиях и трассе, структурированных журналах с временем в UTC; смотрят по порядку: где началось, трасса одного запроса, журнал по трассе, что менялось за полчаса до.
- Авария: сокращают время обнаружения оповещением на симптом, пишут хронологию с отметками, ищут причины в системе, а не виноватых, действия проверяемые и с исполнителем.
- У сетевого вызова три исхода, и третий — «не знаю»: его кодируют отдельным состоянием, а дальше либо повторяют по ключу операции, либо спрашивают статус, либо отправляют в разбор человеку.
- Каскадный отказ разрывают три приёма на вызывающей стороне: повторы с паузой и бюджетом, предел одновременных вызовов с перегородками, быстрый отказ вместо очереди ожидания.
- Проба готовности убирает из ротации, проба живости перезапускает: зависимости проверяют только в первой, иначе падение базы перезапустит все экземпляры, а короткий тайм-аут пробы сам вызывает каскад.
Что почитать дальше
- Согласованность и консенсус — линеаризуемость, причинный порядок и почему консенсус не пишут сами.
- Корректность в распределённых системах — сквозной аргумент, целостность против своевременности и сверка.
- Паттерны отказоустойчивости — тайм-аут из бюджета, повторы, выключатель и деградация сценария в коде.