Эта модель также хорошо работает при пиковых нагрузках, где по мере возрастания потребностей могут запускаться дополнительные экземпляры для соответствия поступающей нагрузке. Пока сама очередь работ будет сохранять устойчивость, эта модель может использовать масштабирование для повышения как пропускной способности работ, так и отказоустойчивости, поскольку становится проще справиться с влиянием отказавшего (или отсутствующего) исполнителя. Работа займет больше времени, но ничего при этом не потеряется.
Я видел, как это вполне успешно работает в организациях, имеющих в определенные периоды времени большие объемы незадействованных вычислительных мощностей. Например, по ночам для работы системы электронной торговли все имеющиеся машины вам не нужны, поэтому временно их можно задействовать для выполнения заданий по созданию отчетов.
Хотя в системах на основе исполнителей самим исполнителям высокая надежность и не нужна, система, содержащая предназначенную для выполнения работу. должна быть надежной. Справиться с этим можно, к примеру запустив круглосуточный брокер сообщений или такую систему, как Zookeeper. Преимущества такого подхода заключаются в том, что при использовании для достижения этих целей существующих программных средств наиболее сложную задачу за нас выполняет кто-то другой. Но нам по-прежнему требуется знать, как настроить и обслуживать эти системы, добиваясь от них безотказной работы.
Архитектура, использовавшаяся сначала, может не стать архитектурой, используемой в дальнейшем, когда вашей системе придется обрабатывать совершенно разные объемы нагрузки. Как Джеф Дин (Jeff Dean) говорил в своей презентации Challenges in Building Large-Scale Information Retrieval Systems (конференция WSDM 2009), вы должны «закладывать в конструкцию возможность десятикратного роста, но планировать ее перезапись под стократный рост». Для поддержки следующего уровня роста в определенные моменты придется делать нечто весьма радикальное.
Вспомним историю Gilt, которую мы уже затрагивали в главе 6. В течение двух лет Gilt вполне устраивало монолитное Rails-приложение. Бизнес развивался успешно, что означало увеличение количества клиентов и рост объема нагрузки. В определенный переломный момент, чтобы справиться с возросшей нагрузкой, компании пришлось переделать приложение.
Переделка может означать разбиение монолита на части, что и было сделано для Gilt. Или выбор новых хранилищ данных, которые смогли бы лучше справиться с нагрузкой, что и будет рассмотрено нами в ближайшем будущем. Это также может означать применение новых технологий, например переход с синхронных систем «запрос — ответ» к системам на основе событий, применение новых платформ развертывания, смену всех технологических стеков или применение сразу всех этих нововведений.
Есть опасение, что люди поймут необходимость изменения архитектуры в момент достижения определенного порога масштабирования и примут это за предлог для создания с нуля системы под более широкий масштаб. Это может иметь весьма пагубные последствия. Запуская новый проект, мы зачастую не знаем в точности, что именно хотим создать, и не знаем, будет ли он успешен. Нам нужно иметь возможность быстрого проведения эксперимента, чтоб понять, какие функциональные возможности следует создавать. Если изначально попытаться создать систему под широкий масштаб, мы сразу же получим большой объем работ для обеспечения готовности к нагрузке, которой может никогда и не быть. Это отвлечет силы от более важных действий, например от выяснения того, захочет ли кто-нибудь вообще воспользоваться нашим продуктом. Эрик Рис (Eric Ries) рассказывал историю о том, как шесть месяцев было потрачено на создание продукта, который никто даже не загрузил. Он размышлял, что можно было даже установить ссылку на несуществующую веб-страницу, при щелчке на которой люди получали бы сообщение об ошибке 404, чтобы посмотреть, была ли вообще потребность в продукте, а вместо работы провести шесть месяцев на пляже и при этом получить более весомый результат!
Потребность во внесении в систему изменений, позволяющих ей справиться с расширением масштаба, нельзя считать провалом. Нужно считать это признаком успеха.
Масштабирование микросервисов без сохранения состояния производится довольно просто. А что делать, если мы сохраняем данные в базе данных? Нам нужно знать, как выполнять масштабирование и в таком случае. Различные типы баз данных требуют разных форм масштабирования, и понимание того, какая из этих форм подойдет наилучшим образом именно для вашего случая, гарантирует выбор нужной технологии баз данных с самого начала.
Изначально важно отделить понятие доступности сервиса от понятия долговечности самих данных. Нужно разобраться в том, что это два разных понятия и поэтому для них будут использоваться разные решения.
Например, я могу хранить копию всех данных, записанных в мою базу данных, в отказоустойчивой файловой системе. Если база данных откажет, данные не пропадут, поскольку у меня есть копия, но сама база данных станет недоступной, что может привести также к недоступности моего микросервиса. В более обобщенной модели будут задействованы резервы. Все данные, записанные в основную базу данных, будут копироваться в резервную базу данных, являющуюся точной копией основной. Если основная база данных даст сбой, мои данные окажутся в безопасности, но без механизма, который либо их вернет, либо повысит резервную базу до статуса основной, доступной базы данных у нас не будет, хотя сами данные будут в безопасности.
Многие сервисы в основном занимаются считыванием данных. Вспомним сервис каталогов, который хранит информацию для выставленных на продажу товарных позиций. Мы добавляем записи для новых товарных позиций на весьма нерегулярной основе, и совсем неудивительно, что на каждую запись в каталог мы имеем по 100 считываний данных нашего каталога. К счастью, масштабирование для чтения дается значительно легче масштабирования для записи. Здесь большую роль может сыграть кэширование данных, которое вскоре будет рассмотрено более подробно. Еще одна модель предусматривает использования реплик чтения.
В системе управления реляционными базами данных (RDBMS), подобной MySQL или Postgres, данные могут копироваться из основного узла в одну или несколько реплик. Зачастую это делается с целью обеспечения безопасного хранения копий данных, но может использоваться также для распределения операций чтения. Сервис может направлять все запросы на запись к единственному основному узлу, но при этом распределять запросы на чтение между несколькими репликами, предназначенными для считывания данных (рис. 11.6). Резервное копирование из основной базы данных к репликам происходит через некоторое время после записи. Это означает, что при такой технологии считывания до завершения репликации данные могут быть устаревшими. Со временем операциям чтения станут доступны согласующиеся данные. Подобная настройка называется согласованностью, возникающей по прошествии некоторого времени, и если вы в состоянии справиться с временной несогласованностью, то ее можно признать довольно простым и весьма распространенным способом, содействующим масштабированию систем. Вскоре, когда дойдет очередь до теоремы CAP, мы рассмотрим его более подробно.
Рис. 11.6. Использование реплик для чтения с целью масштабирования операций считывания данных
Мода на использование реплик чтения в целях масштабирования появилась много лет назад, но сегодня я бы порекомендовал вам присмотреться в первую очередь к кэшированию, от которого можно получить существенно больше преимуществ с точки зрения производительности, зачастую потратив на это меньше времени и сил.
Масштабирование чтения дается сравнительно легко. А как насчет записей? Один из подходов предусматривает применение фрагментации. При этом используются несколько узлов базы данных. Берется часть данных, подлежащих записи, к ним применяется некая функция хеширования для получения ключа данных, и на основании результата работы функции определяется, куда эти данные отправлять. Рассмотрим весьма упрощенный (и совсем негодный) пример: представим, что клиентские записи диапазона A — M попадают в один экземпляр базы данных, а записи диапазона N — Z — в другой. Этим можно управлять самостоятельно в своем приложении, но некоторые базы данных, например Mongo, многое в этом плане делают за вас.
Сложность с фрагментацией операций записи данных заключается в управлении запросами. Когда смотришь на отдельно взятую запись, все представляется несложным, поскольку можно просто применить функцию хеширования, чтобы найти нужный экземпляр данных, а затем извлечь его из соответствующего фрагмента базы. А как быть с запросами, данные которых разбросаны по нескольким узлам, например предписывающими найти всех клиентов старше 18 лет? Если требуется запросить все фрагменты базы, то нужно либо запросить каждый отдельно взятый фрагмент и объединить ответы в памяти, либо иметь альтернативное хранилище для чтения, в котором доступны данные из обоих наборов. Зачастую отправкой запросов, распространяющихся на несколько фрагментов, управляет асинхронный механизм с использованием кэшируемых результатов. Например, в Mongo для выполнения таких запросов используются задания отображения и свертки (map/reduce jobs).
При использовании фрагментированных систем возникает вопрос: что случится, если понадобится добавить еще один узел базы данных? В прошлом для этого нужен был довольно длительный простой, особенно для крупных кластеров, поскольку могла потребоваться остановка работы всей базы данных и перебалансировка хранящейся в ней информации. Совсем недавно во многих системах появилась поддержка добавления фрагментов к не прекращающей работу системе, в которой перебалансировка данных осуществляется в фоновом режиме. К примеру, Cassandra справляется с этим очень хорошо. Добавление фрагментов к существующим кластерам — занятие не для слабых духом, так что все нужно тщательно проверить.