Эта статья — логическое продолжение предыдущей, своего рода вторая её часть.
Допустим, у нас есть веб-проект, который «вырос» из одного веб-сервера (в дальнейшем веб-сервер будем называть просто «сервер»), т.е. этот сервер больше не может справиться с возросшей нагрузкой, хотя все возможные стандартные способы оптимизации уже были использованы.
Также мы убедились на 100%, что узким местом является именно сервер, а не что-нибудь другое типа пропускной способности сети, базы данных, shared cache (общий кэш, доступный всем серверам по сети) и т.п.
Хорошо, не проблема — добавляем еще один сервер и ставим перед ними load balancer, который будет распределять входящие запросы между нашими серверами. И тут возникает малоосвещенный в интернетах, но крайне актуальный вопрос, — какой же способ распределения нагрузки из широкого спектра доступных, выбрать именно в нашем случае?
Прежде, чем рассмотреть некоторые популярные стратегии распределения нагрузки между серверами, скажем пару слов о стратегии Failover, которая в идеале должна быть ортогональна стратегии распределения нагрузки.
Что делать, если какой-либо сервер вышел из строя (понятие «выйти из строя» имеет очень широкий смысл в данном контексте. Не будем вдаваться в детали, т.к. это потребует написания дополнительной статьи)? Наверное, оповестить об этом заинтересованных лиц и не направлять на сбойный сервер запросы до тех пор, пока заинтересованные лица (или специально обученная программа) не разберутся с возникшей проблемой. Не забывайте, что мощности оставшихся серверов должно быть достаточно для возросшей нагрузки. Если это будет не так, то может произойти коллапс всей системы.
В дальнейшем будем предполагать, что стратегия failover уже выбрана. Приступим к рассмотрению стратегий распределения нагрузки.
Разделим их на два семейства:
Для cache-unaware стратегий лучше всего подходит распределение нагрузки на наименее загруженный в данный момент сервер. Эта стратегия позволяет добиться наименьшего времени ожидания в очереди запросов, в соответствии с результатами моделирования, описанными в моей предварительной статье.
Наиболее известная из этих стратегий — sticky load balancing (SLB). Она основывается на справедливом предположении, что запросы от одного и того же пользователя нуждаются в общих данных, которые поддаются локальному кэшированию на сервере. К таким данным можно отнести пользовательские настройки или данные, имеющие смысл лишь для конкретного пользователя. Очевидно, что направление запросов от одного и того же пользователя на один и тот же сервер позволяет максимизировать cache hit ratio.
Эта стратегия хорошо работает при следующих условиях:
Обычно группировка осуществляется либо по IP-адресу входящего запроса, либо по идентификатору пользователя (например, user_id, session_id, auth_token). Идентификатор пользователя может находиться в различных местах. Например, в cookies, в http header’ах, в url’е, в query string’е, в теле POST-запроса.
Главное преимущество группировки по ip в том, что она может быть осуществлена с минимальными затратами ресурсов на сетевом (IP) или транспортном (TCP) уровнях. Более того, в некоторых ОС типа linux, группировка входящих TCP-подключений по source ip
встроена в ядро.
Изучите, например, опцию —persistent
в DNAT target из iptables или опцию —hashmode=sourceip
в CLUSTERIP target там же. Это позволяет построить высокопроизводительный load balancer без применения дополнительного софта. Правда, такой load balancer не сможет автоматически перенаправлять запросы в обход вышедших из строя серверов.
У группировки по IP есть два недостатка:
Группировка по идентификатору пользователя является более затратной в плане потребления ресурсов, т.к. она должна осуществляться на уровне протокола HTTP (aka уровень приложения). С другой стороны, она лишена недостатков группировки по IP.
Основная условие данного принципа — равномерно распределить запросы, сгруппированные по IP или идентификатору пользователя, между имеющимися серверами.
Рассмотрим некоторые алгоритмы, удовлетворяющие этому условию:
Преимущества:
Недостатки:
server_id = hash(group_key) % servers_count
где:
server_id
— порядковый номер сервера из пула серверов размером servers_count
; group_key
— ключ, по которому производится группировка входящих запросов. Например, IP
или user_id
. hash
— хэш-функция, дающая равномерное распределение значений для заданных group_key
’ев. При использовании этого алгоритма нет необходимости хранить какие-либо ассоциации на стороне load balancer’а, поэтому он лишен всех недостатков алгоритма с таблицей ассоциаций.
Недостатки:
Этот алгоритм имеет те же преимущества и недостатки, что и простое хэширование. Но, в отличие от простого хэширования, consistent hashing теряет только небольшую часть ассоциаций между группами запросов и серверами при удалении и добавлении серверов. Так что этот алгоритм можно считать наилучшим для ассоциации серверов с пользовательскими запросами.
Сервера могут иметь различную производительность. Как в этом случае производить выбор серверов для групп запросов? Присвойте каждому серверу вес, прямо пропорциональный его производительности, и добавьте учет этого веса в вышеописанные методы.
Перечислим преимущества cache-aware стратегии перед cache-unaware стратегией:
Недостатки cache-aware стратегии перед cache-unaware стратегией:
В cache-aware стратегии же данные могут быть рассинхронизированы, если группа запросов попадет на короткое время на «чужой» сервер, а затем снова перекинется на «свой» сервер. Это возможно в случае кратковременного ложного «выхода из строя» одного из серверов, который быстро возвращается в строй обратно без потери локального кэша.
Допустим, при запросе на «чужом» сервере пользовательские настройки были изменены. «Свой» сервер ничего про это не знает и использует локально закэшированные настройки пользователя, которые уже устарели.
Каков же выход из этой ситуации? Можно вообще «забить» на эту проблему, если случаи ложного «выхода из строя» серверов достаточно редки и вас не смущает наличие пары недовольных пользователей, потерявших свои данные из-за этого (к слову, это типичный способ решения данной проблемы в высоконагруженных проектах). Можно перед каждым использованием данных из локального кэша проверять наличие изменений во внешнем хранилище. Но это сильно портит вышеуказанные преимущества cache-aware стратегии.
Намного лучше воспользоваться помощью shared cache, но использовать его не по прямому назначению — хранение закэшированных данных, а в качестве вспомогательного средства для optimistic locking. Для этого для каждой группы запросов создаем отдельную запись в shared cache, где хранится счетчик изменений данных, входящих в локальный кэш для данной группы. Обычно такой счетчик называется generation counter. Начальное значение этого счетчика выбирается случайным образом. В начале каждого запроса пользователя считываем значение соответствующего счетчика из shared cache и сравниваем его с локальным значением.
Если эти значения одинаковы, то можно считать, что данные в удаленном хранилище не изменились. В противном случае считываем обновленные данные из удаленного хранилища и обновляем локальный счетчик до значения, полученного из shared cache перед тем, как начать работать с этими данными. Если в процессе запроса данные изменились, то сохраняем их в удаленном хранилище, после чего увеличиваем счетчик на единицу.
Ниже представлен соответствующий псевдокод:
new_random_counter = random.randint(0, 0xffffffff) remote_counter = shared_cache_cas(request_group_key, None, new_random_counter) if remote_counter is None: remote_counter = new_random_counter if remote_counter != local_counter: load_new_data_from_backend(request_group_key) local_counter = remote_counter is_data_changed = process_request(request_group_key) if is_data_changed: save_data_to_backend(request_group_key) remote_counter = shared_cache_cas(request_group_key, local_counter,local_counter + 1) if remote_counter is None: logging.error('It looks like shared cache doesn\ 't work at the moment') elif remote_counter != local_counter: # This case is unlikely for cache-aware load balancing, # since it means the user sent two update requests in # a short period of time and these requests were # directed by load balancer to distinct servers. logging.error('It looks like somebody updated user\'s data ahead of us. Data can be inconsistent') else: local_counter += 1
Хм. Почему бы не использовать shared cache по прямому назначению вместо того, чтобы городить огород с локальными кэшами и generation counter’ами? Ведь в результате мы вынуждены обращаться минимум один раз к shared cache при обработке каждого запроса. А преимущество в том, что в этом случае мы не тратим процессорное время на сериализация больших объемов данных и не засоряем сетевой трафик между серверами и shared cache этими данными.
Если вам кажется, что овчинка выделки не стоит, то вы всегда можете «забить» на проблемы синхронизации, т.к. в случае SLB эти проблемы возникают лишь в исключительных случаях.
На просторах Интернета можно наткнуться на FUD о том, что sticky load balancing — это вчерашний день и всем нужно срочно переходить на stateless servers c shared cache. В качестве главного аргумента приводится то, что SLB может оказаться виновно в потере пользовательских данных при выходе из строя сервера, хранящего данные из пользовательских сессий.
Что ж, такое возможно при неправильном понимании основ кэширования — не стоит полагаться на сохранность данных в кэше, т.к. они в любой момент могут быть утеряны. Это может произойти различными путями — например, кэш разросся до огромных размеров и его нужно сжимать, удаляя оттуда какие-нибудь данные. Или сервис, отвечающий за кэширование, накрылся медным тазом. Так что хранить критические данные пользовательских сессий в локальном кэше без помещения их в хранилище, гарантирующем их сохранность — верх безрассудства. Как видите, вина sticky load balancing в неправильном понимании основ кэширования равняется 0.0
.
Кроме того: какую прорву локального трафа между кэш-сервером и серверами кластера генерит подход stateless servers c shared cache — его пропагандисты, судя по всему в реальной жизни даже не видели. В большинстве случаев понятно, почему им действительно пофиг эта величина, так как они используют Infiniband или 100-Gbit ethernet в локальной сети. Во всех остальных случаях это всё-таки лучше иметь в виду.
Файлообменники — это как раз тот распространенный случай, когда sticky load balancing намного предпочтительнее, чем cache-unaware стратегии. Это — огромные файлопомойки, где любой пользователь может скачивать любой файл. Например, типичные представители из этого многочисленного ныне племени: рапидшара и подобные, различные фотоальбомы и медиаколлекции в соцсетях, ютуб, дропбокс и так далее.
В этом случае целесообразнее проводить группировку запросов на скачивание файлов не по идентификатору пользователя, а по имени файла. Если в локальный кэш серверов складывать наиболее популярные файлы с сопутствующими данными к ним, то можно неплохо поднять cache hit ratio и уменьшить трафик внутри сети между серверами и внешним хранилищем файлов.
Для файлопомоек также можно весьма эффективно использовать CDN’ы или кэширующие обратные прокси (aka веб-ускорители).
После прочтения предыдущего параграфа должно быть ясно, что SLB — очень полезная вещь для высоконагруженных масштабируемых проектов. Так какой же load balancer поддерживает cache-aware стратегии распределения запросов? На рынке их over 9000. Они в основном представляют из себя черные ящики с сетевыми интерфейсами. Цена такого ящика обычно начинается с $10K.
Но есть бесплатная open source альтернатива, которая не уступает по возможностям и производительности большинству из этих черных ящиков — HAProxy. В ней присутствует поддержка SLB как на основе source ip
, так и на основе произвольных данных в cookies, http headers и url. Так что внимательно читаем мануал к нему и пользуемся на здоровье!
5 комментариев
Ссылки пошто на английскую вики? вместо "http://en.wikipedia.org/wiki/Application_Layer" лучше "http://ru.wikipedia.org/wiki/Прикладной_уровень"
Аффтар, забыл упомянуть совершенно канонические и сильнодействующие методики:
- реверсивный кэш-прокси
- Ну и CDN как обобщенный случай реверсивных кэш-прокси
Радикально меняют картину. Особенно в сочетании с балансировщиком.
Причем они не только и не столько для файлопомоек, сколько для вполне себе симпатичных фронт-эндов.
Pest Reject - отпугиватель тараканов, грызунов и насекомых
https://u.to/F4EzFA
Ультразвуковой отпугиватель грызунов и тараканов. Исходящие импульсы устройства воздействуют прямиком на нервную систему вредителей. Не оказывает влияния на домашних животных, безопасен для людей.
Snorest - антихрап с фильтром для воздуха
https://u.to/rHszFA
Snorest - клипса от храпа
Инновационное приспособление, которое стимулирует рефлексогенные точки в области носовой перегородки, расширяет дыхательные пути и повышает концентрацию кислорода в крови. Тем самым - избавляет от храпа и делает ваш сон глубоким и здоровым!