Балансировка: почему случайный выбор двух бьёт круговой перебор
Круговой перебор раздаёт запросы поровну — и именно поэтому проигрывает, как только запросы перестают быть одинаковыми. В модели он дал хвост в 582 мс там, где опрос двух случайных серверов дал 123,6, а опрос всех шестнадцати — 42,9. Урок про то, откуда берётся эта разница и почему на однородной нагрузке её не видно.
Полное техническое изложение
TL;DR
Балансировщик решает одно: какой из одинаковых серверов обслужит очередной запрос. Самое простое правило — раздавать запросы по кругу; оно уравнивает их число, а не работу. Поэтому оно проигрывает ровно тогда, когда запросы перестают быть одинаковыми по стоимости.
Отсюда главное следствие: правило выбора двигает хвост ожидания в разы — но только на неоднородной нагрузке. Модель: p99 ожидания 582,4 мс у кругового перебора против 123,6 мс, если спросить два случайных сервера и отдать запрос менее загруженному из двух, — в 4,7 раза лучше при двух проверках вместо нуля.
Дальше — числа и границы. Опрос всех шестнадцати даёт 42,9 мс: в 2,9 раза лучше опроса двух при работе в восемь раз большей, — но требует знать состояние всех, а цену самого знания модель не считает вовсе. Случайный выбор хуже кругового перебора: 787,7 мс против 582,4 — случайность здесь не «примерно поровну», а «иногда сильно не поровну». А там, где текущая загрузка серверов ничего не сообщает, преимущество исчезает вовсе: при одинаковых запросах круговой перебор и опрос всех дали одни и те же 16,1 мс, а опрос двух — 42,3, то есть проиграл круговому перебору. Числа этого урока модельные: содержательны отношения между правилами, а не абсолютные миллисекунды.
- когда запросов больше, чем тянет одна машина, ставят несколько одинаковых серверов;
- запрос, пришедший на занятый сервер, ждёт своей очереди, и это ожидание видно клиенту;
- запросы к одному и тому же обработчику бывают разной стоимости: один читает запись, другой считает отчёт за год.
- что такое p99 и почему на хвост смотрят отдельно от среднего, как устроена очередь как модель, что означает загрузка 0,85;
least-connections, power of two choices и то, чем в настоящем балансировщике меряют «загруженность».
Что здесь на самом деле спрашивают
Лестница выглядит так:
- «Какие способы балансировки вы знаете?» — разминка на перечисление.
- «Чем плох круговой перебор?» — здесь начинается содержание, и обычный ответ «ничем, он же равномерный» уже неверен.
- «Что такое least-connections и почему его не всегда используют?» — вопрос про цену знания.
- «Что такое power of two choices?» — вопрос, ради которого тема и существует.
- «Почему на нашем нагрузочном тесте разницы не было?» — вопрос-ловушка про однородные запросы.
- «Где балансировка не поможет вовсе?» — вопрос про границы.
Этот урок стоит на модели, а не на замере: у правил балансировки нет общего стандарта — есть документация отдельных продуктов. Модель отвечает не на вопрос «сколько миллисекунд у вас», а на вопрос «от чего зависит разница».
База: кто выбирает сервер и по какому правилу
Когда запросов больше, чем тянет одна машина, ставят несколько одинаковых серверов, а перед ними — балансировщик. Вся его работа описывается одной фразой: пришёл новый запрос — решить, какой из серверов его обслужит. Больше он ничего не делает: не ускоряет обработку и не уменьшает объём работы, а только раскладывает её по машинам.
Правил, по которым принимается это решение, обычно называют три, и все три объясняются обычными словами.
- По кругу. Первый запрос — первому серверу, второй — второму, и так далее по списку; дойдя до конца, начинаем сначала. Знать о серверах для этого не нужно ничего — достаточно помнить, кому отдали прошлый запрос.
- Случайно. На каждом запросе бросаем монетку. Здесь не нужно даже помнить прошлый выбор.
- Наименее загруженному. Смотрим на серверы и отдаём запрос тому, кто сейчас свободнее прочих. Из трёх правил только этому нужно знание о состоянии серверов.
Третье звучит правильнее всех — и в нём же прячется первая трудность: что значит «наименее загруженный»? У этой фразы нет одного значения. Загруженность меряют числом запросов, которые сервер сейчас обрабатывает; числом открытых соединений; длиной очереди перед ним; сглаженной задержкой ответа; загрузкой процессора. Это разные величины, и правило, выбирающее по одной из них, — это уже другое правило, чем правило, выбирающее по другой.
Есть и четвёртое, ради которого тема и существует: спросить два случайных сервера и отдать запрос менее загруженному из двух. Оно похоже на третье, но смотрит не на всех.
И вот главный вопрос урока: если первое правило раздаёт всем поровну, чем оно плохо? Ответ короткий: поровну — по числу запросов, а не по работе. Пока запросы одинаковы по стоимости, разницы между этими двумя «поровну» нет; как только один запрос дороже другого, сервер, которому достался дорогой, занят надолго — а очередь к нему всё равно приходит в свой черёд, потому что правило о его занятости не знает.
Этого уже достаточно, чтобы ответить на базовый вопрос собеседования. Всё дальнейшее — про то, насколько велика эта разница, сколько надо знать о серверах, чтобы её убрать, и почему на нагрузочном тесте её обычно не видно.
Механизм 1: поровну по числу — не поровну по работе
Теперь то же самое в числах. Круговой перебор раздаёт запросы по очереди, и по числу запросов балансировка получается идеальной: у шестнадцати серверов ровно по одной шестнадцатой потока.
Проблема в том, что запросы не одинаковы. Вот что получается, когда 90 % из них стоят 10 мс, а остальные 10 % — 100 мс:
1. THE SAME REQUESTS, FOUR RULES: TIME SPENT WAITING IN A QUEUE
---------------------------------------------------------------
rule mean, ms p50, ms p99, ms worst, ms
round-robin 110.3 69.6 582.4 1041.2
random 161.2 101.8 787.7 1504.8
least-work 4.9 0.0 42.9 90.6
two-choices 24.7 10.7 123.6 287.2
Одни и те же запросы, одни и те же серверы. Отличается только правило — и хвост двигается в разы, а не на проценты.
Почему круговой перебор проигрывает. Он знает только, кому отдал прошлый запрос. Во что обошлась та работа и занят ли сервер сейчас — не знает. Сервер, которому досталась стомиллисекундная работа, получит следующий запрос через шестнадцать шагов независимо от того, занят он или свободен. Пока он занят, очередь перед ним растёт, а соседи стоят пустыми.
И почему случайный выбор ещё хуже — 787,7 против 582,4. Случайность не гарантирует равномерности на коротком отрезке: какой-то сервер получит подряд несколько длинных запросов просто потому, что монетка так легла. Круговой перебор от этого хотя бы защищён.
Обратите внимание на колонку p50 в строке least-work: ноль. При
загрузке 0,85 и шестнадцати серверах свободный обычно есть — и правило, которое
умеет его найти, находит. Остальные три его не ищут: два не смотрят на серверы
вовсе, а опрос двух ищет свободного среди двух случайных.
И сразу про имя. В модели least-work выбирает сервер, который освободится
раньше всех, — то есть по точному остатку работы вместе с длительностью уже
идущего запроса. Настоящий least-connections считает соединения и не
отличает соединение с десятимиллисекундным запросом от соединения со
стомиллисекундным — то есть слеп ровно к тому разбросу, на котором всё здесь и
держится. Поэтому 42,9 мс — потолок правила, которому известно всё, а не
результат least-connections.
Механизм 2: цена знания
Второй уровень — про то, чем правила отличаются на самом деле. Не «умностью», а тем, сколько нужно знать, чтобы принять решение.
2. HOW MUCH EACH RULE NEEDS TO KNOW
-----------------------------------
rule servers inspected p99, ms p99 improvement
round-robin 0 582.4 1.0x
random 0 787.7 0.7x
least-work 16 42.9 13.6x
two-choices 2 123.6 4.7x
Читать надо две колонки рядом. Первые два правила не знают о серверах ничего
и работают хуже всех. least-work знает про всех шестнадцать и работает лучше
всех. А two-choices знает про двух — и забирает заметную часть выигрыша.
Почему выбор двух работает. Из двух правило берёт того, кто освободится раньше, поэтому запрос ждёт столько, сколько заставил бы ждать менее загруженный из двух. Долго — только если загружены оба. Это следствие самого правила, а не отдельный замер: вероятностей модель не считает.
И почему это важнее, чем звучит. Чтобы выбрать лучшего из тысячи, надо знать состояние тысячи, и это знание надо где-то держать и обновлять; двум проверкам размер парка безразличен. Саму эту цену модель не считает — по допущению балансировка в ней бесплатна, — но она следует из формулировки правил: шестнадцать проверок против двух.
И вторая половина этой цены, о которой забывают чаще первой. В распределённой системе состояние серверов не лежит перед балансировщиком готовым. Его надо собрать: опросить серверы по сети или дождаться, пока они сами о себе сообщат, — и то и другое занимает время. Значит, к моменту решения картина описывает не «сейчас», а «когда-то раньше»: чем больше парк, тем больше данных надо успеть собрать и тем старше та часть картины, которую собрали первой. Знание о состоянии всех серверов не просто дорого — оно ещё и устаревает, пока его собирают, и это свойство самой распределённости, а не плохой реализации. Правило с полным знанием платит дважды: за объём знания и за его возраст. Второе не лечится тем, что опрос сделали дешевле.
Отсюда и правило, которым стоит отвечать: опрос двух — это не «почти least-connections», а другой размен. Он меняет часть выигрыша на то, что стоимость решения перестаёт зависеть от размера системы.
Механизм 3: почему на нагрузочном тесте разницы не видно
Третий уровень объясняет, откуда у кругового перебора репутация. Уберём из модели одну вещь — разброс времени обслуживания:
3. WITH UNIFORM REQUESTS, ROUND-ROBIN TIES WITH THE BEST RULE
-------------------------------------------------------------
round-robin: p99 with even service, ms 16.1
random: p99 with even service, ms 272.7
least-work: p99 with even service, ms 16.1
two-choices: p99 with even service, ms 42.3
Круговой перебор дал ровно то же, что и опрос всех шестнадцати: 16,1 против
16,1. И это не совпадение: когда каждый запрос стоит одинаково, «взять того, кто
освободится раньше» — это и есть обход по кругу. Серверы освобождаются в том
порядке, в каком получали работу, поэтому least-work раз за разом указывает на
тот сервер, до которого очередь дошла бы и так.
А случайный выбор остался плохим — 272,7. Ему разброс запросов не нужен, чтобы промахиваться: он промахивается сам по себе.
И обратите внимание на опрос двух: 42,3 — хуже кругового перебора. Две случайные попытки не дают того, что даёт гарантированный обход, когда выбирать не из чего.
Отсюда граница, от которой и надо формулировать правило. Опрос двух случайных серверов выигрывает не «вообще», а там, где текущая загрузка серверов несёт полезную информацию, — то есть там, где серверы в момент решения действительно заняты по-разному. Уберите разброс времени обслуживания — и сообщать становится нечего: правило, сравнивающее двух наугад, проигрывает правилу, которое просто обходит всех по очереди, а само знание о состоянии всех не даёт ничего сверх обхода. Это и есть третий блок целиком: 42,3 у опроса двух против 16,1 у кругового перебора и ровно тех же 16,1 у опроса всех. Верное утверждение звучит поэтому не «выбор двух лучше кругового перебора», а «выбор двух лучше там, где загрузка серверов различается».
И отсюда же вывод, который стоит держать при себе на каждом нагрузочном тесте: измерение на однородных запросах не покажет слабости кругового перебора — на них он сравнивается с правилом, которое знает состояние всех. Если ваш тест шлёт один и тот же запрос в цикле, он покажет, что круговой перебор не хуже, — и будет прав ровно про этот тест.
Настоящая же нагрузка почти никогда не однородна: запрос за одной записью и запрос за отчётом за год идут в один и тот же обработчик.
Глубже: чего в модели нет
Последний уровень — про границы самой модели, и назвать их стоит самому.
Серверы в модели одинаковые. Настоящий парк разнороден: разные поколения машин, разные соседи по хосту, разная степень прогретости кеша. В модели это не проверялось, но из правила видно, чем грозит: круговой перебор раздаёт поровну независимо от того, равны ли машины.
Решение принимается один раз. В модели балансировщик не переигрывает выбор и не умеет забрать запрос обратно. Настоящие балансировщики иногда умеют — например, отправить копию запроса второму серверу, если первый молчит слишком долго.
Ни отказов, ни повторов. Сервер в модели не падает, а клиент не повторяет. Оба добавляют нагрузку именно тогда, когда она и так велика; про это отдельный урок про повторы и джиттер.
Нет цены самой балансировки. Опрос двух серверов в модели бесплатен. В настоящей системе состояние надо откуда-то знать, и у этого знания есть задержка: балансировщик действует по данным, которые уже устарели.
Последнее — не мелочь, а причина, по которой правило с полным знанием в реальности не даёт того выигрыша в 13,6 раза: настоящий балансировщик выбирает по вчерашней картине и вдобавок по суррогату — числу соединений или скользящему среднему времени ответа, а не по точному моменту освобождения, который есть только в модели.
Как отвечать на собеседовании
Короткий ответ: круговой перебор раздаёт поровну число запросов, а не работу, поэтому он проигрывает, как только запросы неоднородны. Опрос двух случайных серверов забирает заметную часть выигрыша полного опроса, но его цена не растёт с размером парка. Модель: p99 ожидания 582,4 мс у кругового перебора, 123,6 у опроса двух и 42,9 у опроса всех.
Этого достаточно, чтобы ответить верно. Дальше — то, что добавляют, если собеседник копает.
Если интервьюер копает глубже
Хороший ответ отличают три вещи. Первая — вы говорите, что различие правил
проявляется только на неоднородной нагрузке, и приводите свой же контрпример:
при одинаковых запросах круговой перебор сравнялся с лучшим правилом, а опрос
двух ему проиграл — 42,3 против 16,1. Вторая —
вы называете цену полного опроса не «сложностью», а конкретно: надо знать
состояние всех, это знание растёт с парком и устаревает, пока его собирают, а
least-connections вдобавок считает соединения, а не работу — и стоит сразу
сказать, что «наименее загруженный» вообще меряют по-разному: числом запросов в
работе, числом соединений, длиной очереди, сглаженной задержкой, загрузкой
процессора. Третья — вы
упоминаете, что случайный выбор хуже кругового перебора, а не лучше: это
проверка на то, думали вы или пересказываете.
Чего говорить не стоит: «мы поставили least-connections, стало лучше на столько-то процентов». Без описания разброса запросов это число ничего не значит — на однородной нагрузке его бы не было вовсе.
Дальше спросят
Почему случайный выбор хуже кругового перебора?
Потому что «равномерно в среднем» и «равномерно на коротком отрезке» — разные вещи. Случайность допускает серию: один и тот же сервер получает несколько запросов подряд просто по совпадению, и на неоднородной нагрузке это те самые длинные запросы.
Круговой перебор от серий защищён по построению — он обходит всех. В модели это разница между 787,7 и 582,4 мс по хвосту. Заметьте, что оба правила при этом одинаково слепы: ни одно не смотрит на состояние сервера.
Почему двух, а не трёх?
Потому что первая проверка не даёт выбора вовсе, а вторая его создаёт: это единственный шаг, который меняет само правило. С двумя запрос ждёт столько, сколько заставил бы ждать менее загруженный из выбранных, — долго только если загружены оба.
В этой модели третий вариант не проверялся, так что за конкретной кратностью идти надо к своей системе. Но соотношение цены и выигрыша ясно и без этого: каждая следующая проверка стоит столько же, сколько первая, а добавляет меньше.
Что считать «свободнее» в настоящем балансировщике?
Это и есть главный практический вопрос темы. В модели сервер описывается одним числом — моментом, когда он освободится. В настоящей системе такого числа нет, и вместо него берут суррогат: число открытых соединений, число запросов в работе, длину очереди перед сервером, сглаженную задержку ответа, загрузку процессора.
Каждый суррогат врёт по-своему. Число соединений не отличает соединение с тяжёлым запросом от простаивающего; сглаженная задержка отстаёт от происходящего; загрузка процессора ничего не говорит о том, кто ждёт ответа от базы. Поэтому переход с кругового перебора на «умное» правило иногда не даёт ничего — метрика, по которой выбирают, оказалась не про то. И это ещё до того, как учтено, что любая из этих величин добирается до балансировщика с задержкой.
Где балансировка не поможет вовсе?
Там, где узкое место общее для всех серверов. Если все шестнадцать ходят в одну базу и упираются в неё, любое правило распределения перекладывает очередь с места на место, но не уменьшает её.
Отсюда и порядок действий при разборе: сначала понять, где именно копится очередь — перед серверами или за ними. Балансировка лечит первое и не лечит второе, а выглядит это снаружи одинаково.
Частые заблуждения
Круговой перебор распределяет нагрузку равномерно
Он равномерно распределяет число запросов, а не работу. В модели каждый из шестнадцати серверов получил ровно одну шестнадцатую потока — и p99 ожидания составил 582,4 мс против 42,9 у правила, которое смотрит на состояние серверов. Равенство по счёту не означает равенства по загрузке.
Случайное распределение не хуже кругового перебора
Хуже: в модели 787,7 мс против 582,4 по хвосту. Случайность допускает серии — один сервер получает несколько запросов подряд по совпадению, — а круговой перебор от них защищён по построению. Оба при этом одинаково слепы к состоянию серверов.
Опрос двух серверов — это упрощённый least-connections
Это другой размен. Модель: опрос двух дал 123,6 мс против 42,9 у опроса всех — то есть заметную часть выигрыша. Но цена полного опроса растёт с парком (состояние всех надо знать и обновлять), а цена опроса двух не растёт вовсе. Меняется не «точность», а зависимость стоимости решения от размера системы.
Мы проверили на нагрузочном тесте — разницы между правилами нет
Значит, запросы в тесте были однородными. Модель: при одинаковом времени обслуживания круговой перебор дал ровно те же 16,1 мс, что и опрос всех серверов. Однородность равняет с полным опросом именно круговой перебор: в том же прогоне случайный выбор дал 272,7 мс, а опрос двух — 42,3. А настоящая нагрузка почти никогда не однородна.
least-connections всегда лучший выбор
В модели такого правила нет: там выбирают сервер, который освободится раньше всех, — по точному остатку работы, и это дало 42,9 мс. Настоящий least-connections считает соединения и потому не видит, тяжёлый в соединении запрос или лёгкий; вдобавок состояние известно ему с задержкой, а цена знания растёт с парком.
Практика
Две задачи. Сначала ответьте, потом сверьтесь с настоящим выводом: в обеих правильный ответ печатает сам скрипт модели.
Практика · что напечатает
print(inspected["round-robin"]) print(inspected["least-work"]) print(inspected["two-choices"])
Практика · оцените
Проверка знаний
Что именно распределяет поровну круговой перебор?
Это не пересказ и не отдельный текст: всё ниже взято из самой статьи — её выжимка, заголовки разборов, колонка «на самом деле» и таблица версий. Поэтому разойтись со статьёй эти тезисы не могут.
Суть
- Балансировщик решает одно: какой из одинаковых серверов обслужит очередной запрос. Самое простое правило — раздавать запросы по кругу; оно уравнивает их число, а не работу. Поэтому оно проигрывает ровно тогда, когда запросы перестают быть одинаковыми по стоимости.
- Отсюда главное следствие: правило выбора двигает хвост ожидания в разы — но только на неоднородной нагрузке. Модель: p99 ожидания 582,4 мс у кругового перебора против 123,6 мс, если спросить два случайных сервера и отдать запрос менее загруженному из двух, — в 4,7 раза лучше при двух проверках вместо нуля.
- Дальше — числа и границы. Опрос всех шестнадцати даёт 42,9 мс: в 2,9 раза лучше опроса двух при работе в восемь раз большей, — но требует знать состояние всех, а цену самого знания модель не считает вовсе. Случайный выбор хуже кругового перебора: 787,7 мс против 582,4 — случайность здесь не «примерно поровну», а «иногда сильно не поровну». А там, где текущая загрузка серверов ничего не сообщает, преимущество исчезает вовсе: при одинаковых запросах круговой перебор и опрос всех дали одни и те же 16,1 мс, а опрос двух — 42,3, то есть проиграл круговому перебору. Числа этого урока модельные: содержательны отношения между правилами, а не абсолютные миллисекунды.
На самом деле
- Он равномерно распределяет число запросов, а не работу. В модели каждый из шестнадцати серверов получил ровно одну шестнадцатую потока — и p99 ожидания составил 582,4 мс против 42,9 у правила, которое смотрит на состояние серверов. Равенство по счёту не означает равенства по загрузке.
- Хуже: в модели 787,7 мс против 582,4 по хвосту. Случайность допускает серии — один сервер получает несколько запросов подряд по совпадению, — а круговой перебор от них защищён по построению. Оба при этом одинаково слепы к состоянию серверов.
- Это другой размен. Модель: опрос двух дал 123,6 мс против 42,9 у опроса всех — то есть заметную часть выигрыша. Но цена полного опроса растёт с парком (состояние всех надо знать и обновлять), а цена опроса двух не растёт вовсе. Меняется не «точность», а зависимость стоимости решения от размера системы.
- Значит, запросы в тесте были однородными. Модель: при одинаковом времени обслуживания круговой перебор дал ровно те же 16,1 мс, что и опрос всех серверов. Однородность равняет с полным опросом именно круговой перебор: в том же прогоне случайный выбор дал 272,7 мс, а опрос двух — 42,3. А настоящая нагрузка почти никогда не однородна.
- В модели такого правила нет: там выбирают сервер, который освободится раньше всех, — по точному остатку работы, и это дало 42,9 мс. Настоящий
least-connectionsсчитает соединения и потому не видит, тяжёлый в соединении запрос или лёгкий; вдобавок состояние известно ему с задержкой, а цена знания растёт с парком.
Что разобрано
- Что здесь на самом деле спрашивают
- База: кто выбирает сервер и по какому правилу
- Механизм 1: поровну по числу — не поровну по работе
- Механизм 2: цена знания
- Механизм 3: почему на нагрузочном тесте разницы не видно
- Глубже: чего в модели нет
- Как отвечать на собеседовании
- Дальше спросят
- Частые заблуждения
- Практика
- Проверка знаний
Источники и что читать дальше
1 ИСТОЧНИК
- Модель этого урока: шестнадцать серверов и четыре правила выбораИсточник. Внешнего первоисточника у этого урока нет: правила балансировки описываются документацией отдельных продуктов, общего стандарта у них нет. Поэтому урок стоит на модели с объявленными допущениями: шестнадцать серверов с очередью без предела, пуассоновский поток с загрузкой 0,85, время обслуживания 10 мс у 90 % запросов и 100 мс у остальных, решение принимается один раз и не переигрывается, сама балансировка бесплатна, отказов и повторов нет, серверы одинаковы по мощности, зёрна случайных чисел фиксированы. Всё, что печатает прогон, выводится из этих правил и проверяется чтением скрипта./ru/bench/balancing/choices.py