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

    Появилась задача: имеется большой поток данных от множества клиентов, который нужно обработать очень быстро (желательно в реалтайме), сложить в БД и разослать другому множеству клиентов, при этом данные не структурируются ни в таблицы, ни в документы. Выбор технологий для реализации пал на Redis + C++.


    Выбор библиотеки для Redis


    Если заглянуть сюда, то видим 4 варианта библиотек для реализации своего клиента для Redis под данную задачу: hiredis, credis, libredis и C++ Client.
    Казалось бы, последнее — это то, что нужно. Но эта библиотека находится в статусе «expiremental», что сразу же намекает на то, что нужно вооружаться напильником, энтузиазмом и большим терпением. А деньги платят не за количество труда, а за результат. К тому же функциональность этого клиента пока что оставляет желать лучшего.
    Libredis. Эта штука видимо является плагином к движку PHP. Не подходит.
    Credis. Очень простенькая библиотека, позволяющая выполнять только базовые команды Redis, без возможности выстроить составную команду (эта возможность стоит у них сейчас в TODO), а ведь есть модель данных, и хотелось бы делать атомарно несколько запросов одновременно.
    Итак, выбор пал на hiredis — официальный клиент.

    Выбор событийно-ориентированной библиотеки


    Следуя свежим веяниям IT-технологий и учитывая огромное количество клиентов, я задумался о том, что необходимо использовать неблокирующее соединение с Redis, тем более эта СУБД предоставляет такую возможность. В свою очередь hiredis предоставляет адаптеры для использования с 3 библиотеками событийно-ориентированной обработки данных: это ae, libevent и libev.
    Тут важно подчеркнуть, что клиент для Redis — это всего лишь часть сервиса, которую нужно отправить в отдельный поток и каким-то образом отправлять в этот поток данные из других потоков, которые обрабатывают соединения с клиентами.
    Ae. Не понял, что за библиотека, гугл мне не помог, если кто знает — просьба откомментить пруфлинком.
    Libevent. Нет поддержки версий 2.x.x, только 1.x.x. Этот факт делает невозможным использование функционала в многопоточном приложении.
    Итак, выбор пал на libev. Далее подробнее.

    Hiredis+libev+multithread


    Статей, как оказалось, нет под данной теме. Есть публикации от производителей hiredis о том, как осуществить однопоточную событийно-ориентированную обработку данных (вот пруф) и есть документация по libev, в которой четко сказано: многопоточность не поддерживается по умолчанию, потому что нет однозначного алгоритма сделать потокобезопасную реализацию, но есть возможность создать отдельный watcher с типом ev_async, который будет принимать данные из других потоков.
    Хочу еще сразу сделать оговорку: я попробовал несколько вариантов написания кода, но либо знаний не хватает, либо надо глубже в исходники рыть, вобщем в итоге лучшим пока что вариантом оказалось просто скопировать предоставленный адаптер для libev в свой проект и дописать необходимый функционал.
    Итак. Для начала в структуру события redisLibevEvents добавляем еще один watcher:
    
    typedef struct redisLibevEvents {
        redisAsyncContext *context;
        struct ev_loop *loop;
        int reading, writing;
        ev_io rev, wev;
        ev_async aev; // Это специально обученный watcher для получения событий из других потоков.
    } redisLibevEvents;
    

    Далее. Добавляем функцию-callback для обработки событий, приходящих из других потоков:
    
    void redisLibevAsyncEvent(EV_P_ ev_async *watcher, int revents) { // Функция написана по образу и подобию callback-ов для watcher-ов типа ev_io.
    #if EV_MULTIPLICITY
        ((void)loop);
    #endif
        ((void)revents);
    
        redisLibevEvents *e = (redisLibevEvents*)watcher->data;
        redisAsyncHandleRead(e->context); // Отправляем событие в обработку.
        free((redisLibevEvents*)watcher->data); // Тут освобождаем память, которую будем динамически выделять для создания события.
    }
    

    Далее. В функцию привязывания контекста к циклу обработки событий, которая называется redisLibevAttach, добавляем в конец следущее:
    
    ev_async_init(&e->aev, redisLibevAsyncEvent); // Инициализируем watcher.
    ev_async_start(e->loop, &e->aev); // И запускаем.
    

    Теперь не хватает только одного: получения указателя на созданный watcher, так как он нужен для отправления событий. Для этого обрабатываем еще разок напильником функцию redisLibevAttach:
    
    int redisLibevAttach(EV_P_ redisAsyncContext *ac, ev_async **_pEvAsync) {
    ...
    *_pEvAsync = &e->aev;
    return REDIS_OK;
    }
    

    Собственно после уже можно использовать.
    Функция инициализации для цикла событий:
    
    m_pRedisAsyncContext = redisAsyncConnect(m_strIP, m_nDBPort); // Создаем контекст.
    m_pEventLoop = ev_loop_new(EVFLAG_AUTO); // Создаем кастомный цикл событий.
    redisLibevAttach(m_pEventLoop, m_pRedisAsyncContext, &m_pAEV); // Привязываем к циклу событий контекст.
    redisAsyncSetConnectCallback(m_pRedisAsyncContext, redisConnectCallbackFunction); // Привязываем callback для события соединения с Redis.
    redisAsyncSetDisconnectCallback(m_pRedisAsyncContext, redisDisconnectCallbackFunction); // Привязываем callback для дисконнекта.
    

    Функция потока:
    
    ev_loop(m_pEventLoop, 0); // Запуск цикла обработки событий.
    

    Ну и функция добавления нового события:
    
    // Сначала ожидаем семафор, или мьютекс (кому как больше нравится) для синхронизации потоков.
    redisAsyncCommand(m_pRedisAsyncContext, redisAsyncCommandCallbackFunction, _pPrivData, _pcQuery); // Добавляем в контекст запрос (_pcQuery) с информацией для его идентификации (_pPrivData) и callback-функцией для получения результата.
    redisLibevEvents * pRedisLibevEvent =  (redisLibevEvents*)malloc(sizeof(redisLibevEvents)); // Создаем структуру для события.
    pRedisLibevEvent->context = m_pRedisAsyncContext; // Помещаем контекст в событие.
    m_pAEV->data = pRedisLibevEvent; // Рассказываем watcher-у про созданное событие.
    ev_async_send(m_pEventLoop, m_pAEV); // Отправляем событие на обработку.
    // Отпускаем семафор/мьютекс.
    


    Ну вот и все. Полученная смесь C и C++ вполне жизнеспособна, хоть и не очень красиво выглядит.
    Надеюсь кому-нибудь пригодится, пока не вышли официальные годные реализации на С++.

    P.S.: Читайте продолжение во второй части.

    Поделиться публикацией

    Похожие публикации

    Комментарии 19

      +6
      А где же комменты, критика и прочие безобразия?
        +3
        Ну пятница же. Завтра на трезвую голову почитаем.
        +1
        Недавно Twitter выпустил некий Storm — github.com/nathanmarz/storm — и описания вашей задачи и этого шторма по крайней мере похожи. К сожалению, я так пока и не понял что именно делает этот шторм, и что конкретно у вас за задача; в частности, почему именно выбор пал на Redis&K (сравнения, показатели, функционал…).

        Вопросы: Не пробовали ли? Не примеряли ли? Как и почему подходит или не подходит? Мне на среднюю перспективу тоже нужна реал-таймовая система обработки большого потока событий, вот и смотрю на чужие опыты на этой полянке.
          0
          Не пробовал. Не примерял. Сейчас почитал, мне не подходит. Причины. 1 — это Java, следовательно оперативки будет кушать аж жуть, а мне под Redis она нужна (начинать буду с одного хостинга), ну и С++ не так прожорлив. 2 — то, что описал я — это часть решения задачи сбора данных, основная нагрузка ляжет на tornado (python), собственно непосредственно в вебе данные будут отображаться, а реалтаймовое изменение данных скорее всего будет реализовано через jabber-протокол и javascript. 3 — мне не нужны распределенные вычисления и кластеризация (ну разве что только для Redis).

          Почему именно Redis — в самом начале было сказано, что у меня данные плохо структурируются в таблицы или документы — это почти самый главный аргумент, плюс быстродействие, Redis способен обрабатывать 10к запросов в секунду. Да, не без минусов, вся база хранится в оперативной памяти и периодически делается сливание на диск, вобщем есть шанс данные потерять, ну и оперативка может быстро закончиться.
            0
            То есть, как я понял между строк, для вас важна именно предельная пропускная способность при достаточно жёсткой структуре потока (в том плане что вы не будете его сильно и постоянно менять/расширять).

            Мне как раз наоборот: нужна легко изменяемая структура, а ещё лучше мульти-структурность, а сопутствующие потери производительности готов покрывать масштабированием; ну и плюс не хочу привносить лишних человеческих компетенций (читай, С/С++) без нужды.

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

            Про redis, кстати, читал замеры — он выдаёт много (3-5-10K/sec — забыл) только на push-операциях; а вот на pull он такой же медленный как и все остальные (1-2К/sec).
              0
              Ну да, мне именно нужна предельная скорость на потоке данных. И если у меня будет эмуляция данных в текстовом поле, я буду терять время либо на парсинге, либо на генерации этих текстовых данных.

              Что касается push и pull. Тут мне как раз важны push-операции + я буду использовать рассылки, которые поддерживаются в Redis. Этим будет обеспечиваться приближение к реалтайму получаемых пользователем данных, ну конечно же при использовании jabber-протокола и javascript. Что касается pull — не страшно, если пользователь подождет 1-2-3 секунды, пока прогрузится непосредственно страница ресурса, ну или если пользователь захочет посмотреть исторические данные.
          +1
          делал что-то похожее, за что очень приятно было почитать, но я пошел другим путем:
          использовал credis — простая но производительная библиотека — именно то, что нужно. Нет ничего лишнего.
          Из событийно ориентированных использую libevent c multithread (использовал в двух проектах). Хотел кинуть ссылку на github, но там оказались старые релизы, с еще однопоточной реалзацией libevent. Как сделать её многопоточной — я изучал из исходников memcached. Думаю пруфлинк излишен.
          еще раз за статью спасибо.
            0
            Напишите свою статью, прямо таки просьба, ибо хотя бы будет из чего выбирать, а то как-то со статьями по Redis + EventLoop + multithread весьма не очень хорошо, откровенно.
              0
              На счет написания статей накладно, так как загружен собственными разработками.
              Очевидно я не так выразился, я credis использовал не в демоническом режиме,
              а вот про EventLoop + multithread можно написать статью: использовал в трех разработках.
                0
                Это тоже очень нужно. Рунет пока беден на подобные статьи.
                  0
                  заявка принята, но как прпавило такие статьи нужны только паре разработчиков, а сил при их подготовке нужно вкладывать много. На Хабре на ура идут статьи как настроить Сфинкс или php-fpm
                    0
                    Я понимаю, что это достаточно узкоспециализированные статьи. Но есть подозрение, что это явление весьма временное. Интернеты не стоят на месте, скоро наверное и интернет-магазины будут писать с расчетом высоконагруза.
                      0
                      если я ее напишу, то статья будет опубликована в блоге *nix
                      так что следи за блогом
                        0
                        ок, я на него подписан.
                0
                Кстати может ссылочка тебе и пригодится libscgi — библиотека для создания WEB приложений на С++
                кажется даже была статья на Хабре:
                В ближайшее время буду переписывать на несколько потоков и неблокируемый режим, хотя и с блокируемым отличная производительность.
                  0
                  сслылка libscgi
                    +1
                    Спасибо за ссылку, но я пожалуй пока все же на tornado остановлюсь для веба. А там посмотрим, может быть что-то буду переделывать. Правила разработки гласят: сначала напиши, чтобы работало, а потом хоть заоптимизируйся.
                      0
                        0
                        >Правила разработки гласят: сначала напиши, чтобы работало, а потом хоть заоптимизируйся.
                        правильное правило

                Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                Самое читаемое