мультиплексирование
Oct. 27th, 2012 11:03 pmЕго ещё нужно уметь тестировать, оказывается.
Клиент написан на NIO, бросается тривиальными запросами, идёт спать с помощью Thread.sleep, чтобы гарантировать нужный arrival rate, и пользуется Selector.select.

Удивительно здесь то, что Response Time не зависит от arrival rate, и то, что другой клиент, написанный полностью на blocking IO и с 6x потоков, добивается более низкого Response Time (на графике не указан). Но если у нас 24 ядра, то что будет делать этот клиент с 75 потоками? Думаю, здесь начинаются какие-то трюки ОС, когда оно пытается не снимать с ядра потоки, даже если они в blocking IO. Тогда даже хотя потоков и 75, но запросов в каждый момент времени-то всего 24, а потому только вот эти 24 сокета и могут стать read-ready, и потому клиент с blocking IO кажется выигрывает. Но это не честный тест, он biases сокеты, и не умеет задавать arrival rate.
Ещё один confounding factor - вы видите, что throughput застрял на 70+К и дальше не смог пройти? Это примерно тот же throughput, что и у blocking IO клиента. Это не показалось бы странным, если бы не следующий эксперимент.
Тот же NIO клиент, но не спит: все Thread.sleep заменены на пустой цикл, проверяющий текущее время, а Selector.select заменено на Selector.trySelect в цикле. Всё направлено на то, чтобы потоки никогда с ядра не слезали.

Вот теперь мы получаем разные Response time для разных arrival rate (ура!) и Response time для 70+К запросов получается такой же, как и для клиента с blocking IO. И мы, оказывается, можем повышать throughput до 200К, а может, и больше.
Остаётся объяснить, что это за разноцветные точки. Это два разных способа мультиплексировать входящие соединения на сервере. Теория говорит, что красные должны победить, но график нам кагбы намекает.
А вот как выглядит картинка, когда тот же неблокирующий бессонный NIO клиент распределяет arrivals по Пуассону.

Нужно из этого извлечь какой-нибудь урок.
Клиент написан на NIO, бросается тривиальными запросами, идёт спать с помощью Thread.sleep, чтобы гарантировать нужный arrival rate, и пользуется Selector.select.

Удивительно здесь то, что Response Time не зависит от arrival rate, и то, что другой клиент, написанный полностью на blocking IO и с 6x потоков, добивается более низкого Response Time (на графике не указан). Но если у нас 24 ядра, то что будет делать этот клиент с 75 потоками? Думаю, здесь начинаются какие-то трюки ОС, когда оно пытается не снимать с ядра потоки, даже если они в blocking IO. Тогда даже хотя потоков и 75, но запросов в каждый момент времени-то всего 24, а потому только вот эти 24 сокета и могут стать read-ready, и потому клиент с blocking IO кажется выигрывает. Но это не честный тест, он biases сокеты, и не умеет задавать arrival rate.
Ещё один confounding factor - вы видите, что throughput застрял на 70+К и дальше не смог пройти? Это примерно тот же throughput, что и у blocking IO клиента. Это не показалось бы странным, если бы не следующий эксперимент.
Тот же NIO клиент, но не спит: все Thread.sleep заменены на пустой цикл, проверяющий текущее время, а Selector.select заменено на Selector.trySelect в цикле. Всё направлено на то, чтобы потоки никогда с ядра не слезали.

Вот теперь мы получаем разные Response time для разных arrival rate (ура!) и Response time для 70+К запросов получается такой же, как и для клиента с blocking IO. И мы, оказывается, можем повышать throughput до 200К, а может, и больше.
Остаётся объяснить, что это за разноцветные точки. Это два разных способа мультиплексировать входящие соединения на сервере. Теория говорит, что красные должны победить, но график нам кагбы намекает.
А вот как выглядит картинка, когда тот же неблокирующий бессонный NIO клиент распределяет arrivals по Пуассону.

Нужно из этого извлечь какой-нибудь урок.
no subject
Date: 2012-10-28 08:21 am (UTC)а. Thread.sleep() не аккуратен, у него жёсткая нижняя граница по задержками около 1мс, и то он там начинает врать на 10-20% (это Solaris 11, и Linux). На Windows там вообще такая жесть, что хоть святых выноси. Поэтому моделирование мелких задержек приходится делать мультиплексированием sender'ов, типа 10 sender'ов с 1мс периодом дают "виртуального" sender'а c 0.1мс.
б. Даже в случае (а) нужен контроль по поводу "сколько же мы действительно отправили за последний интервал", и придерживание коней. А то даже тривиальный STW GC сносит крышу и проглатывает события.
в. Распределение Пуассона -- это прикольно, но есть как минимум две проблемы. Первая, считать его в рантайме при больших arrival rate очень дорого (равно как и нормальное распределение), и оно ещё больше контрибьютит к (б). Вторая, если сумничать и заранее построить массив сэмплов из этого распределения, будут возникать биения между потоками, ибо возникнет фальшивая периодичность в семплах. Мы в итоге скатились к треугольному приближению, которое всё-таки считается в рантайме.
г. Считать время на каждом запросе нельзя. В том числе потому, что на больших arrival rate и больших RT согласно закону Литтла потребуется дохрена свободных sender thread'ов, чтобы обеспечить загрузку. Если этого не учесть, то зависимость rate-RT просто обрезается справа, ибо мы не можем насытить нужный нам rate. Мы скатились к двум группам sender'ов: одна неблокирующе посылает (её цель -- поддерживать rate), вторая блокируется (семплирует время, она вносит очень мало rate, но может блокироваться надолго).
д. В наивном семплировании есть нихреновый bias. Положим, у нас система в норме обрабатывает 10.000 оп/сек. Рассмотрим промежуток времени длиной в секунду, полсекунды из которых система стоит в GC. Положим, у нас один семплирующий тред, работающий без задержек. Тогда за секунду мы получим 5000 сэмплов в 0.1мс, и 1 семпл в 500мс. Т.е. семплов с большим RT будет сильно меньше, что подрывает вычисления всяких моментов, да даже аккуратное вычисление перцентилей начинает лгать. Мы выкрутились ресамплингом: мерим не просто время, а ещё и количество запросов, которое мы должны были бы послать в весь промежуток, и бутстрапим распределение ожидаемого размера из реально полученного. Тогда тот единичный сэмпл растягивается и начинает влиять на статистику.
Ну и плюс там куча подводных камней с машинной точностью, математикой, контролем и т.п. Сложная задача, короче.
no subject
Date: 2012-10-28 09:40 am (UTC)Меня удивило три вещи:
1. blocking IO клиент смог быстрее, чем NIO клиент. Хотя клиент и считает время с учётом client-side inefficiencies, но почему blocking IO изначально даёт 150 микросекунд, а NIO клиент даёт 400 микросекунд, пока не сделать всё на клиенте неблокирующим.
2. теория говорит, одного селектора должно быть достаточно, но это не правда. (а у Гризли сколько, интересно?) Здесь, видимо, какая-то беда в реализации таблицы read-ready дескрипторов.
3. насколько регулярный arrival rate отличается от Пуассоновского!
А в чём сложность с Пуассоном? Сложность убедиться, что получается именно он и подправить в нужную сторону? (Потому что генерировать "примерно Пуассоновское" распределение вроде не сложно)
Ну и дисклеймер, что если строго говоря у меня и найдутся косяки с распределением, то ровно Пуассона мне и не нужно - то есть, усложнять генерацию событий пока не вижу причины - мне нужно какой-то jitter, чтобы не было clock work прибытия событий, как в первом случае.
no subject
Date: 2012-10-28 09:49 am (UTC)2. Селектора одного может и достаточно, а вот обработчика событий с этого селектора -- нет. В Grizzly кучка SelectorRunner'ов работает, если что.
А чем ты собрался приближать Пуассоновское распределение? Нормальным? Его тоже придётся итеративно считать (по крайней мере, как это делает Кнут и java.util.Random вслед за ним).
no subject
Date: 2012-10-28 10:12 am (UTC)2. о, это-то понятно, куча обработчиков конечно нужна.
Сначала был вариант с один селектором, кучей обработчиков событий, но глупой фишкой в этом обработчике. Четыре года назад переписано на кучу селекторов и обнаружено улучшение. Тогда же у меня по прикидкам получалось, что один селектор должен быть лучше, they just didn't do it right, но этот blocking IO client не показал разницы, и в продакшен пошёл вариант с кучей селекторов. Вот сейчас разобрался, что есть проблема с клиентом и способом измерения.
В моём варианте можно варьировать количество селекторов. Конфигурация с одним селектором однозначно проигрывает.
Как чем приближать Пуассоновское? Разбросать время до следующего прибытия сообщения из одного сокета экспоненциально = количество сообщений в единицу времени распределено по Пуассону. Я не ту книжку читал?
no subject
Date: 2012-10-28 10:24 am (UTC)А про Пуассона я однозначно ступил. Это мы тоже пробовали, но воткнулись в горячий логарифм. Нелегко, короче, генерить кучу случайных величин, если их распределение нетривиально.
no subject
Date: 2012-10-28 05:51 pm (UTC)Дык... чему там лагать-то? BlockingQueue, и все дела. Если консюмеры не спят (ещё не прибежали или спинят в take), стоимость равна одному CAS на N read-ready сокетов. Если консюмеры спят (спин кончился), то ещё разбудить нужно. И что, можно сделать быстрее, чем это?
Далее, разница между одним селектором и несколькими селекторами не влияет на очередь. Разница в пропускной способности только за счёт количества селекторов.
Я где-то верю. Selector.select должен пробежаться по какой-то таблице FD, чтобы найти read-ready сокеты. У этой операции должна быть линейная стоимость. Нет?..
А про горячий лошарифм хочу послушать.
no subject
Date: 2012-10-28 05:52 pm (UTC)