Бесплатный интерактивный курс · aicoding.space

Десять решений: пак skills-book-ddia в работе

Практический курс по паку @dzhechkov/skills-book-ddia для инженера, который проектирует систему с большими данными и хочет, чтобы AI-агент подсказывал не пересказ книги, а критерий выбора в конкретной точке решения. Вместе с Глебом ты поставишь пак, разберёшь, как навык включается от обычного описания задачи, поймёшь, зачем у каждого навыка написана граница «чем он НЕ является», и пройдёшь все десять моментов решения — от нефункциональных осей до производных данных — на одном сквозном примере сервиса бронирования билетов.

Содержание курса

1. Зачем нужен пак skills-book-ddia

Что это за пак одной фразой — и почему это не пересказ книги.

Ключевая мысль: десять навыков момента решения

Глеб ведёт сервис бронирования билетов на концерты и на этой неделе впервые собирает архитектуру данных с нуля. Он попросил агента: «расскажи про репликацию». Агент честно пересказал главу — три абзаца определений, ни одного слова про его случай. Пересказ книги никому не помог.

Пак @dzhechkov/skills-book-ddia устроен наоборот: это не десять конспектов, а десять навыков момента решения. Момент решения — это точка, где ты обязан выбрать одно из нескольких и дальше жить с последствиями: «один ведущий узел или несколько?», «какой уровень изоляции транзакций?», «пакетная обработка или потоковая?». Навык включается именно в такой точке и выдаёт критерии выбора, таблицы компромиссов, антипаттерны и ссылки на связанные решения — а не определения.

Под паком лежат 116 проверенных единиц знания (Knowledge Unit — минимальный кусок методики с привязкой к странице первоисточника), распределённые по десяти навыкам. Первоисточник — «Высоконагруженные приложения» Мартина Клеппманна; пак — это оригинальная переформулировка методики, а не текст книги.

Честная метка на паке: trust_tier: 0 в момент дистилляции — «извлечено машиной, человеком не вычитано». Практический смысл метки простой: решение, которое дорого откатывать, сверяй с первоисточником, а не принимай с голоса агента.

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

Где посмотреть сам пак: страница пакета на npm и исходники в репозитории.

2. Установка и активация: описал задачу — навык включился

Как поставить пак и почему имена навыков не надо запоминать.

Ключевая мысль: активация навыка обычным описанием задачи

Первым делом Глеб попытался выучить имена всех десяти навыков. Через полчаса он помнил три и путал два. Это лишняя работа: имена навыков нужны установщику, а не тебе в разговоре.

Ставится пак командой dz init с указанием целевой платформы и выбранных навыков:

dz init --target claude-code --select ddia-replication-topology-choice

Здесь --target claude-code говорит, под какую платформу разложить файлы, а --select берёт ровно один навык по имени. Хочешь весь пак — перечисляешь все десять идентификаторов; хочешь одно решение — берёшь один.

А дальше начинается то, ради чего всё и делалось. Активация навыка происходит от обычного описания задачи: ты пишешь агенту «проектирую репликацию — один ведущий или несколько?», и ddia-replication-topology-choice включается сам, потому что в его описании перечислены такие формулировки на русском и английском. Никаких идентификаторов в разговоре.

В README пака описаны пять типовых ситуаций, где это окупается:

  1. проектируешь систему с нуля — агент проходит связанные решения по порядку: модель → движок хранения → репликация → секционирование;
  2. упёрся в одну развилку прямо сейчас — включается ровно один навык;
  3. ревизия чужой архитектуры — агент называет режимы отказа, о которых предупреждает первоисточник;
  4. подбираешь согласованность и изоляцию под требование корректности;
  5. оцениваешь миграцию хранилища — движок плюс совместимость схемы.

Есть и второй способ добраться до знания — прямой запрос к оцифрованному корпусу: dz recall --books --book vysokonagruzhennye-prilozheniya "<запрос>". Это глубокий поиск по единицам знания, когда навыка мало и нужна первичная формулировка.

Компромисс: установка через монорепозиторий и точечный --select дают чистый набор без лишнего, но требуют один раз узнать идентификаторы; зато после установки разговор с агентом идёт обычными словами.

Ссылки: пакет на npm · исходники и README в репозитории.

3. Граница навыка: чем он НЕ является

Почему у каждого навыка написано, куда идти вместо него — и как это проверено.

Ключевая мысль: граница навыка: чем он НЕ является

Глеб спросил про «разложить данные по узлам» — и получил ответ про кворумы. Формально агент не соврал, но ответил не на тот вопрос: разложить РАЗНЫЕ данные по узлам — это секционирование, а кворумы живут в репликации, где по узлам разложены КОПИИ ОДНИХ И ТЕХ ЖЕ данных.

Именно поэтому в описании каждого навыка стоит граница — явная фраза о том, чем навык НЕ является, и куда идти вместо него. У навыка про репликацию написано: «копирование тех же данных ТОЛЬКО — не разбиение разных данных по шардам (→ ddia-partitioning-strategy)». У навыка про изоляцию транзакций: «одна база, одна нода ТОЛЬКО — не межузловая линеаризуемость (→ ddia-distributed-consistency-consensus)».

Граница — это не украшение описания, а рабочий механизм маршрутизации: она отталкивает чужие запросы к соседу.

И вот неожиданная часть. Когда границы проверяли автоматическим прокси-гейтом на косинусной близости векторов, результат стал ХУЖЕ: 0 из 10, увод чужих запросов доходил до 90%. Причина — вектора слепы к отрицанию: фраза «не про секционирование» тянет к себе запросы про секционирование, потому что слово в тексте есть. Гейт заменили на судью-модель — тот же механизм, каким маршрутизирует настоящий агент, — и результат стал 10 из 10: активация 100% у девяти навыков и 90% у десятого, увод к соседу не выше 10%.

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

Компромисс: явные границы удлиняют описание каждого навыка и требуют держать карту соседей в актуальном виде, зато десять близких тем не сваливаются в один навык и агент попадает в нужный с первого раза.

4. Три оси до первой строчки кода

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

Ключевая мысль: надёжность, масштабируемость, сопровождаемость — сделай каждую ось измеримой

В требованиях к сервису было написано: «система должна быть надёжной и масштабируемой». Глеб перечитал фразу трижды и не смог вывести из неё ни одного проектного решения — и это не его беда, из такой формулировки решение и не выводится. Навык ddia-reliability-scalability-foundations включается именно здесь и требует сделать каждую ось измеримой до того, как выбран хоть один инструмент.

Три оси и способ их заземлить:

  1. Надёжность — работает верно в неблагоприятных условиях. Заземляется вопросом «КАКИЕ типы отказов мы терпим»: аппаратные, программные, человеческие. «Терпим все отказы» — невыполнимое обещание.
  2. Масштабируемость — есть разумные стратегии на случай роста. Заземляется вопросом «КАКОЙ параметр нагрузки растёт»: чтения, записи, объём данных, сложность, веер рассылки.
  3. Сопровождаемость — люди годами остаются продуктивными. Заземляется через управляемость, простоту и способность к развитию.

Дальше — различение, на котором строится всё остальное. Сбой (fault) — это когда один компонент отклонился от спецификации; отказ (failure) — когда система целиком перестала обслуживать пользователей. Отказоустойчивость — это ровно работа по недопущению эскалации первого во второе.

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

Цифры, которые полезно помнить: время наработки диска на отказ — 10–50 лет, то есть на 10 000 дисков приходится примерно один отказ в день. Человеческая ошибка конфигурации — ведущая причина крупных простоев, аппаратные сбои виноваты только в 10–25% случаев.

И главное про производительность: задавай её распределением перцентилей, а не средним. Среднее прячет, скольким пользователям было плохо. Отчитывайся p50, p95, p99, p999, меряй со стороны клиента (иначе не увидишь очередь) и никогда не усредняй перцентили — их агрегируют сложением гистограмм.

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

5. Модель данных: по форме связей, а не по моде

Документная, реляционная или графовая — решает форма связей в твоих данных.

Ключевая мысль: модель выбирается по форме связей, а не по моде

Глеб начал с документной базы, потому что «так делают все». Через месяц ему понадобилось показать «на какие концерты ходили друзья моих друзей» — и он написал соединения руками в коде приложения. Медленно, много кода, легко ошибиться.

Правило навыка ddia-data-model-selection: модель выбирается по ДОМИНИРУЮЩЕЙ форме связей в данных, а не по популярности.

  • Документная — когда данные это самодостаточное дерево «один ко многим», которое читают целиком, а ссылок между документами мало: заказ, анкета, событие. Выигрыш — локальность: одно чтение вместо соединения нескольких таблиц.
  • Реляционная — как только появляются связи «многие ко многим». Без соединений на стороне базы ты либо денормализуешь (и берёшь на себя согласованность копий), либо эмулируешь соединение в приложении — медленнее и сложнее.
  • Графовая — когда связи плотные и произвольные, «всё может быть связано со всем». Обход переменной длины в графовом языке занимает 4 строки против 29 строк рекурсивного SQL — сам этот разрыв и есть сигнал сменить движок.

Отдельное решение внутри выбранной модели: ссылка или встроенная строка. Значение из стандартизованного списка (город, площадка, жанр) хранится идентификатором — одна каноническая копия даёт переименование в одном месте, единое написание и нормальный поиск. Свободный текст, который ввёл пользователь, хранится строкой.

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

И ещё одна честная мысль из критериев: соответствие модели, верное в первой версии, со временем портится. Плотность связей растёт по мере появления функций, и удачно легшая документная модель через год начинает требовать соединений.

Компромисс: документная модель даёт локальность и простую запись целого дерева, но платит отсутствием соединений; реляционная берёт соединения на себя, но требует разложить объект по таблицам; графовая гениальна на плотных связях и избыточна на всём остальном.

6. Движок хранения: чем платишь за скорость записи

LSM или B-дерево, строки или колонки — и почему сравнение всегда упирается в твою нагрузку.

Ключевая мысль: LSM быстрее на записи, B-дерево быстрее на чтении

Перед стартом продаж на большой тур Глеб ждёт всплеск записей и спрашивает: какой движок хранения ставить. Навык ddia-storage-engine-tradeoffs отвечает не названием продукта, а профилем нагрузки.

Опорное правило: LSM-дерево обычно быстрее на записи, B-дерево обычно быстрее на чтении. Расшифруем оба слова, потому что дальше всё держится на них.

LSM-дерево копит записи в отсортированной структуре в памяти и сбрасывает её на диск отсортированными сегментами, которые фоново сливает и уплотняет. Записи ложатся последовательно — отсюда скорость и низкое усиление записи. Плата: чтение может обойти несколько сегментов, а поиск ОТСУТСТВУЮЩЕГО ключа без фильтра Блума медленный; фоновое уплотнение отнимает полосу диска и даёт всплески на верхних перцентилях.

B-дерево хранит данные страницами фиксированного размера (обычно 4 КБ) и обновляет их на месте, записывая сначала журнал упреждающей записи. Отсюда предсказуемая задержка и удобные диапазонные блокировки для транзакций. Плата: каждый элемент пишется минимум дважды — в журнал и в страницу, — а ради нескольких байт переписывается целая страница.

Дальше — отдельный вопрос: нужен ли индекс вообще. Индекс — производная структура: он ускоряет чтения и замедляет каждую запись. Поэтому индексы подбирают по реальным запросам приложения, а не навешивают на все колонки.

И большая развилка: транзакционная нагрузка и аналитическая не живут в одной базе. У транзакционной узкое место — время позиционирования на диске, у аналитической — пропускная способность диска. Тяжёлые аналитические сканирования бьют по задержке транзакций, поэтому аналитику с конца 1980-х выносят в отдельное хранилище — а там уже колоночное хранение с битовыми картами и сжатием повторов.

И честная оговорка самого первоисточника, которую навык не прячет: сравнения LSM и B-дерева неубедительны и сильно зависят от нагрузки. Правило большого пальца — это гипотеза, а решение принимается измерением на твоей нагрузке.

Компромисс: LSM покупает пропускную способность записи ценой непредсказуемого хвоста чтений и обязанности следить за уплотнением; B-дерево покупает предсказуемость ценой усиления записи.

7. Кодирование и эволюция схемы: две совместимости

Обратная и прямая совместимость — и почему без обеих катящееся обновление ломается.

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

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

Навык ddia-encoding-and-schema-evolution требует различать две совместимости, и это единственная пара слов, которую надо запомнить намертво:

  • обратная совместимость — НОВЫЙ код читает СТАРЫЕ данные;
  • прямая совместимость — СТАРЫЙ код читает НОВЫЕ данные.

Катящееся обновление (rolling upgrade — выкатка по частям, когда старая и новая версии какое-то время работают одновременно) требует обеих сразу, потому что оба направления чтения происходят одновременно. Именно поэтому обязательное поле без значения по умолчанию ломает обратную совместимость: новый код не может прочитать старую запись, где поля нет.

Какой формат брать:

  • внутри системы — двоичный со схемой (Protobuf, Thrift, Avro): компактно, совместимость проверяется, схема служит документацией;
  • между организациями — текстовый (JSON, XML, CSV): читается человеком и понимается всеми, но целые числа больше 2⁵³ теряют точность, а двоичные данные в Base64 распухают примерно на треть;
  • встроенная сериализация языка (pickle, Java Serializable) — только для по-настоящему временных данных: привязка к языку, слабое версионирование и выполнение произвольного кода при разборе.

Правила безопасных изменений: у Protobuf и Thrift поля опознаются числовыми тегами, поэтому переименование бесплатно, а менять или переиспользовать тег нельзя никогда — это обесценивает все уже записанные данные. У Avro полей-тегов нет, схемы читателя и писателя сопоставляются по именам, а добавлять и удалять поле можно только при наличии значения по умолчанию.

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

Компромисс: двоичный формат со схемой даёт компактность и проверяемую совместимость, но требует канала распространения схем и не читается глазами; текстовый читается сразу, но не даёт ни компактности, ни гарантий.

Если навыка мало и нужна первичная формулировка, есть прямой запрос к оцифрованному корпусу — его ты и соберёшь в упражнении.

8. Репликация: копии тех же данных

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

Ключевая мысль: полусинхронная репликация — баланс между потерей записи и остановкой

Глеб включил полностью синхронную репликацию на все копии — «чтобы наверняка ничего не потерять». Через день одна реплика подвисла на обслуживании, и встали ВСЕ записи в сервисе. Вот она, неожиданность этой секции: настройка «максимальной надёжности» оказалась настройкой «максимальной хрупкости».

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

Отсюда рабочая середина, которую и рекомендует навык ddia-replication-topology-choice: полусинхронная репликация — ровно ОДИН ведомый синхронный, остальные асинхронные. Свежая копия гарантированно есть минимум на двух узлах, а если синхронный отстал, его роль перехватывает асинхронный. Это и есть баланс между потерей подтверждённой записи и остановкой всей записи.

Базовая топология выбирается так:

  1. Один ведущий — умолчание. Просто, нет конфликтов записи. Плата: пока ведущий недоступен, записи нет, нужен переход на нового ведущего.
  2. Несколько ведущих — только если есть конкретное требование: несколько центров обработки данных, работа клиента без сети, совместное редактирование. Внутри одного центра это добавляет конфликты записи без единой выгоды.
  3. Без ведущего — когда нужна высокая доступность записи и терпимы устаревшие чтения. Плата: гарантии слабее, нужны восстановление при чтении и фоновое сглаживание расхождений.

Про кворумы важно понимать формулу и её границу. Условие w + r > n означает, что множества узлов записи и чтения пересекаются хотя бы в одном узле, поэтому чтение обычно видит последнее значение. Но «обычно» здесь не случайное слово: при нестрогом кворуме, конкурентных или частичных записях, восстановлении отставшей реплики устаревшее чтение всё равно возможно.

И отдельная мина — разрешение конфликтов «выигрывает последний по времени». Оно молча выбрасывает данные и допустимо только там, где ключ пишется один раз. На изменяемых ключах бери номера версий с ручным слиянием, бесконфликтные типы данных или векторы версий.

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

Компромисс: синхронность покупает сохранность подтверждённых записей ценой доступности; асинхронность — наоборот; полусинхронная схема берёт середину, но требует следить, кто именно сейчас синхронный ведомый.

9. Секционирование: разные данные по узлам

Диапазон или хеш, и почему один горячий ключ хешированием не лечится.

Ключевая мысль: горячий ключ не лечится хешированием

Старт продаж на стадионный концерт. Один узел кластера у Глеба раскалён, остальные девять скучают. Это горячая точка — секция с непропорциональной нагрузкой; лежащий под ней перекос называется скосом.

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

  • Диапазонное секционирование — непрерывные отсортированные диапазоны ключей. Берут, когда нужны диапазонные сканирования. Плата: последовательный ключ (например, время впереди) отправляет все сегодняшние записи в одну секцию.
  • Хеш-секционирование — ключ хешируется, секциям раздаются диапазоны хешей. Берут ради равномерной нагрузки. Плата: порядок ключей разрушен, диапазонные запросы разлетаются по всем секциям.
  • Составной ключ — первая часть хешируется, остальные остаются отсортированными. Даёт и равномерность по первой части, и сканирование внутри неё — но только когда первая часть зафиксирована конкретным значением.

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

А теперь ключевой момент секции, который Глеб узнал дорогой ценой. Один горячий ключ хешированием не лечится: одинаковые ключи хешируются одинаково и всё равно попадают в одну секцию. Ручное лекарство — приписать к горячему ключу небольшое случайное число: двузначный суффикс размазывает один ключ по ста ключам и по многим секциям. Плата честная: приложение обязано помнить, какие ключи разбиты, и собирать их обратно при чтении, — поэтому приём применяют к нескольким заранее известным горячим ключам, а не ко всем подряд.

Про ребалансировку достаточно двух правил. Первое: hash(ключ) mod N для размещения по узлам — антипаттерн, потому что смена N переносит почти все ключи. Второе: полностью автоматическая ребалансировка вместе с автоматическим обнаружением отказов опасна — перегруженный медленный узел объявляется мёртвым, ребалансировка сваливает на него и на сеть ещё больше работы, и отказ становится каскадным. Держи человека в контуре: план генерируется автоматически, применяет его администратор.

Компромисс: хеш покупает равномерность ценой диапазонных запросов; диапазон покупает сканирования ценой риска горячей точки; составной ключ берёт середину ценой обязательного условия «первая часть зафиксирована».

10. Уровень изоляции: слабейший, который держит инвариант

От аномалии к уровню: как выбрать изоляцию, не хватаясь сразу за сериализуемость.

Ключевая мысль: снимочная изоляция не ловит перекос записи

Два покупателя одновременно берут последнее место в ряду. Код честно проверяет: «место свободно?» — да, у обоих, — и оба пишут бронь на РАЗНЫЕ строки таблицы. Ни одна транзакция не перезаписала другую, а место продано дважды. Глеб смотрит в журнал и не находит ни одной ошибки: с точки зрения базы всё прошло идеально.

Это перекос записи (write skew): прочитал, принял решение, записал в другое место — а к моменту фиксации предпосылка решения уже неверна. И вот главный факт этой секции: снимочная изоляция его НЕ ловит. Ни «повторяемое чтение» в PostgreSQL и MySQL, ни «сериализуемый» уровень Oracle, ни снимочная изоляция SQL Server. Перекос записи останавливает только настоящая сериализуемость.

Навык ddia-transaction-isolation-choice предлагает обратный ход мысли, и он же — приём саморефлексии: не «какой уровень взять», а «какая аномалия убьёт мой инвариант». Инвариант Глеба — «одно место продано один раз». От аномалии к уровню, а не наоборот:

  1. Грязное чтение (видишь чужую незафиксированную запись) → уровень «чтение зафиксированного».
  2. Перекос чтения, он же неповторяемое чтение (разные части базы видны на разные моменты времени) → снимочная изоляция на многоверсионном управлении.
  3. Потерянное обновление (два цикла «прочитал-изменил-записал», один затирает другой) → атомарная операция записи, явная блокировка на чтении или автоматическое обнаружение.
  4. Перекос записи и фантомы, меняющие результат чужого поиска, → только сериализуемость.

Перед всем этим стоит нулевой шаг, который часто пропускают: нужны ли многообъектные транзакции вообще. Атомарный инкремент или сравнение-с-обменом защищают ОДИН ключ и транзакцией не являются. Многообъектная транзакция нужна, когда есть внешние ключи, денормализованные копии, которые обязаны обновляться вместе, или вторичные индексы.

Если сериализуемость всё-таки нужна, реализацию выбирают по форме нагрузки: последовательное выполнение — когда рабочий набор влезает в память и запись укладывается в одно ядро; двухфазная блокировка — проверенный общий механизм ценой низкой пропускной способности и взаимных блокировок; сериализуемая снимочная изоляция — когда конкуренция низкая, а транзакции короткие, иначе откаты съедят выигрыш.

И отдельная честная поправка: «согласованность» из аббревиатуры ACID — свойство приложения, а не базы. База даёт средства, инвариант формулируешь ты.

Компромисс: каждый шаг вверх по уровню изоляции покупает запрет ещё одного класса аномалий ценой пропускной способности и откатов; поэтому берут слабейший уровень, который держит именно твой инвариант.

11. Согласованность и консенсус: сколько координации покупать

Линеаризуемость дорога всегда — и нужна реже, чем кажется.

Ключевая мысль: бери слабейшую модель согласованности, которая ещё держит требование

Глеб пришёл с формулировкой «нам нужна сильная согласованность везде». Навык ddia-distributed-consistency-consensus начинает с обратного вопроса: какая модель здесь минимально достаточна? Правило звучит так: бери слабейшую модель согласованности, которая ещё держит требование — слабее означает быстрее и доступнее при разрыве сети.

Три уровня, снизу вверх:

  1. Итоговая — реплики когда-нибудь сойдутся. Самая дешёвая, всегда доступна. Годится там, где устаревание безвредно: счётчики просмотров, лента, кеш обнаружения сервисов.
  2. Причинная — причина всегда видна раньше следствия («вопрос раньше ответа»), а конкурентные операции не упорядочены. Её не тормозит сетевая задержка и она остаётся доступной при разрыве сети. Очень часто именно она и нужна там, где команда просит линеаризуемость.
  3. Линеаризуемая — иллюзия единственной копии со свежестью: после завершения записи её видят все чтения. И вот факт, который меняет отношение к ней: линеаризуемость медленна ВСЕГДА, а не только во время сбоев — время отклика не может быть меньше величины, пропорциональной неопределённости сетевой задержки.

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

Отдельно — то, о чём чаще всего ошибаются. Условие w + r > n НЕ даёт линеаризуемости. Строгий кворум допускает ситуацию, когда более поздний читатель видит более старое значение, чем предыдущий. Кворум — это про пересечение множеств, а не про свежесть.

Если задача всё же сводится к консенсусу — не пиши свой алгоритм. Линеаризуемое сравнение-с-обменом, атомарная фиксация распределённой транзакции, рассылка в общем порядке, блокировки и аренды, ограничения уникальности — всё это эквивалентно консенсусу: решив одно, решаешь все. Именно поэтому база с одним ведущим даёт их дёшево — ведущий и есть единственный принимающий решения; консенсус нужен лишь в момент его смены.

Практические границы: кворум большинства требует минимум 3 узлов, чтобы пережить отказ одного, и 5, чтобы пережить два. Распределённой блокировке нужен ограждающий маркер — монотонно растущее число, которое проверяет САМ ресурс, а не клиент: приостановленный на минуту клиент «оживает» с просроченной арендой и портит данные.

Компромисс: координация покупает свежесть и запрет опасных гонок ценой скорости и доступности при разрыве сети; отказ от координации покупает скорость и доступность ценой того, что часть противоречий придётся разгребать потом.

12. Пакетная и потоковая обработка: чьё время считать

Ограниченный вход против бесконечного — и почему брать время обработки опасно.

Ключевая мысль: время события, а не время обработки

Глеб построил метрику «продаж в минуту» и однажды утром увидел ровный всплеск на графике — при том, что продаж в тот момент почти не было. Причина оказалась не в продажах: ночью потребитель перезапустился, разобрал накопившийся хвост разом, и события легли на график по времени ОБРАБОТКИ, а не по времени, когда они произошли.

Отсюда правило, ради которого стоит помнить эту секцию: считай по времени события, а не по времени обработки. Время события — метка внутри самого события; время обработки — момент, когда до него дошли руки. Если часам устройства доверять нельзя (клиент мог работать без сети), пишут три метки — время события по часам устройства, время отправки по часам устройства и время приёма по часам сервера — и восстанавливают истинное время события по разнице между двумя последними.

Корневая развилка секции проще, чем кажется. Пакетная обработка предполагает ОГРАНИЧЕННЫЙ вход: размер известен и конечен, поэтому задание понимает, что чтение закончилось. Для сортировки это принципиально: последняя прочитанная запись может нести наименьший ключ и обязана выйти первой — значит, до конца чтения выдавать нельзя ничего. Реальные данные обычно бесконечны, поэтому их нарезают окнами: суточное задание отражает изменение входа через сутки. Хочешь меньше задержки — либо запускай чаще, либо откажись от фиксированных окон и обрабатывай события по мере прихода. Второе и есть потоковая обработка.

Выбор брокера сообщений тоже сводится к паре вопросов. Брокер на журнале (Kafka и родственники) раздаёт потребителю целые секции журнала со смещением, сохраняет порядок внутри секции и позволяет перечитать историю — но число потребителей ограничено числом секций, и медленное сообщение блокирует очередь за собой. Классическая очередь отдаёт сообщения по одному и удаляет после подтверждения: отлично, когда обработка одного сообщения дорогая, а порядок неважен, — но перечитать историю уже нельзя.

Окна тоже стоит различать по форме: падающее (фиксированная длина, событие ровно в одном окне), прыгающее (фиксированная длина с перекрытием, для сглаживания), скользящее (окно едет непрерывно: любые два события, попавшие в один интервал, оказываются вместе) и сессионное (без фиксированной длины, закрывается по бездействию пользователя).

И про «ровно один раз»: механизмы вроде контрольных точек дают его ВНУТРИ обработчика, но не для внешних побочных эффектов — запись в базу, письмо, сообщение в брокер повторятся при перезапуске. Лечится атомарной фиксацией всех эффектов вместе либо идемпотентностью с внешне хранимым смещением.

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

13. Производные данные: как держать пять хранилищ в согласии

Двойная запись, журнал изменений и право извиниться вместо координации.

Ключевая мысль: двойная запись — антипаттерн, журнал изменений — лекарство

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

Почему она ломается двумя разными способами:

  1. Гонка. Две конкурентные записи одного значения ложатся в базу и в индекс в РАЗНОМ итоговом порядке. Ошибки нет, оба хранилища «успешны», данные молча разъехались.
  2. Частичный отказ. Одна запись прошла, вторая нет. Чтобы сделать их атомарными, нужна распределённая транзакция — дорого и хрупко.

Лекарство: один источник истины плюс журнал изменений. Выбираешь систему-источник (обычно основную базу), извлекаешь из неё поток изменений — захват изменений данных, CDC, — и все остальные хранилища становятся её подписчиками-последователями. Порядок задаёт упорядоченный журнал, а не удача планировщика; отказ потребителя остаётся локальным: журнал буферизует, отставший догоняет и никого не блокирует.

Теперь два различения, ради которых стоит дочитать секцию.

Своевременность против целостности. Своевременность — пользователь видит текущее состояние; её нарушение самоизлечивается ожиданием. Целостность — нет потерянных, противоречивых или ложных данных; её нарушение постоянно и лечится только явной проверкой и починкой. В большинстве приложений целостность важнее: транзакция, невидимая сутки, — терпимо; неверный баланс — катастрофа. Асинхронная обработка не даёт своевременности по умолчанию, но целостность сохраняет.

Сквозной довод. Подавление дублей корректно только при знании на концах: перезапрос пользователем формы, отвалившейся по таймауту, не остановит ни повтор в транспортном протоколе, ни «ровно один раз» в потоковом обработчике. Работает другое — идентификатор операции, сгенерированный НА КЛИЕНТЕ и донесённый до базы, где на него стоит ограничение уникальности. Ограничение уникальности база держит даже на слабом уровне изоляции, в отличие от проверки-и-вставки в коде приложения.

И последнее, самое неожиданное для инженера: иногда правильный ответ — извиниться, а не координировать. Два покупателя взяли одно место, склад продал больше, чем есть, авиакомпания продала больше мест, чем в самолёте. Если цена извинения — возврат, апгрейд, скидка — приемлема бизнесу, то проверять ограничение синхронно перед каждой записью слишком строго. Координация уменьшает извинения из-за противоречий и добавляет извинения из-за недоступности; цель — не ноль координации, а её оптимум.

Компромисс: журнал изменений даёт слабую связанность, независимое развитие систем и работу между центрами обработки данных, но не даёт чтения своих записей по умолчанию; распределённая транзакция даёт свежесть, но усиливает отказы и почти всегда живёт в пределах одного центра.

Твоя очередь: в упражнении ты сам ведёшь разбор — никакого единственно верного пути там нет, есть решения и их цена.

Частые вопросы

Мне надо прочитать книгу, прежде чем ставить пак?

Нет. Пак самодостаточен для рабочих решений: он даёт критерии выбора, таблицы компромиссов и антипаттерны. Но метка trust_tier 0 честно означает «извлечено машиной, человеком не вычитано», поэтому решение, которое дорого откатывать, сверяй с первоисточником. Читать книгу стоит ради глубины рассуждений — пак её не заменяет.

Как поставить только один навык, а не все десять?

Командой dz init --target claude-code --select <идентификатор>. Например, dz init --target claude-code --select ddia-replication-topology-choice. Хочешь весь пак — перечисли все десять идентификаторов из таблицы в README пакета.

Надо ли называть навык по имени, чтобы он включился?

Нет. Опиши задачу обычными словами — «проектирую репликацию, один ведущий или несколько?» — и агент сам подберёт навык. Формулировки-триггеры на русском и английском перечислены в описании каждого навыка; идентификаторы нужны только установщику.

Почему у каждого навыка написано, чем он НЕ является?

Это граница, которая отталкивает чужие запросы к правильному соседу: репликация — про копии тех же данных, секционирование — про разные данные по узлам. Границы проверены оракулом-судьёй на каталоге из 20 описаний: результат 10 из 10, активация 100% у девяти навыков и 90% у десятого, увод к соседу не выше 10%.

Я не нашёл в навыке нужной детали. Куда копать глубже?

Каждый навык везёт свои исходные единицы знания в файле references/knowledge-units.md рядом с собой — это поиск по паку, не выходя из него. Полный корпус запрашивается командой dz recall --books --book vysokonagruzhennye-prilozheniya "<запрос>".

Пак цитирует книгу?

Нет. Это оригинальная переформулировка методики с собственными критериями и таблицами компромиссов; текст книги в нём не воспроизводится. Проверка на дословные совпадения пройдена при оцифровке, а привязки к главам и страницам оставлены, чтобы каждое утверждение можно было сверить с первоисточником.

С чего начать, если проектирую систему с нуля?

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