image

Мы в PushAll обрабатываем несколько тысяч запросов в секунду для получения статистики доставки и открытия уведомлений и для передачи контента оповещений. Обычная БД вроде MySQL не справляется с таким потоком запросов и не может так быстро отвечать.

Стараясь все больше операций перенести на быстрые NoSQL хранилища вроде Redis, мы хотим знать как эффективнее его использовать и не будет ли у нас проблем с большим количеством соединений.
Также для работы мы используем форки PHP и нам было интересно, а как поведет себя Redis, если мы будем делать несколько тысяч соединений в одновременно в нескольких потоках. Мы решили поделиться с сообществом нашими тестами.

Железо


Мы тестируем на одном из VPS PushAll:

CPU: Intel Xeon E5-1650v2 3.5 Ghz — 2 ядра.
RAM: 3 Gb DDR3 1866Mhz

PHP7.
Redis 3.0.7

Условия тестирования


Мы написали многопоточного бота PHP, который:

  • Делает форки в цикле — 100 форков без каких либо задержек
  • Каждый форк в своем цикле, 1000 раз создает соединение с Redis и производит инкремент
  • Родительский процесс ждет 3 секунды и берет значение, если не ждать — Redis вернет не полное значение инкеремента


Также мы протестировали вариант с 1000 форками и как будут отличаться результаты при использовании UNIX-сокета и TCP.

100 форков, 1000 соединений в каждом, TCP


# time php benchmark.php 
End:100000

real 0m8.666s
user 0m0.063s
sys 0m0.073s


100 форков, 1000 соединений в каждом, UNIX-сокет


# time php benchmark.php 
End:100000

real 0m6.021s
user 0m0.023s
sys 0m0.067s


TCP-сокет в среднем на 30% медленнее. (напомню, тут испытывается больше не производительность работы самого Redis, а то, как он обрабатывает соединения)

1000 форков, 1000 соединений в каждом, TCP+UNIX


Повышаем ставки

TCP:
# time php benchmark.php 
End:903505

real 1m7.659s
user 0m0.073s
sys 0m0.753s

За 3 секунды Redis не успел их у себя до конца обработать — это одна из деталей, которую надо учитывать. Если слишком быстро считывать значения, можно поймать момент, когда они еще старые.

Что самое интересное, при проведение того же самого теста, но для unix-сокета, мы получаем ошибки:
Fatal error: Uncaught RedisException: Redis server went away in ....

То есть, unix-сокет не смотря на то, что он быстрее, может обрабатывать несколько меньшее количество запросов. Либо как вариант — возможно, что из за того что он такой быстрый не справляется уже сам сервер Redis'а.

Мы проводили подобные тесты и для php-fpm — там также TCP-сокет давал меньше ошибок со связкой с NGINX чем UNIX-сокет. Разница в скорости была там незначительна.

Прикладываю скрипт:
<?php
declare(ticks = 1);
for($i=0; $i < 1000; $i++){
	$pid = pcntl_fork();
	if ($pid == -1) {
		die('could not fork');
		} else if ($pid) {
			//parent
		} else {
			//child
			for ($a=0; $a < 1000; $a++) { 
				$redis = new Redis();
				//$redis->connect('127.0.0.1:6379');
				$redis->connect('/run/redis/redis.sock');
				$redis->incr('pushall:benchmark');
				$redis->close();
			}
			exit;
			
		}
}
pcntl_wait($status); //wait
sleep(3);
$redis = new Redis();
$redis->connect('/run/redis/redis.sock');
echo 'End:'.$redis->get('pushall:benchmark')."\r\n";
$redis->setTimeout('pushall:benchmark', 1);
$redis->close();


UPD pconnect


Оказывается pconnect работает в форках (странно)

Взял случай 100 процессов TCP:
pconnect
# time php benchmark.php 
End:100000

real    0m4.679s
user    0m0.023s
sys     0m0.080s


connect
# time php benchmark.php 
End:100000

real    0m9.100s
user    0m0.037s
sys     0m0.103s


Для сравнения UNIX сокет на 100 форках:
pconnect
# time php benchmark.php 
End:100000

real    0m4.393s
user    0m0.023s
sys     0m0.073s


connect
# time php benchmark.php 
End:100000

real    0m6.002s
user    0m0.027s
sys     0m0.057s


Причем, что интересно, при использовании pconnect — разница между TCP и UNIX сокетом не такая уж и большая 5-10%. При этом даже сделав все, что мне предлагали в комментариях — мне не удалось заставить работать unix-сокеты при 1000 форках.

UPD 2 Pconnect + 1000 форков



UNIX Socket
pconnect
# time php benchmark.php 
End:1000000

real    0m35.445s
user    0m0.050s
sys     0m0.637s

connect — падает в Fatal error: Uncaught RedisException: Redis server went away in…

TCP
pconnect
# time php benchmark.php 
End:989596

real    0m43.711s
user    0m0.050s
sys     0m0.623s


# time php benchmark.php 
End:903505

real 1m7.659s
user 0m0.073s
sys 0m0.753s

Разница в 20%.

PS. Хабр, а почему хаб MongoDB и MySQL есть, а Redis нет?
Какой метод вы используете, или использовали бы?

Проголосовало 258 человек. Воздержалось 172 человека.

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

Комментарии (43)


  1. noder
    26.03.2016 20:58
    +2

    Что-то не пойму. Первый тест с UNIX сокетом у вас отработал за 8.6 секунд против 6-три для TCP, но вы пишите что TCP на 30% медленнее. В чем ошибка: в тестах или резолюции? Или я что-то не понимаю?


    1. BupycNet
      26.03.2016 20:59

      Поправил, местами перепутал просто. Да TCP на 30% медленнее.


  1. noder
    26.03.2016 20:59

    Сам использую TCP. Так надежней.


  1. boodda
    26.03.2016 21:00
    +2

    Не понятно, тюнинговали ли вы ОС для того, что бы работать с таким количеством соединений (касаемо и сокетов и TCP).

    За 3 секунды Redis не успел их у себя до конца обработать
    а 3 секунды это проблема редиса или то что php не успел?


    1. BupycNet
      26.03.2016 21:07
      -3

      Да лимиты убраны. 3 секунды это скорее проблема редиса. То есть форки отдали на сервер редиса incr, потом завершили работу, потом родительский процесс об этом узнал (что все закончили работу), после чего делает еще одно соединение с редисом и запрашивает количество.
      Как итог — нужно время чтобы оно успело обработаться.
      Тут скорее дело в том, что тот же MySQL блокирует записи на чтение при UPDATE, редис же так не делает, поэтому нужно учитывать, что после завершения работы в фоне множества процессов, сразу получить самые свежие данные не получится.


      1. tonyvelichko
        28.03.2016 16:17
        +1

        Redis работает в 1 потоке, ему не нужно ничего блокировать.


  1. zapimir
    26.03.2016 21:18
    +1

    А не могли бы выложить тестовый скрипт интересно сравнить у себя. И какая версия PHP использовалась?


    1. BupycNet
      26.03.2016 21:32

      PHP7.
      Redis 3.0.7
      Добавил в статью код.


  1. xytop
    26.03.2016 21:52
    +6

    Покажите вывод
    cat /proc/sys/fs/file-max
    Количество одновременно открытых UNIX-сокетов ограничено максимальным количеством файловых дескрипторов. Там у вас и сидит значение меньше миллиона. Его вы не изменили для теста, поэтому и ошибки полезли.


    1. BupycNet
      26.03.2016 22:02
      -1

      Да вы правы — стоит 100 тысяч. Вот кстати еще важная деталь.


      1. xytop
        26.03.2016 22:06
        +14

        Я бы сказал, что это единственная деталь :)
        Если выставить правильный лимит, то стабильность будет намного выше чем у TCP, ну и скорость конечно же.


    1. BupycNet
      26.03.2016 22:07
      -1

      Не совсем правы. Поставил миллион. Попробовал — все равно ошибки.
      Потом вспомнил — тут 1000 процессов делают по 1000 соединений в цикле. Одновременно может быть не более 1000 соединений. (Каждый процесс делает одновременно лишь один коннект а потом дисконнект)
      При этом лимит 100 тысяч.
      Так что лимит тут не виноват, а что то другое.


      1. xytop
        26.03.2016 22:22
        +9

        Не совсем правы Вы :)

        1. Одновременно соединений у вас больше, потому что после close не поставлены скобки. Функция без скобок в PHP не вызывается.
        2. Лимит надо ставить не на миллион, а больше, потому что есть еще другие системные процессы плюс сам редис под себя часть дескриптором забирает. Почитайте тут: http://redis.io/topics/clients

        Попробуйте так задать:
        `
        ulimit -Sn 101000
        sysctl -w fs.file-max=1001000
        `


        1. BupycNet
          27.03.2016 00:08

          Добавил еще в статью пару тестов. Обнаружил, что Redis как то подцепляет pconnect и работает с ним в 1.5-2 раза быстрее!
          При этом разница между tcp и unix сокетами при этом составляет 5-10%.


      1. xytop
        26.03.2016 22:31
        +2

        Ну и еще maxclients для Redis надо увеличить за миллион :)


        1. BupycNet
          26.03.2016 23:25

          Увеличил лимиты еще, поставил max_clients (кстати с tcp и так работало )
          Насчет close — даже если его не писать — все равно при завершении дочернего процесса же срабатывает деструктор. Т.е. соединение закрывается.
          Итог таков что даже увеличив лимиты и т.д. — все равно UNIX сокет не работает при таком количестве запросов, а tcp работает.


          1. xytop
            27.03.2016 11:28
            +1

            Нужно смотреть логи редиса, там должно быть что-то по этому поводу.


  1. ZurgInq
    26.03.2016 21:54
    +1

    Редис работает в однопоточном режиме, хотя бы по этому не стоит слать в него 1000+ одновременных запросов. И возможно именно с этим и связана ошибка "Redis server went away in".


  1. tempico
    26.03.2016 22:02
    +2

    Извиняюсь, а какая ОС участвовала в тесте? По моим наблюдениям, на убунте сокеты работают быстрее, чем на центОС. А на генту — быстрее убунты в разы (но генту-парк серверов сложнее поддерживать)


    1. BupycNet
      26.03.2016 22:03

      ArchLinux 32 bit


  1. rotor
    26.03.2016 22:57
    +2

    Исправьте выводы в посте, пожалуйста. Вам уже подсказали выше в комментариях о причинах ваших не правильных выводов.
    Unix-сокет быстрее и надежнее TCP-сокета. Единственным недостатком использования Unix-сокета является является более сложная экосистемы.
    Кстати, использовать PHP как промежуточный слой для тестирования именно редиса — не самое удачное решение.


    1. BupycNet
      26.03.2016 23:25
      -2

      Мы так до конца и не разобрались. У вас бенчмарк на UNIX сокетах нормально работает на 1000 форках?


      1. rotor
        27.03.2016 00:18
        +1

        Вот этот комментарий: https://habrahabr.ru/company/pushall/blog/280218/#comment_8823146 исчерпывающе объясняет почему у вас появились проблемы.
        От себя могу добавить, что у меня Редис работает c 2011-го через Unix-сокет ни разу не было проблем. Хатя нагрузки были вполне приличные.
        Да и с точки зрения ОС Unix-сокет не может быть менее надежным. Я поэтому удивился когда увидел у вас такие выводы. TCP-сокет добавляет сетевой стек — это его принципиальное отличие.
        У меня не установлен php 7, поэтому, к сожалению, ваш бенчмарк запустить не могу.


        1. BupycNet
          27.03.2016 02:18
          -2

          Этот код работает начиная с php 5.5 где то.
          Насчет комментария — описал же, что не помогло это.
          Причем с pconnect все работает.
          Насчет нагрузок — сомневаюсь что вы писали в редис одновременно в 1000 потоков. Тут вероятно, что сам redis не справляется через UNIX сокет т.к. слишком быстро идут запросы, а TCP из за доп оверхеда соединения уменьшает нагрузку на сам redis.
          Чтобы писать в 1000 потоков одновременно (сами смотрите, это было миллион за 4 секунды) вам нужно делать 250 тысяч запросов в секунду. При этом бенчмарк редиса (redis-benchmark) у меня лично показывает где то 100 тысяч запросов в секунду.
          То есть — чтобы достичь тех же нагрузок — вам нужно редис на 100 загрузить.
          У меня на тестах редис показывал 100% потребления одного ядра. То есть по сути эти был его предел по обработке.
          Но все таки тот факт, что TCP в случае нагрузки замедляется, а в UNIX socket просто отваливается — мне кажется первый вариант лучше.


  1. rotor
    26.03.2016 23:08
    +1

    За 3 секунды Redis не успел их у себя до конца обработать — это одна из деталей, которую надо учитывать. Если слишком быстро считывать значения, можно поймать момент, когда они еще старые.

    Редис же однопоточный и, как следствие, все операции выполняет атомарно. Или я отстал от жизни и редис сейчас уже многопоточный?


    1. BupycNet
      26.03.2016 23:26
      -1

      Да тоже странно. Может действительно не до конца соединения закрываются. Либо он обрабатывает уже после того как отпускает клиент.


      1. rotor
        27.03.2016 00:25
        +1

        Честно скажу, исходники Редиса не ковырял, но как я себе представляю его устройство: он может держать открытыми несколько соединений, но обрабатывает их он один за одним. Полностью. Могу ошибаться, конечно, но как иначе в рамках однопоточной архитектуры?
        Может быть проблема в php-клиенте или вашем коде?


        1. BupycNet
          27.03.2016 01:17
          -1

          Я же говорю — сам увидился. Конечно как вариант, может php-redis вообще сразу передает управление дальше, т.е. я отправляю incr, после чего у меня идет закрытие соединения, а вот сам модуль php-redis возможно в этот момент все это отправляет.
          Но вообще я сильно сомневаюсь в этом.


          1. zapimir
            27.03.2016 17:52
            +1

            Поковырял пример, это просто кривовато сделано сообщение об ошибке в pecl-redis, я прикрутил TinyRedisClient там выдается ошибка "Resource temporarily unavailable" при подключении к сокету, т.е. до Redis дело даже не доходит.
            Проблема из-за того, что Redis все подключения по сокету делает неблокирующимися, и на каком-то этапе заполняется буфер, и выдается ошибка EAGAIN. Теоретически можно лечить установкой таймаута сокета SO_SNDTIMEO. Т.е. по сути это бага не Redis'а, а конкретного драйвера PHP. Что подтверждается тем, что redis-benchmark с параметром -с 1000 (т.е. одновременное подключение 1000 клиентов) нормально отрабатывает через сокеты.


            1. BupycNet
              27.03.2016 18:17

              В итоге пришли к тому что я и имел в виду — в данной вариации, если работать через php с сокетом, то существует вероятность, что буфер заполнятся и скрипт упадет.
              Можно конечно переписать этот драйвер, но через tcp все работает без проблем и разница с pconnect незначительна.


  1. Sanasol
    27.03.2016 22:42
    +1

    Обычная БД вроде MySQL не справляется с таким потоком запросов и не может так быстро отвечать.
    Открыл статистику на нашем сервере, воскресенье вечер: 5740 запросов в секунду.
    Как мы работаем в будни вообще не представляю.


    1. BupycNet
      27.03.2016 22:53

      5740 запросов в секунду.
      А тестах выше у нас миллион за 35 секунд. Т.е. 28 тысяч в секунду.
      Теперь интересно, какое у вас железо? У нас вот 28 тысяч в секунду на одном ядре Intel Xeon E5-1650v2 3.5 Ghz обрабатывается. И еще деталь — 28 тысяч — инкрементов в секунду.
      И самая важная деталь — это 28 тысяч соединений в секунду.
      Я немного модифицировал бенчмарк, сделал 100 потоков по 10000 инкрементов через 100 соединений. И о чудо миллион инкрементов за 10 секунд.
      100 тысяч инкрементов за 1 секунду. В 20 раз больше инкрементов, чем у вас запросов в секунду, на одном ядре. При этом нагрузка не сильно большая, все внутри оперативки работает.
      MySQL при таком количестве селектов (5000) в секунду, скорее всего ест несколько ядер.
      А теперь я еще модифицировал скрипт. Вот он теперь делает один SET и миллион GETов (типичные SELECT'ы для MySQL) и о чудо — за 3 секунды миллион GET'ов. Т.е. 333 тысячи селектов за секунду на одном ядре. В 60 раз быстрее.
      И теперь на секундочку — сколько у вас в секунду идет UPDATE и INSERT? У нас проблема именно в том, что к нам идут несколько тысяч ответных запросов на установление статуса уведомления у определенного пользователя, можете ли вы обработать 5000 инсертов, а потом еще 5000 обновление полей? И так, чтобы не спавнить 5000 форков PHP-FPM, т.е. нужно обрабатывать запросы быстрее чем за 10-20 мс, иначе слишком много процессов скопится.


      1. Sanasol
        27.03.2016 23:06
        +1

        Ну так вы определитесь. Несколько тысяч запросов в секунду или десятки тысяч.
        Синтетика типа инкременты в секунду ни о чем не говорит.
        У нас сложная бизнес логика. Почти всё селекты, да.
        У вас же не основана вся логика на одном инкременте?
        Чтобы не спавнить 5000 форков PHP-FPM, я бы вообще не стал делать это на php, он для такого крайне не подходит.
        Так что все очень относительно.
        Какой размер данных, что вы пишите/обновляете.
        Все это влияет на то как быстро отработает и сколько вытянет.
        И конечно разница существенная с оперативкой работать или с файлами.


        1. BupycNet
          27.03.2016 23:16

          Фраза
          "Обычная БД вроде MySQL не справляется с таким потоком запросов и не может так быстро отвечать."
          Как раз имела в виду, что делать такое число UPDATE'ов в MySQL очень тяжело.
          "У нас сложная бизнес логика. Почти всё селекты, да." Вот тут и выходит. Селекты делать не проблема. Хотя проблема начинается, когда идет 1000 UPDATE'ов и селекты одновременно связанные с обновляемыми строками — а они все лочатся.
          Собственно поэтому мы будем частично использовать Redis + еще использовать его как буфер. Т.е. набрали за пару секунд данных, а потом уже сбросили это в БД одиночным запросом.
          Насчет тысяч и десятков тысяч — скорее всего у вас на эти 5000 запросов, всего 100-200 UPDATE/INSERT в секунду. Так что да — нам нужно те же 5000 — только UPDATE запросов.


  1. chiliec
    28.03.2016 11:05
    +3

    Если у вас 1000 форков, и каждый инициирует по 1000 соединений, легко представить ситуацию, когда открыто более 100 соединений. Полагаю, что в этом случае стоит увеличить значение net.core.somaxconn до значения большего, чем пиковое количество одновременных соединений.

    /proc/sys/net/core/somaxconn: Limit of socket listen() backlog, known in userspace as SOMAXCONN. Defaults to 128. The value should be raised substantially to support bursts of request. For example, to support a burst of 1024 requests, set somaxconn to 1024.

    В свою очередь, для TCP соединений, вы используете ту же машину, которая на каждое соединение "расходует исходящий порт", соотвественно, возможна ситуация, когда вы израсходуете весь local_range. Стоит посмотреть диапазон указанный в net.ipv4.ip_local_port_range и увеличить его.
    Также, полагаю, что за время теста в dmesg насыпало более понятных логов о причинах проблем.


  1. merc
    28.03.2016 13:58
    -1

    Смотрели в сторону aerospike?

    И очень интересно было бы увидеть этот же бенчмарк на другом языке.


  1. shamanis
    28.03.2016 14:48

    Лично я с такими вещами, как Redis, MySQL и т.п. использую всегда TCP, а вот с php-fpm, uwsgi и gunicorn всегда использую UNIX-сокет.


    1. GamePad64
      30.03.2016 14:17

      А почему именно так?


  1. RUnnerTomsk
    28.03.2016 18:40

    Просто чтобы внести некоторую ясность.

    Обычная БД вроде MySQL не справляется с таким потоком запросов и не может так быстро отвечать.

    Я описывал тут кейс, как со своими кривыми руками я добился 12 тыщ в секунду на одном сервере, insert/update, на InnoDb под мускулом (Percona 5.6), естественно это все через веб входило, nginx принимал данные в виде json запросов.
    Сами перконовцы до 16 тыщ разгоняют на одном сервере.
    Понятно что миллион в секунду не сделать, и тут нужны другие решения. Но просто для проформы упоминаю что 5 тысяч это не меганагрузка.


  1. simpleadmin
    30.03.2016 15:52
    +1

    TCP (т.к. он стабильнее, или нет?

    С чего вдруг?
    Сокеты одного типа SOCK_STREAM. За исключением того, что в домене AF_INET мы несём накладные расходы на именование сокета, htonX'ы, муршрутизацию, asc'и принципиальных отличий от AF_UNIX нет. Как следствие, чем меньше длина пакета данных, тем больше будет выигрыш при использовании сокетов в домене файловой системы. Как правило, это порядка 50%.
    Поэтому (если исключить правку кода для внедрения в редис IPS с использованием разделяемой памяти) AF_UNIX сокеты будут самыми производительными. А ввиду того, что read/write для них реализован одинаково сравнивать стабильность не приходится.
    обрабатываем несколько тысяч запросов в секунду для получения статистики доставки и открытия уведомлений и для передачи контента оповещений. Обычная БД вроде MySQL не справляется с таким потоком запросов и не может так быстро отвечать.

    Ни чистых Insert'ах отправлял в продакшине данные из nginx в MySQL порядка 30'000 qps и это был не предел. На чистой синтетике (в режиме храниения данных а-ля redis — ключ/значение) думаю можно добиться и более 100'000 qps.


    1. BupycNet
      30.03.2016 19:07

      Тут дело не в самих сокетах, а в том как с ними работает драйвер php и сам redis.
      Например также читал, что у редиса была (или еще есть?) бага, связанная с тем, что UDS не отвечает в момент записи редисом данных на диск, в то же время tcp просто ожидает ответа.


      1. simpleadmin
        30.03.2016 20:35
        +1

        Тут дело не в самих сокетах, а в том как с ними работает драйвер php и сам redis.

        А что PHP или Redis используют некие несистемные средства работы с сокетами?
        Листаю исходники redis. Как и ожидалось все функции anetTcp отличаются от anetUnix именно "добавлением" в Tcp-функциях getaddrinfo, bind, ntohs.
        Чтение и запись реализованы в "общих" anetRead, anetWrite. И пока не могу полагать, что работа с сокетами одного типа может отличаться чем-то кроме дополнительных расходов на tcp-обработку.
        Например также читал, что у редиса была (или еще есть?) бага, связанная с тем, что UDS не отвечает в момент записи редисом данных на диск, в то же время tcp просто ожидает ответа.

        Ссылкой поделитесь?


        1. BupycNet
          30.03.2016 21:24
          -2

          "И пока не могу полагать, что работа с сокетами одного типа может отличаться чем-то кроме дополнительных расходов на tcp-обработку."
          Как выше писали — проблема в буферах php-redis. Именно в нем разные реализации.
          "Ссылкой поделитесь?"
          https://toster.ru/q/223524
          Вот еще проблема — https://github.com/phpredis/phpredis/issues/70 у многих решается как раз переходом с UDS на TCP.