LINUX.ORG.RU

скорость работы пре-форка

 , ,


1

2

Написал тестовый вариант сервера, который принмает соединения, в соединении запрос некоторого сообщения (в формате json), затем добавляет информацию и отправляет ответ обратно.

Провёл два теста: 1. с закрытием клиентского сокета; 2. без закрытия сокета. Особенность сервера в том, что он по требованию создаёт новые процессы, которые после отправки ответа переходят в состояние «свободны» и могут быть использованы повторно. Мастер-процесс принимает соединение, подыскивает свободный процесс и передает ему дескриптор.

Результаты теста: когда клиент каждый раз делает-коннект-посылка-прием-закрытие, то скорость на локалхосте 6 тыщ запросов в секунду; посылка-прием по кругу без закрытия коннекта примерно 50 тыщ запросов в секунду.

Теперь вопрос. Достаточная ли это скорость или надо ещё подумать?

★★★★★

Странный вопрос. Если тебе хватает, то достаточная.

Если бы я делал сервер с претензией на скорость, то выбрал бы модель сервера, основанную на мультиплексировании, а не prefork.

Olegymous ★★★
()
Ответ на: комментарий от Olegymous

основанную на мультиплексировании,

понимаю тебя, но есть моменты, которые фиг так просто мультиплексируешь, например DBI. вот хоть разбейся, но я нашел всего два драйвера, которые можно приспособить к мультиплексированию, а все остальные нельзя.

Если тебе хватает, то достаточная.

на текущий момент хватает более чем, но хочется заранее заложить масштабируемость.

Casus ★★★★★
() автор топика
Ответ на: комментарий от Casus

Всё равно снизится во несколько раз, когда начнёшь логику реализовывать. Обычно, обработка соединений не самая долгая часть.

ЕМНИП FCGI::ProcManager с 4 воркерами у меня выжимал около 5 krps, но тут уже от железа зависит

заранее заложить масштабируемость.

масштабируемость, это не так, масштабируемость от логики зависит, когда добавление второго сервера позволит увеличить производительность в 2 раза(в идеале)

disarmer ★★★
()
Ответ на: комментарий от disarmer

ЕМНИП FCGI::ProcManager с 4 воркерами у меня выжимал около 5 krps, но тут уже от железа зависит

гуд. значит 6к на одном процессе нормально. на двух было 10к.

масштабируемость от логики зависит, когда добавление второго сервера позволит увеличить производительность в 2 раза

вопрос тонкий. можно добавить памяти, процессоры и только затем уже про сервер думать. добавить процессоров в контейнер openvz совсем не сложно — а вот и смасштабировал :) ну или там в облаке добавить ресурсов. вопрос был в бутылочном горлышке. я ещё подумаю, конечно, но морально уже готов признать, что результат «ок».

Casus ★★★★★
() автор топика
Ответ на: комментарий от Casus

добавить памяти, процессоры

Это вертикальное масштабирование, с ним всё понятно, но оно не бесконечно, и чем дальше тем дороже.

Я имел ввиду горизонтальное масштабирование, когда один сервер уже никак не справляется. Тут уже прирост производительности зависит от логики приложения, например традиционные РСУБД довольно сложно масштабировать горизонтально.

disarmer ★★★
()
Ответ на: комментарий от disarmer

это в данном случае для меня далёкая перспектива. заказать сервер со 128гб рам и кучей ядер плюс ссд диски сейчас не дорого совсем, а больше мне ещё не нужно было. если понадобится больше, то тогда уже совсем что-то другое думать.

Casus ★★★★★
() автор топика

Это оно у тебя с такой скоростью статику раздает? Тогда еще подумай)

И да, к разговору про горизонтальную масштабируемость... Ты определись, тебе хватит, или ты хочешь быстрее? Если хватает, то к чему этот вопрос? Если нет — то думай именно о горизотальной масштабируемости.

kardapoltsev ★★★★★
()
Ответ на: комментарий от kardapoltsev

Это оно у тебя с такой скоростью статику раздает?

нет, это был просто пример сервера что в работе есть, а тест был на моём домашнем локалхосте :)

В текущем варианте меня всё устроит, но есть странное желание сделать лучше...

Casus ★★★★★
() автор топика
Ответ на: комментарий от Casus

гуд. значит 6к на одном процессе нормально. на двух было 10к.

Все не так. Если accept у тебя на одном процессе, то предел там до 100к и зависит от проца/ядра/кода. Если accept срабатывает одновременно в нескольких процессах, то никакой линейной зависимости производительности нет, т.к. ядро делает блокировку на дескриптор. В худшем случае производительность упадёт даже.

Что касается цифр. nginx на одном процессе выдаёт 40к+ рпс для данных менее 100кб для локалхоста. Ядро без патча на оптимизацию сокетов на 127.0.0.1. Проц 2.5ГГц/FSB 1333МГц. Перл на том же железе в лучшем случае выдаёт 25к рпс для тех же данных (libev + HTTP::Parser::XS). И опять же, что для nginx, что для перла, кол-во воркеров не играет роли. Просто потому, что для этого теста нечем загрузить nginx, perl (отдавались страницы статикой). Естественно nginx на чистом си просто обыгрывал перл по меньшему кол-ву функций и более коротких структурах данных (perl/xs api содержит дофигища функций оберток).

Т.о. я бы не стал на твоём месте мечтать о хорошей производительности для кода на перле. В лучшем случае будет в 1,5 раза медленнее чем pure c. При этом чем меньше PP и больше XS кода, тем лучше. Если сразу ясно, что нужно максимум производительности: забудь перл, пиши сразу на си/плюсах.

gh0stwizard ★★★★★
()
Ответ на: комментарий от gh0stwizard

Спасибо за цифры, буду думать. Да, accept в одном процессе, чтобы раздавать задания по остальным. Тут такой вопрос: допустим, каждый воркер тоже слушает главный сокет, получил коннект, занимается заданием, не получится ли, что к нему может прийти следующий коннект, когда он еще не готов продолжить принимать соединения на главный сокет, а другие воркеры будут простаивать? Т.е. допустим я сделаю даже у него сокращённую очередь listen до 1, но после вызова accept там снова появится место, может ли туда попасть следующее соединение и простаивать до момента, пока снова управление не вернётся в EV?

Casus ★★★★★
() автор топика
Ответ на: комментарий от Casus

допустим, каждый воркер тоже слушает главный сокет

Ядро на вызов accept() отдаст клиентский сокет тому процессу, который готов. При этом дескриптор блокируется ядром, скажем так, в однопоточном режиме. Т.о. кол-во воркеров не увеличивает производительность вызова accept(), т.к. bottleneck в самом ядре, а не сишной либе/еще где-то. Это справедливо для линукса, в бсд, возможно, иначе, не интересовался.

пока снова управление не вернётся в EV?

В EV ты передашь только клиентский сокет. Для fork-модели есть модуль FDPass (или как-то так, Марк Лемман, автор libev делал свой модуль для fdpass).

gh0stwizard ★★★★★
()
Последнее исправление: gh0stwizard (всего исправлений: 1)
Ответ на: комментарий от gh0stwizard

Понимаю, что без показа кода сложно вести беседу, но вижу, что местами ты меня не понял :)

тому процессу, который готов

К чему готов, вопрос. Свободное место в очереди прослушивания же есть, по идее при этом выбор конкретного процесса должен быть случаен или опираться на какие то данные и мне кажется маловероятным, что при этом смотрится текущая нагрузка на процессор от конкретного процесса. Остальная часть твоего ответа немного пролетает мимо вопроса. Ускорить accept я понимаю что нельзя, это уже другая тема «горизонтального масштабирования».

Для fork-модели есть модуль FDPass

Я непонятно выразился. Эта часть решена, это всё работает. Мне один мастер-процесс нужен для связи чайлдов между собой, для связи чайлдов с мастером и для слежения за блокировками ресурсов приложения. Если переходить к мульти-мастерам, то нужны новые усложнения. И вот я пока в раздумьях: а) париться ли; б) если париться, то чем, где веники?

Casus ★★★★★
() автор топика
Ответ на: комментарий от Casus

К чему готов, вопрос.

Если процесс занят чем-то, то он не висит на accept(). Очевидно, что таким образом новое подключение к нему не поступит пока он занят. Если мы говорим про стандартный цикл принятия новых соединений

while (1) {
	accept($client, $server) or next; # ждем подключения
	
	# делаем что нужно
	# пока не вызовем accept() на следующей итерации
	# новое подключение в этот процесс не придёт
	...
}
Olegymous ★★★
()
Ответ на: комментарий от Olegymous

Если процесс занят чем-то, то он не висит на accept().

Ты пишешь что-то нереальное. Сокетов больше одного, значит у нас есть select/poll и пр, сокеты неблокирующиеся (для определённости). Процесс не «висит» на accept, твой вариант годится только для одного сокета. Для listen-сокетов надо ждать события доступности на чтение и затем звать accept, нет никакого «висения». Я говорю про «стандартный цикл», а в твоём примере вырожденный случай одного блокирующегося соединения.

Casus ★★★★★
() автор топика
Ответ на: комментарий от Olegymous

# новое подключение в этот процесс не придёт

Кстати, почему бы это? Я не вижу причин для такого даже в твоём примере. У listen-сокетов есть очередь, соединения приходят туда на приём раньше, чем процесс позовёт accept, вызов accept только завершает процесс установки соединения. Допустим, у тебя всего один процесс делает accept, запросы на соединение идут потоком, ты делаешь accept и выполняешь полезную работу (принял соединение, прочитал запрос, отправил ответ, для сохранения числа сокетов тут же закрыл соединение), думаешь, что все пришедшие соединения в момент выполнения работы сразу дропнутся? Дропнутся, когда очередь переполнится, но до этого момента будут там сидеть и ждать accept.

Casus ★★★★★
() автор топика
Ответ на: комментарий от Casus

Потому что в дочернем процессе мы имеем дело с тем же сокетом, что был создан в родительском. Мы же про форкающийся сервер еще говорим? Так вот если несколько дочерних процессов делают accept() на этом сокете созданном в главном процессе, то никаких описанных тобой проблем не будет. Потому что сокет этот один и тот же на все дочерние процессы.

Olegymous ★★★
()
Ответ на: комментарий от Casus

Итак, хочешь знать как все устроено. Поехали.

1. http://man7.org/linux/man-pages/man2/accept.2.html

If no pending connections are present on the queue, and the socket is
not marked as nonblocking, accept() blocks the caller until a
connection is present.  If the socket is marked nonblocking and no
pending connections are present on the queue, accept() fails with the
error EAGAIN or EWOULDBLOCK.

2. http://perldoc.perl.org/perlipc.html#Sockets:-Client/Server-Communication

После примера сервера с fork():

Within the while loop we call accept() and check to see if it returns a false value. This would normally indicate a system error needs to be reported. However, the introduction of safe signals (see Deferred Signals (Safe Signals) above) in Perl 5.8.0 means that accept() might also be interrupted when the process receives a signal. This typically happens when one of the forked subprocesses exits and notifies the parent process with a CHLD signal.

If accept() is interrupted by a signal, $! will be set to EINTR. If this happens, we can safely continue to the next iteration of the loop and another call to accept(). It is important that your signal handling code not modify the value of $!, or else this test will likely fail. In the REAPER subroutine we create a local version of $! before calling waitpid(). When waitpid() sets $! to ECHILD as it inevitably does when it has no more children waiting, it updates the local copy and leaves the original unchanged.

3. http://perldoc.perl.org/perlipc.html#Deferred-Signals-(Safe-Signals)

Особенность системных вызовов в перле (к которым относится accept()):

When a signal is delivered (e.g., SIGINT from a control-C) the operating system breaks into IO operations like read(2), which is used to implement Perl's readline() function, the <> operator. On older Perls the handler was called immediately (and as read is not «unsafe», this worked well). With the «deferred» scheme the handler is not called immediately, and if Perl is using the system's stdio library that library may restart the read without returning to Perl to give it a chance to call the %SIG handler. If this happens on your system the solution is to use the :perlio layer to do IO--at least on those handles that you want to be able to break into with signals. (The :perlio layer checks the signal flags and calls %SIG handlers before resuming IO operation.)

The default in Perl 5.8.0 and later is to automatically use the :perlio layer.

Иными словами, пример Olegymous корректен и будет работать как задумано безо всяких select/poll (они нужны на клиентских сокетах). НО, будет жрать cpu из-за того, что происходит next. Грубо говоря, в случае неблокирующих сокетов (accept() вернет EAGAIN моментально, когда никто не подключается) будет следущее: while() { next; }. В случае блокирующих сокетов accept() будет заблокирован, как описано в accept(2), процесс уйдет в sleep-state, перл будет ожидать возвращения результата из сишного вызова, цикл «зависнет» до первого соединения/получения сигнала.

Как желательно делать описано здесь:

http://man7.org/linux/man-pages/man2/select_tut.2.html

Тогда в перле придется вместо read, write переходить на sysread, syswrite со всеми вытекающими :)

gh0stwizard ★★★★★
()
Ответ на: комментарий от Casus

Прям вообще нереальное :) Ты вроде нигде не писал, что у тебя несколько сокетов и всё построено на select/poll. Что тогда в твоем понимании «стандартный цикл» я не знаю, давай пример.

Olegymous ★★★
()
Ответ на: комментарий от gh0stwizard

poc

#!perl

use strict;
use warnings;
use Socket;
use Carp qw(croak);

my $port = shift || 12345;
croak "invalid port" if $port !~ /^\d{1,5}$/;

my $proto = getprotobyname 'tcp';

socket( SERVFD, PF_INET, SOCK_STREAM, $proto )
  || croak "socket: $!";
setsockopt( SERVFD, SOL_SOCKET, SO_REUSEADDR, pack( "l", 1 ) )
  || croak "setsockopt: $!";
bind( SERVFD, sockaddr_in( $port, INADDR_ANY ) )
  || croak "bind: $!";
listen( SERVFD, SOMAXCONN )
  || croak "listen: $!";

while () {
  my $paddr = accept( CLIFD, SERVFD );
  my ( $port, $ip ) = sockaddr_in $paddr;
  printf "connection from %s\n", inet_ntoa( $ip );
  close CLIFD;
}
gh0stwizard ★★★★★
()
Последнее исправление: gh0stwizard (всего исправлений: 1)
Ответ на: комментарий от gh0stwizard

Итак, хочешь знать как все устроено. Поехали.

Если _внимательно_ читать, что я писал, то я нигде не противоречил документации, а скорее даже дополнял её. Весь твой пост не имеет отношения к моей ситуации и вопросу вообще. Т.е. твои ценные указания на документацию несут мне 0 (ноль) новой информации. Предлагаю начать внимательнее читать собеседника перед нажатием кнопки ответа и сначала понять вопрос.

Иными словами, пример Olegymous корректен и будет работать как задумано безо всяких select/poll

Я не опровергал. Будет, но выполнять он будет ровно одну функцию, чего мне мало. Когда Olegymous упомянул про мультиплексирование, я подумал, что он знает о чём речь, но в его примере этого не было. Более того, формально не должно быть никакой разницы между комбинацией select+accept и просто блокирующимся accept, а даже если б была сделана оптимизация этого узкого случая, то она была бы мало востребована.

Casus ★★★★★
() автор топика
Ответ на: комментарий от Olegymous

Сорри, не заметил сообщения.

Текущий тест у меня 600 строчек кода, я не могу просто взять и привести его тут.

Потому что в дочернем процессе мы имеем дело с тем же сокетом, что был создан в родительском.

Для простоты обозначим гипотетический случай. Допустим, мы открыли один сокет на прослушивание и форкнулись 10 раз. Т.е. один сокет слушают 10 процессов одновременно. Приходит новое соединение, какой из процессов его акцепнет? Вот тут я знаком с формальным принципом, но не с конкретной имплементацией. Поэтому, если меня аргументированно поправят, то буду рад. Акцептнет его только один из воркеров, остальные продолжат спать. Всё хорошо. Теперь, допустим, что пришло 10 соединений, все 10 акцептнулись разными воркерами. Тоже замечательно. Приходит 11е, что происходит? 11е соединение встанет в очередь к одному из воркеров в ожидании, что его акцепнут. Момент выбора воркера для меня при этом покрыт туманом, я могу придумать несколько разумных стратегий, например, пропорционально заявленной глубине очереди, заданной в вызове listen. А может быть обязательно сначала найдут того, кто в данный момент в состоянии сетевого сна (select). Если последний случай гарантирован, то это открывает новые возможности оптимизации для меня, хотя и не исключает ложных попаданий «не туда».

Так вот если несколько дочерних процессов делают accept() на этом сокете созданном в главном процессе, то никаких описанных тобой проблем не будет.

Если сначала понять о чём именно я говорю, то станет проще обсуждать вопрос. Все процессы в определённый момент времени должны выполнять «полезную работу», а не висеть в сетевом ожидании. Если приходит соединение в тот момент, когда заняты _все_ процессы, куда оно попадёт?

Casus ★★★★★
() автор топика
Ответ на: poc от gh0stwizard

#!perl

я не понял чего это доказательство. лет 15 назад я так же писал.

Casus ★★★★★
() автор топика
Ответ на: комментарий от Casus

Соединение попадает в очередь сокета, а не конкретного процесса. А так как сокет у всех процессов один и тот же, то первый воркер сделавший accept() и получит его из очереди.

Olegymous ★★★
()
Ответ на: комментарий от Casus

я не понял чего это доказательство

Принципа работы accept в перле. Можешь заспамить соединениями процесс и посмотреть, что будет. Не забудь про ulimit.

15 назад я так же писал

А в этой теме ни одной строчки кода от тебя. Не можешь привести код: нарисуй блок-схему.

Продолжим переход на личности из-за тупых придирок?

gh0stwizard ★★★★★
()
Ответ на: комментарий от Olegymous

О, это хороший ответ. Пожалуй, действительно, решает мой вопрос. Спасибо.

Casus ★★★★★
() автор топика
Последнее исправление: Casus (всего исправлений: 1)
Ответ на: комментарий от gh0stwizard

Продолжим переход на личности из-за тупых придирок?

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

А в этой теме ни одной строчки кода от тебя. Не можешь привести код: нарисуй блок-схему.

Я готов обсуждать свой код, но никак не могу определиться с местом для обсуждения, вот в чём дело.

Принципа работы accept в перле.

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

Casus ★★★★★
() автор топика
Ответ на: комментарий от Casus

1. http://www.faqs.org/docs/iptables/tcpconnections.html

Внизу таблица с таймаутами. Если какой-то воркер не успеет отправить ack, тогда клиент отвалится с ошибкой timeout (состояние syn-sent). Если клиент не успеет отправить syn/ack+ack, то ядро (accept()) будет ждать этого пакета (состояние SYN-RECV[IEVED]).

Твой вопрос заключается в размере очереди и скорострельности. Так вот, на стороне сервера это регулируется значением таймаута SYN-RECV. Если ядро на уровне сетевой карты + tcp/ip стека будет иметь все нужные пакеты, т.е. готово перейти в состояние ESTABLISHED, то в случае перла произойдет следующее:

  • accept() вернет валидный $fd в состоянии ESTABLISHED
  • Без других причин отключения соединения (потери пакетов, послылка fin одной из сторон и т.п.), соеденинение между хостами будет сохранятся в состоянии ESTABLISHED до наступления системного таймаута.

Еще раз. До тех пор пока не вызван accept() на сервере клиент находится в состоянии SYN_SENT и ожидает, что сервер соизволит отправить ack по вызову accept.

gh0stwizard ★★★★★
()
Вы не можете добавлять комментарии в эту тему. Тема перемещена в архив.