что такое высоконагруженные системы
Высоконагруженные системы: решение основных проблем
Сегодня я хочу рассказать о некоторых решениях проблем, которые возникают во время использования высоконагруженных систем. Все, о чем пойдет речь в этом материале, проверено на собственном опыте: я – Social Games Server Team Lead в компании Plarium, которая занимается разработкой социальных, мобильных, браузерных игр.
Для начала немного статистики. Plarium занимается разработкой игр с 2009 года. На данный момент наши проекты запущены во всех наиболее популярных социальных сетях («Вконтакте», «Мой мир», «Одноклассники», Facebook), несколько игр интегрированы в крупные игровые порталы: games.mail.ru, Kabam. Отдельно существует браузерная и мобильная (iOS) версии стратегии «Правила войны». В базах числятся более 80 миллионов пользователей (5 игр, локализация на 7 языках, 3 миллиона уникальных игроков в день), в итоге все наши серверы получают в среднем около 6500 запросов в секунду и 561 миллион запросов в сутки.
Учитывая специфику нашей работы, необходимо не только обеспечить стабильное функционирование систем, но и гарантировать, что они смогут выдержать большой скачок нагрузки. Увы, многие популярные подходы и технологии не выдерживают проверку высокой нагрузкой и быстро становятся узким местом в системе. Поэтому, проанализировав множество проблем, мы нашли оптимальные для нас (как мне кажется) пути их решения. Я расскажу, какие технологии мы выбрали, от каких отказались, и объясню, почему.
NoSQL vs. Relational
В этом сражении чистый NoSQL показал себя слабым бойцом: существовавшие на тот момент решения не поддерживали вменяемую консистентность данных и не обладали достаточной устойчивостью к падениям, что давало о себе знать в процессе работы. Хотя в итоге выбор пал на реляционные СУБД, которые позволяли использовать транзакционность в необходимых местах, в целом в качестве основного подхода используется NoSQL. В частности, таблицы зачастую имеют очень простую структуру типа ключ-значение, где данные представлены в виде JSON, который хранится в упакованном виде в колонке BLOB. В результате схема остается простой и стабильной, при этом структура поля данных может легко расширяться и изменяться. Как ни странно, это дает очень хороший результат – в нашем решении мы объединили преимущества обоих миров.
Учитывая тот факт, что чистый ADO.NET имеет минимальный оверхед, а все запросы созданы вручную, знакомы и греют душу, он отправляет любые ORM в глубокий нокаут. А всё потому, что объектно-реляционное отображение имеет в нашем случае ряд минусов, таких как низкая производительность и низкий контроль запросов (или его отсутствие). При использовании многих решений ORM приходится долго и часто бороться с библиотекой и терять главное – скорость. А уж если речь заходит о хитром флаге для правильной обработки тайм-аутов клиентской библиотеки или о чем-то аналогичном, то попытки представить установку такого флага с использованием ORM окончательно расстраивают.
Distributed transactions vs. Own Eventual Consistency
Основная задача транзакций – обеспечить согласованность данных после завершения операции. Изменения либо успешно сохраняются, либо полностью откатываются, если что-то пошло не так. И если в случае одной базы мы были рады использовать этот, без сомнения, важный механизм, то распределенные транзакции при высоких нагрузках показали себя не с лучшей стороны. Результат использования – увеличенное время ожидания, усложнение логики (здесь вспоминаем еще про необходимость обновления кэшей в памяти экземпляров приложений, возможность возникновения мертвых блокировок, слабую устойчивость к физическим сбоям).
В итоге мы разработали свой вариант механизма для обеспечения Eventual Consistency, построенный на очередях сообщений. В результате мы получили: масштабируемость, устойчивость к сбоям, подходящее время наступления согласованности, отсутствие мертвых блокировок.
SOAP, WCF, etc. vs. JSON over HTTP
MVC.NET, Spring.NET vs. Naked ASP.NET
Опираясь на опыт работы, могу сказать, что MVC.NET, Spring.NET и им подобные фреймворки создают лишние промежуточные конструкции, которые мешают выжимать максимальную производительность. Наше решение построено на самых базовых возможностях, предоставляемых ASP.NET. Фактически точкой входа являются несколько обычных хендлеров. Мы не используем ни одного стандартного модуля, в приложении нет ни одной активной сессии ASP.NET. Всё понятно и просто.
Немного о велосипедах
Если ни один из существующих методов решения проблемы не подходит, приходится становиться немного изобретателем и заново искать ответы на вопросы. И пусть иногда ты изобретаешь велосипед, если этот велосипед критически важен для проекта – оно того стоит.
JSON сериализация
Чуть больше трети используемого нами времени CPU тратится на сериализацию/десериализацию больших объемов данных в формате JSON, поэтому вопрос эффективности данной задачи является очень важным в контексте производительности системы в целом.
Изначально в работе мы использовали Newtonsoft JSON.NET, но в определенный момент пришли к выводу, что его скорости недостаточно, и мы можем реализовать нужное нам подможество фич более быстрым способом, без необходимости поддержки слишком большого количества вариантов десериализации и «замечательных» фич вроде валидации схем JSON, десериализации в JObject и т.д.
Поэтому мы самостоятельно написали сериализацию с учетом специфики своих данных. При этом на тестах получившееся решение оказалось в 10 раз быстрее JSON.Net и в 3 раза быстрее fastJSON.
Критически важной для нас была совместимость с уже существующими данными, сериализованными с помощью Newtonsoft. Чтобы убедиться в совместимости, перед включением нашей сериализации в продакшн мы провели тестирование на нескольких крупных базах: вычитывали данные в формате JSON, десериализовали с помощью нашей библиотеки, снова выполняли сериализацию и проверяли исходный и конечный JSON на равенство.
Из-за нашего подхода к организации данных мы получили отрицательный эффект в виде слишком большого размера кучи больших объектов (large object heap). Для сравнения, ее размер в среднем составлял порядка 8 гигабайт против 400—500 мегабайт в объектах второго поколения. В итоге эту проблему решили путем разбивки больших блоков данных на блоки меньшего размера с использованием пула ранее выделенных блоков. Благодаря такой схеме куча больших объектов значительно уменьшилась, сборки мусора стали происходить реже и легче. Пользователи довольны, а это главное.
Работая с памятью, мы используем несколько кэшей разных размеров с различными политиками устаревания и обновления, при этом конструкция некоторых кэшей предельно проста, без всяческих излишеств. В результате показатель эффективности всех кэшей – не ниже 90—95%.
Опробовав Memcached, мы решили отложить его на будущее, т.к. пока что в нем нет необходимости. В целом результаты были неплохие, но общий выигрыш по производительности не превысил стоимость дополнительной сериализации/десериализации при помещении данных в кэш.
• Профилировщик
Так как привычные профилировщики значительно замедляют скорость работы приложения при подключении к нему, фактически делая невозможным профилировку достаточно нагруженных приложений, мы используем свою систему счетчиков производительности:
На этом тестовом примере видно, что мы оборачиваем основные операции в счетчики с именами. Статистика накапливается в памяти и собирается с серверов вместе с другой полезной информацией. Поддерживаются иерархии счетчиков для анализа цепочек вызовов. В результате можно получить подобный отчет:
Среди положительных сторон:
— счетчики всегда включены;
— минимальные издержки (менее 0,5% ресурса используемого CPU);
— простой и гибкий подход к указанию профилируемых участков;
— автоматическая генерация счетчиков для точек входа (сетевых запросов, методов);
— возможность просмотра и агрегации по принципу parent—child;
— можно оценивать не только real-time данные, но и сохранять значения измерений счетчиков по времени с возможностью дальнейшего просмотра и анализа.
• Логирование
Зачастую это единственный способ диагностики ошибок. В работе мы используем два формата: human readable и JSON, при этом пишем всё, что можно писать, пока хватает места на диске. Собираем логи с серверов и используем для анализа. Всё сделано на базе log4net, поэтому не используется ничего лишнего, решения максимально просты.
• Администрирование
В дополнение к богатому графическому веб-интерфейсу нашей админки мы разработали веб-консоль, в которую можно добавлять команды прямо на игровом сервере, при этом не внося изменения в админку. Также с помощью консоли можно очень просто и быстро добавить новую команду для диагностики, получения технических данных в режиме онлайн или возможности подстройки системы без перезагрузки.
• Развертывание
С увеличением количества серверов выливать что-либо вручную стало невозможно. Поэтому всего за неделю работы одного программиста мы разработали простейшую систему для автоматизированного обновления серверов. Скрипты были написаны на C#, что позволяло достаточно гибко модифицировать и поддерживать логику развертывания. В результате мы получили очень надежный и простой инструмент, который в критических ситуациях позволяет обновить все продакшн-серверы (порядка 50) за несколько минут.
Для того чтобы добиться скорости при высокой нагрузке на серверы, необходимо использовать более простой и тонкий стек технологий, все инструменты должны быть знакомы и предсказуемы. Конструкции должны быть одновременно простыми, достаточными для решения текущих проблем и иметь запас прочности. Оптимально использовать горизонтальное масштабирование, производить кэш-контроль производительности. Логирование и мониторинг состояния системы – must have для жизнеобеспечения любого серьезного проекта. А система развертывания значительно упростит жизнь, поможет сэкономить нервы и время.
Лекции Технопарка. 3 семестр. Проектирование высоконагруженных систем
И снова в эфире наша постоянная рубрика «Лекции Технопарка». На этот раз предлагаем вам ознакомиться с материалами курса «Проектирование высоконагруженных систем». Цель курса — получение студентами навыков проектирования высокоэффективных программных систем.
Лекция 1. Введение в Highload
В начале лекции даётся определение — что можно считать высоконагруженной системой и в каких единицах измеряется нагрузка. Объясняются особенности подобных систем, вкратце говорится о Slashdot-эффекте. Приводятся критерии высокой доступности сайта с точки зрения времени простоя за месяц и год. Описываются различные архитектуры веб-серверов, устройство типичного веб-сайта, LAMP-технология. Далее рассказывается о методах подключения динамического содержимого: CGI, FastCGI, UWSGI, mod_perl, mod_php, самописных модулей, node.js, content_by_lua. Рассматривается понятие блокирующих операций и способы неблокирующей обработки.
Лекция 2. Сетевая подсистема
Начинается лекция с объяснения того, какие факторы влияют на пропускную способность системы: сетевые задержки, скорость света и расстояние между ДЦ, устройство TCP-handshake, packetloss и TCP-retransmit. Объясняется, как выявить узкие места с точки зрения пропускной способности. Рассматривается понятие Looking Glass в качестве одного из инструментов для диагностики проблем с пропускной способностью. Далее говорится о модели OSI применительно к TCP/IP, о нюансах маршрутизации. Затем рассказывается о возможных сетевых проблемах и различных способах их решения (UDP, multicast, Jumbo-frames, socket на процесс, многопоточные сетевые карты). Существенная часть лекции посвящена оптимизации TCP/IP-стека под высокую нагрузку.
Лекция 3. Протокол HTTP и веб-оптимизация
Перечисляются некоторые правила веб-оптимизации. Рассматриваются особенности браузеров, используемые для оптимизации времени загрузки страницы. Затрагиваются вопросы сжатия данных gzip, уменьшения количества запросов, минимизации количества запросов к DNS, а также файлы скриптов и CSS принудительного кеширования статики. Объясняется, как анализировать информацию, полученную с помощью Conditional GET. После этого рассказывается о возможностях по оптимизации редиректов, о CSS-спрайтах. Затем даётся информация о keep-alive, chunked, о грамотном использовании cookies. Объясняются преимущества нескольких соединений на домен, вынос долгих запросов в AJAX или iframe.
Лекция 4. Масштабирование нагрузки
Сначала даётся определение масштабирования нагрузки и ее видов (вертикального и горизонтального). Далее подробно рассказывается об алгоритмах балансировки нагрузки (random, round-robin, weighted round-robin, least connections, least response time, load-based). Рассматриваются инструменты для балансировки: Round-Robin DNS, xixi DNS, L4-балансеры (Cisco CSS, LVS), L7-балансеры (Cisco ACE, LVS, nginx).
Лекция 5. Оперативная память
Разбирается аппаратная конфигурация типичного сервера, объясняются устройство физической памяти и причины снижения общей производительности системы. Рассказывается, как организуется кэширование на аппаратном уровне, а также рассматриваются практические способы ускорения работы с памятью сервера (последовательное чтение с запасом, чтение без перескоков между рядами, prefetching).
Лекция 6. Базы данных и дисковая подсистема
Сначала рассказывается о развитии жёстких дисков и текущем состоянии в плане производительности при линейном, случайном и конкурентном доступе. Сравниваются особенности, преимущества и недостатки разных видов дисковых массивов, в том числе программных. Затем рассматриваются файловые системы Ext4 и XFS. Упоминается третий уровень виртуализации жёстких дисков — LVM (Logical volume manager). Вторая часть лекции посвящена базам данных. Сначала подробно раскрываются достоинства и недостатки СУБД MySQL и PostgreSQL. Разбирается структура расходов на выполнение запроса, а также планирование самогó запроса. Также рассказывается о методах ускорения систем, построенных на базах данных: тюнинг, репликация, шардинг, минимизация сетевой задержки, NoSQL, написание специализированной БД.
Лекция 7. Типовые архитектурные решения
В начале лекции объясняется, чем отличаются frontend- и backend-серверы, рассматривается создание специализированных групп серверов по типам нагрузки (по функциям, по важности, по стабильности, по шардам). Перечисляются критерии сложности и надёжности тех или иных архитектурных решений, даются советы по выбору компонентов, технологий и языков программирования. Далее рассказывается о способах оптимизации (замена оборудования, использование другого алгоритма, переписка кода, распараллеливание задач на разные серверы и т.д.). После этого обсуждаются способы обработки ошибок при выполнении запросов, способы кэширования данных для снижения нагрузки в пиковые часы. Затем рассказывается о записи и обработке логов, о мониторинге нагрузки и работы как всей системы, так и её компонентов.
Книга «Высоконагруженные приложения. Программирование, масштабирование, поддержка»
В этой книге вы найдете ключевые принципы, алгоритмы и компромиссы, без которых не обойтись при разработке высоконагруженных систем для работы с данными. Материал рассматривается на примере внутреннего устройства популярных программных пакетов и фреймворков. В книге три основные части, посвященные, прежде всего, теоретическим аспектам работы с распределенными системами и базами данных. От читателя требуются базовые знания SQL и принципов работы баз данных.
В обзорном посте рассматривается раздел «Знание, истина и ложь».
Если у вас нет опыта работы с распределенными системами, то последствия этих проблем могут оказаться весьма дезориентирующими. Узел сети ничего не знает наверняка — он способен только делать предположения на основе получаемых (или не получаемых) им по сети сообщений. Один узел в силе узнать состояние другого узла (какие данные на нем хранятся, правильно ли он работает), только обмениваясь с ним сообщениями. Если удаленный узел не отвечает, то нет никакого способа выяснить его состояние, поскольку невозможно отличить сетевые проблемы от проблем в узле.
Обсуждения этих систем граничат с философией: что в нашей системе правда, а что ложь? Можем ли мы полагаться на это знание, если механизмы познания и измерения ненадежны? Должны ли программные системы подчиняться законам физического мира, например закону причины и следствия?
К счастью, нам не требуется искать смысл жизни. Для распределенной системы можно описать допущения относительно поведения (модель системы) и спроектировать ее так, чтобы она соответствовала этим допущениям. Существует возможность проверки корректной работы алгоритмов в рамках определенной модели системы. А значит, надежность вполне достижима, даже если лежащая в основе системы модель обеспечивает очень мало гарантий.
Однако хотя можно обеспечить должную работу системы при ненадежной модели, сделать это непросто. В оставшейся части главы мы обсудим понятия знания и истины в распределенных системах, что поможет нам разобраться с нужными допущениями и гарантиями. В главе 9 мы перейдем к рассмотрению ряда примеров распределенных систем и алгоритмов, которые обеспечивают конкретные гарантии при конкретных допущениях.
Истина определяется большинством
Представьте сеть с асимметричным сбоем: узел получает все отправляемые ему сообщения, но все его исходящие сообщения задерживаются или вообще отбрасываются. И хотя он отлично работает и получает запросы от других узлов, эти другие не получают его ответов. Как следствие, по истечении некоторого времени ожидания другие узлы объявляют его неработающим. Ситуация превращается в какой-то кошмар: наполовину отключенный от сети узел насильно тащат «накладбище», а он отбивается и кричит: «Я жив!». Но, поскольку никто его криков не слышит, похоронная процессия неуклонно продолжает движение.
В чуть-чуть менее кошмарном сценарии наполовину отключенный от сети узел может заметить отсутствие подтверждений доставки своих сообщений от других узлов и понимает, что имеет место сбой сети. Тем не менее другие узлы ошибочно объявляют наполовину отключенный узел неработающим, а он не способен ничего с этим поделать.
В качестве третьего сценария представьте узел, приостановленный на долгое время в связи с длительной всеобъемлющей сборкой мусора. Все его потоки вытесняются из памяти процессом сборки мусора и приостанавливаются на минуту, и, следовательно, запросы не обрабатываются, а ответы не отправляются. Другие узлы ждут, повторяют отправку, теряют терпение, в конце концов объявляют узел неработающим и «отправляют его в катафалк». Наконец процесс сборки мусора завершает работу и потоки узла возобновляют работу как ни в чем не бывало. Остальные узлы удивляются «воскрешению» в полном здравии объявленного «мертвым» узла и начинают радостно болтать с очевидцами этого. Сначала данный узел даже не понимает, что прошла целая минута и он был объявлен «мертвым», — с его точки зрения с момента последнего обмена сообщениями с другими узлами прошло едва ли не мгновение.
Мораль этих историй: узел не может полагаться только на свое собственное мнение о ситуации. Распределенная система не может всецело полагаться на один узел, поскольку он способен отказать в любой момент, приведя к отказу и невозможности восстановления системы. Вместо этого многие распределенные алгоритмы основывают свою работу на кворуме, то есть решении большинства узлов (см. пункт «Операции записи и чтения по кворуму» подраздела «Запись в базу данных при отказе одного из узлов» раздела 5.4): принятие решений требует определенного минимального количества «голосов» от нескольких узлов. Данное условие позволяет снизить зависимость от любого одного конкретного узла.
Это включает принятие решений об объявлении узлов неработающими. Если кворум узлов объявляет другой узел неработающим, то он считается таковым, пусть и прекрасно работает в то же время. Отдельные узлы обязаны подчиняться решениям кворума.
Обычно кворум представляет собой абсолютное большинство от более чем половины узлов (хотя существуют и другие разновидности кворумов). Кворум по большинству позволяет системе работать в случае сбоев отдельных узлов (при трех узлах допустим отказ одного, при пяти — отказ двух). Такой способ безопасен, поскольку система может насчитывать только одно большинство — два большинства одновременно, чьи решения конфликтуют, невозможны. Мы обсудим использование кворумов подробнее, когда доберемся до алгоритмов консенсуса в главе 9.
Ведущий узел и блокировки
Зачастую системе необходимо наличие только одного экземпляра чего-либо. Например:
Случай, когда узел продолжает вести себя как «избранный», несмотря на то, что кворум остальных узлов объявил его неработающим, может привести к проблемам в недостаточно тщательно спроектированной системе. Подобный узел способен отправлять сообщения другим узлам в качестве самозваного «избранного», и если другие узлы с этим согласятся, то система в целом может начать работать неправильно.
Например, на рис. 8.4 показана ошибка с порчей данных вследствие неправильной реализации блокировки (это отнюдь не теоретическая ошибка: она часто возникает в СУБД HBase). Допустим, мы хотели бы убедиться, что одновременно обратиться к находящемуся в сервисе хранения файлу может только один клиент, поскольку, если несколько клиентов сразу попытаются выполнить в него запись, файл будет испорчен. Мы попытаемся реализовать это с помощью обязательного получения клиентом договора аренды от сервиса блокировок до обращения к файлу.
Описанная проблема представляет собой пример того, что мы обсуждали в подразделе «Паузы при выполнении процессов» раздела 8.3: если арендующий клиент приостанавливается слишком надолго, то срок его договора аренды истекает. После этого другой клиент может получить договор аренды на тот же файл и начать запись в него данных. После возобновления работы приостановленный клиент верит (ошибочно), что у него все еще есть действующий договор аренды, и переходит к записи в файл. В результате операции записи двух клиентов перемешиваются и файл портится.
Ограждающие маркеры
При использовании блокировки или договора аренды для защиты доступа к какому-либо ресурсу, например файловому хранилищу на рис. 8.4, необходимо убедиться, что узел, ошибочно считающий себя «избранным», не нарушит работу всей остальной системы. Для этой цели существует достаточно простой метод, показанный на рис. 8.5. Он называется ограждением (fencing).
Представим, что при каждом предоставлении блокировки или договора аренды сервер блокировок также возвращает ограждающий маркер (fencing token), представляющий собой номер, наращиваемый при каждом предоставлении блокировки (например, его может наращивать сервис блокировок). Кроме того, потребуем, чтобы клиент при каждой отправке запроса на запись сервису хранения включал в текущий запрос такой маркер.
На рис. 8.5 клиент 1 получает договор аренды с маркером 33, после чего надолго приостанавливается, и срок договора аренды истекает. Клиент 2 получает договор аренды с маркером 34 (номер монотонно возрастает), после чего отправляет запрос на запись сервису хранения, включая в запрос этот маркер. Позднее клиент 1 возобновляет работу и отправляет сервису хранения операцию записи с маркером 33. Однако сервис хранения помнит, что уже обработал операцию записи с большим номером маркера (34) и отклоняет данный запрос.
При использовании в качестве сервиса блокировок ZooKeeper в роли ограждающего маркера можно задействовать идентификатор транзакции zxid или версию узла cversion. Поскольку ZooKeeper гарантирует их монотонное возрастание, они обладают нужными для этого свойствами.
Обратите внимание: этот механизм требует, чтобы сам ресурс принимал активное участие в проверке маркеров, отклоняя все операции записи с маркерами, более старыми, чем уже обработанные, — проверки статуса блокировки на самих клиентах недостаточно. Можно обойти ограничение и для ресурсов, не поддерживающих явным образом ограждающие маркеры (например, в случае сервиса хранения файлов ограждающий маркер включается в имя файла). Однако некоторая проверка все равно нужна, во избежание обработки запросов без защиты блокировкой.
Проверка маркеров на стороне сервера может показаться недостатком, но, вполне вероятно, это не так уж плохо: для сервиса было бы неразумно считать, что все его клиенты всегда «ведут себя хорошо», поскольку клиенты часто запускают люди совсем не с такими приоритетами, как у владельцев сервиса. Следовательно, для любого сервиса будет хорошей идеей защищаться от непредумышленных неправильных действий со стороны клиентов.
Византийские сбои
Ограждающие маркеры способны обнаруживать и блокировать узел, непреднамеренно выполняющий ошибочные действия (например, потому, что еще не обнаружил истечение срока его договора аренды). Однако узел, умышленно желающий подорвать системные гарантии, может легко это сделать, отправив сообщение с поддельным маркером.
В этой книге мы предполагаем, что узлы ненадежны, но «добропорядочны»: они могут работать медленно или не отвечать вообще (вследствие сбоя), их состояние может быть устаревшим (из-за паузы на сборку мусора или сетевых задержек), но если узел вообще отвечает, то он «говорит правду» (соблюдает правила протокола в рамках имеющейся у него информации).
Проблемы распределенных систем значительно усугубляются при наличии риска, что узлы могут «лгать» (отправлять произвольные сбойные или поврежденные ответы) — например, если узел может заявить о получении определенного сообщения, когда на самом деле этого не было. Подобное поведение носит название византийского сбоя (Byzantine fault), и задача достижения консенсуса в подобной, не заслуживающей доверия среде известна как задача византийских генералов (Byzantine generals problem).
Система защищена от византийских сбоев, если продолжает работать правильно даже в случае нарушения работоспособности некоторых узлов и не соблюдения ими протокола или при вмешательстве злоумышленников в работу сети. Это может быть важно в следующих обстоятельствах.
В то же время веб-приложениям имеет смысл ожидать произвольного и злонамеренного поведения от находящихся под контролем конечных пользователей клиентов, например браузеров. Поэтому проверка вводимых данных, контроль корректности и экранирование вывода столь важны: например, для предотвращения внедрения SQL-кода (SQL injection) и межсайтового выполнения сценариев (cross-site scripting). Однако при этом обычно не применяются защищенные от византийских сбоев протоколы, а серверу просто делегируются полномочия принятия решения, допустимо ли поведение клиента. В одноранговых сетях, где подобного центрального органа нет, защищенность от византийских сбоев более уместна.
Можно рассматривать ошибки в программном обеспечении как византийские сбои, однако если одно и то же ПО используется во всех узлах, то алгоритмы защиты от византийских сбоев вас не спасут. Большинство таких алгоритмов требуют квалифицированного большинства из более чем двух третей нормально работающих узлов (то есть в случае четырех узлов может не работать максимум один). Чтобы решить проблему ошибок с помощью такого подхода, вам пришлось бы использовать четыре независимые реализации одного и того же программного обеспечения и надеяться на присутствие ошибки только в одной из них.
Аналогично кажется заманчивым, чтобы сам протокол защищал нас от уязвимостей, нарушений требований безопасности и злонамеренных действий. К сожалению, в большинстве систем это нереально. Если злоумышленник сумеет получить несанкционированный доступ к одному узлу, то очень вероятно, что сможет получить доступ и к остальным, поскольку на них, скорее всего, работает одно и то же программное обеспечение. Следовательно, традиционные механизмы (аутентификация, контроль доступа, шифрование, брандмауэры и т. д.) остаются основной защитой от атак.
Слабые формы «лжи». Хотя мы предполагаем, что узлы преимущественно «добропорядочны», имеет смысл добавить в ПО механизмы защиты от слабых форм «лжи» — например, некорректных сообщений вследствие аппаратных проблем, ошибок в программном обеспечении и неправильных настроек. Подобные механизмы нельзя считать полномасштабной защитой от византийских сбоев, ведь они не смогут спасти от решительно настроенного злоумышленника, но это простые и практичные шаги по усилению надежности. Вот несколько примеров.
Модели системы на практике
Для решения задач распределенных систем было разработано множество алгоритмов, например, мы рассмотрим решения для задачи консенсуса в главе 9. Чтобы принести какую-то пользу, эти алгоритмы должны уметь справляться с различными сбоями распределенных систем, которые мы обсуждали в этой главе.
Алгоритмы должны как можно меньше зависеть от особенностей аппаратного обеспечения и настроек ПО системы, на которой они работают. Это, в свою очередь, требует формализации типов вероятных сбоев. Для этого мы опишем модель системы — абстракцию, описывающую принимаемые алгоритмом допущения.
Что касается допущений по хронометражу, часто используются три системные модели.
Корректность алгоритмов
Чтобы дать определение корректности (correctness) алгоритма, я опишу его свойства. Например, у результатов работы алгоритма сортировки есть следующее свойство: для любых двух различных элементов выходного списка элемент слева меньше, чем элемент справа. Это просто формальный способ описания сортировки списка.
Аналогично формулируются свойства, которые требуются от корректного распределенного алгоритма. Например, при генерации ограждающих маркеров для блокировки можно требовать от алгоритма следующие свойства: