2026 г.

Курс «Распределённые системы». Глава 12. Проектирование: собираем всё вместе

Цели главы. Финальная глава устроена иначе: это семинар. Теория закончилась в главе 11; здесь мы трижды проектируем систему с нуля — счётчик, платежи, чат — проговаривая каждый выбор языком курса и явно называя его цену. Затем — каталог антипаттернов, методические указания преподавателю (глава рассчитана на семинарское занятие с обсуждением) и заключение курса. Цель — переход от «знаю алгоритмы» к «умею выбирать»: распределённые системы проектируются не подбором модных компонентов, а честными ответами на десяток вопросов.

12.1. Метод: вопросы раньше архитектуры

Перед любой схемой — анкета; каждая строка отсылает к главе, где живёт ответ:

  1. Объём и рост: данные и поток запросов сегодня и через два года; влезает ли на один узел с репликой? (Если да — глава 5 закрывает задачу; см. «когда не распределяться» в обзоре.)
  2. Профиль нагрузки: соотношение чтений и записей; равномерность по ключам; горячие ключи (глава 8).
  3. Инварианты: что обязано быть верным всегда (уникальность, неотрицательность, «ровно один раз»). Глобальный инвариант обычно требует координации, но локализованный ключом или замкнутый относительно слияния можно обеспечить дешевле; что терпит расхождение — тому итоговая согласованность (глава 4) и CRDT (глава 5).
  4. Поведение при разделении по классам операций: отказывать или расходиться? (Глава 3 — этот выбор не сделать «потом».)
  5. Потери: что случится при отказе лидера с асинхронным хвостом? Допустимо ли? (Глава 5 — sync/async это обещание.)
  6. Семантика повторов: где нужны ключи идемпотентности (статья о надёжности, главы 9–10).
  7. Срок жизни и аудит: нужна ли история изменений (журнал — глава 10)?
  8. Эксплуатация: кто и как будет это чинить в три часа ночи — компонентов должно быть столько, сколько команда способна содержать.

12.2. Разбор первый: счётчик лайков

Задача: лайки к постам, пики до 100 тыс. записей/с на горячие посты, чтения на каждом просмотре, «не более одного лайка от пользователя».

Анкета: инвариантов глобальных нет — кроме «не более одного», но он локализуем: все операции над одной парой (пользователь, пост) должны попадать в одну логическую партицию. Значение счётчика терпит расхождение на секунды — итоговая согласованность. Горячие посты — определяющий фактор.

Решение: факт лайка хранится с ключом (пост, пользователь), а повтор с тем же ключом идемпотентен. Уникальное ограничение обеспечивает это только при единственном авторитетном шарде или согласованной группе реплик; принимать независимые записи в обеих половинах разделения и одновременно полагаться лишь на локальный UNIQUE нельзя. Для добавлений без удаления альтернативой служит сливаемое множество идентификаторов пользователей. Счётчик — производная величина: подсоленные ячейки (глава 8) или счёт по множеству, агрегируемый асинхронно и кэшируемый с меткой свежести; события лайков идут в журнал (глава 10) для лент и аналитики. При разделении точный счёт может временно отставать, а запись в недоступный авторитетный шард — получить отказ; выбранное поведение должно быть явным. Чего мы не делаем: линеаризуемого глобального счётчика — кворум на каждый лайк не требуется для точного устранения повторов по паре.

12.3. Разбор второй: платежи

Задача: переводы между счетами, «не уйти в минус», внешний платёжный шлюз, полный аудит; поток умеренный, цена ошибки максимальна.

Анкета — зеркальная первому разбору: инварианты жёсткие и глобальные в пределах счёта, потери недопустимы, при разделении — отказывать (CP: лучше «попробуйте позже», чем двойное списание), повторы неизбежны (клиенты, шлюз) — идемпотентность всюду.

Решение: остаток счёта — линеаризуемая сущность; счёт — единица шардирования (глава 8), внутри шарда — ACID-транзакции поверх синхронной репликации (глава 5) либо распределённый SQL с консенсусом на группу (глава 9). Перевод между собственными шардами по возможности выполняется одной распределённой ACID-транзакцией: для денежного инварианта она предпочтительнее саги. Если границы систем не позволяют общую транзакцию, нужен устойчивый процесс с резервированием, двойной записью в неизменяемый бухгалтерский журнал, идемпотентными шагами, сверкой и ручным разбором неустранимых расхождений; компенсация не должна притворяться настоящей атомарностью. Вызов внешнего шлюза — отдельный шаг с ключом идемпотентности, outbox и обязательной сверкой. Журнал событий помогает аудиту, но не делает его бесплатным: нужны контроль доступа, срок хранения, проверка полноты и отдельные бухгалтерские проводки. Чего мы не делаем: 2PC с внешним шлюзом и микросервисную нарезку денежного ядра без необходимости — перевод внутри одной управляемой СУБД проще доказуемо корректен.

12.4. Разбор третий: чат

Задача: личные и групповые диалоги, миллионы пользователей, мобильные клиенты с плохой связью, «сообщения не теряются и не путаются».

Анкета: «не путаются» — порядок нужен в пределах диалога, глобальный не нужен ни для чего; «не теряются» — долговечность подтверждённого; клиент офлайн — норма (часть II статьи о надёжности); чтения — «хвост диалога», классика журнала.

Решение: диалог = ключ партиционирования = единица порядка — журнал сообщений, партиционированный по диалогу (главы 8, 10; Kafka-стиль или своя таблица с монотонным номером в диалоге). Для Kafka одного acks=all недостаточно: нужны подходящие min.insync.replicas, фактор репликации, размещение по зонам и запрет выбора лидера из отставшей реплики. Клиент ведёт локальную очередь исходящих с ключами идемпотентности и синхронизируется по номерам «всё после N». Статусы следует различать точно: «принято сервером», «доставлено на устройство» и «прочитано получателем» — разные подтверждения, а не синонимы долговечности записи. Причинность между диалогами (переслал из одного в другой) либо поддерживается метаданными главы 2, либо явно не гарантируется. Fan-out групповых чатов выполняют потребители журнала. Чего мы не делаем: глобального порядка всех сообщений и синхронной доставки офлайн-получателям.

12.5. Антипаттерны

  • Распределённый монолит: сервисы нарезаны, но каждый запрос синхронно проходит все — доступность перемножилась (глава 1, упражнение 2), задержки сложились, деплой всё равно совместный. Худшее из двух миров.
  • 2PC между сервисами/организациями — глава 9; саги и outbox существуют именно поэтому.
  • Глобальная блокировка «на всякий случай» — сериализация всей системы через один замок; и почти всегда без ограждения (глава 6), то есть ещё и некорректно.
  • LWW по умолчанию — молчаливые систематические потери (глава 2); стратегия конфликтов должна быть выбрана, а не унаследована от настроек.
  • «Кафка всё стерпит» — журнал с acks=1 и неидемпотентными потребителями — это конвейер по производству потерь и дублей с высокой пропускной способностью (глава 10).
  • Согласованность «на глазок» — слово consistency в ТЗ без указания модели главы 4; исполнителю достанется линеаризуемость по цене, заказчику — итоговая по факту.
  • Микросервисы как цель — сетевые границы без причины; каждая обязана назвать свою (обзор, раздел 9).

12.6. Преподавателю: как вести семинар

Формат, обкатанный жанром: группа 8–15 человек, 2 занятия по паре. Кейс выдаётся заранее (варианты — ниже); на занятии один студент ведёт разбор у доски по анкете 12.1, группа атакует вопросами «что при разделении?», «что при повторе?», «покажи потерю» — роль немезиды из главы 11, перенесённая в дискуссию. Преподаватель вмешивается тремя вопросами: какой инвариант защищаем? какой главой это куплено? что мы сознательно НЕ гарантируем? Оценивается не «правильная архитектура» (их несколько), а явность компромиссов: решение без названной цены — незачёт независимо от модности. Хорошая заключительная точка курса — вернуть группу к сводной таблице границ главы 3 и попросить разметить на ней принятые в кейсе решения.

12.7. Заключение курса

Курс прошёл дугу от «почему это трудно» к «как это строить»: модель и её честные пределы (часть I), согласие и его цена (часть II), масштаб и его механика (часть III), проверка и синтез (часть IV). Если сжать двенадцать глав в три принципа, получится так. Называйте предположения — каждая гарантия куплена моделью отказов, кворумом или тайм-аутом, и авария — это всегда встреча с неназванным предположением. Выбирайте согласованность по данным, а не по моде — спектр от линеаризуемости до CRDT существует затем, чтобы платить только за нужное. Не верьте — проверяйте — гарантии систем проверяемы вашими руками за вечер, и глава 11 оставила вам для этого полный инструментарий. А самое обнадёживающее в предмете мы отмечали ещё в обзоре: его фундамент — Лэмпорт, FLP, кворумы — не стареет; выучив его один раз, вы будете читать документацию систем, которых ещё не существует, как знакомый текст. Дальнейшее чтение: Клеппманн — настольной книгой; первоисточники из списков литературы — по мере столкновения с темами; и лучшая практика — та самая «свободная охота» из лабораторной 5.

Кейсы для семинара

Каждый кейс — по анкете 12.1; в скобках — узлы напряжения, вокруг которых должна закрутиться дискуссия.

  1. Сокращатель ссылок на 10 млрд переходов/мес (уникальность кода — единственный инвариант: где он локализуется? чтения — образцовый кэш; счётчики переходов — первый разбор в миниатюре; разделение — что важнее, редирект или счётчик?).
  2. Складские остатки для маркетплейса (резервирование против oversell — линеаризуемость на товар-склад или сага с компенсацией «извините, товар кончился»? — обсудить обе и цену каждой; горячие товары в распродажу — глава 8).
  3. Лента новостей (fan-out на записи против fan-in на чтении; знаменитости — предельный горячий ключ; согласованность — какие сессионные гарантии реально нужны читателю ленты?).
  4. Совместное редактирование документа (CRDT против центрального арбитра; офлайн-правки; что означает «версия документа» при слиянии — и почему «кто последний, тот и прав» здесь оскорбителен).
  5. Тарификация звонков оператора (события из сотен коммутаторов: журнал, дедупликация, окна опоздавших событий; деньги — сверка вместо веры; идемпотентность повторной тарификации).
  6. Система голосования на 10 млн избирателей (провокационный кейс: «один человек — один голос» — где инвариант локализуем, а где нет; доверие и глава 7 — единственный кейс курса, где византийская модель уместна; и честный финальный вопрос — а должна ли эта система быть распределённой в обсуждавшемся смысле вообще?).

Литература к главе

  1. M. Kleppmann, "Designing Data-Intensive Applications," O'Reilly, 2017 — гл. 12 («Будущее систем данных») — идейный родственник этой главы.
  2. A. Xu, "System Design Interview," т. 1–2, 2020–2022 — сборник разборов в близком жанре (относиться как к задачнику, не как к учебнику).
  3. B. Beyer et al. (ред.), "Site Reliability Engineering," O'Reilly, 2016 — эксплуатационная сторона принятых здесь решений.
  4. Статьи и материалы курса: обзор «Распределённые системы», «Надёжность поверх ненадёжной сети» — практические приложения к разборам.

Предыдущая глава || Содержание курса

404 Not Found

404 Not Found


nginx/1.24.0 (Ubuntu)

Связь с редакцией