Глава 1 Надежные, масштабируемые и удобные в сопровождении приложения

Масштабируемость

Книга
Высоконагруженные приложения. Программирование, масштабирование, поддержка

Designing Data-Intensive Applications: The Big Ideas Behind Reliable, Scalable, and Maintainable Systems

Главная книга для тех, кто хочет научиться создавать крупные, быстрые и надежные ИТ-системы вроде Telegram, Netflix или крупных интернет-магазинов.

Что такое масштабируемость

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

2023 100 пользователей
2024 1 000 пользователей
2025 1 000 000 пользователей
2026 10 000 000 пользователей

Система продолжит работать нормально — или начнёт тормозить и падать? И во сколько нам обойдётся ответ «нормально»? Система может отлично держать миллион мелких запросов и падать на десятке тяжёлых, каждый из которых перемалывает гигабайты. Поэтому «масштабируемая» без указания нагрузки — пустое слово.

Краткая идея автора

Клеппман не даёт рецепта «сделай так — и будет масштабируемо». Он даёт порядок рассуждения, и весь раздел построен по нему.

  1. 01 Система перестаёт справляться

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

  2. 02 Описать нагрузку

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

  3. 03 Описать производительность

    Производительность — это насколько хорошо система справляется с этой работой. В пакетной обработке смотрят на пропускную способность, в онлайн-системах — на время отклика, причём не среднее, а по процентилям.

  4. 04 Выбрать стратегию и архитектуру

    И только теперь выбираем: расти вверх (машина мощнее) или вширь (машин больше), что делать с состоянием, где кэшировать и что можно потерять. Архитектура вырастает из нагрузки, а не наоборот.

Дальше в докладе
Нагрузка

Параметры нагрузки и то, как их выбирают. Разбираем на примере Twitter — там разница между «правильным» и «неправильным» параметром видна лучше всего.

Производительность

Пропускная способность и время отклика, почему среднее обманывает и зачем нужны процентили — вплоть до хвоста в 0,1 % запросов.

Масштабирование

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

Шаг первый: описать нагрузку

Параметр нагрузки

Число, которое описывает, сколько работы система должна выполнять. Обычно их несколько.

запросов в секунду Сколько запросов сервер получает каждую секунду. Классический параметр веб-сервиса — и почти всегда не единственный. соотношение чтений и записей На одну запись сто чтений или наоборот. От этого зависит, где кэшировать и что готовить заранее. одновременно активных пользователей Сколько людей работают с системой прямо сейчас, а не сколько зарегистрировано за всё время. объём данных Сколько всего лежит в хранилище и как быстро растёт: миллион записей и миллиард обслуживаются по-разному. доля попаданий в кэш Какая часть запросов не доходит до базы. Падение на несколько процентов умножает нагрузку на базу в разы. объём переданных данных Мегабайты на запрос. Там, где отдают видео или файлы, предел определяют они, а не число запросов. адресатов у одного сообщения Сколько получателей у одной отправки. Именно этот параметр ломает соцсети и мессенджеры — дальше в докладе.
Видеосервис
  • просмотров в секунду
  • загрузок видео
  • объём переданных данных
  • активных пользователей
трафик важнее запросов
База данных
  • запросов в секунду
  • соотношение чтений и записей
  • размер запроса
  • объём хранимых данных
соотношение важнее объёма
Чат
  • активных пользователей
  • сообщений в секунду
  • сколько адресатов у сообщения
доставка важнее отправки
Мартин Клеппман

Сначала описываем нагрузку — и только потом смотрим, как система при ней себя ведёт. Выбрать не тот параметр — значит промахнуться мимо реальной проблемы.

Пример из книги: Twitter

Числа Twitter из книги. Кажется, что главная нагрузка здесь — публикация твитов. На самом деле нет.

4 600 /с твитов в среднем
12 000+ /с твитов в пике
300 000 /с запросов на просмотр домашней ленты
Один твит одна операция записи fan-out Лента подписчика Лента подписчика Лента подписчика Лента подписчика …и ещё десятки таких же × 75 подписчиков у среднего пользователя

12 000 записей в секунду современная система переварит. Проблема в другом: один твит нужно доставить всем подписчикам автора — операция размножается на выходе. Это разветвление и называют fan-out.

Два способа собрать ленту

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

подход 01 Собирать при чтении

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

твиты Ани твиты Бориса твиты Васи Слияние по времени на каждое открытие ленты Лента
публикация дёшево одна запись в общее хранилище
чтение ленты дорого сборка из всех подписок заново
подход 02 Готовить при записи

В момент публикации система смотрит, кто подписан на автора, и кладёт твит в персональную ленту каждого подписчика. Открытие ленты — чтение готового списка.

Твит Разложить по лентам один раз, при публикации лента Ани лента Бориса лента Васи
публикация дорого запись в ленту каждого подписчика
чтение ленты дёшево список уже готов, просто отдать

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

Почему выгоднее платить при записи

Потому что у Twitter записей несоизмеримо меньше, чем чтений — и это свойство нагрузки выгодно использовать.

4 600 /с твитов публикуется
× 75 подписчиков у среднего пользователя
≈ 345 000 /с записей в ленты подписчиков
345 тысяч записей в секунду — это много?

Много. Но взамен исчезает работа на чтении: 300 000 запросов ленты в секунду превращаются в 300 000 выборок готового списка. Один раз посчитать при записи дешевле, чем пересчитывать на каждом из чтений.

подход 02 из предыдущего слайда
Принцип

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

приём общего назначения, а не рецепт для соцсетей

Вот зачем нужен был шаг «описать нагрузку»: решение принимает не архитектурный вкус, а соотношение чисел — 4 600 записей против 300 000 чтений.

Пока не появляется знаменитость

Среднее — 75 подписчиков. У аккаунтов из верхнего ряда их сотни миллионов, и один твит такого автора превращается в десятки миллионов записей, которые Twitter хотел доставлять примерно за пять секунд.

Новый твит Сколько у автора подписчиков? Обычные пользователи десятки подписчиков — записей мало Знаменитости миллионы подписчиков — записей не унести подход 02 · готовить при записи push: разложить твит по лентам подписчиков подход 01 · собирать при чтении pull: подмешать твит в ленту при открытии правильная архитектура = гибрид 02 для всех + 01 для немногих

Среднее число подписчиков прячет крайние случаи. Архитектуру определяет распределение параметра нагрузки, а не его среднее — поэтому в итоге в Twitter живут оба подхода сразу.

Шаг второй: описать производительность

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

Пакетная обработка

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

throughput
Онлайн-системы

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

response time
latency Ожидание в очереди запрос уже пришёл, но им ещё не занялись
service time Обработка то самое время, которое видно в логах сервера
network Сеть и передача ответа дорога до сервера и обратно к клиенту

Response time — всё это вместе, то есть ровно то, что видит клиент.

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

Одно среднее число ничего не описывает

99 запросов по 10 мс и один на 10 секунд дают «в среднем 110 мс» — число, которого не видел ни один пользователь. Поэтому время отклика описывают распределением и процентилями.

100 мс 1 с 10 с
p50 p95 p99
1-й запрос 100-й запрос
среднее
p50 · медиана
p95
p99

Хвост — не мелочь: при 10 млн запросов 1 % — это 100 000 медленных ответов, и достаются они чаще самым активным клиентам, у которых больше всего данных. В Amazon считали, что лишние 100 мс времени отклика — это около −1 % продаж.

Откуда берётся хвост

Медленный запрос редко медленный сам по себе — чаще он просто стоял в очереди за чужим.

Блокировка головы очереди
A B — 900 мс C D время обработки одним потоком C и D ждут ≈ 940 мс

A, C и D — по 40 мс каждый. Но обрабатываются они по очереди, и всё, что встало за медленным B, ждёт вместе с ним. Один тяжёлый запрос делает медленными несколько лёгких — и все они попадают в хвост.

Нагрузочный тест, который врёт

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

  • в системе одновременно оказывается меньше запросов, чем в реальности
  • очередей почти нет — значит, нет и хвоста
  • система выглядит быстрее, чем есть на самом деле

Генератор нагрузки должен слать запросы независимо от того, ответили ли на предыдущие.

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

В распределённых системах хвост усиливается

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

1,0 %
1
5
10
20
50
100

На сотне вызовов быстрыми окажутся все сразу лишь в 0,99100 ≈ 36,6 % случаев — значит, в 63,4 % пользовательский запрос упрётся хотя бы в один медленный. Это и есть усиление хвостового времени ожидания.

Как это измерять
  • скользящее окно последних 10 минут: каждую минуту считаем p50, p95, p99, p999 по запросам за окно
  • строим график и смотрим не на число, а на его изменение
  • p50 стабилен, а p99 пополз вверх — обычным пользователям хорошо, небольшой доле стало заметно плохо
Процентили нельзя усреднять

p99 сервера A = 100 мс, p99 сервера B = 300 мс. Общий p99 — не 200 мс: так математика не работает. Складывать нужно распределения — гистограммы, а не готовые числа.

forward decay · t-digest · HdrHistogram

Шаг третий: чем масштабировать

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

Вертикальноеvertical scaling · scale up

4 CPU 16 ГБ 32 CPU 128 ГБ

Та же одна машина, только мощнее.

Плюсы

  • архитектура остаётся простой
  • нет распределённых проблем
  • проще эксплуатация

Минусы

  • мощные машины дорожают непропорционально
  • есть физический предел
  • одна машина — точка отказа

Горизонтальноеhorizontal scaling · scale out · shared-nothing

балансировщик сервер сервер сервер сервер

Много обычных машин, нагрузка распределяется между ними.

Плюсы

  • растём по мере надобности
  • отказ одной машины не убивает систему
  • для stateless-сервиса — почти бесплатно

Минусы

  • состояние приходится синхронизировать
  • репликация и согласованность
  • отказы становятся штатной ситуацией

Разница вся в состоянии. Stateless-сервис размножается почти даром — запрос можно отдать любому серверу. Но если сервер помнит корзину пользователя, соседний о ней не знает, и появляются репликация, согласованность, обработка отказов. Поэтому базы данных так долго старались держать на одной машине; инструменты стали лучше, но распределённость сама по себе не цель.

Главный вывод раздела

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

Система A

100 000 запросов в секунду, каждый по 1 КБ

  • огромное количество запросов
  • время отклика и параллельность
  • процессор, сеть, очереди
≈ 100 МБ/с
Система B

3 запроса в минуту, каждый по 2 ГБ

  • объём памяти
  • последовательное чтение с диска
  • дисковая пропускная способность
≈ 100 МБ/с

Пропускная способность одинаковая — около 100 МБ/с у обеих. Архитектуры будут совершенно разными, потому что нагрузка у них разная по характеру, а не по объёму.

Мартин Клеппман

Масштабируемость — не свойство системы, а ответ на вопрос: что мы сделаем, когда конкретный параметр нагрузки вырастет в десять раз?

вывод по разделу 1.3 книги «Высоконагруженные приложения» Мартина Клеппмана

Что далее

Надежность Пройдено
Артём Никифоров
Масштабируемость Пройдено
Антон Помазков
Удобство сопровождения
Артём Никифоров