Глава 2. Определение нефункциональных требований

Перевод из книги «Designing Data-Intensive Applications, 2nd Edition» подготовлен автором сайта

Table of Contents

Глава 2. Определение нефункциональных требований

Интернет был сделан настолько хорошо, что большинство людей воспринимают его как природный ресурс вроде Тихого океана, а не как нечто, созданное человеком. Когда в последний раз технология такого масштаба была настолько свободна от ошибок?

— Алан Кэй, в интервью журналу Dr Dobb’s Journal (2012)


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

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

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

  • Как определить и измерить производительность системы (см. «Описание производительности»);
  • Что означает надёжность сервиса — а именно, его способность продолжать правильно работать, даже когда что-то идёт не так (см. «Надёжность и устойчивость к отказам»);
  • Обеспечение масштабируемости системы за счёт эффективных способов увеличения вычислительных ресурсов при росте нагрузки (см. «Масштабируемость»);
  • Упрощение долгосрочного сопровождения системы (см. «Сопровождаемость»).

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

Case Study: Social Network Home Timelines

Представьте, что вам поручили реализовать социальную сеть по типу X (ранее Twitter), в которой пользователи могут публиковать сообщения и подписываться друг на друга. Это будет сильное упрощение того, как подобный сервис работает на самом деле, но оно поможет продемонстрировать некоторые вопросы, возникающие в системах большого масштаба.

Предположим, что пользователи публикуют 500 миллионов сообщений в день, или в среднем 5 700 сообщений в секунду. Время от времени скорость может подскакивать до 150 000 сообщений в секунду. Также предположим, что в среднем один пользователь подписан на 200 человек и имеет 200 подписчиков (хотя разброс очень большой: у большинства людей всего несколько подписчиков, а у некоторых знаменитостей, таких как Барак Обама, их более 100 миллионов).

Представление пользователей, сообщений и подписок

Представим, что мы храним все данные в реляционной базе данных, как показано на рисунке 2-1. У нас есть одна таблица для пользователей, одна таблица для сообщений и одна таблица для подписок между пользователями.

Рисунок 2-1. Простая реляционная схема социальной сети, в которой пользователи могут подписываться друг на друга

Предположим, что основной операцией чтения, которую должна поддерживать наша социальная сеть, является домашняя лента — отображение недавних сообщений от людей, на которых вы подписаны (для простоты мы будем игнорировать рекламу, рекомендации и другие расширения). Мы могли бы написать следующий SQL-запрос, чтобы получить домашнюю ленту для конкретного пользователя:

Чтобы выполнить этот запрос, база данных использует таблицу follows, чтобы найти всех, на кого подписан current_user, затем находит недавние публикации этих пользователей и сортирует их по времени, чтобы получить 1000 самых последних публикаций от любых из подписанных пользователей.

Публикации предполагаются актуальными, поэтому предположим, что после того как кто-то публикует сообщение, его подписчики должны увидеть его в течение 5 секунд. Один из способов добиться этого — клиент пользователя будет повторять вышеуказанный запрос каждые 5 секунд, пока пользователь в сети (это называется опросом, polling). Если предположить, что одновременно в сети находятся 10 миллионов пользователей, это означает выполнение запроса 2 миллиона раз в секунду. Даже если увеличить интервал опроса, это всё равно очень много.

Более того, сам запрос довольно затратен: если вы подписаны на 200 человек, необходимо получить список последних публикаций от каждого из этих 200 пользователей и объединить эти списки. 2 миллиона запросов к ленте в секунду означают, что база данных должна извлекать последние публикации от какого-то отправителя 400 миллионов раз в секунду — огромное число. И это в среднем. Некоторые пользователи подписаны на десятки тысяч аккаунтов; для них такой запрос крайне затратен и сложно сделать его быстрым.

Материализация и обновление ленты

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

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

Недостаток этого подхода в том, что теперь нам нужно выполнять больше работы каждый раз, когда пользователь публикует сообщение, потому что домашние ленты являются производными данными, которые нужно обновлять. Этот процесс показан на рисунке 2-2. Когда один исходный запрос приводит к нескольким последующим, мы используем термин фан-аут (fan-out), чтобы описать, во сколько раз увеличивается количество запросов.

Рисунок 2-2. Фан-аут: доставка новых публикаций каждому подписчику пользователя, сделавшего публикацию

При скорости 5700 публикаций в секунду и среднем количестве подписчиков на одну публикацию — 200 (т. е. коэффициент фан-аута 200) — нам нужно выполнять чуть больше 1 миллиона записей в домашние ленты в секунду. Это много, но всё же значительно меньше, чем 400 миллионов обращений к публикациям отправителей в секунду, которые пришлось бы делать в противном случае.

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

Этот процесс предварительного вычисления и обновления результатов запроса называется материализацией, а кэш ленты — пример материализованного представления. Недостаток материализации в том, что каждый раз, когда знаменитость публикует сообщение, нам нужно проделать большой объём работы, чтобы вставить это сообщение в домашние ленты каждого из миллионов её подписчиков.

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

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

Большинство обсуждений производительности программного обеспечения рассматривают два основных типа метрик:

  • Время отклика
    Промежуток времени от момента, когда пользователь делает запрос, до момента получения ответа. Единицы измерения — секунды (или миллисекунды, или микросекунды).
  • Пропускная способность
    Количество запросов в секунду или объём данных в секунду, обрабатываемых системой. При заданном объёме ресурсов оборудования существует максимальная пропускная способность, которую можно выдержать. Единица измерения — «что-то в секунду».

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

Между пропускной способностью и временем отклика часто существует связь; пример такой зависимости для онлайн-сервиса показан на рисунке 2-3. Сервис демонстрирует низкое время отклика при низкой нагрузке, но время отклика растёт по мере увеличения нагрузки. Это связано с очередями: когда запрос поступает на сильно загруженную систему, скорее всего, процессор уже занят обработкой другого запроса, и поэтому входящий запрос должен подождать, пока предыдущий завершится. По мере приближения пропускной способности к пределу аппаратных возможностей задержки из-за очередей растут резко.

Рисунок 2-3. По мере приближения пропускной способности сервиса к его предельной нагрузке, время отклика резко возрастает из-за очередей

КОГДА ПЕРЕГРУЖЕННАЯ СИСТЕМА НЕ ВОССТАНАВЛИВАЕТСЯ

Если система близка к перегрузке, с пропускной способностью, доведённой до предела, она иногда может войти в порочный круг, при котором становится менее эффективной и, следовательно, ещё более перегруженной. Например, если существует длинная очередь запросов, ожидающих обработки, время отклика может увеличиться настолько, что клиенты начинают прерывать ожидание (таймаут) и повторно отправлять запрос. Это приводит к ещё большему росту числа запросов и усугубляет проблему — так называемый «шторм повторов» (retry storm). Даже если нагрузка затем снижается, такая система может оставаться в перегруженном состоянии до перезагрузки или другого способа сброса. Это явление называется метастабильным сбоем (metastable failure) и может вызывать серьёзные сбои в производственных системах.

Чтобы избежать перегрузки службы из-за повторных запросов, можно увеличить и рандомизировать интервал между повторными попытками на стороне клиента (экспоненциальная задержка повторов, exponential backoff) и временно прекратить отправку запросов в службу, которая недавно возвращала ошибки или не отвечала (автоматический предохранитель, circuit breaker или алгоритм токен-бакета, token bucket). Сервер также может сам обнаружить приближение к перегрузке и начать проактивно отклонять запросы (сброс нагрузки, load shedding), а также отправлять клиентам ответы с просьбой замедлить скорость запросов (обратное давление, backpressure). Выбор алгоритмов очередей и балансировки нагрузки также может сыграть роль.

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

В этом разделе мы сосредоточимся в первую очередь на времени отклика, а к пропускной способности и масштабируемости вернёмся в разделе “Scalability”.

Latency (Задержка) и Response Time (Время Отклика)

«Задержка» (latency) и «время отклика» (response time) иногда используются как синонимы, но в этой книге термины применяются в конкретном смысле (показано на рисунке 2-4):

  • Время отклика — это то, что видит клиент; включает все задержки, возникающие где-либо в системе.
  • Время обслуживания — это длительность, в течение которой служба активно обрабатывает запрос пользователя.

Задержки в очереди могут возникать в нескольких точках: например, после получения запроса, он может ждать доступности ЦП перед обработкой; ответ может быть задержан в буфере перед отправкой по сети, если другие процессы на той же машине активно передают данные через сетевой интерфейс.

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

Рисунок 2-4. Время отклика, время обслуживания, сетевая задержка и задержка в очереди

На рисунке 2-4 время течёт слева направо, каждое взаимодействующее звено показано в виде горизонтальной линии, а сообщение запроса или ответа изображено в виде толстой диагональной стрелки от одного узла к другому. С таким стилем диаграмм вы будете часто сталкиваться в этой книге.

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

Задержки в очереди часто составляют большую часть вариации во времени отклика. Поскольку сервер может обрабатывать только ограниченное количество запросов параллельно (например, по числу ядер процессора), достаточно лишь нескольких медленных запросов, чтобы задержать последующие — это явление называется блокировкой начала очереди (head-of-line blocking). Даже если последующие запросы имеют быстрое время обслуживания, клиент увидит общее медленное время отклика из-за ожидания завершения предыдущего запроса. Задержка в очереди не является частью времени обслуживания, поэтому важно измерять время отклика на стороне клиента.

Среднее, Медиана и Процентили

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

Рисунок 2-5. Иллюстрация среднего и процентилей: время отклика для выборки из 100 запросов к сервису

Часто сообщается среднее время отклика сервиса (технически — арифметическое среднее: то есть сумма всех времен отклика, делённая на количество запросов). Среднее время отклика полезно для оценки пределов пропускной способности. Однако среднее — не очень хорошая метрика, если вы хотите узнать «типичное» время отклика, потому что оно не показывает, сколько пользователей фактически испытали ту или иную задержку.

Обычно лучше использовать процентили. Если взять список всех времен отклика и отсортировать его от самого быстрого до самого медленного, то медиана будет находиться посередине: например, если медианное время отклика составляет 200 мс, это значит, что половина запросов возвращается быстрее 200 мс, а половина — медленнее. Это делает медиану хорошей метрикой, если вы хотите знать, сколько времени пользователи обычно ждут. Медиана также называется 50-м процентилем и иногда обозначается как p50.

Чтобы понять, насколько плохи выбросы, можно посмотреть на более высокие процентили: 95-й, 99-й и 99.9-й процентили являются распространёнными (обозначаются соответственно как p95, p99 и p999). Это пороговые значения времени отклика, при которых 95%, 99% или 99.9% запросов выполняются быстрее соответствующего порога. Например, если 95-й процентиль времени отклика составляет 1.5 секунды, это означает, что 95 из 100 запросов выполняются быстрее 1.5 секунды, а 5 из 100 — за 1.5 секунды или дольше. Это показано на рисунке 2-5.

Высокие процентили времени отклика, также известные как латентность хвоста (tail latencies), важны, потому что они напрямую влияют на пользовательский опыт. Например, Amazon определяет требования ко времени отклика внутренних сервисов по 99.9-му процентилю, даже если это затрагивает лишь 1 из 1000 запросов. Это потому, что пользователи с самыми медленными запросами часто имеют наибольшее количество данных в своих аккаунтах из-за большого числа покупок — другими словами, это самые ценные клиенты. Важно удерживать этих клиентов, обеспечивая для них быструю работу сайта.

С другой стороны, оптимизация 99.99-го процентиля (самый медленный 1 из 10 000 запросов) была признана слишком дорогой и не дающей достаточной выгоды для целей Amazon. Снижение времени отклика на очень высоких процентилях трудно, так как они легко зависят от случайных факторов вне вашего контроля, а отдача от улучшений снижается.

ВЛИЯНИЕ ВРЕМЕНИ ОТКЛИКА НА ПОЛЬЗОВАТЕЛЯ

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

Некоторые часто цитируемые статистики ненадёжны. В 2006 году Google сообщала, что замедление выдачи результатов поиска с 400 мс до 900 мс было связано с снижением трафика и доходов на 20%. Однако другое исследование Google в 2009 году сообщало, что увеличение задержки на 400 мс привело лишь к 0.6% снижению количества поисковых запросов в день, а в том же году Bing обнаружила, что увеличение времени загрузки на две секунды снизило доход от рекламы на 4.3%. Более свежие данные от этих компаний, по-видимому, не публиковались.

Более недавнее исследование от Akamai утверждает, что увеличение времени отклика на 100 мс снижает коэффициент конверсии для сайтов электронной коммерции на до 7%; однако при более внимательном рассмотрении то же исследование показывает, что очень быстрое время загрузки страниц также коррелирует с низкими коэффициентами конверсии! Это кажущееся противоречие объясняется тем, что страницы, загружающиеся быстрее всего — это часто страницы без полезного контента (например, страницы ошибки 404). Однако, поскольку исследование не пытается отделить влияние содержимого страницы от времени загрузки, его результаты, вероятно, не имеют особого значения.

Исследование Yahoo сравнивало показатели кликов (CTR) по результатам поиска, загружающимся быстро и медленно, при прочих равных условиях качества результатов. Оно показывает на 20–30% больше кликов по быстрым результатам, если разница во времени между быстрым и медленным ответом составляет 1.25 секунды или больше.

ИСПОЛЬЗОВАНИЕ МЕТРИК ВРЕМЕНИ ОТКЛИКА

Высокие процентили особенно важны в бэкенд-сервисах, к которым делается несколько обращений в рамках одного пользовательского запроса. Даже если вызовы выполняются параллельно, финальный ответ пользователю всё равно должен ждать самого медленного из этих параллельных вызовов. Достаточно одного медленного вызова, чтобы весь пользовательский запрос оказался медленным, как показано на рисунке 2-6. Даже если только небольшой процент бэкенд-запросов медленные, вероятность получить медленный вызов увеличивается, если для одного пользовательского запроса требуется много обращений к бэкенду. В результате большая доля пользовательских запросов становится медленной — это явление называется усилением латентности хвоста (tail latency amplification).

Рисунок 2-6. Когда для обработки одного запроса требуется несколько бэкенд-вызовов, достаточно одного медленного вызова, чтобы замедлить весь пользовательский запрос

Процентили часто используются в целевых показателях обслуживания (SLO) и соглашениях об уровне обслуживания (SLA) как способ определить ожидаемую производительность и доступность сервиса. Например, SLO может устанавливать цель: медианное время отклика менее 200 мс, 99-й процентиль — менее 1 секунды, и не менее 99.9% корректных запросов должны завершаться без ошибки. SLA — это контракт, определяющий, что произойдёт, если SLO не будет выполнен (например, клиент может получить компенсацию). По крайней мере, это базовая идея; на практике определить хорошие метрики доступности для SLO и SLA — задача непростая.

ВЫЧИСЛЕНИЕ ПРОЦЕНТИЛЕЙ

Если вы хотите добавить процентили времени отклика на панель мониторинга своих сервисов, необходимо эффективно вычислять их на постоянной основе. Например, вы можете отслеживать скользящее окно с временами отклика за последние 10 минут. Каждую минуту вы рассчитываете медиану и различные процентили по значениям в этом окне и отображаете эти метрики на графике.

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

Среди open-source библиотек для оценки процентилей: HdrHistogram, t-digest, OpenHistogram и DDSketch.

Имейте в виду, что усреднение процентилей (например, для уменьшения временного разрешения или объединения данных с нескольких машин) не имеет математического смысла — правильный способ агрегирования данных о времени отклика — это суммирование гистограмм.

Надёжность и отказоустойчивость

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

  • Приложение выполняет функции, которых ожидает пользователь.
  • Оно способно выдерживать ошибки пользователя или использование в неожиданных сценариях.
  • Его производительности достаточно для предполагаемого случая использования при ожидаемой нагрузке и объёме данных.
  • Система предотвращает несанкционированный доступ и злоупотребления.

Если всё это вместе означает «работает правильно», то надёжность можно понимать как «продолжает работать правильно, даже когда что-то идёт не так». Чтобы точнее говорить о том, что может пойти не так, различим сбои и отказы:

  • Сбой (Fault)
    Сбой — это когда какая-то часть системы перестаёт работать правильно: например, выходит из строя жёсткий диск, или падает одна из машин, или происходит сбой внешнего сервиса, от которого зависит система.
  • Отказ (Failure)
    Отказ — это когда система в целом перестаёт предоставлять требуемую услугу пользователю; другими словами, когда она не выполняет заданные целевые показатели обслуживания (SLO).

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

Отказоустойчивость (Fault Tolerance)

Система называется отказоустойчивой, если она продолжает предоставлять требуемую услугу пользователю несмотря на определённые сбои. Если система не может выдержать отказ какой-либо своей части, такая часть называется единой точкой отказа (SPOF), потому что сбой в ней приводит к отказу всей системы.

Например, в кейсе социальной сети возможным сбоем является отказ машины, участвующей в обновлении материализованных лент новостей (fan-out). Чтобы сделать этот процесс отказоустойчивым, нужно, чтобы другая машина могла взять на себя задачу без пропуска сообщений и без их дублирования. (Эта идея называется семантикой «ровно один раз» — exactly-once semantics).

Отказоустойчивость всегда ограничена определённым числом определённых типов сбоев. Например, система может выдержать максимум два одновременных отказа дисков, или выход из строя одной из трёх нод. Было бы бессмысленно пытаться выдержать любое количество сбоев: если выйдут из строя все узлы, уже ничего нельзя сделать. Если вся планета Земля (и все её серверы) будет поглощена чёрной дырой, то для устойчивости к такому сбою потребовался бы хостинг в космосе — удачи в согласовании такого бюджета.

Парадоксально, но в таких отказоустойчивых системах иногда имеет смысл умышленно вызывать сбои, например, случайным образом убивая отдельные процессы без предупреждения. Это называется внедрение сбоев (fault injection). Многие критические ошибки возникают из-за плохой обработки ошибок; умышленно провоцируя сбои, вы гарантируете, что механизмы отказоустойчивости регулярно используются и тестируются, что повышает уверенность в их корректной работе при реальных сбоях. Chaos engineering — это дисциплина, цель которой — повысить уверенность в отказоустойчивости системы через эксперименты с преднамеренными сбоями.

Хотя мы обычно предпочитаем устойчивость к сбоям предотвращению сбоев, бывают случаи, когда предотвращение лучше, потому что «лечения» не существует. Например, в случае с безопасностью: если злоумышленник получил доступ к конфиденциальным данным, это событие нельзя отменить. Однако в этой книге в основном рассматриваются такие сбои, с которыми можно справиться, как описано далее.

Аппаратные и программные сбои (Hardware and Software Faults)

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

Примерно 2–5% магнитных жёстких дисков выходят из строя в год; в кластере из 10 000 дисков можно ожидать в среднем один отказ в день. Новые данные показывают, что диски становятся надёжнее, но уровень отказов остаётся значительным.

Примерно 0.5–1% SSD-дисков выходят из строя в год. Небольшие ошибки на уровне битов автоматически корректируются, но некорректируемые ошибки случаются примерно раз в год на диск, даже если диск довольно новый (т.е. мало использовался); у SSD частота таких ошибок выше, чем у магнитных дисков.

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

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

Данные в ОЗУ также могут быть повреждены — либо из-за случайных явлений (например, космических лучей), либо из-за физических дефектов. Даже при использовании памяти с ECC (кодами коррекции ошибок), более 1% машин сталкиваются с некорректируемой ошибкой в течение года, что обычно приводит к сбою машины и необходимости замены модуля памяти. Более того, некоторые патологические паттерны доступа к памяти могут с высокой вероятностью переворачивать биты.

Целый датацентр может стать недоступен (например, из-за отключения электроэнергии или сетевой ошибки) или даже быть полностью уничтожен (например, пожаром или наводнением). Хотя такие крупные инциденты случаются редко, их последствия могут быть катастрофическими, если сервис не выдерживает потерю датацентра.

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

Устойчивость к аппаратным сбоям достигается с помощью избыточности.

Наша первая реакция на ненадёжное оборудование — добавить избыточность в отдельные аппаратные компоненты, чтобы снизить уровень отказов системы. Диски могут быть объединены в конфигурации RAID (распределение данных по нескольким дискам в одной машине, чтобы отказ одного диска не привёл к потере данных), серверы могут иметь два блока питания и горячую замену процессоров, а датацентры могут использовать аккумуляторы и дизельные генераторы для резервного питания. Такая избыточность может поддерживать работу машины без перебоев в течение многих лет.

Избыточность наиболее эффективна, когда отказы компонентов независимы, то есть возникновение одного сбоя не влияет на вероятность другого. Однако опыт показывает, что существуют значимые корреляции между отказами компонентов; например, недоступность целой стойки или даже всего датацентра всё ещё происходит чаще, чем хотелось бы.

Избыточность оборудования увеличивает время бесперебойной работы одной машины, однако, как обсуждалось в разделе «Распределённые против одноузловых систем», существуют преимущества использования распределённых систем, например, возможность пережить полную недоступность одного датацентра. По этой причине облачные системы меньше полагаются на надёжность отдельных машин, а вместо этого стремятся обеспечить высокую доступность сервисов, выдерживая отказ узлов на уровне программного обеспечения. Облачные провайдеры используют зоны доступности, чтобы указать, какие ресурсы физически находятся рядом друг с другом; ресурсы, размещённые в одном месте, с большей вероятностью выйдут из строя одновременно, чем географически разделённые.

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

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

Программные сбои

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

Например:

Ошибка в ПО, вызывающая отказ всех узлов одновременно при определённых условиях. Так, 30 июня 2012 года добавление високосной секунды вызвало зависание многих Java-приложений из-за ошибки в ядре Linux, что привело к сбоям интернет-сервисов. Из-за ошибки в прошивке все SSD определённой модели внезапно выходили из строя ровно через 32 768 часов работы (менее чем через 4 года), делая данные на них невосстановимыми.

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

Зависимый сервис замедляется, перестаёт отвечать или начинает возвращать повреждённые ответы.

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

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

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

Быстрого решения проблемы систематических программных ошибок не существует. Есть множество мелких полезных подходов: вдумчивое рассмотрение предположений и взаимодействий в системе; тщательное тестирование; изоляция процессов; разрешение процессам аварийно завершаться и перезапускаться; избежание «бурь повторных попыток» (см. «Когда перегруженная система не восстанавливается»); измерение, мониторинг и анализ поведения системы в продакшене.

Люди и надёжность

Программные системы проектируют и создают люди, и операторы, поддерживающие их работу, тоже люди. В отличие от машин, люди не просто следуют правилам; их сила — в креативности и адаптивности. Но именно эта особенность также ведёт к непредсказуемости и иногда к ошибкам, приводящим к сбоям, несмотря на лучшие намерения. Например, в одном исследовании крупных интернет-сервисов было установлено, что изменения конфигурации, внесённые операторами, были основной причиной сбоев, тогда как аппаратные отказы (серверы или сеть) сыграли роль лишь в 10–25% инцидентов.

Возникает соблазн назвать такие случаи «человеческой ошибкой» и надеяться, что их можно устранить с помощью более строгих процедур и контроля. Однако обвинение людей в ошибках — контрпродуктивно. То, что мы называем «человеческой ошибкой» — это не причина инцидента, а симптом проблемы в социотехнической системе, в которой люди стараются делать свою работу. Часто сложные системы обладают побочным поведением, при котором неожиданные взаимодействия компонентов тоже приводят к отказам.

Существуют различные технические меры, помогающие снизить влияние человеческих ошибок, включая: тщательное тестирование (как ручные тесты, так и property-based тестирование на большом количестве случайных входных данных), механизмы отката конфигураций, постепенное развёртывание нового кода, подробный мониторинг, инструменты наблюдаемости для диагностики проблем в продакшене (см. «Проблемы распределённых систем»), и интерфейсы, которые побуждают делать «правильное» и мешают сделать «неправильное».

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

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

В качестве общего принципа при разборе инцидентов следует остерегаться упрощённых объяснений. Утверждение «Боб должен был быть внимательнее при развёртывании» — бесполезно. Но и «Нам нужно переписать бэкенд на Haskell» — тоже. Вместо этого руководство должно использовать инцидент как возможность понять, как реально работает система глазами людей, которые с ней работают каждый день, и улучшить её на основе этих данных.

НАСКОЛЬКО ВАЖНА НАДЁЖНОСТЬ?

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

Во многих приложениях временный сбой на несколько минут или даже часов допустим, но потеря или повреждение данных навсегда — это катастрофа. Представьте себе родителя, хранящего все фотографии и видео своих детей в вашем фото-приложении. Как он себя почувствует, если база данных внезапно будет повреждена? Знает ли он, как восстановить её из резервной копии?
Другой пример того, как ненадёжное программное обеспечение может навредить людям — это скандал с системой Horizon в британской почтовой службе. С 1999 по 2019 год сотни людей, управлявших отделениями почты в Великобритании, были осуждены за кражу или мошенничество, потому что бухгалтерское ПО показывало недостачу в их отчётах. Позже выяснилось, что многие из этих недостач были вызваны ошибками в программном обеспечении, и многие приговоры были пересмотрены. Причиной этого, вероятно самого крупного судебного заблуждения в истории Великобритании, стало то, что английское законодательство предполагает, что компьютеры работают правильно (и, следовательно, данные, предоставленные компьютером, являются надёжными), если нет доказательств обратного. Инженеры-программисты могут смеяться над идеей, что ПО может быть полностью без ошибок, но это слабое утешение для людей, которые были несправедливо осуждены, объявлены банкротами или даже покончили с собой из-за приговора, основанного на ненадёжной компьютерной системе.

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

МАСШТАБИРУЕМОСТЬ

Даже если система работает надёжно сегодня, это не значит, что она будет работать надёжно в будущем. Одна из частых причин ухудшения работы — рост нагрузки: возможно, система выросла с 10 000 до 100 000 одновременных пользователей или с 1 миллиона до 10 миллионов. Возможно, она обрабатывает гораздо большие объёмы данных, чем раньше.

Масштабируемость — это термин, которым описывают способность системы справляться с возросшей нагрузкой. Иногда при обсуждении масштабируемости говорят что-то вроде: «Вы не Google и не Amazon. Перестаньте думать о масштабах и просто используйте реляционную базу данных». Применимо ли это к вам — зависит от типа приложения, которое вы разрабатываете.
Если вы делаете новый продукт, у которого пока немного пользователей, например в стартапе, основная инженерная цель обычно — сохранять систему как можно более простой и гибкой, чтобы легко вносить изменения и адаптироваться к потребностям клиентов. В такой ситуации думать о гипотетических масштабах, которые могут понадобиться в будущем, вредно: в лучшем случае — это потраченные впустую усилия и преждевременная оптимизация, в худшем — это может зафиксировать вас в негибкой архитектуре и усложнить развитие приложения.

Причина в том, что масштабируемость — не одноосный ярлык: бессмысленно говорить «X масштабируется» или «Y не масштабируется». Вместо этого обсуждение масштабируемости предполагает вопросы вроде:

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

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

ОПИСАНИЕ НАГРУЗКИ

Сначала нужно кратко описать текущую нагрузку на систему; только после этого можно обсуждать вопросы роста (что произойдёт, если нагрузка удвоится?). Чаще всего измеряется пропускная способность: например, количество запросов в секунду к сервису, сколько гигабайт новых данных приходит в день или сколько оформляется покупок в час. Иногда важен пиковый показатель переменной величины, например, количество одновременно онлайн-пользователей в разделе «Кейс: домашняя лента в соцсети».

Часто на масштабируемость влияют и другие статистические характеристики нагрузки. Например, может потребоваться знать соотношение чтений и записей в базе данных, частоту попаданий в кэш или количество элементов данных на пользователя (например, число подписчиков в соцсети). Может быть важен средний случай, а может быть — крайние случаи, которые и создают основную нагрузку. Всё зависит от деталей конкретного приложения.

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

  • При увеличении нагрузки при неизменных ресурсах (CPU, память, пропускная способность сети и т. д.) — как изменяется производительность системы?
  • При увеличении нагрузки — насколько нужно увеличить ресурсы, чтобы сохранить прежнюю производительность?

Обычно цель — сохранить производительность в рамках требований SLA (см. раздел “Использование метрик времени отклика”), одновременно минимизируя стоимость эксплуатации системы. Чем больше требуемые ресурсы, тем выше стоимость. Возможно, некоторые типы оборудования более экономичны, и эти параметры могут меняться со временем с появлением новых типов оборудования.

Если вы можете удвоить ресурсы, чтобы справиться с удвоенной нагрузкой, сохранив ту же производительность, это называется линейной масштабируемостью, и это считается хорошим показателем. Иногда даже удаётся справиться с удвоенной нагрузкой, добавив меньше, чем вдвое ресурсов — за счёт эффекта масштаба или более равномерного распределения пиковой нагрузки. Но чаще затраты растут быстрее, чем линейно, и причин для такой неэффективности может быть много. Например, при большом объёме данных обработка одного запроса на запись может требовать больше работы, чем при небольшом объёме данных, даже если сам запрос по размеру одинаковый.

Shared-Memory, Shared-Disk, and Shared-Nothing Architecture

Самый простой способ увеличить аппаратные ресурсы сервиса — перенести его на более мощную машину. Отдельные ядра CPU уже не становятся значительно быстрее, но можно купить машину (или арендовать облачный экземпляр) с большим количеством ядер, большим объёмом оперативной памяти и дискового пространства. Такой подход называется вертикальным масштабированием или масштабированием «вверх».

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

Другой подход — это архитектура с общим диском (shared-disk), в которой используется несколько машин с независимыми CPU и RAM, но данные хранятся на массиве дисков, который общий для всех машин и к которому они подключаются через быструю сеть: Network-Attached Storage (NAS) или Storage Area Network (SAN). Такая архитектура традиционно применялась для внутренних хранилищ данных (data warehousing), но конфликты доступа и издержки блокировок ограничивают масштабируемость подхода с общим диском.

В противоположность этому, архитектура без общего ресурса (shared-nothing) (также называемая горизонтальным масштабированием или масштабированием «вширь») завоевала большую популярность. В этом подходе используется распределённая система с несколькими узлами, каждый из которых имеет собственные CPU, RAM и диски. Любая координация между узлами осуществляется на уровне программного обеспечения через обычную сеть.

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

Некоторые облачные базы данных используют раздельные сервисы для хранения данных и выполнения транзакций (см. «Разделение хранения и вычислений»), при этом несколько вычислительных узлов получают доступ к одному и тому же сервису хранения. Эта модель в чём-то похожа на архитектуру с общим диском, но она избегает проблем масштабируемости старых систем: вместо предоставления абстракции файловой системы (NAS) или блочного устройства (SAN), сервис хранения предлагает специализированный API, разработанный под конкретные нужды базы данных.

Принципы масштабируемости

Архитектура систем, работающих в крупных масштабах, как правило, сильно зависит от конкретного приложения — не существует универсальной архитектуры, масштабируемой «на все случаи жизни» (неформально известной как «волшебный соус масштабирования»). Например, система, рассчитанная на 100 000 запросов в секунду, каждый объёмом 1 КБ, будет сильно отличаться от системы, рассчитанной на 3 запроса в минуту, каждый по 2 ГБ — несмотря на то, что у обеих одинаковая пропускная способность по данным (100 МБ/сек).

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

Хороший общий принцип масштабируемости — разделять систему на более мелкие компоненты, которые могут работать относительно независимо друг от друга. Это основной принцип, лежащий в основе микросервисов, шардирования, потоковой обработки и архитектуры без общего ресурса. Однако сложность в том, чтобы определить, что должно быть объединено, а что — разделено.

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

Сопровождаемость (Maintainability)

Программное обеспечение не изнашивается и не страдает от усталости материалов, поэтому оно не ломается так же, как механические объекты. Но требования к приложению часто меняются, среда, в которой работает ПО, также меняется (например, его зависимости и базовая платформа), и в нём есть ошибки, которые нужно исправлять.
Широко признано, что основная часть затрат на программное обеспечение связана не с его первоначальной разработкой, а с последующим обслуживанием — исправлением ошибок, поддержанием работы систем, расследованием сбоев, адаптацией к новым платформам, модификацией под новые случаи использования, устранением технического долга и добавлением новых функций.

Однако сопровождение также сложно. Если система успешно работает долгое время, вполне возможно, что она использует устаревшие технологии, которые сегодня мало кто понимает (например, мейнфреймы и код на COBOL); институциональные знания о том, как и почему система была спроектирована определённым образом, могли быть утрачены, когда сотрудники покинули организацию; возможно, потребуется исправлять ошибки других людей. Более того, компьютерная система часто тесно связана с человеческой организацией, которую она обслуживает, а это значит, что сопровождение таких унаследованных систем — это не только техническая, но и социальная проблема.

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

  • Работоспособность (Operability)
    Облегчить организациям поддержание бесперебойной работы системы.
  • Простота (Simplicity)
    Облегчить новым инженерам понимание системы, реализуя её с помощью хорошо понятных, согласованных шаблонов и структур и избегая ненужной сложности.
  • Развиваемость (Evolvability)
    Облегчить инженерам внесение изменений в систему в будущем, её адаптацию и расширение под непредвиденные сценарии по мере изменения требований.

Работоспособность: упрощаем жизнь операторам

Ранее мы обсуждали роль эксплуатации в разделе «Operations in the Cloud Era» и увидели, что человеческие процессы не менее важны для надёжной работы, чем программные инструменты. На самом деле, высказывалось мнение, что «хорошая эксплуатация зачастую может компенсировать недостатки плохого (или неполного) ПО, но хорошее ПО не может надёжно работать при плохой эксплуатации».

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

Кроме того, если автоматизированная система даёт сбой, её часто труднее отлаживать, чем систему, в которой оператор выполняет часть действий вручную. По этой причине нельзя сказать, что больше автоматизации всегда означает лучшую работоспособность. Тем не менее, определённый уровень автоматизации важен, а «золотая середина» зависит от конкретного приложения и организации.

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

Предоставление средств мониторинга ключевых метрик системы и поддержка инструментов наблюдаемости (см. «Проблемы распределённых систем»), обеспечивающих представление о поведении системы во время работы. Здесь могут помочь различные коммерческие и открытые инструменты.

Отсутствие зависимости от конкретных машин (возможность отключения машин для обслуживания без остановки всей системы)

Предоставление хорошей документации и простой для понимания операционной модели («Если я сделаю X, произойдёт Y»)

Предоставление хорошего поведения по умолчанию, но с возможностью для администраторов при необходимости переопределять эти значения

Самовосстановление там, где это уместно, но при этом возможность вручную управлять состоянием системы, когда это нужно

Предсказуемое поведение, сведение к минимуму неожиданностей

Простота: управление сложностью

Небольшие программные проекты могут иметь восхитительно простой и выразительный код, но по мере роста проектов они часто становятся очень сложными и трудными для понимания. Эта сложность замедляет работу всех, кто должен взаимодействовать с системой, что ещё больше увеличивает стоимость сопровождения. Программный проект, погрязший в сложности, иногда описывают как «большой ком грязи» (big ball of mud).

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

Простые системы легче понять, и поэтому мы должны стремиться решать задачи максимально простым способом. К сожалению, это проще сказать, чем сделать. То, является ли что-либо простым, часто субъективно и зависит от вкуса, поскольку не существует объективного стандарта простоты. Например, одна система может скрывать сложную реализацию за простым интерфейсом, тогда как другая может иметь простую реализацию, но раскрывать больше внутренних деталей пользователю — какая из них проще?

Одна из попыток осмыслить сложность — разделить её на два типа: существенную и случайную (essential и accidental complexity). Считается, что существенная сложность присуща предметной области приложения, а случайная возникает из-за ограничений используемых инструментов. Однако и это разделение несовершенно, поскольку границы между существенным и случайным меняются по мере развития наших инструментов.

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

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

Абстракции для прикладного кода, призванные уменьшить его сложность, можно создавать с помощью методологий вроде шаблонов проектирования и предметно-ориентированного проектирования (DDD). Эта книга не о таких прикладных абстракциях, а о более универсальных, на основе которых вы можете строить свои приложения — таких как транзакции в базах данных, индексы и журналы событий. Если вы хотите использовать такие техники, как DDD, вы можете реализовать их поверх фундаментальных компонентов, описанных в этой книге.

Развиваемость: упрощение изменений

Чрезвычайно маловероятно, что требования к вашей системе останутся неизменными навсегда. Намного вероятнее, что они будут постоянно меняться: вы узнаёте новые факты, появляются ранее не учтённые сценарии, меняются бизнес-приоритеты, пользователи запрашивают новые функции, новые платформы заменяют старые, появляются юридические или регуляторные требования, рост системы требует архитектурных изменений и т. д.

С точки зрения организационных процессов, гибкие (Agile) рабочие подходы предоставляют структуру для адаптации к изменениям. Сообщество Agile также разработало технические инструменты и процессы, полезные при разработке ПО в условиях частых изменений, такие как разработка через тестирование (TDD) и рефакторинг. В этой книге мы ищем способы повышения гибкости на уровне системы, состоящей из нескольких различных приложений или сервисов с разными характеристиками.

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

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

Краткая сводка по 2 главе

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

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

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

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

0
Оставьте комментарий! Напишите, что думаете по поводу статьи.x