Перейти к содержимому
TelegramGitHub

Designing Data-Intensive Applications, глава 1: Надёжность, масштабируемость, сопровождаемость

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

Три источника сбоев (hardware, software, human) и способы снизить риск

Большинство современных приложений ограничены не мощностью CPU, а объёмом, сложностью и скоростью изменения данных. Их собирают из стандартных блоков:

  • базы данных (database): хранят данные;
  • кэши (cache): помнят результат дорогой операции;
  • поисковые индексы (search index): поиск по ключевым словам и фильтры;
  • потоковая обработка (stream processing): асинхронные сообщения;
  • пакетная обработка (batch processing): периодический разбор больших объёмов данных.

Границы между категориями размываются. Redis работает как хранилище и как очередь. Kafka даёт гарантии сохранности, как база данных.

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

Надёжность значит: система продолжает работать правильно, даже когда что-то идёт не так.

Fault — отклонение одного компонента от спецификации. Failure — когда вся система перестала обслуживать пользователей. Нельзя сделать вероятность fault нулевой. Нужно не дать fault превратиться в failure. Системы, которые это умеют, называют fault-tolerant.

Книга советует иногда вызывать сбои нарочно. Пример: Netflix Chaos Monkey случайно убивает процессы. Так механизмы восстановления проверяются постоянно.

Тип faultОсобенностиЧто помогает
HardwareСлучайные и независимые. В кластере из 10 000 дисков в среднем один диск в день выходит из строяRedundancy (RAID, второй блок питания), отказоустойчивость на уровне ПО
SoftwareСистемные, связаны между узлами, долго “спят”. Пример: баг с leap second в 2012 годуТесты, изоляция процессов, рестарт, мониторинг, самопроверки
HumanОшибки конфигурации часто главная причина простоевСм. список ниже

Против human errors помогает комбинация мер:

  • удобные API и интерфейсы, где легко сделать правильно;
  • sandbox-среды с реальными данными, где можно экспериментировать;
  • тесты на всех уровнях;
  • быстрый откат конфигурации и постепенный rollout кода;
  • возможность пересчитать данные, если старый расчёт был неверным;
  • подробный мониторинг (telemetry).

Современные облака (например, AWS) не обещают, что виртуальная машина проживёт долго. Поэтому растёт доля систем, которые переживают потерю целой машины. Бонус: можно обновлять узлы по одному без простоя.

Scalability — способность системы справляться с ростом нагрузки. Нельзя сказать “X масштабируем”. Нужно спросить: если нагрузка вырастет вот так, какие у нас варианты?

Нагрузку описывают несколькими числами. Это могут быть запросы в секунду, доля чтений и записей, число активных пользователей, hit rate кэша. Лучший выбор зависит от архитектуры.

Пример из книги: Twitter. Два действия: post tweet (в среднем 4,6 тыс. запросов в секунду) и home timeline (300 тыс. запросов в секунду). Проблема не в объёме твитов, а в fan-out: у каждого пользователя много подписчиков.

Три подхода к home timeline: fan-out on read, fan-out on write и гибрид

ПодходЗаписьЧтениеПроблема
Fan-out on readОдна вставка в общую таблицуJOIN по подпискам, слияние по времениЧтений слишком много
Fan-out on writeКопия твита в timeline-кэш каждого подписчикаДёшево, уже посчитаноУ пользователя с миллионами подписчиков одна запись даёт миллионы записей
ГибридОбычные пользователи: on writeЗнаменитости подмешиваются при чтенииСложнее, но стабильная скорость

Вывод: ключевой load parameter здесь не число твитов, а распределение числа подписчиков.

Есть два вопроса:

  • Если нагрузка растёт, а ресурсы те же, что будет с производительностью?
  • Сколько ресурсов добавить, чтобы производительность осталась прежней?

Для batch-систем (Hadoop) важен throughput: записей в секунду или время выполнения job. Для онлайн-систем важно response time: время от запроса до ответа клиента. Latency — это только время ожидания в очереди. Response time включает ещё service time и сетевые задержки.

Response time — это распределение, а не одно число. Среднее (mean) плохо описывает типичный опыт. Лучше использовать перцентили:

ПерцентильЗначение
p50 (median)Половина запросов быстрее
p95, p99, p999Порог, быстрее которого 95%, 99%, 99,9% запросов. Описывает “хвост” (tail)

Перцентили времени ответа, tail latency и head-of-line blocking

Важные детали:

  • Медленные запросы часто идут от самых ценных клиентов (у них больше данных). Amazon задаёт требования по p999.
  • Оптимизация p9999 слишком дорога. Выигрыш мал.
  • Tail latency amplification: если один запрос пользователя вызывает несколько backend-вызовов, достаточно одного медленного.
  • Head-of-line blocking: несколько медленных запросов задерживают быстрые. Поэтому время нужно мерить на стороне клиента.
  • При нагрузочном тесте клиент должен слать запросы независимо от ответов. Иначе очереди искусственно короче.
  • Нельзя усреднять перцентили. Правильно складывать гистограммы. Для расчёта на лету есть t-digest, HdrHistogram, forward decay.
  • Перцентили используют в SLO и SLA (целях и соглашениях об уровне сервиса).
  • Scaling up (vertical): более мощная машина. Просто, но дорого.
  • Scaling out (horizontal): нагрузка на много машин, shared nothing.
  • Elastic: система сама добавляет ресурсы. Полезно при непредсказуемой нагрузке. Ручное масштабирование проще и даёт меньше сюрпризов.
  • Stateless-сервисы распределять легко. Stateful-хранилища сложнее. Традиционный совет: держать базу на одном узле, пока цена или требования к доступности не заставят распределить.

Универсальной масштабируемой архитектуры нет. Система на 100 000 запросов по 1 КБ в секунду выглядит совсем иначе, чем система на 3 запроса в минуту по 2 ГБ. Архитектура строится на предположениях, какие операции частые, а какие редкие. На раннем этапе продукта скорость итераций важнее гипотетической будущей нагрузки.

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

ПринципИдеяПримеры
OperabilityОперационной команде легко держать систему в рабочем состоянииМониторинг, автоматизация, нет зависимости от одной машины, понятная модель (“сделаю X, будет Y”), хорошие defaults
SimplicityНовому инженеру легко понять системуУбирать accidental complexity (она не часть задачи), использовать хорошие abstractions
EvolvabilityЛегко менять систему под новые требованияПростые системы и хорошие абстракции; ещё называют extensibility

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

Термин (EN)Что значит
Data-intensiveОграничение в объёме, сложности и скорости изменения данных, а не в CPU
FaultОтказ одного компонента
FailureСистема в целом не работает
Fault-tolerantСистема переживает определённые fault
MTTFСреднее время до отказа (диска)
Load parametersЧисла, описывающие нагрузку
Fan-outЧисло запросов или записей, порождённых одним входящим запросом
ThroughputОбъём работы в единицу времени
Response timeВремя от запроса до ответа, как его видит клиент
Percentile (p50, p99)Порог, быстрее которого отвечает заданная доля запросов
Tail latencyВремя самых медленных запросов
SLO / SLAЦелевой уровень сервиса / соглашение с клиентом
Scaling up / outМощнее машина / больше машин
ElasticАвтоматическое добавление ресурсов
Operability, Simplicity, EvolvabilityТри принципа сопровождаемости
  • Описывайте нагрузку числами: объём данных, частота запусков, доля чтений и записей. Только потом выбирайте архитектуру.
  • Для пайплайнов и API измеряйте p95 и p99, а не только среднее. Для batch-задач следите за throughput и за skew (перекос данных).
  • Планируйте отказы. Делайте задачи idempotent, чтобы повтор после сбоя не портил данные. Умейте быстро откатить конфигурацию и пересчитать данные.
  • Не усредняйте перцентили при агрегации метрик. Храните гистограммы.
  • Если вы собираете систему из базы, кэша и поискового индекса, вы отвечаете за их согласованность. Решите заранее, кто источник истины и как идёт синхронизация.
  • Не строите масштабируемость заранее без причины. Сначала понятная и простая система.
  1. Чем fault отличается от failure? Почему цель в том, чтобы одно не вело к другому?
  2. Почему в примере с Twitter ключевой load parameter — число подписчиков, а не число твитов?
  3. Почему среднее время ответа плохо описывает опыт пользователя? Что лучше?
  4. Что такое accidental complexity и как с ней помогает abstraction?

Предыдущей главы нет. Следующая: Модели данных и языки запросов.