Проект ЦИТадель
Распределённая система — это набор компьютеров, работающих над общей задачей и взаимодействующих через сеть. Определение звучит буднично, пока не осознаешь его следствие, сформулированное Лесли Лэмпортом: «распределённая система — это такая система, в которой отказ компьютера, о существовании которого вы даже не подозревали, делает ваш собственный компьютер непригодным к использованию». Практически любая современная информационная система распределена: веб-приложение с базой данных на другом сервере, сервис с репликой в резервном ЦОДе, мессенджер с миллионами пользователей — всё это распределённые системы со всеми их фундаментальными проблемами.
Распределяются по трём причинам: отказоустойчивость (один сервер — одна точка отказа), масштаб (нагрузка или данные не помещаются на одну машину) и география (пользователи в Новосибирске не должны ждать ответа из Франкфурта). Цена — качественный скачок сложности: у распределённой системы появляются виды отказов и парадоксы, которых в одиночном компьютере не существует в принципе. Эта статья — карта таких проблем и выработанных за полвека решений; она открывает новый подраздел «Инфраструктуры», за ней последует систематический курс.
Сообщение по сети может потеряться, задержаться на неопределённое время, прийти в другом порядке или дважды. Хуже того: отправив запрос и не получив ответа, невозможно узнать, что произошло — потерялся запрос, упал получатель или потерялся ответ (и тогда операция выполнена!). Любой сетевой вызов обязан иметь тайм-аут и стратегию повтора, а любая операция, которую могут повторить, должна быть идемпотентной — безопасной при многократном исполнении. Народную мудрость на эту тему собирают «заблуждения распределённых вычислений» (Питер Дойч и коллеги, Sun, 1990-е): сеть надёжна, задержка нулевая, полоса бесконечна, топология неизменна и т.д. — восемь допущений, которые молча делает каждый новичок и которые все ложны.
У каждой машины свои часы, и они расходятся — на миллисекунды при NTP-синхронизации, на что угодно при её сбое. Следствие: по меткам времени нельзя надёжно установить порядок событий на разных машинах — «кто записал последним» становится вопросом без ответа. Классическое решение — отказаться от физического времени в пользу логического: часы Лэмпорта (1978) нумеруют события так, что причинно связанные события упорядочены (каждое сообщение несёт счётчик; получатель берёт максимум своего и полученного плюс один), а векторные часы позволяют ещё и распознать, что два события конкурентны — произошли «одновременно» в строгом смысле независимости. Идея «упорядочивать причинность, а не время» — одна из самых глубоких в предмете, и мы встретим её следы везде: от репликации до журналов событий.
Одиночный компьютер отказывает целиком — программа либо работает, либо нет. Распределённая система отказывает частично: три узла из пяти живы, сеть между ЦОДами порвалась, а внутри каждого всё прекрасно (сетевое разделение, split brain). Знаменитый теоретический итог — теорема FLP (Фишер, Линч, Патерсон, 1985): в асинхронной системе, где нельзя отличить упавший узел от медленного, ни один детерминированный алгоритм не гарантирует достижения консенсуса за конечное время. Это не приговор практике: Raft и Paxos сохраняют безопасность без временных предположений, а живость получают при частичной синхронности, когда сеть достаточно долго ведёт себя предсказуемо. Случайные тайм-ауты уменьшают вероятность повторного раскола голосов, но не дают гарантии завершения при любом поведении сети.
Самая цитируемая (и самая перевираемая) теорема предмета — CAP (гипотеза Брюера 2000, доказательство Гилберта и Линч 2002): при сетевом разделении (Partition) система вынуждена выбирать между линеаризуемостью (Consistency — завершённая запись видна всем последующим чтениям) и доступностью (Availability — каждый запрос к неотказавшему узлу получает содержательный ответ). Популярная формулировка «выберите два из трёх» вводит в заблуждение: разделения не выбирают — они случаются; реальный выбор — это поведение системы в момент разделения: отвечать, рискуя отдать устаревшее (AP), или отказывать той части системы, которая не может подтвердить актуальное состояние (CP). Полезное уточнение — формула PACELC: при разделении (P) выбор A или C, а в остальное время (Else) — выбор между задержкой (L) и согласованностью (C): плата за строгую согласованность взимается не только в аварию, но и в каждом запросе, ожидающем подтверждения реплик.
Согласованность — не бинарный флаг, а спектр моделей:
Инженерный вывод: модель согласованности — не свойство «хорошей» или «плохой» системы, а осознанно выбираемый компромисс по каждому классу данных. Остаток счёта требует линеаризуемости; счётчик лайков — нет.
Репликация — хранение копий данных на нескольких узлах — решает сразу отказоустойчивость, масштаб чтения и географию, порождая единственную, но вечную проблему: копии надо синхронизировать.
Ряд задач не решается без строгого согласия узлов: выбор лидера (два лидера — split brain и порча данных), членство в кластере, атомарная фиксация. Консенсус — протокол, приводящий узлы к единому решению несмотря на отказы части из них — вершина инженерной мысли предмета.
Классика — Paxos (Лэмпорт, 1989/1998), доказуемо корректный и легендарно трудный для понимания и реализации. Ответом на его сложность стал Raft (Онгаро и Аустерхаут, 2014), спроектированный с явной целью «быть понятным» — и потому разберём его идею в три абзаца.
Время делится на нумерованные термы; в каждом терме не более одного лидера. Узел, не слышавший лидера дольше случайного тайм-аута, объявляет новый терм и просит голоса; получив большинство — становится лидером. Случайность тайм-аутов разводит конкурентов, а единственность обеспечивается тем, что любые два большинства пересекаются и каждый узел голосует не более одного раза за терм.
Все изменения идут через лидера, который дописывает их в реплицируемый журнал и рассылает подписчикам; запись считается зафиксированной, когда её подтвердило большинство, — и лишь тогда применяется к состоянию. Журнал — первичен, состояние машины — производное от него (парадигма реплицируемого автомата: одинаковый журнал + детерминированное применение = одинаковое состояние).
При выборах побеждать разрешено лишь кандидату, чей журнал не отстаёт от журналов голосующих узлов; вместе с правилом фиксации записей текущего терма это гарантирует сохранность зафиксированного префикса. Кластер из 2f+1 узлов сохраняет безопасность при любых f отказах и продолжает работу, пока связное большинство доступно: три узла терпят один отказ, пять — два.
Практика: консенсус дорог (кворум на каждую запись), поэтому область его применения стараются ограничивать. Координационные сервисы хранят маленькое критичное ядро: etcd (сердце Kubernetes) и Consul реализуют Raft, ZooKeeper — родственный протокол ZAB; поверх них строятся выборы лидеров, распределённые блокировки и конфигурация. Распределённые СУБД применяют тот же принцип непосредственно к пользовательским данным, разбивая их на небольшие группы репликации, каждая со своим журналом консенсуса.
Репликация копирует данные целиком; когда данные или нагрузка на запись не влезают в один узел, их шардируют — режут на части по ключу. Два базовых способа: по диапазону ключа (эффективные сканы соседних ключей, но риск «горячих» диапазонов) и по хешу (равномерно, но диапазонные запросы рассыпаются по всем шардам). Наивный hash(key) mod N обладает пороком: смена N перетасовывает почти все ключи. Согласованное хеширование (consistent hashing) чинит это: узлы и ключи отображаются на кольцо, ключ принадлежит ближайшему по кольцу узлу, и добавление узла трогает лишь соседний участок — классика Dynamo, memcached-клиентов и CDN. Зрелые системы ведут явную таблицу принадлежности шардов с фоновым ребалансом. Вечная головная боль шардирования — операции поверх нескольких шардов: они превращаются в распределённые транзакции.
Атомарность «всё или ничего» поверх нескольких узлов — исторически самое слабое место. Классический двухфазный коммит (2PC): координатор спрашивает участников «готовы?», собрав единогласие — рассылает «фиксируй». Порок в середине: участник, ответивший «готов», обязан ждать вердикта — а если координатор в этот момент погиб, участник блокирован с удержанными блокировками на неопределённый срок. 2PC жив в корпоративных стыковках (XA), но в веб-масштабе его избегают. Современные ответы с двух сторон:
Синхронный вызов сервис–сервис распространяет отказы: упавший получатель валит отправителя. Асинхронная альтернатива — брокер сообщений между ними: отправитель кладёт сообщение и свободен; получатель разбирает в своём темпе; пик нагрузки превращается в очередь, а не в каскад тайм-аутов. Два жанра: классические очереди (например, очереди RabbitMQ: подтверждённое сообщение удаляется; у RabbitMQ есть и отдельный журнальный режим Streams) и журналы событий (Kafka: сообщения хранятся упорядоченным журналом, потребители независимо движутся по нему каждый со своей позицией — доступную по политике удержания историю можно перечитать заново). Отрезвляющее замечание о семантике доставки: «ровно один раз» как общее свойство ненадёжной доставки недостижимо; реальные сквозные решения используют повторы и идемпотентность или дедупликацию в явно очерченной транзакционной границе.
Соберём упомянутое в один абзац-путеводитель. Координация и метаданные: etcd, ZooKeeper, Consul (Raft/ZAB). Kubernetes — по сути огромная распределённая система над etcd, превратившая выборы лидеров и желаемое-состояние в бытовую практику. Журналы событий: Kafka и совместимые (Redpanda). Кворумные NoSQL: Cassandra, ScyllaDB. Распределённый SQL: Spanner, CockroachDB, TiDB, YDB (последняя — открытая российская разработка, что для нашей аудитории существенно). Объектные хранилища требуют чтения конкретного контракта: Amazon S3 сегодня даёт сильную согласованность операций с объектами и списков, тогда как некоторые S3-совместимые реализации и вспомогательные конфигурационные операции могут иметь более слабые гарантии. Практически каждая из этих систем — воплощение двух-трёх разделов этой статьи, и в курсе мы будем разбирать их именно так: как теоремы, дожившие до продакшена.
Финальный раздел — противоядие от моды. Каждый механизм этой статьи — консенсус, кворумы, саги — это цена, которую платят за необходимость, а не признак зрелой архитектуры. Один сервер 2026 года — это сотни ядер, терабайты памяти и NVMe-массивы: монолит с грамотной вертикалью и простой репликацией «лидер + реплика» покрывает потребности подавляющего большинства систем, оставаясь на порядок проще в отладке, эксплуатации и рассуждении о корректности. Микросервисы решают организационную проблему (независимые команды и релизы) ценой превращения вызова функции в сетевой RPC со всеми заблуждениями Дойча в комплекте. Честное инженерное правило: распределяйтесь от необходимости, а не от архитектурной моды; каждая сетевая граница в системе должна уметь назвать причину своего существования.
Полвека распределённых систем дали удивительно устойчивый корпус знаний: часы Лэмпорта (1978), FLP (1985), Paxos (1989) актуальны сегодня буквально — в отличие от почти любой другой области ИТ, здесь фундамент не устаревает, потому что вырастает из физических ограничений, частичных и нередко коррелированных отказов, а не из технологической конъюнктуры. Карта, которую стоит унести из этой статьи: сеть ненадёжна и времени нет → порядок даёт причинность и журналы → копии требуют выбора модели согласованности → строгое согласие требует консенсуса и большинства → масштаб требует шардирования → атомарность поверх шардов дорога, и её либо покупают у новых СУБД, либо разменивают на саги. В курсе, который последует за этим обзором, каждый пункт карты развернётся в главу с разбором алгоритмов и лабораторными на реальных системах.