Делай как вGoogle
Скачать 5.77 Mb.
|
Написание ПО для управляемых вычислений Переход от списков машин, управляемых вручную, к автоматизированному плани- рованию и настройке значительно упростил управление парком машин, но также потребовал глубоких изменений в подходах к проектированию и разработке ПО. Проектирование с учетом возможности отказов Представьте, что инженер должен обработать пакет, включающий миллион доку- ментов, и проверить их правильность. Если обработка одного документа занимает одну секунду, то на обработку всего пакета на одной машине понадобится примерно 12 дней. Чтобы сократить время обработки до более приемлемых 100 минут, мы рас- пределяем работу между 200 машинами. Как обсуждалось в разделе «Автоматизация планирования» выше, планировщик Borg может в одностороннем порядке остановить любую из 200 рабочих реплик и пере- местить вычисления на другую машину 1 . То есть он может запустить новую реплику без участия человека, настройки переменных окружения или установки пакетов. Переход от «инженер должен вручную контролировать каждую из 100 задач и вме- шиваться в случае поломки» к «если что-то пойдет не так с одной из задач, система автоматически остановит вычисления и запустит их на другой машине» был описан много лет спустя с помощью аналогии «домашние любимцы против скота» 2 1 Планировщик делает по конкретным причинам (например, если появится необходимость обновить ядро, обнаружится неисправность диска на машине или в ходе перепланирования для более равномерного распределения рабочих нагрузок в центре обработки данных). Однако смысл наличия вычислительной услуги в том, что автор ПО не должен думать о причинах, по которым это может произойти. 2 Автором метафоры «домашние любимцы и скот», по мнению Рэнди Биаса (Randy Bias), считается Билл Бейкер (Bill Baker, https://oreil.ly/lLYjI ). Она чрезвычайно популярна как 516 Глава 25. Вычисления как услуга Если сервер — это домашний любимец, то, когда он ломается, приходит чело- век (обычно в панике), чтобы посмотреть, что произошло, и исправить проблему, если получится. Заменить любимца сложно. Если серверы — это рабочий скот, то им даются безликие имена от replica001 до replica100, и когда один из них выходит из строя, автоматический планировщик удалит соответствующий экземпляр и создаст взамен новый. Отличительная черта «рабочего скота» — простота запуска нового экземпляра для выполнения задания: эта процедура не требует ручной настройки и может выполняться полностью автоматически. Это обеспечивает свойство само- восстановления, описанное выше, — в случае сбоя автоматизация может удалить неисправный экземпляр и заменить его новым без участия человека. Обратите вни- мание: оригинальная метафора относится к серверам (виртуальным машинам), но она в равной степени применима и к контейнерам: если есть возможность запустить новую версию контейнера из образа без участия человека, то автоматизация сможет поддерживать услугу в работоспособном состоянии. Если серверы — это домашние любимцы, то бремя их обслуживания будет расти линейно или даже сверхлинейно с размером парка серверов, и ни одна организация не должна легкомысленно относиться к этому бремени. Если же серверы — это рабо- чий скот, то ваша система сможет сама вернуться в стабильное состояние после сбоя и вам не придется тратить силы и время на восстановление ее работоспособности. Однако недостаточно просто превратить виртуальные машины или контейнеры в скот, чтобы гарантировать автоматическое восстановление системы после сбоя. Управляя парком из 200 машин, система Borg почти наверняка остановит хотя бы одну реплику, возможно, даже не один раз, и каждая такая остановка будет увеличивать общую продолжительность вычислений на 50 минут (или на время, потраченное на остановленную обработку). Чтобы преодолеть эту проблему, нужна другая архитек- тура обработки: вместо статического распределения задач нужно разбить весь набор из миллиона документов, например на 1000 блоков по 1000 документов в каждом. Когда какая-то реплика закончит обработку фрагмента, она сообщит полученные результаты и возьмет другой фрагмент. То есть в случае сбоя мы потеряем времени не больше, чем требуется на обработку одного фрагмента. Это очень хорошо согла- суется с архитектурой обработки данных, которая была стандартной в Google в то время: работа не распределялась равномерно между репликами в начале вычисле- ний — задания назначались динамически в ходе обработки, что позволяло снизить затраты из-за отказов реплик. Точно так же для систем, обслуживающих пользовательский трафик, желательно, чтобы перепланирование контейнеров не приводило к ошибкам у пользователей. Когда по каким-то причинам планировщик Borg решает остановить контейнер и за- пустить новый, он заранее уведомляет контейнер о своем намерении. Контейнер может отреагировать на это, отклонив новые запросы, и при этом у него останется способ описания идеи «тиражируемого программного модуля». Ее можно использовать также для описания других понятий, а не только серверов (глава 22). Написание ПО для управляемых вычислений 517 время, чтобы завершить текущие запросы. Это, в свою очередь, требует, чтобы си- стема балансировки нагрузки понимала ответ реплики: «Я не могу принять новые запросы» и перенаправляла трафик на другие реплики. Итак, если контейнеры или серверы действуют как рабочий скот, то ваша служба сможет автоматически вернуться в работоспособное состояние, но, чтобы обеспечить бесперебойную работу при умеренном уровне сбоев, необходимы дополнительные усилия. Пакетные задания и задания обслуживания Документ «Global WorkQueue» (описанный в первом разделе этой главы) решает проблему так называемых «пакетных заданий» — программ, которые, как ожидается, будут выполнять конкретные задания (например, обработку данных) от начала и до конца. Каноническим примером пакетных заданий может служить анализ журна- лов или обучение модели машинного обучения. Пакетные задания отличаются от «заданий обслуживания» — программ, которые, как ожидается, будут выполняться бесконечно и обслуживать входящие запросы. Каноническим примером таких за- даний может служить задание, обслуживающее поисковые запросы пользователей на основе предварительно созданного индекса. Эти два типа заданий имеют (обычно) разные характеристики 1 , в частности: y Пакетные задания в первую очередь ориентированы на высокую скорость об- работки. Обслуживающие задания ориентированы на сокращение задержки в обслуживании одного запроса. y Пакетные задания недолговечны (выполняются минуты или максимум часы). Обслуживающие задания обычно долговечны (по умолчанию перезапускаются только после развертывания новых версий). y Поскольку обслуживающие задания долговечны, они обычно требуют больше времени для запуска. До сих пор мы приводили в пример в основном пакетные задания, и, как мы видели, чтобы адаптировать пакетное задание к сбоям, достаточно разбить работу на не- большие части и динамически распределять эти части между рабочими репликами. Исторически для этого в Google использовался MapReduce 2 , позже замененный фреймворком Flume 3 1 Эта классификация, как и любые другие, не идеальна, и есть программы, которые не впи- сываются ни в одну из категорий или обладают характеристиками, типичными как для обслуживающих, так и для пакетных заданий. Однако, как и в большинстве других случаев, эта классификация все же фиксирует различие, наблюдаемое во многих реальных ситуациях. 2 Dean J., Ghemawat S. MapReduce: Simplified Data Processing on Large Clusters. 6th Symposium on Operating System Design and Implementation (OSDI), 2004. 3 Chambers C., Raniwala A., Perry F., Adams S., Henry R., Bradshaw R., Weizenbaum N. Flume-Java: Easy, Efficient Data-Parallel Pipelines. ACM SIGPLAN Conference on Programming Language Design and Implementation (PLDI), 2010. 518 Глава 25. Вычисления как услуга Обслуживающие задания во многих отношениях лучше подходят для обеспечения отказоустойчивости, чем пакетные задания. Их работа естественным образом раз- бивается на небольшие части (индивидуальные запросы пользователей), которые динамически назначаются репликам в соответствии со стратегией обработки большого потока запросов посредством балансировки нагрузки в кластере серверов, которая использовалась с первых дней обслуживания интернет-трафика. Однако некоторые обслуживающие приложения не укладываются в этот паттерн. Каноническим примером может служить любой сервер, который интуитивно опи- сывается как «лидер» определенной системы. Такой сервер обычно хранит состоя- ние системы (в памяти или в своей локальной файловой системе), и если машина, на которой он работает, выйдет из строя, вновь созданный экземпляр обычно не может воссоздать состояние системы. Другой пример: обработка больших объемов данных — больше, чем умещается на одной машине, — из-за чего данные приходится сегментировать, например, между 100 серверами, каждый из которых хранит 1 % данных, и обрабатывать запросы к этой части данных. Это похоже на статическое распределение работы между пакетными заданиями: если один из серверов выйдет из строя, на время теряется возможность обслуживать часть данных. Последний пример: ваш сервер известен в других частях системы по имени хоста. В этом слу- чае, независимо от устройства сервера, если сетевое соединение с этим конкретным хостом будет потеряно, то другие части системы не смогут с ним связаться 1 Управление состоянием В предыдущем описании мы обращали особое внимание на состояние как источник проблем в подходе «реплики как скот» 2 . Каждый раз, когда одно задание заменяется другим, теряется все его текущее состояние (а также все, что находилось в локальном хранилище, если задание было перемещено на другой компьютер). Это означает, что текущее состояние следует рассматривать как временное, а «реальное хранилище» должно находиться в другом месте. Проще всего организовать хранение во внешнем хранилище. То есть все, что должно существовать дольше, чем обслуживается один запрос (в случае обслуживающих заданий) или обрабатывается один блок данных (в случае пакетных заданий), не- обходимо хранить вне компьютера, выполняющего задание, в надежном постоянном 1 Adya A. et al. Auto-sharding for datacenter applications, OSDI, 2019; Atul Adya, Daniel Myers, Henry Qin, and Robert Grandl, «Fast key-value stores: An idea whose time has come and gone», HotOS XVII, 2019. 2 Обратите внимание, что кроме распределенного состояния существуют и другие требо- вания к настройке эффективного решения «серверы как скот», такие как наличие систем обнаружения и балансировки нагрузки (чтобы приложение, которое перемещается по центру обработки данных, оставалось доступным). Поскольку эта книга посвящена не построению полной инфраструктуры CaaS, а тому, как такая инфраструктура соотносится с искусством программной инженерии, мы не будем вдаваться в дополнительные подроб- ности. Написание ПО для управляемых вычислений 519 хранилище. Если локальное состояние не изменяется в ходе работы, то сделать при- ложение отказоустойчивым можно относительно безболезненно. К сожалению, большинство приложений не настолько просты. Возникает вопрос: «Как реализовать долговременное и надежное хранение — тоже как рабочий скот?» Ответ — «да». Управлять хранимым состоянием скот может посредством его репли- кации (копирования). Аналогичную идею, хотя и на другом уровне, представляют RAID-массивы: диски интерпретируются как временные хранилища (учитывая, что любой из них может выйти из строя), но все вместе они хранят общее состоя- ние. В мире серверов эту идею можно реализовать с помощью нескольких реплик, хранящих один фрагмент данных, и механизма синхронизации, гарантирующего создание достаточного количества копий каждого фрагмента данных (обычно от 3 до 5). Обратите внимание, что правильно настроить такую систему очень сложно (для работы с записями требуется их согласовать), поэтому в Google был разработан ряд специализированных решений для хранения данных 1 , подходящих для большинства приложений, использующих модель, в которой все состояния являются временными. Другие типы локальных хранилищ, которые может использовать скот, — «воссоздава- емые» данные, которые хранятся локально для уменьшения задержки обслуживания. Наиболее очевидным примером является кеширование: кеш — это не что иное, как временное локальное хранилище, где хранится временное состояние, но хранили- ща с состоянием не исчезают постоянно, что позволяет улучшить характеристики производительности. Ключевым уроком для производственной инфраструктуры Google стала организация кеша для достижения целевых показателей по задержке при полной нагрузке основного приложения. Это позволило нам избежать сбоев при потере кеша, потому что имелся другой путь для обработки общей нагрузки (хотя и с большей задержкой). Однако требуется компромисс в решении, сколько ресурсов потратить на избыточность, чтобы снизить риск сбоя при потере кеша. При «разогреве» приложения данные могут извлекаться из внешнего хранилища в локальное, чтобы уменьшить задержку обслуживания запросов. Еще один случай использования локального хранилища — на этот раз для данных, которые записываются чаще, чем читаются, — пакетная запись. Эта стратегия широко используется для мониторинга данных (представьте сбор статистики об использованном процессорном времени в парке виртуальных машин с поддержкой автоматического масштабирования). Ее можно использовать везде, где допустимо исчезновение части данных, например, потому что не требуется 100%-ный охват дан- ных (случай мониторинга) или исчезающие данные можно воссоздать (характерно 1 См., например, Ghemawat S., Gobioff H., Leung S.-T. The Google File System. Proceedings of the 19th ACM Symposium on Operating Systems, 2003; Chang F. et al. Bigtable: A Distributed Storage System for Structured Data. 7th USENIX Symposium on Operating Systems Design and Implementation (OSDI); Corbett J. C. et al. Spanner: Google’s Globally Distributed Database. OSDI, 2012. 520 Глава 25. Вычисления как услуга для пакетных заданий, которые обрабатывают данные по частям и записывают не- которые результаты для каждой части). Обратите внимание, что во многих случаях, даже если конкретное вычисление занимает много времени, его можно разделить на более мелкие временные окна, периодически записывая контрольные точки с со- стоянием в долговременное хранилище. Подключение к услуге Как упоминалось выше, если какой-то узел в системе, где выполняется программа, имеет жестко определенное имя (или даже определяемое в виде параметра кон- фигурации при запуске), реплики программы перестают быть скотом. Однако для подключения к вашему приложению другому приложению необходимо откуда-то получить ваш адрес. Но откуда? Эта проблема решается введением дополнительного уровня косвенности, чтобы другие приложения могли получать ссылку на ваше приложение, используя иден- тификатор, который не изменяется при перезапуске «серверных» экземпляров. Определение ссылки по идентификатору может выполняться другой системой, в ко- торой планировщик регистрирует ваше приложение, запуская его на определенном компьютере. Чтобы избежать операций поиска в распределенном хранилище при выполнении запросов к вашему приложению, клиенты, скорее всего, будут опреде- лять адрес вашего приложения, устанавливать соединение с ним во время запуска и следить за работоспособностью соединения в фоновом режиме. Этот прием обыч- но называется обнаружением услуг, и многие вычислительные услуги имеют свои встроенные или модульные решения. Большинство таких решений также включают некоторую форму балансировки нагрузки, которая еще больше снижает привязку к конкретным сетевым узлам. При использовании этой модели иногда может потребоваться повторять запросы, потому что сервер с приложением, которому вы посылаете запросы, может отклю- читься до того, как ему удастся ответить 1 . Повторные запросы являются стандартной практикой в сетевых взаимодействиях (например, между мобильным приложением и сервером) из-за возможных ошибок в сети, но они не используются, например, во взаимодействиях сервера с базой данных. По этой причине важно проектировать API серверов так, чтобы они правильно обрабатывали такие сбои. В случае изме- няющихся запросов организация повторных попыток запросов может оказаться сложной задачей. В таких ситуациях важно гарантировать идемпотентность — результат, возвращаемый в ответ на запрос, не должен зависеть от количества от- 1 Обратите внимание, что повторные попытки должны быть реализованы правильно — с отсрочкой, постепенным увеличением задержки между попытками и инструментами, позволяющими избежать каскадных сбоев. По этой причине такие реализации, вероятно, должны быть частью библиотеки вызова удаленных процедур, а не создаваться вручную каждым разработчиком. См., например, главу 22 в «Site Reliability Engineering. Надежность и безотказность как в Google» ( https://oreil.ly/aVCy4 ). Написание ПО для управляемых вычислений 521 правленных запросов. Одним из полезных инструментов, помогающих добиться идемпотентности, являются идентификаторы, назначаемые клиентом: если вы заказываете пиццу домой, клиент присваивает заказу идентификатор, и если заказ с этим идентификатором уже был зафиксирован, сервер предполагает, что это по- вторный запрос, и сообщает об успешном выполнении (он также может проверить соответствие параметров заказа). Бывает, что планировщик теряет связь с определенной машиной из-за какой-то се- тевой проблемы. В этом случае он решает, что вся работа потеряна, и переносит ее на другие машины, а затем соединение с машиной восстанавливается! В результате появляются два экземпляра программы на двух разных машинах, и обе думают, что они, например, «replica072». Для устранения неоднозначности нужно проверить, на какую из них ссылается система разрешения адресов (а другая должна остано- вить программу или прекратить работу), — это еще один вид идемпотентности: две реплики, выполняющие одну и ту же работу, являются еще одним потенциальным источником дублирования запросов. |