Начни здесь

Десять решений: пак 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 "<запрос>".

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

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

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

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

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

Лицензии шрифтов

Onest

Copyright 2021 The Onest Project Authors (https://github.com/googlefonts/onest)

This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
https://scripts.sil.org/OFL


-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------

PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.

The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded, 
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.

DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.

"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).

"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).

"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.

"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.

PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:

1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.

2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.

3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.

4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.

5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.

TERMINATION
This license becomes null and void if any of the above conditions are
not met.

DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.

JetBrainsMono

Copyright 2020 The JetBrains Mono Project Authors (https://github.com/JetBrains/JetBrainsMono)

This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
https://scripts.sil.org/OFL


-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------

PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.

The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded, 
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.

DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.

"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).

"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).

"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.

"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.

PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:

1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.

2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.

3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.

4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.

5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.

TERMINATION
This license becomes null and void if any of the above conditions are
not met.

DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.