Иэн Гортон
Основы масштабируемых систем

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

Масштабируемый (scalable) — это термин, используемый в программной инженерии для описания программных систем, способных к росту. В этой главе я рассмотрю, что именно подразумевается под способностью к масштабированию, известной (что неудивительно) как масштабируемость (scalability). Я также опишу несколько примеров, позволяющих оценить возможности и характеристики современных приложений, и дам краткую историю происхождения больших систем, которые мы сегодня создаём. Наконец, я опишу два общих принципа достижения масштабируемости — репликацию и оптимизацию, которые в разных формах будут повторяться на протяжении всей книги, и рассмотрю неразрывную связь между масштабируемостью и другими характеристиками качества архитектуры ПО.
Что такое масштабируемость?
Интуитивно масштабируемость — довольно простое понятие. Если мы попросим Википедию дать определение, то получим следующее: «Масштабируемость — это свойство системы справляться с растущим объёмом работы путем добавления ресурсов в систему». Мы все знаем, как масштабируется система автомобильных дорог — добавляются дополнительные полосы движения, чтобы она могла обслуживать большее количество автомобилей. Некоторые из моих любимых людей знают, как масштабировать производство пива — они увеличивают количество и размер пивоваренных ёмкостей, количество персонала, который будет осуществлять и управлять процессом пивоварения, а также количество бочонков, которые они могут наполнить свежим и вкусным пивом. Подумайте о любой физической системе — транспортной, аэропортовой, лифтах в здании — и то, как мы увеличиваем пропускную способность, станет очевидным.

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

  • Количество одновременных пользовательских или внешних (например, от датчиков) запросов, которые может обрабатывать система;
  • Объём данных, которые система может эффективно обрабатывать и управлять ими;
  • Ценность, которую можно извлечь из данных, хранящихся в системе, с помощью прогнозной аналитики;
  • Способность поддерживать стабильное, постоянное время отклика при увеличении количества запросов.
Например, представим, что крупная сеть супермаркетов быстро открывает новые магазины и увеличивает количество киосков самообслуживания в каждом магазине. Это требует от основных программных систем супермаркета выполнения следующих функций:

  • Работа с увеличенным объёмом сканирования товаров без снижения времени отклика. Мгновенная реакция на сканирование товаров необходима для того, чтобы клиенты были довольны.
  • Обработка и хранение больших объёмов данных, генерируемых в результате роста продаж. Эти данные необходимы для управления запасами, учёта, планирования и, вероятно, многих других функций.
  • Получение в режиме реального времени (например, ежечасно) сводных данных о продажах в каждом магазине, регионе и стране и сравнение их с историческими тенденциями. Эти данные о тенденциях могут помочь выявить необычные события в регионах (неожиданные погодные условия, большое скопление людей на мероприятиях и т.д.) и оперативно отреагировать на них.
  • Совершенствовать подсистему прогнозирования заказа запасов, чтобы она могла правильно прогнозировать продажи (и, следовательно, потребность в пополнении запасов) по мере роста числа магазинов и клиентов.
Эти параметры фактически являются требованиями к масштабируемости системы. Если в течение года сеть супермаркетов открывает 100 новых магазинов и увеличивает продажи в 400 раз (некоторые из новых магазинов очень большие!), то программная система должна масштабироваться, чтобы обеспечить необходимую вычислительную мощность для эффективной работы супермаркета. Если система не будет масштабироваться, мы можем потерять продажи из-за недовольства покупателей. Мы можем хранить запасы, которые не смогут быть быстро реализованы, что приведёт к увеличению затрат. Мы можем упустить возможности увеличить продажи, реагируя на местные условия специальными предложениями. Все эти факторы снижают удовлетворённость клиентов и прибыль. Ни один из них не является положительным для бизнеса.

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

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

Увеличение пропускной способности системы в каком-либо измерении за счёт увеличения ресурсов называется вертикальным (scaling up) или горизонтальным (scaling out) масштабированием, разница между которыми будет рассмотрена ниже. Кроме того, в отличие от физических систем, часто не менее важно иметь возможность уменьшать мощность системы для снижения затрат.

Каноническим примером является компания Netflix, которая имеет предсказуемую региональную суточную нагрузку, которую ей необходимо обрабатывать. Проще говоря, в 9 часов вечера в любом географическом регионе Netflix смотрит гораздо больше людей, чем в 5 часов утра. Это позволяет Netflix сокращать свои вычислительные ресурсы в периоды снижения нагрузки. Это позволяет экономить на стоимости работы вычислительных узлов, используемых в облаке Amazon, а также на таких полезных для общества вещах, как снижение энергопотребления центров обработки данных. Сравните это с автомагистралью. Ночью, когда на дороге мало машин, мы не убираем полосы (за исключением ремонта). Вся пропускная способность дороги доступна для тех немногих водителей, которые могут ехать так быстро, как им хочется. В программных системах мы можем увеличивать и уменьшать вычислительную мощность за считанные секунды, чтобы удовлетворить мгновенную нагрузку. По сравнению с физическими системами, стратегии, которые мы применяем, значительно отличаются.

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

Примеры масштабирования систем
в начале 2000-х годов
Заглядывать в будущее в этой технологической игре всегда чревато. В 2008 году я писал:
Петабайтные массивы данных и гигабитные потоки данных — это сегодняшние рубежи для приложений, интенсивно использующих данные, но, несомненно, через 10 лет мы будем с нежностью вспоминать о проблемах такого масштаба и с тревогой думать о трудностях, которые создают надвигающиеся экзафлопсные приложения.
Разумные чувства, это правда, но экзафлопсные вычисления? В современном мире это уже почти обыденность. В 2014 году Google сообщила о нескольких экзабайтах Gmail, а к настоящему времени все сервисы Google работают с йоттабайтами и более? Я не знаю. Я даже не уверен, что знаю, что такое йоттабайт! Google не рассказывает нам о своих хранилищах, но я бы не стал спорить. Аналогично, сколько данных хранит компания Amazon в различных хранилищах данных AWS для своих клиентов? И сколько запросов, скажем, DynamoDB обрабатывает в секунду, в совокупности, для всех поддерживаемых клиентских приложений? Если долго думать об этом, то голова просто взорвётся.
Отличным источником информации, позволяющим иногда получить представление о современных масштабах работы, являются технические блоги крупных интернет-компаний. Существуют также сайты, анализирующие интернет-трафик, которые весьма наглядно демонстрируют его объёмы. Давайте рассмотрим несколько примеров, иллюстрирующих то, что мы знаем сегодня. Не забывайте, что через год-четыре эти примеры покажутся вам почти диковинными:

  • В инженерном блоге Facebook описывается Scribe — решение для сбора, агрегирования и передачи петабайтов данных журналов в час с низкой задержкой и высокой пропускной способностью. Вычислительная инфраструктура Facebook состоит из миллионов машин, каждая из которых генерирует файлы журналов, фиксирующие важные события, связанные с состоянием системы и приложений. Обработка таких журналов, например, с веб-сервера, может дать командам разработчиков представление о поведении и производительности их приложений, а также помочь в поиске неисправностей. Scribe — это специализированное решение с буферизованной очередью, способное передавать журналы с серверов со скоростью несколько терабайт в секунду и доставлять их в системы последующего анализа и хранилища данных. Это, друзья мои, очень много данных!
  • На сайте Internet Live Stats можно посмотреть реальный интернет-трафик для множества сервисов. Покопавшись, вы найдете ошеломляющую статистику: например, Google обрабатывает около 3,5 миллиардов поисковых запросов в день, пользователи Instagram загружают около 65 миллионов фотографий в день, а всего насчитывается около 1,7 миллиарда веб-сайтов. Это интересный сайт с большим количеством информации. Обратите внимание, что данные не являются реальными, а представляют собой оценки, основанные на статистическом анализе многочисленных источников данных.
  • В 2016 году компания Google опубликовала документ с описанием характеристик своей кодовой базы. Среди множества поразительных фактов, приведенных в документе, есть и такой: «Репозиторий содержит 86 ТБ данных, включая около двух миллиардов строк кода в девяти миллионах уникальных исходных файлов». Напомним, что это был 2016 год.
Тем не менее, реальные конкретные данные о масштабах услуг, предоставляемых крупнейшими интернет-площадками, остаются под покровом коммерческой тайны. К счастью, мы можем получить некоторые углубленные представления об объёмах запросов и данных, обрабатываемых в масштабах Интернета, благодаря ежегодному отчёту об использовании услуг одной технологической компании. Осторожно, это отчёт от Порнхаба. Ознакомиться с невероятно подробной статистикой использования за 2019 год можно здесь. Это увлекательный взгляд на возможности масштабных систем.
Как мы к этому пришли?
Краткая история развития системы
Уверен, что многим читателям трудно поверить в то, что цивилизованная жизнь существовала до появления интернета, YouTube и социальных сетей. На самом деле, первое видео, загруженное на YouTube, появилось в 2005 году. Да, в это трудно поверить даже мне. Итак, давайте вкратце рассмотрим, как мы пришли к масштабам современных систем. Ниже приведены некоторые исторические вехи:

1980-е

Эпоха, в которой доминировали мэйнфреймы с разделением времени и мини-компьютеры. В начале 1980-х годов появились персональные компьютеры, но они редко объединялись в сеть. К концу 1980-х годов лаборатории разработчиков, университеты и (всё чаще) предприятия имели электронную почту и доступ к примитивным ресурсам интернета.

1990-95

Сети стали более распространёнными, что создало благоприятную среду для создания Всемирной паутины (WWW) на основе технологии HTTP/HTML, которая была впервые применена в Европейской Организации по Ядерным Исследованиям (CERN) Тимом Бернерсом-Ли в 1980-х годах. К 1995 году количество веб-сайтов было ничтожно мало, но семена будущего были заложены такими компаниями, как Yahoo! в 1994 году, Amazon и eBay в 1995 году.

1996-2000

Количество веб-сайтов выросло примерно с 10 000 до 10 миллионов, что стало поистине взрывным периодом роста. Пропускная способность сетей и доступ к ним также быстро росли. Такие компании, как Amazon, eBay, Google и Yahoo!, стали пионерами в разработке многих принципов проектирования и ранних версий передовых технологий для высокомасштабируемых систем, которые мы знаем и используем сегодня. Повседневный бизнес поспешил воспользоваться новыми возможностями, которые открывал электронный бизнес, и это вывело масштабируемость систем на первый план, о чем рассказывается во врезке «Как масштабирование повлияло на бизнес-системы» ниже.

2000-2006

За этот период количество веб-сайтов выросло примерно с 10 до 80 миллионов, появились новые сервисы и бизнес-модели. В 2005 году был запущен YouTube. В 2006 году появилась социальная сеть Facebook. В том же году компания Amazon Web Services (AWS), которая начинала свою деятельность в 2004 году, начала предоставлять услуги S3 и EC2.

2007-сегодня

В настоящее время в мире насчитывается около 2 миллиардов веб-сайтов, из которых около 20% являются активными. Число пользователей интернета составляет около 4 миллиардов человек. По всей планете разбросаны огромные центры обработки данных, управляемые публичными облачными операторами, такими как AWS, Google Cloud Platform (GCP) и Microsoft Azure, а также множество частных центров обработки данных, например, операционная инфраструктура Twitter. В «облаках» размещаются миллионы приложений, а инженеры предоставляют и эксплуатируют свои вычислительные системы и системы хранения данных с помощью сложных порталов управления «облаками». Мощные облачные сервисы позволяют создавать, развёртывать и масштабировать системы буквально несколькими кликами мыши. Всё, что требуется от компаний, — это оплатить счёт облачного провайдера в конце месяца.
Именно на такой мир нацелена данная книга. Мир, в котором наши приложения должны использовать ключевые принципы построения масштабируемых систем и задействовать высокомасштабируемые инфраструктурные платформы. Следует помнить, что в современных приложениях большая часть исполняемого кода написана не вашей организацией. Он входит в состав контейнеров, баз данных, систем обмена сообщениями и других компонентов, которые вы включаете в своё приложение с помощью API-вызовов и директив сборки. Поэтому выбор и использование этих компонентов не менее важны, чем проектирование и разработка собственной логики работы. Это архитектурные решения, которые не так просто изменить.

Как масштабирование повлияло на бизнес-системы


Резкий рост числа пользователей, имеющих доступ в Интернет в 1990-х годах открыл бизнесу новые возможности для заработка. Возникла огромная потребность открыть пользователям доступ к бизнес-функциям (продажам, услугам и т.д.) через веб-браузер. Это стало предвестником глубоких изменений в подходах к построению систем.


Возьмем, к примеру, банк, работающий с физическими лицами. До появления онлайн-услуг можно было точно спрогнозировать нагрузку, которую будут испытывать бизнес-системы банка. Мы знали, сколько человек работает в банке и пользуется внутренними системами, сколько терминалов/ПК подключено к сетям банка, сколько банкоматов необходимо поддерживать, а также количество и характер соединений с другими финансовыми учреждениями. Вооружившись этими знаниями, мы могли бы создавать системы, поддерживающие, скажем, не более 3 000 одновременных пользователей, будучи уверенными, что это число не может быть превышено. Кроме того, рост будет происходить относительно медленно, и большую часть времени (т.е. в нерабочее время) нагрузка будет значительно меньше пиковой. Это значительно упростило принятие решений по разработке программного обеспечения и выделению аппаратных средств.


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

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

В 1932 году было открыто одно из самых известных в мире чудес инженерной мысли — Сиднейский мост через гавань. Сейчас можно с уверенностью предположить, что интенсивность движения в 2021 году будет несколько выше, чем в 1932 году. Если вы случайно проезжали по мосту в часы пик за последние 30 лет, то знаете, что его пропускная способность значительно превышается каждый день. Как же увеличить пропускную способность таких физических инфраструктур, как мосты?

Эта проблема стала очень актуальной в Сиднее в 1980-х годах, когда стало ясно, что пропускная способность переправы через гавань должна быть увеличена. Решением этой проблемы стал не столь культовый Сиднейский туннель, который, по сути, проходит по тому же маршруту под гаванью. Он обеспечивает четыре дополнительные полосы движения и, следовательно, увеличивает пропускную способность переправы примерно на треть. В не столь далеком Окленде проблема пропускной способности портового моста также была актуальна, поскольку он был построен в 1959 году и имел всего четыре полосы движения. По сути, они приняли то же решение, что и Сидней, а именно — увеличили пропускную способность. Но вместо того, чтобы строить туннель, они изобретательно удвоили количество полос движения, расширив мост с помощью уморительно названных «японских клипонов», которые расширяют мост с каждой стороны.

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

К счастью, в облачных программных системах репликация осуществляется одним кликом мыши, и мы можем эффективно реплицировать наши вычислительные ресурсы тысячи раз. В этом отношении нам гораздо легче, чем строителям мостов. Тем не менее, необходимо тщательно подходить к тиражированию ресурсов для устранения реальных узких мест. Добавление мощностей в пути обработки, которые не перегружены, приведет к ненужным затратам, не обеспечивающим преимущества масштабируемости.
Рисунок 1-1. Увеличение мощности за счёт репликации
Вторую стратегию масштабируемости можно также проиллюстрировать на примере моста. В Сиднее некий наблюдательный человек заметил, что по утрам гораздо больше машин пересекает мост с севера на юг, а днем наблюдается обратная картина. Поэтому было придумано разумное решение: утром выделять больше полос для движения в направлении, пользующемся повышенным спросом, а днем менять их местами. Это позволило эффективно увеличить пропускную способность моста без выделения новых ресурсов — мы оптимизировали уже имеющиеся.

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

Каноническим примером этого является создание компанией Facebook HipHop for PHP (ныне снятой с эксплуатации), которая позволила увеличить скорость генерации веб-страниц Facebook в шесть раз за счёт компиляции PHP-кода в C++.

К этим двум принципам проектирования — репликации и оптимизации — я буду возвращаться на протяжении всей книги. Вы увидите, что применение этих принципов имеет множество сложных последствий, вытекающих из того факта, что мы строим распределённые системы. Распределённые системы обладают свойствами, которые делают построение масштабируемых систем «интересным», что в данном контексте имеет как положительную, так и отрицательную коннотацию.
Масштабируемость и затраты
Рассмотрим связь между масштабируемостью и стоимостью на тривиальном гипотетическом примере. Предположим, что у нас есть веб-система (например, веб-сервер и база данных), которая может обслуживать нагрузку в 100 одновременных запросов со средним временем ответа в 1 секунду. Мы получили бизнес-запрос, согласно которому нам необходимо масштабировать систему, чтобы она справлялась с 1.000 одновременных запросов с тем же временем отклика. Без внесения каких-либо изменений простое нагрузочное тестирование этой системы показало производительность, представленную на рисунке 1-2 (слева). По мере увеличения количества запросов мы видим, что среднее время отклика неуклонно растёт и достигает 10 секунд при целевой нагрузке. Очевидно, что в текущей конфигурации развёртывания эта система не удовлетворяет нашим требованиям. Система не масштабируется.
Рисунок 1-2. Масштабирование приложения; слева представлена немасштабируемая производительность, справа — масштабируемая производительность
Для достижения требуемых характеристик необходимо приложить некоторые инженерные усилия. На рисунке 1-2 (справа) показана производительность системы после того, как эти усилия были приложены. Теперь она обеспечивает заданное время отклика при 1.000 одновременных запросах. Таким образом, мы успешно масштабировали систему. Время пить шампанское!

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

  • База данных становится менее отзывчивой при 1.000 запросов в секунду, что требует перехода на новую машину.
  • Веб-сервер генерирует большое количество контента динамически, что снижает время отклика под нагрузкой. Возможным решением является изменение кода для более эффективной генерации содержимого, что позволяет сократить время обработки одного запроса.
  • Нагрузка на запросы создаёт «горячие точки» в базе данных, когда множество запросов пытаются одновременно получить доступ и обновить одни и те же записи. Это требует перепроектирования схемы и последующей перезагрузки базы данных, а также изменения кода на уровне доступа к данным.
  • В выбранном фреймворке для веб-сервера удобство разработки превалирует над масштабируемостью. Применяемая им модель означает, что код просто не может быть масштабирован для удовлетворения запрошенных требований по нагрузке, и требуется его полное переписывание. Использовать другой фреймворк? Или даже другой язык программирования?
Существует огромное количество других возможных причин, но, надеюсь, даже эти иллюстрируют возрастающие усилия, которые могут потребоваться при переходе от возможности (1) к возможности (4).

Теперь предположим, что вариант (1) — модернизация сервера баз данных — требует 15 часов работы и тысячу долларов дополнительных расходов на облачные вычисления в месяц для более мощного сервера. Это не запредельно дорого. А вариант (4) — переписывание слоя веб-приложений — требует 10 000 часов разработки из-за внедрения нового языка (например, Java вместо Ruby). Варианты (2) и (3) находятся где-то посередине между вариантами (1) и (4). Стоимость 10 000 часов разработки является серьёзно значимой. Ещё хуже, что пока идет разработка, приложение может терять долю рынка, а значит, и деньги из-за неспособности удовлетворить нагрузку на клиентские запросы. Подобные ситуации могут привести к краху систем и предприятий.
Этот простой сценарий иллюстрирует, как неразрывно связаны с масштабируемостью такие аспекты, как затраты ресурсов и усилий. Если система по своей сути не рассчитана на масштабирование, то последующие затраты и ресурсы на увеличение её возможностей для удовлетворения требований могут быть огромными. Для некоторых приложений, таких как HealthCare.gov, эти затраты (более 2 миллиардов долларов США) оказываются посильными, и система модифицируется, чтобы в конечном итоге удовлетворить потребности бизнеса. Для других, таких как биржа здравоохранения штата Орегон, невозможность быстрого масштабирования при низких затратах может стать дорогостоящим (в случае штата Орегон — 303 миллиона долларов) предвестником смерти.

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

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

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

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

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

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

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

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

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

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

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

На сетевом уровне системы обычно используют протокол Transport Layer Security (TLS), который работает поверх TCP/IP (см. главу 3). TLS обеспечивает шифрование, аутентификацию и целостность с использованием асимметричной криптографии. Это связано с затратами производительности при установлении защищённого соединения, поскольку обеим сторонам необходимо генерировать и обмениваться ключами. Установление соединения TLS также включает в себя обмен сертификатами для проверки подлинности сервера (и, возможно, клиента) и выбор алгоритма для проверки того, что данные не были подделаны при передаче. После установления соединения данные на лету шифруются с помощью симметричной криптографии, которая приводит к незначительному снижению производительности, поскольку современные процессоры оснащены специальными средствами шифрования. Установление соединения обычно требует двух обменов сообщениями между клиентом и сервером и поэтому является сравнительно медленным. Максимально возможное переиспользование соединений позволяет минимизировать эти накладные расходы.

Существует множество вариантов защиты данных в состоянии покоя. Популярные системы баз данных, такие как SQL Server и Oracle, имеют такие функции, как прозрачное шифрование данных (tansparent data encryption TDE), обеспечивающее эффективное шифрование на уровне файлов. Более тонкие механизмы шифрования, вплоть до уровня поля, всё чаще требуются в регулируемых отраслях, таких как финансы. Поставщики «облачных» услуг также предлагают различные функции, обеспечивающие безопасность данных, хранящихся в «облачных» хранилищах данных. Накладные расходы на обеспечение безопасности данных в состоянии покоя — это просто затраты, которые необходимо понести для достижения безопасности. По данным исследований, эти накладные расходы составляют около 5-10% бюджета.

Другая точка зрения на безопасность — это триада CIA, которая расшифровывается как Конфиденциальность, Целостность и Доступность [Confidentiality, Integrity and Availability] (русский вариант — КГБ: Конфиденциальность, соГласованность, Бесперебойность). Первые два пункта практически полностью совпадают с тем, что я описал выше. Под доступностью понимается способность системы надёжно работать в условиях атак со стороны злоумышленников. Такими атаками могут быть попытки использовать недостатки конструкции системы, чтобы вывести её из строя. Другая атака — классический распределённый отказ в обслуживании (DDoS), при котором противник получает контроль над множеством систем и устройств и координирует поток запросов, фактически делающих систему недоступной.

В общем случае безопасность и масштабируемость являются противоположными силами. Безопасность неизбежно влечёт за собой снижение производительности. Чем больше уровней безопасности в системе, тем больше нагрузка на производительность, а значит, и на масштабируемость. В конечном итоге это сказывается на конечном результате — для достижения требований к производительности и масштабируемости системы требуются более мощные и дорогие ресурсы.
Управляемость
По мере того как создаваемые нами системы становятся всё более распределёнными и сложными в своём взаимодействии, на первый план выходит управление и эксплуатация. Мы должны уделять внимание тому, чтобы каждый компонент работал так, как ожидается, и его производительность постоянно соответствовала ожиданиям.
Платформы и технологии, используемые нами для построения систем, предоставляют множество стандартных и собственных средств мониторинга, которые могут быть использованы для этих целей. Для проверки текущего состояния и поведения каждого компонента системы можно использовать панели мониторинга. Эти панели, построенные с использованием открытых инструментов, таких как Grafana, могут отображать системные метрики и отправлять оповещения при возникновении различных пороговых значений или событий, требующих внимания оператора. Термин, используемый для обозначения этой сложной возможности мониторинга, — наблюдаемость (observability).

Существуют различные API, такие как Java MBeans, AWS CloudWatch и Python App-Metrics, которые инженеры могут использовать для сбора пользовательских метрик для своих систем (типичный пример — время отклика на запрос). С помощью этих API-интерфейсов можно настроить панели мониторинга, которые будут отображать графики и диаграммы в реальном времени, дающие подробное представление о поведении системы. Такие данные неоценимы для обеспечения непрерывной работы и выявления тех частей системы, которые могут нуждаться в оптимизации или репликации.
Масштабирование системы неизменно означает добавление новых компонентов системы — аппаратных и программных. С ростом числа компонентов появляется больше движущихся частей, которые необходимо контролировать и которыми необходимо управлять. Это никогда не обходится без усилий. Это усложняет работу системы и увеличивает затраты на код мониторинга, который требует разработки, и на эволюцию платформы наблюдаемости.

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

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