Как стать автором
Обновить

Redis, hiredis, libev и multithread. Часть 2

Время на прочтение3 мин
Количество просмотров5.8K
В продолжение первой части хочется рассказать, как это все действительно работает. Много времени было положено на тесты и отладку, и сейчас хочется выложить подробные рекомендации по результатам исследований, которые были проведены.

Внимание! Исследования проводились не для понимания, зачем мне это нужно, а для понимания как оно работает!



Итак, что мы имели в первой части: чистый C-код, который просто включался в С++-код, радостно компилировался и давал какие-то результаты, при не очень частом внесении данных в базу. Но мы все еще хотим приближения к реалтайму, поэтому делаем стресс-тест, и получаем…

Hiredis + Libev: попингуй меня.


Первое, на что я наткнулся — это отваливание клиента от сервера через секунд 10-15, что логично, конечно же, когда данные ожидаются сервером в асинхронном режиме, а они не приходят. Для избавления от этого недоразумения необходимо прикрутить отправку команды «PING» серверу с некоторой периодичностью, ну скажем, раз в 100мкс (сто микросекунд) после выполнения предыдущего запроса к серверу. На работу сервера никак это не повлияет, а приложение будет поддерживать соединение, когда данные отсутствуют.

Hiredis + Libev: будь чистоплотен, но без фанатизма.


После прикручивания пинга, я удивленно начал наблюдать выпадания в кору, причем в том месте, где мануал по hiredis сам рекомендовал чистить память, вот цитата: «If the reply for a command with a NULL callback is read, it is immediately free'd. When the callback for a command is non-NULL, it is responsible for cleaning up the reply.». Однако reply был не NULL, но при этом стабильное падение в одной и той же функции freeReplyObject. Ну что делать, залез в исходники hiredis, и… Память оказывается чистится 2 раза! Я для начала убрал очистку памяти только для пинга, однако в дальнейшем выяснилось, что в Callback-функции вообще чистить память не надо. А еще не надо ее чистить в функциях, которые относятся к получению запросов из других потоков (те функции, которые относятся к async-watcher), потому что передаваемые указатели чистятся в функции ev_io_stop, которая вызывается для каждого из io-wacher'ов во время удаления запроса из очереди. При этом с так называемой «private data» конечно же поступать надо как Тарас Бульба: «я тебя породил — я тебя и убью». Кстати, для пинга можно private data указать как NULL, ничто нигде не падает, утечек памяти не наблюдается.

Hiredis + Libev + Multithread: Я захлебываюсь!


Удивительно с одной стороны. При попытке запилить в базу 10к push-запросов из одного потока в течение 10 секунд (цифра вообще ни о чем при заявленной производительности Redis в 120к запросов в секунду), я встретился опять же с проблемой отваливания соединения от СУБД. Лезем опять в недра, делаем трассировку и… Начинаем осознавать фразу «многопоточность не поддерживается по умолчанию, потому что нет однозначного алгоритма сделать потокобезопасную реализацию». Что же происходит? Происходит следующее: семафор, который ограничивает доступ к записи в буфер запросов из других потоков недостаточен, потому что этот самый буфер (а в hiredis он присутствует и может потреблять бесконечное количество ресурсов) растет, а данные в Redis при этом не отправляются, видимо поток с event loop не имеет доступа к чтению буфера, пока идет запись. Изначально я решал проблему выставлением таймаута между запросами, потом переместил семафор в недра адаптера к libev, выставляя ожидание семафора на добавление нового запроса и отпуская семафор после отправки в СУБД. Однако до конца разобраться с проблемой необходимости таймаута в 1мкс между запросами мне пока не удалось. Возможно в следущей части (если она будет) я уже опишу рецепт. Исходя из всего этого:

Hiredis + Libev + Multithread: из одного потока выжимаем максимум.


В итоге мне удалось добиться в одном потоке ~600 push-запросов в секунду к Redis с предварительной обработкой данных в сервисе (сколько это времени занимает не считал). В целом мне пока этого достаточно для начала, но далее буду рыть в увеличение количества потоков и еще более правильную синхронизацию добавления запросов в буфер.

Hiredis: перед сборкой тщательно обработать напильником.


Как ни крути, hiredis — это молодая библиотека, в процессе отладки своего кода, я не единожды залез в ее недра. Что сильно не обрадовало, так это то, что часто встречаются конструкции вот такого примерно вида:

<some_struct> *p = (some_struct*)malloc(sizeof(*p));
// Не нуждается в комментариях, думаю.



// Некая переменная типа char[] где-то как-то обретает свое значение, например, строка ошибки (err), а потом следует вызов вот такого вида:
... = ... sizeof(err) ...;
// извините, меня на первом курсе еще по голове били больно за подобное...


Вобщем для себя я пропатчил, отписал разработчикам, быстро не отреагировали, но думаю поправят.

Спасибо за внимание. Ожидаю критики и комментариев.
Теги:
Хабы:
Всего голосов 19: ↑18 и ↓1+17
Комментарии9

Публикации