Архитектура и системный дизайн
От одной программы к системе: требования в числах, масштабирование, отказоустойчивость, данные, наблюдаемость
Вячеслав Федоров
Лекция 14
Разработка и применение ПО
в физических исследованиях
Изложение. Четырнадцатая лекция посвящена архитектуре программных систем и системному дизайну. Программа, которой пользуется группа, со временем превращается в систему: несколько процессов на разных машинах, общая база, очередь между сбором и обработкой, балансировщик перед веб-интерфейсом. Её поведение определяется уже не только кодом, но и тем, как части ведут себя под нагрузкой и при отказах. В рамках лекции рассматриваются требования и их запись числами, масштабирование, отказоустойчивость, работа с данными, наблюдаемость и сопровождение, а в конце — проект системы сбора данных установки. Надпись на титуле — SLO, цель уровня обслуживания: требование к системе, записанное числом.
Демонстрации. Слайды с розовым выделением слова «Демонстрация» отмечают места, где изложение прерывается для работы в терминале виртуальной машины lab с Python 3.12, FastAPI 0.142, uvicorn 0.54, httpx 0.28 и nginx 1.31, далее называемой стендом: на слайде проигрывается запись тех же команд. Сервисы на стенде слушают петлевой интерфейс 127.0.0.1, а nginx работает в контейнере Docker с сетью хоста; поэтому распределения запросов, коды ответов и счётчики на другой машине получаются те же, а пропускная способность и задержки зависят от процессора.
Содержание
От требований к работающей системе
Изложение. Лекция состоит из шести частей. Первая переводит требования в числа: нагрузку, перцентили задержки, цели уровня обслуживания и бюджет ошибок. Вторая посвящена масштабированию: вертикальному и горизонтальному, сервисам без состояния, балансировке нагрузки и консистентному хешированию. Третья — отказоустойчивости: резервированию, проверкам работоспособности, тайм-аутам, повторам, идемпотентности и автомату защиты. Четвёртая — данным: репликации, шардированию, кешированию и очередям. Пятая — сопровождению: наблюдаемости, выбору между монолитом и микросервисами, сетке сервисов и ограничению частоты запросов. Шестая собирает сказанное в проект системы сбора данных установки.
Цель лекции
Записывать требования числами и выбирать устройство системы, которое их выдерживает
Система из нескольких программ отказывает по частям: медленный экземпляр, зависший процесс, переполненная очередь, повторы, умножающие нагрузку. Знание типовых решений — балансировки, резервирования, тайм-аутов, повторов, кеша, очереди — и привычка проверять их числами позволяют построить сервис, переживающий рост нагрузки и отказы, и не строить лишнего там, где достаточно одной программы.
Изложение. Цель лекции — научиться записывать требования к системе числами и выбирать её устройство так, чтобы эти числа выдерживались. Одна программа либо работает, либо нет; система из нескольких программ отказывает по частям, и каждая часть отказывает по-своему: один экземпляр отвечает медленно, другой завис, очередь переполнилась, а повторы клиентов многократно умножили нагрузку после кратковременного сбоя. Для этих ситуаций существуют типовые решения, и каждое имеет цену: балансировщик добавляет сетевой переход, реплика — отставание данных, кеш — риск устаревания, повтор — дополнительную нагрузку. Поэтому решения принимаются по числам: сколько запросов в секунду, какая задержка допустима, какая доля ответов может быть неуспешной. Вторая половина цели — не строить лишнего: для лабораторного сервиса с десятью пользователями достаточно одной программы и одной базы, и такое решение лучше распределённой системы.
Четыре свойства
системы
01
Масштабируемость
задержка и доля ошибок сохраняются при росте нагрузки за счёт новых ресурсов
02
Отказоустойчивость
отказ машины, процесса или зависимости не останавливает обслуживание
03
Наблюдаемость
журналы, метрики и трассировки показывают, что происходит внутри
04
Сопровождаемость
изменение, выкатка и откат выполняются быстро и предсказуемо
Для физика
Сквозной пример — архив измерений установки: оцифровщики пишут данные, скрипты сотрудников читают запуски и графики по HTTP.
Пояснение
Каждое свойство проверяется числом из требований: запросы в секунду, перцентиль задержки, доля успешных ответов.
Изложение. Хорошо спроектированную систему описывают четырьмя свойствами. Масштабируемость — способность сохранять заданные показатели, прежде всего задержку и долю ошибок, при росте нагрузки за счёт добавления ресурсов; это не то же самое, что отсутствие простоя при изменениях. Отказоустойчивость — способность продолжать обслуживание при отказе машины, процесса, сети или зависимости, пусть и с ухудшением. Наблюдаемость позволяет по журналам, метрикам и трассировкам понять, что происходит внутри работающей системы. Сопровождаемость — возможность изменять систему, выкатывать новые версии и откатывать неудачные быстро и предсказуемо. Свойства связаны и конкурируют за один бюджет: каждая копия, добавленная ради отказоустойчивости, требует затрат, а каждый новый компонент усложняет сопровождение. Сквозной пример лекции — архив измерений установки: оцифровщики пишут данные, а сотрудники и их скрипты читают запуски и графики по HTTP. Каждое свойство проверяется числом из требований, о которых идёт речь в первой части.
Требования
Функциональные и нефункциональные требования, нагрузка, перцентили, SLI, SLO и SLA, бюджет ошибок
01
Требования
Масштабирование
Отказоустойчивость
Данные
Сопровождение
Проектирование
Изложение. Первая часть посвящена требованиям: как перевести «быстро» и «надёжно» в числа, по которым проверяется система.
Требования
до проектирования
Требование Пример для архива измерений
функциональное выдача запуска по номеру, график канала за интервал
нагрузка запись 4 МБ/с непрерывно, чтение — 50 запросов/с в пике
задержка p99 выдачи запуска меньше 300 мс
доступность 99,9 % успешных ответов за 30 дней
сохранность ни одного потерянного измерения
стоимость три машины и диски на год записи
Пояснение
Функциональные требования говорят, что система делает; нефункциональные — насколько хорошо: числами нагрузки, задержки, доступности, объёма и цены.
Важно
Требование без числа непроверяемо: «быстро» становится перцентилем задержки, «надёжно» — долей успешных ответов.
Изложение. Проектирование начинается не со схемы, а с требований. Функциональные требования перечисляют, что система делает: принимает данные с оцифровщиков, выдаёт запуск по номеру, строит график канала за интервал. Нефункциональные описывают, насколько хорошо она это делает, и записываются числами: ожидаемая нагрузка в запросах в секунду и её пики, допустимая задержка, доля успешных ответов, объём и срок хранения данных, бюджет. Архитектуру определяют именно нефункциональные требования: один и тот же набор функций для десяти пользователей и для ста тысяч запросов в секунду реализуется совершенно разными системами. Требование без числа проверить нельзя: «быстро» превращается в перцентиль задержки, «надёжно» — в долю успешных ответов за окно времени. Сбор требований включает и вопрос о цене: каждая дополнительная девятка доступности кратно увеличивает число машин, площадок и объём работы.
Нагрузка
и задержка
lat = sorted(latencies_ms) q = statistics.quantiles(lat, n=100, method='inclusive') p50, p95, p99 = q[49], q[94], q[98] mean = statistics.fmean(lat) # смещено хвостом
Для физика
Страница архива из 20 запросов задевает медленный 1 % с вероятностью 1 − 0,99^20 ≈ 18 %.
01
rps
запросов в секунду: среднее за сутки и пик
02
p50, p95, p99
задержка, которую не превышают 50, 95 и 99 % запросов
03
Среднее
сдвигается редкими медленными ответами и скрывает хвост
04
Закон Литтла
L = λW: 100 запросов/с по 30 мс — в работе в среднем 3 запроса
Изложение. Нагрузку описывают числом запросов в секунду, rps, причём важно не только среднее за сутки, но и пик: у архива измерений он приходится на утро после ночной смены, когда скрипты обработки запускаются одновременно. Задержка является не одним числом, а распределением, и его описывают перцентилями: p50, медиана, — время, которого не превышает половина запросов, p95 и p99 — время, которого не превышают 95 и 99 % запросов. Среднее для этого непригодно: несколько очень медленных ответов сдвигают его, но не показывают, насколько велика задержка именно этих ответов. Хвост распределения затрагивает многих пользователей: страница, для которой выполняется двадцать запросов, хотя бы раз задевает медленный процент с вероятностью около 18 %, поэтому требования записывают по p95 или p99. Перцентили вычисляет statistics.quantiles; для малых выборок метод inclusive не выходит за пределы наблюдённых значений. Закон Литтла связывает три величины: среднее число запросов в системе равно интенсивности потока, умноженной на среднее время пребывания. Оценим по нему загрузку сервиса: при 100 запросах в секунду и 30 мс на запрос одновременно обрабатываются в среднем три запроса; если сервис способен обрабатывать четыре, рост нагрузки выше его ёмкости приводит к неограниченному росту очереди и задержки.
Демонстрация:
перцентили задержки
01
Сервис svc.py: 4 запроса одновременно, каждый 25-й — 200 мс
02
uvicorn на 127.0.0.1:8000
03
aload.py на httpx: 50–200 запросов/с по расписанию, по 4 с
04
Среднее, p50, p95, p99 и максимум
Что наблюдается
До 140 запросов/с: p50 22–23 мс, p99 202–208 мс. При 160 — p50 265 мс, при 200 — 792 мс: очередь растёт весь тест.
root@lab — запись со стенда
Демонстрация. Сервис svc.py на FastAPI представляет собой модель системы ограниченной ёмкости: семафор пропускает к работе не больше четырёх запросов одновременно, обычный запрос длится 20 мс, а каждый двадцать пятый, то есть 4 %, обращается к «медленному диску» и длится 200 мс. Среднее время обработки — 27,2 мс, поэтому ёмкость сервиса около 4 / 0,0272 ≈ 147 запросов в секунду. Генератор aload.py на httpx отправляет запросы по расписанию с заданной частотой, не дожидаясь ответов, как независимые пользователи; каждая частота поддерживается в течение 4 с. При 50, 100 и 140 запросах в секунду медиана — 22–23 мс, среднее — 29–32 мс, а p99 — 202–208 мс: медленные 4 % не видны в медиане, слегка сдвигают среднее и полностью определяют p99. При 160 запросах в секунду, всего на 9 % выше ёмкости, медиана вырастает до 265 мс, а при 200 — до 792 мс с максимумом 1819 мс: запросы приходят быстрее, чем обслуживаются, и очередь растёт всё время теста. Ошибок нет ни в одном прогоне — перегрузка проявляется задержкой задолго до отказов. Генератор замкнутого типа, отправляющий следующий запрос только после ответа на предыдущий, при перегрузке сам замедлился бы и не учёл ожидания запросов, которые не успел отправить; эту ошибку измерения называют coordinated omission, поэтому задержку необходимо измерять генератором с расписанием.
SLI, SLO
и SLA
Термин Что это Пример
SLI измеряемый показатель доля ответов не 5xx; доля быстрее 300 мс
SLO цель для SLI за окно 99,9 % успешных за 30 дней
SLA договор с последствиями ниже 99,5 % — возврат части оплаты
бюджет ошибок 1 − SLO 0,1 % запросов за 30 дней
Пояснение
Доступность считают по запросам — доля успешных ответов за скользящее окно, — а не по времени работы машины.
Для физика
Архив: 99,9 % чтений успешны, 99 % выдач запуска быстрее 300 мс; измерение попадает в архив не позже 10 с после снятия.
Изложение. Требования к качеству обслуживания записывают тремя связанными понятиями из практики эксплуатации Google, Site Reliability Engineering. SLI, индикатор уровня обслуживания, представляет собой измеряемую величину: доля ответов без ошибки сервера, доля запросов быстрее 300 мс, задержка поступления данных в архив. SLO, цель уровня обслуживания, — требуемое значение индикатора за окно времени, например 99,9 % успешных ответов за скользящие 30 дней. SLA, соглашение об уровне обслуживания, — договор с внешним заказчиком, в котором записаны последствия нарушения, например возврат части оплаты; его порог обычно мягче внутреннего SLO, что позволяет команде отреагировать до нарушения договора. Доступность распределённого сервиса удобнее считать не по времени работы машины, а по запросам: машина может работать, а пользователи — получать ошибки. Разность между единицей и SLO называют бюджетом ошибок: при 99,9 % это 0,1 % запросов за окно, которые можно потратить на выкатки, эксперименты и аварии. Для архива измерений индикаторов несколько: доля успешных чтений, доля быстрых чтений и задержка поступления данных.
Девятки
и бюджет ошибок
budget = 1 - slo / 100 # доля неуспешных downtime = budget * 30 * 86400 # с за 30 дней chain = math.prod(a) # последовательно copies = 1 - math.prod(1 - x for x in a)
Важно
Две копии по 99 % дают 99,99 % только при независимых отказах: общий диск, сеть или выкатка делают их одной.
01
Бюджет
1 − SLO: при 99,9 % — 0,1 % запросов или 43 мин за 30 дней
02
Расход
выкатки, эксперименты и аварии тратят один бюджет
03
Последовательно
A = A1·A2·A3: цепочка хуже каждого звена
04
Параллельно
1 − (1 − A1)(1 − A2): отказ, только если отказали все
Изложение. Доступность часто называют числом девяток: 99,9 % — три девятки, 99,99 % — четыре. Каждому уровню соответствует допустимое время простоя: при 99,9 % это около 43 мин за 30 дней, при 99,99 % — около четырёх минут, то есть меньше, чем занимает одна неудачная выкатка с откатом. Бюджет ошибок является общим для аварий, выкаток новых версий и экспериментов, и когда он исчерпан, выкатки приостанавливают до восстановления. Определим доступность составной системы по правилам теории надёжности. Если запрос проходит последовательно через несколько сервисов и успешен только при успехе каждого, доступности перемножаются, и цепочка оказывается хуже каждого звена. Если же работают копии и отказ наступает, только когда отказали все, перемножаются вероятности отказа, и доступность растёт. Ключевое условие — независимость: две копии на одном диске, в одной стойке или обновлённые одной выкаткой отказывают вместе.
Демонстрация:
девятки
01
Простой для пяти уровней SLO: год, 30 дней, неделя, сутки
02
Цепочка из трёх сервисов по 99,9 %
03
Две независимые копии по 99 %
04
Бюджет ошибок при 100 000 запросов/с
Что наблюдается
99,99 % — 52 мин 34 с в год и 4 мин 19 с за 30 дней; цепочка — 99,7 %; две копии по 99 % — 99,99 %.
root@lab — запись со стенда
Демонстрация. Скрипт nines.py переводит уровни SLO в допустимое время простоя. Для 99 % это 3 д 15 ч 36 мин в год и 7 ч 12 мин за 30 дней, для 99,9 % — 8 ч 45 мин 36 с в год и 43 мин 12 с за 30 дней, для 99,99 % — 52 мин 34 с в год, 4 мин 19 с за 30 дней и 8,6 с в сутки, для 99,999 % — 5 мин 15 с в год и 25,9 с за 30 дней. С ключом --chain скрипт перемножает доступности звеньев: цепочка из трёх сервисов по 99,9 % даёт 99,700 %. С ключом --copies он вычисляет доступность копий с независимыми отказами: две копии по 99 % дают 99,990 %. С ключом --budget бюджет ошибок пересчитывается в запросы: при 100 000 запросов в секунду за 30 дней приходит около 259 млрд запросов, и при SLO 99,99 % неуспешными допустимо около 26 млн из них.
Масштабирование
Вертикальное и горизонтальное, сервисы без состояния, балансировка нагрузки, консистентное хеширование
02
Требования
Масштабирование
Отказоустойчивость
Данные
Сопровождение
Проектирование
Изложение. Вторая часть посвящена масштабированию: как нарастить мощность сервиса и как распределить запросы между его экземплярами.
Вертикальное
и горизонтальное
Вертикальное Горизонтальное
ресурс CPU и память машины число экземпляров
доработка не нужна сервис без состояния
предел самая большая машина общие зависимости, бюджет
отказ машины остановка всего сервиса потеря доли мощности
изменение обычно с перезапуском по одному, без простоя
Для физика
Процесс обычной сборки CPython выполняет байт-код в одном потоке за раз: машину с 32 ядрами загружают 32 процесса.
docker update --cpus 2 svc uvicorn app:app --workers 4
Изложение. Когда сервис перестаёт справляться, мощность наращивают двумя способами. Вертикальное масштабирование даёт той же машине больше процессоров и памяти: доработка программы не нужна, но предел задан самой большой доступной машиной, а сама машина остаётся единой точкой отказа. Изменение размера виртуальной машины обычно требует её перезапуска; квоту процессора работающего контейнера Docker можно изменить без его остановки командой docker update --cpus. Горизонтальное масштабирование добавляет одинаковые экземпляры за балансировщиком: рост ограничен бюджетом и общими зависимостями, например базой, отказ одного экземпляра снимает лишь часть мощности, а выкатка по одному экземпляру позволяет обновлять сервис без простоя. Цена — доработка сервиса, который не должен хранить состояние в себе, и сетевой переход через балансировщик. Для Python граница между двумя способами проходит внутри машины: из-за глобальной блокировки интерпретатора процесс обычной сборки CPython выполняет байт-код только в одном потоке за раз и при вычислениях на чистом Python загружает не больше одного ядра, поэтому большую машину загружают несколько процессов, как это делают uvicorn --workers и gunicorn. Свободнопоточная сборка, официально поддерживаемая с Python 3.14, снимает это ограничение, но используемой по умолчанию пока не стала.
Сервисы
без состояния
# состояние в процессе: другой экземпляр не видит results = {} results[job_id] = fit(data) # состояние снаружи: видят все экземпляры db.execute('INSERT INTO results VALUES (?, ?)', (job_id, fit(data)))
Для физика
Сервис расчётов: исполнитель берёт задание, считает и пишет результат в общее хранилище — исполнители взаимозаменяемы.
01
Без состояния
любой экземпляр обслуживает любой запрос: сессии, файлы, задания — снаружи
02
С состоянием
базы, кеши, очереди: новую копию сначала наполняют данными
03
Балансировщик
хранит не данные приложения, а соединения, счётчики и результаты проверок
Изложение. Горизонтально масштабируется прежде всего сервис без состояния: всё, что должно пережить запрос, — сессии пользователей, загруженные файлы, результаты расчётов, — хранится вне процесса, в базе, общем кеше или хранилище файлов. Такое устройство обеспечивает взаимозаменяемость экземпляров: каждый обслуживает любой запрос, экземпляры можно добавлять, убирать и перезапускать, а балансировщик может направить следующий запрос на любой из них. Словарь в памяти процесса нарушает это свойство: результат, записанный одним экземпляром, не виден другому и пропадает при перезапуске. Сервисы с состоянием — базы данных, кеши, брокеры очередей — масштабируются иначе: новую копию базы необходимо сначала наполнить данными, что при нескольких терабайтах занимает часы, а холодный кеш ещё не снимает нагрузку с базы. Даже балансировщик, не хранящий данных приложения, имеет рабочее состояние: открытые соединения, счётчики активных соединений, результаты проверок работоспособности, — и у каждого экземпляра балансировщика оно своё.
Демонстрация:
процессы и ядра
01
Сервис cpu.py: около 10 мс вычислений на запрос
02
uvicorn с одним процессом: 300 запросов по 8 одновременно
03
uvicorn --workers 2 на двух ядрах стенда
04
Новое соединение на каждый запрос (--fresh)
Что наблюдается
Один процесс — 62 запроса/с, p50 118 мс; два — 126 запросов/с, p50 57–61 мс, запросы поделены 142 и 158.
root@lab — запись со стенда
Демонстрация. Сервис cpu.py выполняет на каждый запрос около 10 мс вычислений на чистом Python и возвращает номер своего процесса; на стенде цикл вместе с обработкой HTTP занимает около 16 мс процессора, поэтому один процесс обслуживает не 100, а 62 запроса в секунду. Обычную функцию-обработчик FastAPI выполняет в пуле потоков, но байт-код Python внутри процесса выполняется одним потоком за раз. Сервер uvicorn с одним рабочим процессом обрабатывает 300 запросов, поступающих по восемь одновременно, за 4,9 с — 62 запроса в секунду при медиане 118 мс: восемь запросов делят одно ядро. С ключом --workers 2 uvicorn запускает два рабочих процесса на общем слушающем сокете, и новые соединения между ними распределяет ядро ОС: 142 и 158 запросов за 2,4 с, 126 запросов в секунду, медиана 57–61 мс. Таким образом, на двух ядрах стенда второй процесс удвоил пропускную способность. Клиент открывает новое соединение на каждый запрос: постоянные соединения закрепляются за процессом, который их принял, и распределение по процессам было бы неравномерным.
Балансировка
нагрузки
Алгоритм Как выбирает Когда подходит
round robin по очереди, с весами одинаковые короткие запросы
least_conn меньше активных соединений разная длительность
hash по ключу: пользователь, URL привязка, локальный кеш
random two лучший из двух случайных много балансировщиков
Пояснение
L4 распределяет соединения TCP, не читая HTTP; L7 разбирает запрос и выбирает экземпляр по пути, заголовку или cookie.
upstream app { zone app 64k; # общее состояние least_conn; # или hash $arg_user; server 127.0.0.1:8001 weight=2; server 127.0.0.1:8002; server 127.0.0.1:8003; } location / { proxy_pass http://app; }
Изложение. Балансировщик распределяет запросы между экземплярами, и способ выбора зависит от характера нагрузки. Round robin отправляет запросы по очереди, а веса учитывают неравные машины: сервер с весом 2 получает вдвое больше. Он подходит для одинаково коротких запросов, но не учитывает скорость экземпляров: медленный получает ту же долю и задерживает каждый третий запрос. Least connections выбирает экземпляр с наименьшим числом активных соединений и поэтому автоматически снижает нагрузку на медленный. Хеш по ключу — пользователю, адресу страницы, номеру запуска — всегда отправляет одинаковые ключи на один экземпляр, что позволяет сохранить его локальный кеш. Выбор лучшего из двух случайных экземпляров почти так же хорош, как least connections, но не требует общего счётчика и подходит, когда балансировщиков несколько. Балансировщик уровня L4 распределяет соединения TCP, не разбирая HTTP, а уровня L7 разбирает запрос и может выбирать по пути или заголовку, но тратит на это больше процессора, прежде всего на завершение TLS. В nginx алгоритм задаётся в блоке upstream, а директива zone делает состояние группы общим для рабочих процессов nginx, иначе каждый процесс ведёт свою очередь и свои счётчики.
Демонстрация:
балансировка в nginx
01
Три экземпляра на 8001–8003, задержка app2 — 200 мс
02
nginx:alpine в контейнере с сетью хоста, rr.conf
03
300 запросов по 12 одновременно: round robin
04
least_conn и hash $arg_user для восьми пользователей
Что наблюдается
Round robin: по 100 за 2,0 с; least_conn: 144, 12 и 144 за 0,6 с; hash: каждый пользователь у одного экземпляра.
root@lab — запись со стенда
Демонстрация. Скрипт apps.sh запускает три экземпляра сервиса inst.py на портах 8001–8003; каждый отвечает своим именем, причём app2 тратит на ответ 200 мс, а остальные — 10 мс. Перед ними работает nginx в контейнере nginx:alpine с сетью хоста и конфигурацией rr.conf: группа upstream из трёх серверов с общей зоной состояния. Три запроса curl получают ответы app1, app2 и app3 — round robin по очереди. 300 запросов по двенадцать одновременно распределяются ровно по 100, но треть из них ждёт медленный app2 с медианой 202 мс, и прогон занимает 2,0 с, 148 запросов в секунду. Конфигурация lc.conf отличается одной строкой least_conn; после перезагрузки nginx тот же прогон занимает 0,6 с, 465 запросов в секунду: медленный app2 получил только 12 запросов, 4 %, а app1 и app3 — по 144. Конфигурация hash.conf выбирает экземпляр по хешу параметра user: 32 запроса восьми пользователей дают 12, 12 и 8 запросов на app1, app2 и app3, и каждый пользователь обслуживается одним экземпляром, например u0 — app3, u4 — app1.
Консистентное
хеширование
ключ принадлежит первому узлу по ходу часовой стрелки
новый s4 забирает ключи только у s1
def h(s): # одинаков в любом процессе d = hashlib.md5(s.encode()).digest() return int.from_bytes(d[:8], 'big') nodes[h(key) % len(nodes)] # остаток max(nodes, key=lambda n: h(f'{n}:{key}'))
Пояснение
Остаток от деления при переходе с 4 на 5 узлов переносит 4/5 ключей, кольцо и рандеву — около 1/5.
Важно
Встроенный hash() строк меняется от запуска к запуску интерпретатора: для распределения ключей непригоден.
Изложение. Когда экземпляр или шард выбирают по хешу ключа, важно, что происходит при изменении их числа. Простейший способ — остаток от деления хеша на число узлов — при переходе от N к N + 1 узлам меняет узел почти для всех ключей: доля перенесённых ключей равна N/(N + 1), и при переходе с четырёх узлов на пять получим 80 %. Для кеша это означает одновременный промах почти по всем ключам, для шардов базы — перенос почти всех данных. Консистентное хеширование размещает на кольце значений хеша и узлы, и ключи; ключ принадлежит первому узлу по ходу кольца. Новый узел забирает ключи только с одной дуги, в среднем 1/(N + 1) от общего числа, а остальные остаются на месте. Чтобы дуги были равномерными, каждый узел ставят на кольцо во многих точках, и тогда новый узел забирает понемногу ключей у каждого из прежних. Рандеву-хеширование достигает того же иначе: для ключа вычисляется хеш пары «узел, ключ» для каждого узла и выбирается узел с наибольшим значением. Необходимо, чтобы хеш был одинаковым во всех процессах и на всех машинах: встроенная функция hash() для строк рандомизируется при каждом запуске интерпретатора и для этой задачи непригодна. В nginx консистентный хеш включается словом consistent в директиве hash.
Демонстрация:
остаток и кольцо
01
100 000 ключей вида run-000123 на четырёх шардах
02
Остаток от деления, кольцо со 100 точками на узел, рандеву
03
Доля ключей, сменивших шард при добавлении пятого
04
Встроенный hash() одной строки в двух процессах
Что наблюдается
Остаток переносит 80,0 % ключей, кольцо — 20,0 %, рандеву — 20,1 %; кольцо неравномернее: от 23 749 до 26 014 ключей на шард.
root@lab — запись со стенда
Демонстрация. Скрипт shard.py распределяет 100 000 ключей вида run-000123 по четырём шардам тремя способами, используя стабильный хеш MD5, и считает, сколько ключей сменят шард при добавлении пятого. Остаток от деления распределяет ключи равномерно, от 24 890 до 25 230 на шард, но при добавлении шарда переносит 80,0 % ключей. Кольцо со ста точками на узел переносит 20,0 % — ровно ту долю, которую должен забрать новый шард, — но распределяет менее равномерно: от 23 749 до 26 014 ключей. Рандеву-хеширование сочетает достоинства обоих способов: от 24 897 до 25 114 ключей и 20,1 % переноса. Последняя команда дважды запускает интерпретатор и печатает встроенный hash() одной и той же строки: числа разные, −4560564022589687027 и 8255076724221628678, поскольку хеш строк рандомизирован при каждом запуске интерпретатора, если не задана переменная окружения PYTHONHASHSEED.
Отказоустойчивость
Резервирование и кворум, проверки работоспособности, тайм-ауты и дедлайн, повторы, идемпотентность, автомат защиты
03
Требования
Масштабирование
Отказоустойчивость
Данные
Сопровождение
Проектирование
Изложение. Третья часть посвящена отказоустойчивости: как система переживает отказ машины, процесса или зависимости и как не превратить частичный отказ в полный.
Резервирование
и кворум
Узлов Кворум Переживает отказов
1 1 0
2 2 0
3 2 1
4 3 1
5 3 2
Для физика
Две площадки после разрыва связи не выберут главную: нужна третья, хотя бы со свидетелем без данных.
01
N + 1
на экземпляр больше, чем нужно в пик: отказ одного не перегружает остальных
02
Домены отказа
копии на разных машинах, стойках, линиях питания, площадках
03
Кворум
решение принимает большинство: целая часть N/2 плюс 1
04
Нечётное N
четвёртый узел не добавляет устойчивости к отказам
Изложение. Основой отказоустойчивости является резервирование: экземпляров на один больше, чем необходимо для пиковой нагрузки, — схема N + 1, — чтобы отказ одного не перегружал остальные. Копии разносят по доменам отказа — разным машинам, стойкам, линиям питания и площадкам: три реплики на одной машине переживут отказ процесса, но не отказ диска или питания. Когда копии хранят данные и должны договориться, например о том, какая из них главная, применяется кворум: решение принимает большинство, целая часть N/2 плюс один узел. Три узла переживают отказ одного, пять — двух, а четыре — тоже только одного, поэтому число узлов берут нечётным. Без большинства система обязана остановить запись: иначе после разрыва связи две половины выберут по главному узлу и начнут расходиться — это состояние называют split brain. Поэтому две площадки не обеспечивают автоматического переключения при разрыве связи между ними: добавляют третью, хотя бы со свидетелем, который не хранит данных и только голосует.
Проверка
работоспособности
upstream app { server 127.0.0.1:8001 max_fails=1 fail_timeout=10s; server 127.0.0.1:8002 max_fails=1 fail_timeout=10s; } location / { proxy_pass http://app; proxy_read_timeout 1s; proxy_next_upstream error timeout; }
Важно
/health только о процессе пропустит экземпляр без базы; /health обо всех зависимостях снимет все экземпляры из-за одной.
01
Активная
опрос /health по расписанию: HAProxy, Kubernetes; в открытом nginx нет
02
Пассивная
по настоящим запросам: max_fails неудач за fail_timeout
03
Повтор на соседе
GET — да; уже отправленный POST — нет
04
Точка входа
несколько адресов в DNS, плавающий адрес VRRP, anycast
Изложение. Чтобы перестать отправлять запросы отказавшему экземпляру, балансировщик должен узнать об отказе. Активная проверка периодически опрашивает служебный адрес /health и исключает экземпляр после нескольких неудач подряд; в открытой версии nginx её нет, она есть в HAProxy, в Kubernetes и в коммерческой версии nginx. Пассивная проверка следит за настоящими запросами: в nginx экземпляр, давший max_fails неудач за время fail_timeout, исключается на fail_timeout, после чего получает пробный запрос. Неудачный запрос nginx повторяет на соседнем экземпляре по правилам директивы proxy_next_upstream, по умолчанию — при ошибке соединения и тайм-ауте. Запросы с неидемпотентными методами, POST и PATCH, nginx на соседа не передаёт, если запрос уже был отправлен: повтор мог бы выполнить действие дважды. Сама точка входа тоже резервируется: несколько адресов в DNS, плавающий адрес по протоколу VRRP, который соседняя машина забирает при отказе, или anycast — один адрес, анонсируемый из нескольких мест. Выбор содержания /health является компромиссом: проверка только живости процесса пропустит экземпляр, потерявший базу, а проверка всех зависимостей при отказе базы снимет все экземпляры сразу.
Демонстрация:
отказ экземпляра
01
hc.conf: max_fails=1, fail_timeout=10s, ожидание ответа 1 с
02
Три экземпляра за nginx, остановка app2 сигналом STOP
03
Три POST, шесть GET и через 10 с ещё шесть GET
04
Журнал nginx: адреса и коды попыток
Что наблюдается
POST на app2 — 504 через 1,0 с; затем GET идут мимо app2; через 10 с GET пробует app2 и повторяется на app1: st=504, 200.
root@lab — запись со стенда
Демонстрация. Конфигурация hc.conf задаёт для каждого из трёх экземпляров max_fails=1 и fail_timeout=10s, ожидание ответа не дольше 1 с и повтор на соседнем экземпляре при ошибке и тайм-ауте. Процесс app2 останавливается сигналом STOP: он не отвечает, но ядро продолжает принимать его соединения — так выглядит зависший процесс. Заметим, что у завершённого процесса порт закрыт: ядро сразу отклоняет соединение, запрос ещё не отправлен, и nginx передал бы соседу даже POST. Три запроса POST по очереди попадают на app1, app2 и app3: запрос к app2 через 1,0 с завершается кодом 504, потому что уже отправленный POST nginx на соседа не передаёт. После этой неудачи app2 исключён на 10 с, и шесть запросов GET делят app1 и app3 по три без задержек. Через 10 с app2 снова получает пробный запрос GET; nginx ждёт ответа 1 с и, не дождавшись, повторяет запрос на app1: клиент получает ответ 200, но примерно через 1 с — шесть запросов занимают 1,1 с вместо 0,1 с, а p95 у app1, интерполированный по трём ответам, равен 920 мс. Журнал nginx показывает обе ситуации: POST с кодом 504 и одной попыткой на 127.0.0.1:8002 и GET с двумя попытками, 504 и 200. В конце процесс app2 продолжает работу по сигналу CONT.
Тайм-ауты
и дедлайн
10 с
30 с
60 с
клиент получает 504 через 10 с, а gateway и articles ждут до 30 и 60 с — ответ уже никому не нужен
left = deadline - time.monotonic() # остаток if left <= 0: raise TimeoutError('бюджет исчерпан') r = client.get(url, timeout=left, headers={'X-Deadline-Ms': str(int(1000 * left))})
01
Убывание
тайм-аут вызываемого меньше тайм-аута вызывающего
02
Значение
по распределению задержки: p99,9 — 0,1 % ложных отмен
03
Дедлайн
остаток бюджета передаётся дальше; исчерпан — работа прекращается
Изложение. Каждый сетевой вызов должен иметь тайм-аут: без него зависший сервис удерживает вызывающего, его потоки и соединения бесконечно, и отказ распространяется вверх по цепочке. Значения тайм-аутов в цепочке необходимо согласовать. Рассмотрим пример на слайде: nginx ждёт ответа gateway 10 с, gateway ждёт articles 30 с, articles ждёт базу 60 с. Клиент получит ответ 504 через 10 с, а gateway и articles продолжат работу, результат которой уже никому не нужен, и под нагрузкой такие запросы накапливаются. Правило — тайм-аут вызываемого меньше тайм-аута вызывающего на запас сетевых расходов. Одинаковые значения на всех уровнях не помогают: внутренний и внешний тайм-ауты сработают одновременно. Надёжнее передавать по цепочке дедлайн — остаток времени, отведённого на запрос, например в заголовке; каждый уровень вычитает потраченное, а получив исчерпанный бюджет, прекращает работу. Передают именно остаток, а не абсолютный момент, потому что часы разных машин расходятся; так устроены дедлайны gRPC. Само значение тайм-аута выбирают по распределению задержки вызываемого сервиса: тайм-аут на уровне p99,9 означает одну ложную отмену на тысячу запросов.
Демонстрация:
тайм-ауты в цепочке
01
deadline.py: client → gateway → articles в одном процессе
02
Ожидание клиента — 1 с, gateway — 6 с, работа articles — 3 с
03
Без дедлайна и с остатком бюджета в X-Deadline-Ms
Что наблюдается
Клиент в обоих случаях получает отказ через 1,0 с; без дедлайна articles и gateway работают до 3,1 с, с дедлайном останавливаются через 1,0 с.
root@lab — запись со стенда
Демонстрация. Скрипт deadline.py запускает в одном процессе цепочку из двух серверов, gateway и articles, и отправляет запрос от клиента; времена короче, чем в примере прошлого слайда, но соотношение то же — каждый уровень ждёт дольше вызывающего: клиент ждёт ответа 1 с, gateway ждёт articles 6 с, собственный тайм-аут articles — 60 с, а его работа занимает 3 с и выполняется частями по 0,1 с. Команда grep показывает, где заголовок X-Deadline-Ms читается и где gateway передаёт дальше остаток бюджета за вычетом 50 мс запаса. Без дедлайна клиент через 1,0 с прекращает ждать с TimeoutError, а articles доделывает работу, и gateway отвечает 200 через 3,1 с — ответ, который никто не прочтёт. С ключом --deadline клиент так же получает отказ через 1,0 с, но gateway в тот же момент прекращает ждать и отвечает 504, а articles, обнаружив исчерпанный бюджет, прерывает работу через 1,0 с вместо 3. Таким образом, для клиента результат одинаков, а дедлайн избавляет систему от работы, которая выполнялась бы впустую.
Повторы
с бюджетом
for attempt in range(4): try: return call(timeout=0.3) except TimeoutError: cap = min(1.0, 0.05 * 2 ** attempt) time.sleep(random.uniform(0, cap)) raise RuntimeError('попытки исчерпаны')
Важно
2 попытки на каждом из трёх уровней — до 8 запросов к последнему сервису; 3 попытки на пяти уровнях — 243.
01
Одна точка
повторяет один уровень цепочки, остальные — нет
02
Бюджет
повторов не больше 10 % запросов, попыток на запрос не больше трёх
03
Пауза
растёт вдвое и выбирается случайно: клиенты расходятся во времени
04
Что повторять
тайм-аут, обрыв, 429, 502–504 — и только идемпотентное
Изложение. Повтор неудачного запроса позволяет пережить кратковременный сбой, но при перегрузке усиливает её. Если повторяет каждый уровень цепочки, попытки перемножаются: по две попытки на трёх уровнях дают до восьми запросов к последнему сервису, а в примере Amazon три попытки на каждом из пяти уровней увеличивают нагрузку на базу в 243 раза. Поэтому повторяют в одной точке стека, а число повторов ограничивают бюджетом: в практике Google на запрос не больше трёх попыток, а повторы составляют не больше 10 % всех запросов клиента, так что при полном отказе нагрузка растёт лишь на десятую часть. Пауза между попытками растёт экспоненциально и выбирается случайно в пределах от нуля до текущей границы: иначе клиенты, одновременно получившие отказ, одновременно и повторят запросы, создав новый пик. Повторяют только временные ошибки — тайм-аут, обрыв соединения, коды 429, 502, 503 и 504 с учётом заголовка Retry-After — и только идемпотентные операции: тайм-аут не означает, что действие не выполнено.
Идемпотентность
и ключ операции
Операция Безопасность повтора
GET, HEAD — чтение да
PUT — замена целиком да
DELETE — удаление да
POST — создание, списание нет, без ключа
«выставить ток 10 А» да
«увеличить ток на 1 А» нет
key = str(uuid.uuid4()) # один на операцию for attempt in range(4): try: return client.post(url, json=payment, headers={'Idempotency-Key': key}) except httpx.TransportError: time.sleep(random.uniform(0, 0.1 * 2**attempt)) # сервер: известный ключ — прежний ответ # без повторного списания
Для физика
Программа управления повторяет уставку «10 А» с проверкой прочитанного значения, а не приращение «+1 А».
Изложение. Операция является идемпотентной, если повторное выполнение оставляет систему в том же состоянии, что и однократное; ответ на повтор может отличаться: повторный DELETE получает 404. По стандарту HTTP идемпотентны чтение GET и HEAD, замена ресурса целиком PUT и удаление DELETE, поэтому их можно повторять после обрыва. POST, создающий запись или выполняющий списание, при повторе создаст вторую запись. Идемпотентность POST обеспечивает ключ операции: клиент создаёт уникальный ключ один раз на логическую операцию, а не на попытку, и передаёт его во всех попытках, например в заголовке Idempotency-Key, принятом в платёжных API; сервер, увидев известный ключ, возвращает сохранённый ответ, не повторяя действия. Тот же принцип применим к управлению установкой: команда «выставить ток квадруполя 10 А» идемпотентна и может безопасно повторяться до подтверждения, а «увеличить ток на 1 А» при потерянном ответе и повторе изменит ток на 2 А. Поэтому программы управления повторяют уставку с проверкой прочитанного значения.
Демонстрация:
повтор и ключ
01
backoff.py: удвоение предела паузы от 0,1 до 2 с
02
pay.py: задержка каждого третьего ответа на 1 с
03
Тайм-аут 0,3 с, до 4 попыток: без ключа и с ключом
Что наблюдается
Паузы трёх клиентов различны; без ключа 10 платежей дали 14 записей и списание 1400, с ключом — 10 записей и 1000.
root@lab — запись со стенда
Демонстрация. Скрипт backoff.py печатает паузы трёх клиентов перед шестью повторами: предел растёт вдвое от 0,1 до 1,6 с и ограничен 2 с, а пауза выбирается случайно в его пределах, поэтому у клиентов она разная — например, перед пятым повтором 0,79, 1,34 и 1,00 с. Сервис платежей в pay.py записывает каждый платёж, но каждый третий ответ задерживает на 1 с; grep показывает, где сервер читает заголовок Idempotency-Key и где клиент его передаёт. Клиент ждёт ответа 0,3 с и делает до четырёх попыток с паузами от 0,01 до 0,04 с в этом прогоне. Без ключа десять платежей по 100 потребовали 14 попыток, и сервер записал 14 платежей на сумму 1400: каждый повтор после задержанного ответа списал деньги ещё раз. С ключом, одинаковым для всех попыток одного платежа, при тех же 14 попытках на сервере 10 записей и 1000: повтор с известным ключом получил сохранённый ответ.
Автомат защиты
и деградация
Закрыт
запросы проходят, неудачи считаются
Открыт
сразу заглушка, сервис не вызывается
Полуоткрыт
один пробный настоящий запрос
3 неудачи подряд
пауза 1 с
неудача
успех
Пояснение
Пока автомат открыт, вызывающий отвечает заглушкой: последняя известная версия, скрытый блок страницы, 503 с Retry-After.
Для физика
Страница запуска с таблицей и последним сохранённым графиком лучше страницы с ошибкой 504.
Изложение. Когда вызываемый сервис отказал, повторы и даже обычные запросы мешают ему восстановиться и занимают ресурсы вызывающего ожиданием тайм-аутов. Автомат защиты, circuit breaker, представляет собой шаблон из книги М. Найгарда «Release It!»: он ставится на стороне вызывающего и имеет три состояния. В закрытом запросы проходят, а неудачи считаются; после нескольких неудач подряд автомат открывается и в течение заданной паузы сразу возвращает ошибку или заглушку, не обращаясь к сервису. По истечении паузы автомат переходит в полуоткрытое состояние и пропускает один пробный настоящий запрос: при успехе закрывается, при неудаче снова открывается. Автомат вносит в систему режимы, которые трудно проверить заранее, и может задержать восстановление, поэтому в Amazon предпочитают ограничивать повторы локально, ведром жетонов. Вместе с автоматом применяют деградацию функциональности: вместо ошибки 504 пользователь получает страницу без неработающего блока, последнюю сохранённую версию данных или ответ 503 с заголовком Retry-After. Для архива измерений это означает: если сервис построения графиков недоступен, страница запуска показывает таблицу и последний сохранённый график, а не ошибку.
Демонстрация:
повторы и автомат
01
chain.py: client → nginx → gateway → articles, ответ articles — 503
02
Попытки по уровням: 2·2·2, 3·3·3 и повтор только в gateway
03
breaker.py: 10 запросов/с, отказ articles на 2,5 с
04
Автомат: после 3 неудач подряд — пауза 1 с и пробный запрос
Что наблюдается
До articles дошло 8, 27 и 2 запроса; автомат пропустил к нему 13 запросов из 40 и закрылся по пробному запросу в 3,2 с.
root@lab — запись со стенда
Демонстрация. Скрипт chain.py запускает цепочку из трёх серверов — nginx, gateway и articles — на портах 8031–8033; articles перегружен и на каждый запрос отвечает 503. Клиент и каждый уровень повторяют неудачный запрос заданное число раз. При двух попытках на каждом уровне до articles доходит 8 запросов, при трёх — 27: попытки перемножаются, 2³ и 3³. Если повторяет только gateway, а остальные уровни делают по одной попытке, до articles доходит 2 запроса. Скрипт breaker.py воспроизводит работу автомата защиты в модельном времени: gateway получает 10 запросов в секунду, articles недоступен первые 2,5 с. После третьей неудачи подряд, в 0,2 с, автомат открывается, и до 1,2 с запросы получают заглушку. Пробные запросы в 1,2 и 2,2 с неудачны, и автомат открывается снова, а пробный запрос в 3,2 с успешен, и автомат закрывается. Из 40 запросов к gateway до articles дошло 13 вместо 40.
Данные
Репликация, шардирование, CAP и PACELC, кеширование и инвалидация, очереди и обратное давление
04
Требования
Масштабирование
Отказоустойчивость
Данные
Сопровождение
Проектирование
Изложение. Четвёртая часть посвящена данным: как масштабировать и защищать от отказов то, что хранит состояние, — базы, кеши и очереди.
Репликация
и отставание
Для физика
Метаданные запусков: запись — в главную, поиск и графики — с реплик.
01
Асинхронная
запись подтверждается сразу; реплика отстаёт, обычно меньше секунды
02
Синхронная
подтверждение после записи на реплику: медленнее, без потерь
03
Свои записи
после записи читать с главной или дождаться реплики
04
Переключение
реплику повышают до главной; асинхронная теряет хвост
Изложение. Базу данных нельзя запустить в нескольких экземплярах так же, как сервис без состояния: копии должны содержать одни и те же данные. Репликация решает это так: запись принимает одна главная база, а изменения по журналу — в PostgreSQL по журналу предзаписи WAL — передаются репликам, которые обслуживают чтение. Такая схема позволяет масштабировать чтение: если читают в сотни раз чаще, чем пишут, добавление реплик снимает нагрузку с главной. По умолчанию потоковая репликация PostgreSQL асинхронна: главная подтверждает запись, не дожидаясь реплики, и реплика отстаёт, обычно меньше чем на секунду. Отсюда два следствия. Чтение с реплики сразу после записи может не увидеть только что записанное, поэтому для гарантии «читать свои записи» такие чтения необходимо направлять в главную. При отказе главной реплику повышают до главной, и транзакции, не успевшие до неё дойти, теряются. Синхронная репликация подтверждает запись только после её сохранения на реплике — без потерь, но медленнее и с остановкой записи при недоступности реплики. Переключение выполняет автоматика, например Patroni для PostgreSQL, принимающая решение по кворуму.
Шардирование
по ключу
Ключ Плюс Минус
время интервал — с одного шарда вся запись — в последний
хеш номера запуска нагрузка равномерна интервал — со всех шардов
канал, затем время канал за интервал — вместе популярный канал перегружен
Важно
Смена ключа — перенос всех данных: ключ выбирают по главным запросам заранее.
01
Секционирование
сначала внутри одной базы: PARTITION BY RANGE
02
Шарды
части на разных машинах, когда одной не хватает
03
Перебалансировка
консистентный хеш переносит около 1/(N+1) ключей
04
Между шардами
соединения и транзакции дороже или невозможны
Изложение. Когда объём данных или поток записи превышает возможности одной машины, данные делят на части — шарды, каждая из которых хранится на своей машине со своими репликами. Шард определяется ключом, и выбор ключа является главным решением. Деление по диапазону времени удобно для архива измерений: выборка за интервал читается с одного шарда, но вся текущая запись приходится на последний. Хеш номера запуска распределяет нагрузку равномерно, но выборка за интервал времени требует обращения ко всем шардам. Составной ключ «канал, время» размещает данные одного канала вместе, но популярный канал перегружает свой шард. При добавлении шардов данные перераспределяются, и консистентное хеширование ограничивает перенос долей 1/(N + 1). Запросы и транзакции, затрагивающие несколько шардов, дороже или вовсе недоступны. Смена ключа означает перенос всех данных, поэтому ключ выбирают заранее по главным запросам. К шардированию приходят не сразу: секционирование таблицы в рамках одной базы — PARTITION BY RANGE в PostgreSQL или расширение TimescaleDB для временных рядов — решает многие задачи архива без распределённой системы.
CAP
и
PACELC
01
CAP
при разрыве сети (P) — отказ ради согласованности (C) или ответ, возможно устаревший (A)
02
PACELC
иначе (E) — выбор между задержкой (L) и согласованностью (C)
03
Кворум
N копий, запись на W, чтение с R: при R + W > N чтение видит последнюю запись
N, W, R = 3, 2, 2 assert R + W > N # множества пересекаются # запись: ждать 2 подтверждения из 3 # чтение: опросить 2 копии, взять новейшую
Для физика
График в архиве допускает отставание в секунду; номер нового запуска выдаёт одна главная база.
Изложение. Хранилище с несколькими копиями при разрыве сети неизбежно выбирает между согласованностью и доступностью. Теорема CAP, сформулированная Э. Брюером и доказанная С. Гилбертом и Н. Линч, утверждает: при разрыве сети, разделяющем копии, система либо отказывает части запросов, сохраняя согласованность, либо отвечает на все, рискуя выдать устаревшие данные. Разрыв сети является не выбором, а условием эксплуатации, поэтому выбор делается между C и A. Расширение PACELC, предложенное Д. Абади, добавляет обычный режим: и без разрыва синхронное подтверждение от нескольких копий увеличивает задержку, а асинхронное допускает расхождение. Кворумы делают выбор настраиваемым: из N копий запись ждёт подтверждения W, чтение опрашивает R, и при R + W > N множества пересекаются, так что чтение видит последнюю подтверждённую запись. Так устроены Cassandra и подобные системы с уровнями согласованности ONE, QUORUM и ALL. Практический вывод для физика: для каждой части данных необходимо решить, допустимо ли устаревание и насколько. График канала может отставать на секунду, а номер нового запуска должен быть единственным, поэтому его выдаёт одна главная база.
Кеширование
и инвалидация
def get_run(run_id): key = f'run:{run_id}' if (v := cache.get(key)) is not None: return v # попадание v = db.fetch_run(run_id) # промах cache.set(key, v, ex=60) # срок 60 с return v def update_run(run_id, data): db.update_run(run_id, data) cache.delete(f'run:{run_id}') # сброс
01
Где
браузер, CDN, nginx, память процесса, Redis
02
Срок жизни
ограничивает устаревание, если сброс не дошёл
03
Лавина промахов
ключ истёк — все идут в базу: нужен один загрузчик
04
Холодный старт
база выдерживает пустой кеш, или кеш прогревают
Изложение. Кеш хранит результаты дорогих операций ближе к потребителю, и уровней у него несколько: браузер и CDN, кеш nginx, память процесса приложения, общий кеш, например Redis. Распространённая схема — cache-aside: приложение сначала ищет значение в кеше, при промахе читает базу и помещает результат в кеш со сроком жизни, а при записи обновляет базу и удаляет ключ из кеша. Удаление при записи называют инвалидацией, и она является самым трудным местом схемы: сброс может не дойти, а чтение, начавшееся до записи, может вернуть в кеш устаревшее значение уже после сброса, поэтому срок жизни ограничивает время, в течение которого ошибка видна. Вторая опасность — лавина промахов: когда истекает популярный ключ, все экземпляры одновременно идут за ним в базу. Защиту обеспечивает один загрузчик на ключ, результата которого ждут остальные, или выдача устаревшего значения на время обновления; в nginx это директивы proxy_cache_lock и proxy_cache_use_stale. Третья — холодный старт: после перезапуска кеша вся нагрузка приходится на базу, и база должна её выдерживать, иначе кеш прогревают постепенно. Локальный кеш процесса быстрее всех, но у каждого экземпляра он свой и теряется при перезапуске.
Демонстрация:
кеш перед базой
01
Метод get: попадание, промах, один загрузчик под замком
02
lru_cache перед «базой» с чтением за 200 мс
03
Запись 12 А в базу: кеш без сброса и после сброса
04
50 одновременных запросов после истечения срока жизни
Что наблюдается
Чтение — 200,1 мс, повтор — 0,0 мс; без сброса кеш отдаёт 10 А вместо 12; без защиты 50 обращений к базе, с одним загрузчиком — одно.
root@lab — запись со стенда
Демонстрация. Скрипт cache.py моделирует базу словарём с током квадруполя Q1, чтение которого занимает 200 мс. Команда sed показывает метод get кеша со сроком жизни: попадание возвращается сразу, при промахе без защиты каждый поток идёт в базу сам, а в режиме одного загрузчика потоки ждут замок и после него проверяют кеш повторно. Функция с декоратором lru_cache первое чтение выполняет за 200,1 мс, второе — за 0,0 мс; cache_info подтверждает одно попадание и один промах. Затем в базу записывается 12,0 А: кеш без сброса продолжает отдавать 10,0 А, а после удаления ключа — 12,0 А. Наконец, срок жизни ключа истекает, и 50 потоков одновременно запрашивают его: без защиты в базу идут все 50, с одним загрузчиком — один; время в обоих случаях 0,21 с.
Очереди
и обратное давление
Обработчики
2 по 30 мс: около 66/с
данные
давление
полна: ждать или отклонить
01
Развязка
источник и обработчики работают в своём темпе, пик сглаживается
02
Предел
очередь без предела накапливает перегрузку до исчерпания памяти
03
Давление или сброс
полная очередь замедляет источник или отклоняет: 429, 503
04
Хотя бы один раз
брокер доставляет повторно: обработчик идемпотентен
Для физика
Измерения сбрасывать нельзя: буфер на диске и давление на оцифровщик. Запросы просмотра при перегрузке можно отклонить.
Изложение. Очередь между частями системы развязывает их во времени: источник отдаёт работу и продолжает свою, обработчики разбирают её в своём темпе, а кратковременный пик нагрузки сглаживается. Между процессами и машинами очередь реализует брокер сообщений — Kafka, RabbitMQ, Redis Streams. Важнейшим параметром очереди является предел длины. Неограниченная очередь не защищает от перегрузки, а откладывает её проявление: если поток стабильно выше ёмкости обработчиков, очередь и время ожидания растут без предела, пока не кончится память. Полная ограниченная очередь оставляет два допустимых варианта. Обратное давление: источник ждёт места и сам замедляется до ёмкости обработчиков; этот вариант позволяет сохранить все данные, но требует, чтобы источник умел ждать. Сброс нагрузки: лишнее отклоняется сразу, например ответом 429 или 503, и источник сохраняет темп ценой потерь. Брокеры обычно гарантируют доставку «хотя бы один раз»: после сбоя сообщение может прийти повторно, поэтому обработчик делают идемпотентным. Для системы сбора данных измерения сбрасывать нельзя — нужен буфер на диске и давление на источник, — а запросы просмотра графиков при перегрузке можно отклонить.
Демонстрация:
очередь с пределом
01
Источник: 100 измерений в секунду в течение 3 с
02
Два обработчика по 30 мс: ёмкость около 66 в секунду
03
Очередь без предела, maxsize=10 с ожиданием и со сбросом
Что наблюдается
Без предела очередь растёт до 108, ожидание 1,74 с; с давлением выдача растянулась до 4,6 с; со сбросом потеряно 98 измерений.
root@lab — запись со стенда
Демонстрация. Скрипт queue_demo.py на asyncio моделирует систему сбора: источник выдаёт 100 измерений в секунду в течение 3 с, два обработчика тратят по 30 мс на измерение, откуда получим ёмкость около 66 измерений в секунду. Команда sed показывает два способа поместить измерение в очередь: put_nowait со сбросом при переполнении и await put, ожидающий места. Неограниченная очередь принимает всё: источник укладывается в 3,0 с, но длина очереди по секундам — 36, 71 и 108, максимальное ожидание измерения — 1,74 с, а последнее обработано через 4,8 с. Очередь из десяти элементов с ожиданием передаёт давление источнику: выдача растягивается до 4,6 с, очередь всё время полна, но ожидание не превышает 0,20 с и ничего не потеряно. Та же очередь со сбросом сохраняет темп источника, 3,0 с, и короткое ожидание, 0,16 с, ценой 98 сброшенных измерений из 300.
Сопровождение
Журналы, метрики и трассировки, монолит и микросервисы, сетка сервисов, защита от перегрузки
05
Требования
Масштабирование
Отказоустойчивость
Данные
Сопровождение
Проектирование
Изложение. Пятая часть посвящена сопровождению работающей системы: наблюдаемости, выбору между монолитом и микросервисами, сетке сервисов и защите от перегрузки.
Журналы, метрики
и трассировки
Сигнал Вопрос Инструменты
журналы что случилось с этим запросом? logging, Loki, Elasticsearch
метрики сколько, как быстро, сколько ошибок? Prometheus, Grafana
трассировки где запрос провёл время? OpenTelemetry, Jaeger
proxy_set_header X-Request-ID $request_id; log_format rid '$request_id $status $request_time'; access_log /var/log/nginx/access.log rid;
01
Четыре сигнала
задержка, трафик, ошибки, насыщение
02
Оповещение
о симптоме — расходе бюджета ошибок, а не о загрузке CPU
03
Расход бюджета
2 % за час — вызов дежурного; 10 % за трое суток — заявка
Изложение. Наблюдаемость — возможность по внешним сигналам понять, что происходит внутри системы. Журналы отвечают на вопрос, что случилось с конкретным запросом, и нужны для расследования. Метрики — числа во времени: запросы в секунду, доля ошибок, перцентили задержки, заполнение диска; по ним строят графики и оповещения. Трассировки показывают путь одного запроса через все сервисы и время на каждом участке. Связь между ними обеспечивает идентификатор запроса: nginx на входе вычисляет переменную $request_id и передаёт её приложению заголовком, и все записи о запросе в журналах всех сервисов содержат одно значение; стандартный формат передачи контекста трассировки — заголовок traceparent из рекомендации W3C Trace Context, реализуемый библиотеками OpenTelemetry. Книга Google SRE рекомендует для каждого сервиса следить за четырьмя сигналами: задержкой, трафиком, ошибками и насыщением. Оповещение должно требовать действия человека и срабатывать по симптому — по скорости расходования бюджета ошибок, а не по загрузке процессора: при SLO 99,9 % дежурного вызывают, если за час израсходовано 2 % месячного бюджета или за шесть часов — 5 %, а заявку заводят, если за трое суток израсходовано 10 %.
Монолит
и микросервисы
Монолит Микросервисы
выкатка целиком по сервису
масштабирование целиком по сервису
отказ части общий процесс частичный, через сеть
данные одна транзакция согласование между базами
команды одна несколько независимых
цена старта низкая сеть, наблюдаемость, выкатка
Пояснение
Модульный монолит — чёткие границы модулей внутри одной программы; сервис выделяют, когда граница мешает выкатке или масштабированию.
Для физика
Группа из пяти человек с одной установкой: монолит с модулями сбора, хранения и веб-интерфейса.
Изложение. Монолит представляет собой одну программу, которая выкатывается и масштабируется целиком; микросервисы — набор небольших сервисов, каждый со своей базой, выкатываемых и масштабируемых независимо. Выбор между ними — вопрос организации команды не меньше, чем техники. Монолит дешевле на старте: вызов функции вместо сетевого запроса, одна транзакция вместо согласования нескольких баз, одна выкатка. Его проблемы появляются с ростом: десяток команд мешает друг другу в одном коде, а выкатка целого становится рискованной. Микросервисы снимают эти проблемы ценой всего, о чём говорилось в третьей части: частичных отказов, тайм-аутов и повторов, а также затрат на наблюдаемость и согласование данных. Распространённый совет — начинать с модульного монолита, в рамках которого модули имеют чёткие интерфейсы, и выделять сервис, когда граница модуля действительно мешает выкатке или масштабированию. Известен и обратный путь: в 2023 году команда Prime Video перенесла мониторинг качества видео из набора распределённых компонентов в одно приложение и сократила затраты на инфраструктуру более чем на 90 %.
Service mesh:
сетка сервисов
Control plane
адреса, правила, сертификаты
data plane: посредник рядом с каждым экземпляром
01
Data plane
балансировка, повторы, тайм-ауты, mTLS — вне кода сервиса
02
Control plane
раздаёт посредникам конфигурацию и сертификаты
03
Цена
два посредника на каждый вызов, CPU и память на каждом узле
Пояснение
Оправдана при десятках сервисов и нескольких командах; трём сервисам хватает библиотеки и nginx.
Изложение. Когда сервисов десятки, тайм-ауты, повторы, балансировку, шифрование и метрики неудобно реализовывать в каждом из них. Сетка сервисов, service mesh, позволяет вынести эти функции из кода в посредника, который запускается рядом с каждым экземпляром, — это data plane. Сервис обращается к соседу через локального посредника, а тот выбирает экземпляр, повторяет, соблюдает тайм-ауты, шифрует соединение взаимным TLS и собирает метрики. Control plane знает топологию системы и раздаёт посредникам адреса экземпляров, правила и сертификаты. Первой реализацией был Linkerd в 2016 году, наиболее известной является Istio на основе посредника Envoy, с 2020 года управляемая одним процессом istiod. Цена — два посредника на пути каждого вызова и их потребление ресурсов: по данным проекта Istio, около 0,2 vCPU и 60 МБ памяти на тысячу запросов в секунду; режим ambient заменяет посредника у каждого экземпляра общим посредником на узле. Отказ control plane не останавливает работающих посредников: они продолжают с последней полученной конфигурацией. Для трёх-пяти сервисов сетка избыточна: для тех же задач достаточно библиотеки клиента и nginx.
Защита
от перегрузки
limit_req_zone $binary_remote_addr zone=perip:10m rate=5r/s; location / { limit_req zone=perip burst=10 nodelay; limit_req_status 429; proxy_pass http://app; }
Пояснение
Ведро с темпом r и запасом b: запрос сверх темпа и запаса отклоняется кодом 429.
01
Ограничение частоты
по адресу или ключу API, до дорогой обработки
02
429 и Retry-After
клиент понимает, что ждать, а не что сервер отказал
03
Сброс нагрузки
отклонить часть сразу лучше, чем ответить всем медленно
04
Объёмные атаки
отсекают до сервиса: провайдер, CDN, anycast
Изложение. Перегрузка приходит не только от атаки: наплыв пользователей после публикации данных, собственный нагрузочный тест, запущенный не на том сервере, скрипт коллеги без паузы между запросами. Ограничение частоты позволяет отклонить запросы сверх заданного темпа до дорогой обработки. В nginx его реализует модуль limit_req методом «дырявого ведра»: каждый клиент, определяемый ключом, например адресом, получает темп r запросов в секунду и запас b на залп; с параметром nodelay такое ведро ведёт себя как ведро жетонов, пропуская залп сразу. По умолчанию nginx отвечает на лишние запросы кодом 503, который клиент примет за отказ сервера, поэтому код необходимо заменить на 429 Too Many Requests; заголовок Retry-After, подсказывающий, когда повторить, limit_req сам не добавляет — его задают директивой add_header с параметром always. Сброс нагрузки — тот же принцип внутри сервиса: перегруженный сервис отклоняет часть запросов сразу и вовремя отвечает остальным, а не всем — с задержкой в секунды. От объёмных атак на канал ограничение частоты в nginx не защищает: их отсекают до сервиса — у провайдера, в CDN, распределением адреса через anycast; межсетевые экраны прикладного уровня отклоняют запросы с признаками атак, а капча отделяет автоматические запросы от людей.
Демонстрация:
ограничение частоты
01
rl.conf: 5 запросов/с с адреса, запас 10, код 429
02
Три экземпляра за nginx в контейнере
03
Залп из 30 одновременных запросов
04
Поток 20 запросов/с в течение 4 с
Что наблюдается
Из залпа прошло 11, 19 получили 429; в потоке — 15 в первую секунду и по 5 в каждую следующую, как задано темпом.
root@lab — запись со стенда
Демонстрация. Конфигурация rl.conf ограничивает каждый адрес клиента пятью запросами в секунду с запасом на залп в десять запросов; nodelay пропускает запросы залпа сразу, а limit_req_status заменяет код отказа 503 на 429. Скрипт rate.py на httpx сначала отправляет 30 запросов одновременно: 11 получают ответ 200 — один в счёт темпа и десять в счёт запаса, — а 19 получают 429. Через 3 с, когда запас восстановился, тот же скрипт отправляет поток 20 запросов в секунду в течение 4 с: в первую секунду проходит 15 запросов, во вторую, третью и четвёртую — ровно по 5, остальные по 15 в секунду отклоняются. Журнал ошибок nginx содержит 69 записей limiting requests — по одной на каждый отклонённый запрос, 19 из залпа и 50 из потока.
Сопровождаемость
и простота
01
Простота
компонент добавляют, когда без него измеримо хуже
02
Знакомые технологии
новую сначала проверяют под нагрузкой на тестовом контуре
03
Выкатка и откат
постепенно, с оповещением, откат одной командой
04
Изоляция контуров
тест не пишет в рабочую базу; группы — на своих экземплярах
Для физика
Новая версия программы сбора — сначала на стенде с моделью установки, затем в смену с готовым откатом.
Важно
Зеркалирование рабочих запросов в тест повторяет и запросы с побочным действием: письма, списания, команды оборудованию.
Изложение. Сопровождаемость — свойство системы, позволяющее изменять её и управлять ею целиком без чрезвычайных усилий. Первое правило — простота: каждый компонент является ещё одним источником отказов и требует обновления, поэтому кеш, очередь или второй тип базы добавляют, когда без них измеримо хуже, а не на будущее. Второе — знакомые технологии: новую базу или брокер сначала проверяют под нагрузкой на тестовом контуре и только затем вводят в работу. Третье — управляемая выкатка: конвейер CI/CD, постепенная выкатка с оповещениями о её ходе и откат одной командой, рассмотренные в главе о жизненном цикле ПО. Четвёртое — изоляция контуров: тестовый контур не пишет в рабочую базу, а разные группы пользователей или установки обслуживаются разными экземплярами, чтобы ошибка или наплыв в одном контуре не затрагивали другой. Зеркалирование рабочих запросов в тестовый контур помогает проверить новую версию, но опасно для запросов с побочным действием — писем, списаний, команд оборудованию.
Проектирование
Система сбора данных установки: требования, оценки, схема, отказы и реакции
06
Требования
Масштабирование
Отказоустойчивость
Данные
Сопровождение
Проектирование
Изложение. Шестая часть применяет сказанное к задаче физика: шаг за шагом проектируется система сбора данных установки.
Задача:
система сбора данных
01
Функции
приём с оцифровщиков, архив, выборка канала за интервал, графики
02
Поток
200 каналов × 10 кГц × 2 байта — непрерывно
03
Сохранность
ни одного потерянного измерения при отказе одной машины
04
Чтение
20 сотрудников и их скрипты: до 50 запросов/с, p99 выборки меньше 1 с
rate = 200 * 10_000 * 2 # байт/с day = rate * 86_400 / 1e12 # ТБ в сутки year = day * 365 # ТБ в год print(rate / 1e6, round(day, 2), round(year)) # 4.0 0.35 126
Пояснение
Две копии архива — 252 ТБ в год; сжатие и прореживание старых данных — тоже требования.
Изложение. Заключительная часть применяет сказанное к задаче, знакомой физику: системе сбора данных установки. Функциональные требования: принимать данные с оцифровщиков, хранить их в архиве, выдавать выборку канала за интервал времени и строить графики. Запишем нефункциональные требования числами. Поток: 200 каналов по 10 кГц и 2 байта на отсчёт дают 4 МБ/с непрерывной записи, около 0,35 ТБ в сутки и 126 ТБ в год, а с двумя копиями — 252 ТБ. Отсюда сразу следуют требования к дискам и к сети между машинами, а также вопрос о сжатии и прореживании старых данных. Сохранность: ни одно измерение не теряется при отказе одной машины — это требование важнее скорости чтения. Чтение: двадцать сотрудников и их скрипты, до пятидесяти запросов в секунду в пике, p99 выборки канала меньше секунды, доступность чтения 99,9 % за 30 дней. Приближённой оценки до выбора технологий достаточно, чтобы отсеять неподходящие решения: например, хранение каждого отсчёта отдельной строкой реляционной таблицы означало бы почти 173 млрд строк в сутки.
Схема
системы
Приёмники ×2
буфер на диске
Писатели ×2
файлы по часам
Метаданные
PostgreSQL и реплика
Сотрудники
скрипты, браузер
4 МБ/с
50 запросов/с
Изложение. Схема разделяет путь записи и путь чтения, потому что требования к ним разные. Рассмотрим сначала путь записи. Он начинается с оцифровщиков, которые отправляют данные двум приёмникам. Каждый приёмник сначала пишет поток в буфер на локальном диске, чтобы пережить кратковременный отказ следующих звеньев, и затем передаёт его в очередь — брокер сообщений на трёх узлах, хранящий сообщения с репликацией и переживающий отказ одного узла. Писатели забирают данные из очереди, складывают их в файлы HDF5 по часам и каналам в архив с двумя копиями и регистрируют каждый файл в метаданных; запись идемпотентна, поэтому повторная доставка сообщения не создаёт дубликатов. Метаданные — запуски, каналы, интервалы и пути к файлам — хранятся в PostgreSQL с репликой. Путь чтения начинается с сотрудников и их скриптов, которые обращаются к nginx; nginx ограничивает частоту запросов и распределяет их между двумя экземплярами API на FastAPI без состояния. API находит файлы по метаданным, читает нужные интервалы из архива и помещает построенные графики в кеш. Метрики и журналы всех компонентов собираются отдельно и на схеме не показаны.
Отказы
и реакции
Отказ Реакция системы Требование
приёмник приём вторым приёмником, буфер в оцифровщике сохранность
узел очереди работа кворума из двух оставшихся узлов сохранность
писатель ожидание данных в очереди, второй писатель сохранность
заполнение диска оповещение за 7 суток по прогнозу сохранность
главная база повышение реплики, пауза регистрации файлов доступность
экземпляр API исключение в nginx, повтор GET на соседе доступность
наплыв запросов ответ 429 от limit_req, графики из кеша задержка
Для физика
Проверка схемы — опыт: нагрузочный тест, остановка экземпляра, переполнение очереди, как в демонстрациях лекции.
Изложение. Последний шаг проектирования — пройти по схеме и для каждого компонента ответить на вопрос, что произойдёт при его отказе и какое требование при этом окажется под угрозой. Отказ приёмника не теряет данных: второй приёмник продолжает приём, а оцифровщик хранит данные в буфере на время переключения. Отказ одного узла очереди переживается кворумом из двух оставшихся. Отказ писателя задерживает появление данных в архиве, но не теряет их: они ждут в очереди, пока их не заберёт второй писатель. Заполнение диска является медленным отказом, и о нём оповещают заранее по прогнозу заполнения. Отказ главной базы метаданных приостанавливает регистрацию новых файлов до повышения реплики, а чтение продолжается. При отказе экземпляра API nginx повторяет запросы GET на соседнем экземпляре. Наплыв запросов чтения ограничивается limit_req, а графики отдаются из кеша. Каждая реакция проверяется опытом — нагрузочным тестом, остановкой экземпляра, переполнением очереди, — так же, как в демонстрациях лекции. Наблюдаемость замыкает проект: индикаторами служат задержка поступления измерений в архив, длина очереди, доля успешных и быстрых чтений, а оповещения срабатывают по расходу бюджета ошибок и по прогнозу заполнения дисков. Отметим, что две копии архива защищают от отказа диска и машины, но не от ошибочного удаления или порчи файлов программой: вторая копия повторит ошибку. Поэтому архив дополняют резервной копией на другой площадке с регулярной проверкой восстановления, а требования к ней записывают числами — допустимой потерей данных (RPO) и временем восстановления (RTO). Таким образом, пройденный порядок универсален: требования, оценки, схема, отказы и наблюдаемость.
Итоги
лекции
01
Требования записываются числами: запросы в секунду, перцентиль задержки, SLO и бюджет ошибок.
02
Горизонтальное масштабирование требует сервисов без состояния и выбора алгоритма балансировки.
03
Отказ переживают резервирование, проверки работоспособности, тайм-ауты с дедлайном и повторы с бюджетом.
04
Повторяют только идемпотентное; автомат защиты и заглушка дают зависимости восстановиться.
05
Реплики, шарды, кеш и очередь решают задачи данных, и у каждого — своя цена согласованности.
Изложение. Проектирование начинается с требований, записанных числами: нагрузки в запросах в секунду, перцентиля задержки, цели уровня обслуживания и бюджета ошибок. Условием горизонтального масштабирования является сервис без состояния, а алгоритм балансировки выбирают по характеру запросов: round robin для одинаковых, least connections для разной длительности, хеш для привязки. Отказы переживают резервирование с кворумом, проверки работоспособности, согласованные тайм-ауты или передача дедлайна и повторы с бюджетом и случайной паузой. Повторяют только идемпотентные операции, а автомат защиты и заглушка позволяют отказавшей зависимости восстановиться. Реплики, шарды, кеш и очередь решают задачи данных, и каждое решение имеет свою цену: отставание реплики, перенос данных при смене ключа, устаревание кеша, выбор между давлением и сбросом в очереди.
Упражнения
и задание
01
Бюджет ошибок
SLO 99,5 % своего сервиса: простой и неуспешные запросы за 30 дней
02
Нагрузочный тест
aload.py против своего сервиса: ёмкость и рост задержки за ней
03
Отказ экземпляра
три экземпляра за nginx: weight, least_conn, kill -STOP одного
04
Проект архива
оценки, схема и таблица отказов для данных своей установки
Пояснение
Итоговый проект: путь решения в документе первого этапа удобно описать требованиями с числами, оценками и схемой.
Для физика
Компонент добавляется, когда расчёт или замер показывает, что без него хуже.
Изложение. Упражнения служат самопроверке. Первое — записать SLO 99,5 % для собственного сервиса и вычислить допустимый простой и число неуспешных запросов за 30 дней. Второе — нагрузить свой сервис генератором aload.py, найти его ёмкость и проследить, как растёт задержка за её пределом. Третье — повторить демонстрацию с тремя экземплярами за nginx, добавить веса, сравнить round robin и least_conn и остановить один экземпляр сигналом STOP. Четвёртое — спроектировать архив данных своей установки: оценить поток и объём, нарисовать схему и составить таблицу отказов. В рамках итогового проекта курса по той же последовательности удобно описать предполагаемый путь решения в документе первого этапа: требования с числами, оценки нагрузки и объёма данных и схема.
Источники
1
Книга курса: главы «Системный дизайн», «От скрипта к приложению», «Компьютерные сети», «Веб-технологии», «Базы данных», «Работа с базами данных», «Docker», «Жизненный цикл ПО»
phys-dev.github.io/soft-dev-book
3
Beyer B. et al. Site Reliability Engineering. – O'Reilly, 2016; The Site Reliability Workbook. – 2018
sre.google/books
4
Kleppmann M., Riccomini C. Designing Data-Intensive Applications. – 2nd ed. – O'Reilly, 2026 (рус. пер. 1-го изд.: Высоконагруженные приложения. – Питер, 2018)
dataintensive.net
Изложение. Лекции соответствует глава книги курса «Системный дизайн»; близкие темы рассмотрены в главах «От скрипта к приложению», «Компьютерные сети», «Веб-технологии», «Базы данных», «Работа с базами данных», «Docker» и «Жизненный цикл ПО». Порядок изложения частично следует лекции «Архитектура и системный дизайн» Курсов информационных технологий 2024 года. Для самостоятельного изучения рекомендуются книги Google о практике эксплуатации — Site Reliability Engineering и The Site Reliability Workbook, откуда взяты понятия SLO, бюджета ошибок и бюджета повторов, книга М. Клеппмана и К. Риккомини о системах, работающих с данными, во втором издании 2026 года, документация nginx по балансировке и ограничению частоты, статья М. Брукера о тайм-аутах, повторах и случайной паузе и заметка М. Фаулера об автомате защиты.
Спасибо за внимание
Вопросы
phys-dev.github.io/soft-dev-book
Изложение. Надпись на последнем слайде — 99,9, значение цели уровня обслуживания, парное к надписи SLO на титуле: требование, записанное числом, с которого начинается проектирование и которым проверяется готовая система. Лекция завершается ответами на вопросы.