Бесплатный интерактивный курс · 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 пака описаны пять типовых ситуаций, где это окупается:
- проектируешь систему с нуля — агент проходит связанные решения по порядку: модель → движок хранения → репликация → секционирование;
- упёрся в одну развилку прямо сейчас — включается ровно один навык;
- ревизия чужой архитектуры — агент называет режимы отказа, о которых предупреждает первоисточник;
- подбираешь согласованность и изоляцию под требование корректности;
- оцениваешь миграцию хранилища — движок плюс совместимость схемы.
Есть и второй способ добраться до знания — прямой запрос к оцифрованному корпусу: 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 включается именно здесь и требует сделать каждую ось измеримой до того, как выбран хоть один инструмент.
Три оси и способ их заземлить:
- Надёжность — работает верно в неблагоприятных условиях. Заземляется вопросом «КАКИЕ типы отказов мы терпим»: аппаратные, программные, человеческие. «Терпим все отказы» — невыполнимое обещание.
- Масштабируемость — есть разумные стратегии на случай роста. Заземляется вопросом «КАКОЙ параметр нагрузки растёт»: чтения, записи, объём данных, сложность, веер рассылки.
- Сопровождаемость — люди годами остаются продуктивными. Заземляется через управляемость, простоту и способность к развитию.
Дальше — различение, на котором строится всё остальное. Сбой (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: полусинхронная репликация — ровно ОДИН ведомый синхронный, остальные асинхронные. Свежая копия гарантированно есть минимум на двух узлах, а если синхронный отстал, его роль перехватывает асинхронный. Это и есть баланс между потерей подтверждённой записи и остановкой всей записи.
Базовая топология выбирается так:
- Один ведущий — умолчание. Просто, нет конфликтов записи. Плата: пока ведущий недоступен, записи нет, нужен переход на нового ведущего.
- Несколько ведущих — только если есть конкретное требование: несколько центров обработки данных, работа клиента без сети, совместное редактирование. Внутри одного центра это добавляет конфликты записи без единой выгоды.
- Без ведущего — когда нужна высокая доступность записи и терпимы устаревшие чтения. Плата: гарантии слабее, нужны восстановление при чтении и фоновое сглаживание расхождений.
Про кворумы важно понимать формулу и её границу. Условие w + r > n означает, что множества узлов записи и чтения пересекаются хотя бы в одном узле, поэтому чтение обычно видит последнее значение. Но «обычно» здесь не случайное слово: при нестрогом кворуме, конкурентных или частичных записях, восстановлении отставшей реплики устаревшее чтение всё равно возможно.
И отдельная мина — разрешение конфликтов «выигрывает последний по времени». Оно молча выбрасывает данные и допустимо только там, где ключ пишется один раз. На изменяемых ключах бери номера версий с ручным слиянием, бесконфликтные типы данных или векторы версий.
Переход на нового ведущего лучше делать вручную, когда цена ошибки выше цены простоя: автоматический переход умеет потерять неотреплицированные записи, раздать заново уже использованные идентификаторы и развалиться в двух ведущих сразу.
Компромисс: синхронность покупает сохранность подтверждённых записей ценой доступности; асинхронность — наоборот; полусинхронная схема берёт середину, но требует следить, кто именно сейчас синхронный ведомый.
9. Секционирование: разные данные по узлам
Диапазон или хеш, и почему один горячий ключ хешированием не лечится.
Ключевая мысль: горячий ключ не лечится хешированием
Старт продаж на стадионный концерт. Один узел кластера у Глеба раскалён, остальные девять скучают. Это горячая точка — секция с непропорциональной нагрузкой; лежащий под ней перекос называется скосом.
Сначала — как вообще выбирается ключ секции, потому что половина горячих точек рождается здесь:
- Диапазонное секционирование — непрерывные отсортированные диапазоны ключей. Берут, когда нужны диапазонные сканирования. Плата: последовательный ключ (например, время впереди) отправляет все сегодняшние записи в одну секцию.
- Хеш-секционирование — ключ хешируется, секциям раздаются диапазоны хешей. Берут ради равномерной нагрузки. Плата: порядок ключей разрушен, диапазонные запросы разлетаются по всем секциям.
- Составной ключ — первая часть хешируется, остальные остаются отсортированными. Даёт и равномерность по первой части, и сканирование внутри неё — но только когда первая часть зафиксирована конкретным значением.
Практическое правило: если в ключе есть время, никогда не ставь его первым — поставь впереди другое поле, чтобы записи сначала расходились по нему.
А теперь ключевой момент секции, который Глеб узнал дорогой ценой. Один горячий ключ хешированием не лечится: одинаковые ключи хешируются одинаково и всё равно попадают в одну секцию. Ручное лекарство — приписать к горячему ключу небольшое случайное число: двузначный суффикс размазывает один ключ по ста ключам и по многим секциям. Плата честная: приложение обязано помнить, какие ключи разбиты, и собирать их обратно при чтении, — поэтому приём применяют к нескольким заранее известным горячим ключам, а не ко всем подряд.
Про ребалансировку достаточно двух правил. Первое: hash(ключ) mod N для размещения по узлам — антипаттерн, потому что смена N переносит почти все ключи. Второе: полностью автоматическая ребалансировка вместе с автоматическим обнаружением отказов опасна — перегруженный медленный узел объявляется мёртвым, ребалансировка сваливает на него и на сеть ещё больше работы, и отказ становится каскадным. Держи человека в контуре: план генерируется автоматически, применяет его администратор.
Компромисс: хеш покупает равномерность ценой диапазонных запросов; диапазон покупает сканирования ценой риска горячей точки; составной ключ берёт середину ценой обязательного условия «первая часть зафиксирована».
10. Уровень изоляции: слабейший, который держит инвариант
От аномалии к уровню: как выбрать изоляцию, не хватаясь сразу за сериализуемость.
Ключевая мысль: снимочная изоляция не ловит перекос записи
Два покупателя одновременно берут последнее место в ряду. Код честно проверяет: «место свободно?» — да, у обоих, — и оба пишут бронь на РАЗНЫЕ строки таблицы. Ни одна транзакция не перезаписала другую, а место продано дважды. Глеб смотрит в журнал и не находит ни одной ошибки: с точки зрения базы всё прошло идеально.
Это перекос записи (write skew): прочитал, принял решение, записал в другое место — а к моменту фиксации предпосылка решения уже неверна. И вот главный факт этой секции: снимочная изоляция его НЕ ловит. Ни «повторяемое чтение» в PostgreSQL и MySQL, ни «сериализуемый» уровень Oracle, ни снимочная изоляция SQL Server. Перекос записи останавливает только настоящая сериализуемость.
Навык ddia-transaction-isolation-choice предлагает обратный ход мысли, и он же — приём саморефлексии: не «какой уровень взять», а «какая аномалия убьёт мой инвариант». Инвариант Глеба — «одно место продано один раз». От аномалии к уровню, а не наоборот:
- Грязное чтение (видишь чужую незафиксированную запись) → уровень «чтение зафиксированного».
- Перекос чтения, он же неповторяемое чтение (разные части базы видны на разные моменты времени) → снимочная изоляция на многоверсионном управлении.
- Потерянное обновление (два цикла «прочитал-изменил-записал», один затирает другой) → атомарная операция записи, явная блокировка на чтении или автоматическое обнаружение.
- Перекос записи и фантомы, меняющие результат чужого поиска, → только сериализуемость.
Перед всем этим стоит нулевой шаг, который часто пропускают: нужны ли многообъектные транзакции вообще. Атомарный инкремент или сравнение-с-обменом защищают ОДИН ключ и транзакцией не являются. Многообъектная транзакция нужна, когда есть внешние ключи, денормализованные копии, которые обязаны обновляться вместе, или вторичные индексы.
Если сериализуемость всё-таки нужна, реализацию выбирают по форме нагрузки: последовательное выполнение — когда рабочий набор влезает в память и запись укладывается в одно ядро; двухфазная блокировка — проверенный общий механизм ценой низкой пропускной способности и взаимных блокировок; сериализуемая снимочная изоляция — когда конкуренция низкая, а транзакции короткие, иначе откаты съедят выигрыш.
И отдельная честная поправка: «согласованность» из аббревиатуры ACID — свойство приложения, а не базы. База даёт средства, инвариант формулируешь ты.
Компромисс: каждый шаг вверх по уровню изоляции покупает запрет ещё одного класса аномалий ценой пропускной способности и откатов; поэтому берут слабейший уровень, который держит именно твой инвариант.
11. Согласованность и консенсус: сколько координации покупать
Линеаризуемость дорога всегда — и нужна реже, чем кажется.
Ключевая мысль: бери слабейшую модель согласованности, которая ещё держит требование
Глеб пришёл с формулировкой «нам нужна сильная согласованность везде». Навык ddia-distributed-consistency-consensus начинает с обратного вопроса: какая модель здесь минимально достаточна? Правило звучит так: бери слабейшую модель согласованности, которая ещё держит требование — слабее означает быстрее и доступнее при разрыве сети.
Три уровня, снизу вверх:
- Итоговая — реплики когда-нибудь сойдутся. Самая дешёвая, всегда доступна. Годится там, где устаревание безвредно: счётчики просмотров, лента, кеш обнаружения сервисов.
- Причинная — причина всегда видна раньше следствия («вопрос раньше ответа»), а конкурентные операции не упорядочены. Её не тормозит сетевая задержка и она остаётся доступной при разрыве сети. Очень часто именно она и нужна там, где команда просит линеаризуемость.
- Линеаризуемая — иллюзия единственной копии со свежестью: после завершения записи её видят все чтения. И вот факт, который меняет отношение к ней: линеаризуемость медленна ВСЕГДА, а не только во время сбоев — время отклика не может быть меньше величины, пропорциональной неопределённости сетевой задержки.
Когда линеаризуемость действительно нужна — список короткий и его стоит держать перед глазами: блокировки и выбор лидера (иначе получишь двух ведущих сразу), ограничения уникальности (уникальный логин, «не продать одно место дважды»), и гонки между двумя каналами связи между компонентами.
Отдельно — то, о чём чаще всего ошибаются. Условие w + r > n НЕ даёт линеаризуемости. Строгий кворум допускает ситуацию, когда более поздний читатель видит более старое значение, чем предыдущий. Кворум — это про пересечение множеств, а не про свежесть.
Если задача всё же сводится к консенсусу — не пиши свой алгоритм. Линеаризуемое сравнение-с-обменом, атомарная фиксация распределённой транзакции, рассылка в общем порядке, блокировки и аренды, ограничения уникальности — всё это эквивалентно консенсусу: решив одно, решаешь все. Именно поэтому база с одним ведущим даёт их дёшево — ведущий и есть единственный принимающий решения; консенсус нужен лишь в момент его смены.
Практические границы: кворум большинства требует минимум 3 узлов, чтобы пережить отказ одного, и 5, чтобы пережить два. Распределённой блокировке нужен ограждающий маркер — монотонно растущее число, которое проверяет САМ ресурс, а не клиент: приостановленный на минуту клиент «оживает» с просроченной арендой и портит данные.
Компромисс: координация покупает свежесть и запрет опасных гонок ценой скорости и доступности при разрыве сети; отказ от координации покупает скорость и доступность ценой того, что часть противоречий придётся разгребать потом.
12. Пакетная и потоковая обработка: чьё время считать
Ограниченный вход против бесконечного — и почему брать время обработки опасно.
Ключевая мысль: время события, а не время обработки
Глеб построил метрику «продаж в минуту» и однажды утром увидел ровный всплеск на графике — при том, что продаж в тот момент почти не было. Причина оказалась не в продажах: ночью потребитель перезапустился, разобрал накопившийся хвост разом, и события легли на график по времени ОБРАБОТКИ, а не по времени, когда они произошли.
Отсюда правило, ради которого стоит помнить эту секцию: считай по времени события, а не по времени обработки. Время события — метка внутри самого события; время обработки — момент, когда до него дошли руки. Если часам устройства доверять нельзя (клиент мог работать без сети), пишут три метки — время события по часам устройства, время отправки по часам устройства и время приёма по часам сервера — и восстанавливают истинное время события по разнице между двумя последними.
Корневая развилка секции проще, чем кажется. Пакетная обработка предполагает ОГРАНИЧЕННЫЙ вход: размер известен и конечен, поэтому задание понимает, что чтение закончилось. Для сортировки это принципиально: последняя прочитанная запись может нести наименьший ключ и обязана выйти первой — значит, до конца чтения выдавать нельзя ничего. Реальные данные обычно бесконечны, поэтому их нарезают окнами: суточное задание отражает изменение входа через сутки. Хочешь меньше задержки — либо запускай чаще, либо откажись от фиксированных окон и обрабатывай события по мере прихода. Второе и есть потоковая обработка.
Выбор брокера сообщений тоже сводится к паре вопросов. Брокер на журнале (Kafka и родственники) раздаёт потребителю целые секции журнала со смещением, сохраняет порядок внутри секции и позволяет перечитать историю — но число потребителей ограничено числом секций, и медленное сообщение блокирует очередь за собой. Классическая очередь отдаёт сообщения по одному и удаляет после подтверждения: отлично, когда обработка одного сообщения дорогая, а порядок неважен, — но перечитать историю уже нельзя.
Окна тоже стоит различать по форме: падающее (фиксированная длина, событие ровно в одном окне), прыгающее (фиксированная длина с перекрытием, для сглаживания), скользящее (окно едет непрерывно: любые два события, попавшие в один интервал, оказываются вместе) и сессионное (без фиксированной длины, закрывается по бездействию пользователя).
И про «ровно один раз»: механизмы вроде контрольных точек дают его ВНУТРИ обработчика, но не для внешних побочных эффектов — запись в базу, письмо, сообщение в брокер повторятся при перезапуске. Лечится атомарной фиксацией всех эффектов вместе либо идемпотентностью с внешне хранимым смещением.
Компромисс: пакетная обработка проще, надёжно перечитывается целиком и платит задержкой в часы; потоковая даёт малую задержку и инкрементальный результат, но требует явных решений про время, окна и повторную обработку.
13. Производные данные: как держать пять хранилищ в согласии
Двойная запись, журнал изменений и право извиниться вместо координации.
Ключевая мысль: двойная запись — антипаттерн, журнал изменений — лекарство
К финалу у Глеба пять хранилищ на одних и тех же данных: основная база, поисковый индекс, кеш, аналитическое хранилище и витрина признаков. Глеб честно писал в базу и в индекс подряд, из одного места кода. Это двойная запись — и это антипаттерн.
Почему она ломается двумя разными способами:
- Гонка. Две конкурентные записи одного значения ложатся в базу и в индекс в РАЗНОМ итоговом порядке. Ошибки нет, оба хранилища «успешны», данные молча разъехались.
- Частичный отказ. Одна запись прошла, вторая нет. Чтобы сделать их атомарными, нужна распределённая транзакция — дорого и хрупко.
Лекарство: один источник истины плюс журнал изменений. Выбираешь систему-источник (обычно основную базу), извлекаешь из неё поток изменений — захват изменений данных, 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 "<запрос>".- Пак цитирует книгу?
Нет. Это оригинальная переформулировка методики с собственными критериями и таблицами компромиссов; текст книги в нём не воспроизводится. Проверка на дословные совпадения пройдена при оцифровке, а привязки к главам и страницам оставлены, чтобы каждое утверждение можно было сверить с первоисточником.
- С чего начать, если проектирую систему с нуля?
С секции про три оси: назови, какие типы отказов терпишь, какой параметр нагрузки растёт и какой перцентиль держишь. Дальше агент проведёт связанные решения по порядку — модель данных, движок хранения, репликация, секционирование, — и на каждом шаге покажет, как предыдущий выбор ограничивает следующий.