Во время собеседований на роль linux/unix администратора во многих IT-компаниях спрашивают, что такое load average, чем nginx отличается от apache httpd и что такое fork. В этой статье я постараюсь объяснить, что рассчитывают услышать в ответ на эти вопросы, и почему.


Здесь важно очень хорошо понимать основы администрирования. В идеальной ситуации при постановке задачи системному администратору выставляют ряд требований. Если же ситуация не идеальная, то, по сути, требование к администратору одно: «Хочу, чтобы всё работало». Иными словами, сервис должен быть доступен 24/7 и, если какое-то решение не удовлетворяет этим требованиям (масштабирование и отказоустойчивость относятся к доступности), то можно сказать, что администратор плохо сделал свою работу. Но если разные решения двух администраторов работают 24/7, как понять, какое из них лучше?


Хороший системный администратор при выборе решения при заданных требованиях ориентируется на два условия: минимальное потребление ресурсов и их сбалансированное распределение.


Вариант, когда одному специалисту нужно 10 серверов для выполнения задания, а второму всего 2, мы рассматривать не будем, что тут лучше – очевидно. Далее под ресурсами я буду понимать ЦПУ (cpu), ОЗУ (ram) и диск (hdd).


Давайте рассмотрим ситуацию: один администратор создал решение, которое требует 10% cpu, 5% ram и 10% hdd от всего вашего оборудования, а второй использовал для этого 1% cpu, 40% ram и 20% hdd. Какое из этих решений лучше? Тут все становится уже не так очевидно. Поэтому хороший администратор всегда должен уметь грамотно подобрать решение, исходя из имеющихся ресурсов.



Представим, что мы программисты начального уровня, и нас просят написать элементарную программу по работе с сетью. Требования простые: нужно обработать два соединения одновременно по протоколу tcp и записать то, что мы приняли, в файл.


До разработки приложения нужно вспомнить, какие средства нам предоставляет операционная система Linux (далее в статье все примеры только на основе этой ОС). В Linux у нас есть набор системных вызовов (т.е. функций в ядре ОС, которые мы можем вызвать напрямую из нашей программы, тем самым принудительно отдавая процессорное время ядру):


1) socket — выделяет место в буфере ядра ОС под наш сокет. Адрес выделенного места возвращается из функции в программу;
2) bind — позволяет менять информацию в структуре сокета, которую нам выделила ОС linux по команде socket;
3) listen – так же как и bind меняет данные в нашей структуре, позволяя указывать ОС, что мы хотим принимать подключения по этому сокету;
4) connect – говорит нашей ОС, что она должна подключиться к другому удаленному сокету;
5) accept – говорит нашей ОС, что мы хотим принять новое подключение от другого сокета;
6) read – мы просим ОС выдать нам из своего буфера определенное количество байт, которое она получила от удаленного сокета;
7) write – мы просим ОС послать определенное количество байт на удаленный сокет.


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


socket > bind > connect > read/write


Но если вы доверяете ОС сделать выбор исходящего порта за вас (а так же и ip адреса), то bind делать необязательно:


socket > connect > read/write


Для того, чтобы принимать входящие сообщения, нам нужно выполнить:


socket > bind > listen > accept > read/write


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


Программа differ1.c
//Порт, который мы слушаем
#define PORT_NO 2222

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <netinet/in.h>

int main(int argc, char *argv[])
{
        //Буфер, куда мы будем считывать данные из сокета
        long buffersize=50;
        int sockfd, newsockfd;
        socklen_t clilen;
        // Переменная, в которой будет храниться адрес нашего буфера
        char *buffer;
        struct sockaddr_in serv_addr, cli_addr;
        FILE * resultfile;
        // выделяем память
        buffer = malloc (buffersize+1);
        //открываем файл для записи наших сообщений        
        resultfile = fopen("/tmp/nginx_vs_apache.log","a");
        bzero((char *) &serv_addr, sizeof(serv_addr));
        bzero(buffer,buffersize+1);
        serv_addr.sin_family = AF_INET;
        serv_addr.sin_addr.s_addr = INADDR_ANY;
        serv_addr.sin_port = htons(PORT_NO);
        //создаем структуру (сокет), тут SOCK_STREAM это tcp/ip сокет.
        sockfd = socket(AF_INET, SOCK_STREAM, 0);
        if (sockfd < 0) error("ERROR opening socket");
        //определяем структуру нашего сокета, будем слушать порт 2222 на всех ip адресах
        if (bind(sockfd, (struct sockaddr *) &serv_addr, sizeof(serv_addr)) < 0) error("ERROR on binding");
        // говорим нашей ОС, чтобы она принимала входящие коннекты для нашего сокета, максимум 50
        listen(sockfd,50);
        while (1) {
                //в замкнутом цикле обрабатываем входящие подключения и читаем из них
                newsockfd = accept(sockfd, (struct sockaddr *) &cli_addr, &clilen);
                if (newsockfd < 0) error("ERROR on accept");
                read(newsockfd,buffer,buffersize);
                fprintf(resultfile, buffer);
                fflush (resultfile);
        }
        close(sockfd);
        return 0;
}

Компилируем и запускаем наш демон:


[tolik@ localhost]$     
[tolik@localhost]$ ./differ

Смотрим, что получилось:


[root@ localhost]# ps axuf | grep [d]iffer
tolik      45409  0.0  0.0   4060   460 pts/12   S+   01:14   0:00  |   \_ ./differ
[root@localhost ]# netstat -tlnp | grep 2222
tcp        0      0 0.0.0.0:2222                0.0.0.0:*                   LISTEN      45409/./differ
[root@localhost ]# ls -lh /proc/45409/fd
итого 0
lrwx------ 1 tolik tolik 64 Апр 19 01:16 0 -> /dev/pts/12
lrwx------ 1 tolik tolik 64 Апр 19 01:16 1 -> /dev/pts/12
lrwx------ 1 tolik tolik 64 Апр 19 01:16 2 -> /dev/pts/12
l-wx------ 1 tolik tolik 64 Апр 19 01:16 3 -> /tmp/nginx_vs_apache.log
lrwx------ 1 tolik tolik 64 Апр 19 01:16 4 -> socket:[42663416]
[root@localhost ]# netstat -apeen | grep 42663416
tcp        0      0 0.0.0.0:2222                0.0.0.0:*                   LISTEN      500        42663416   45409/./differ
[root@localhost ]# strace -p 45409
Process 45409 attached - interrupt to quit
accept(4, ^C <unfinished ...>
Process 45409 detached
[root@localhost ]#

Процесс находится в состоянии sleep (S+ в команде ps).


Эта программа продолжит выполняться (получит процессорное время) только при появлении нового коннекта на порт 2222. Во всех остальных случаях программа никогда не получит процессорное время: она даже не будет требовать его от ОС и, следовательно, не будет влиять на load avarage (далее LA), потребляя только память.


С другой консоли запускаем первого клиента:


[tolik@localhost ]$ telnet localhost 2222
Connected to localhost.
Escape character is '^]'.
test client 1

Смотрим файл:


[root@localhost ]# cat /tmp/nginx_vs_apache.log
test client 1

Открываем второе соединение:


[tolik@localhost ]$ telnet localhost 2222
Connected to localhost.
Escape character is '^]'.
test client 2

Смотрим результат:


[root@localhost ]# cat /tmp/nginx_vs_apache.log
test client 1

По содержимому файла видно, что пришло только первое сообщение от первого клиента. Но при этом второе сообщение мы уже отправили, и оно где-то находится. Все сетевые подключения осуществляет ОС, значит и сообщение test client 2 сейчас в буфере операционной системы, в памяти, которая нам недоступна. Единственный способ забрать эти данные – обработать новое соединение командой accept, затем вызвать read.


Попробуем что-нибудь написать в первом клиенте:


[tolik@localhost ]$ telnet localhost 2222
Connected to localhost.
Escape character is '^]'.
test client 1
blablabla

Проверяем лог:


[root@localhost ]# cat /tmp/nginx_vs_apache.log
test client 1

Новое сообщение не попало в лог. Это происходит из-за того, что мы вызываем команду read только один раз, следовательно, в лог попадает только первое сообщение.


Попробуем закрыть наше первое соединение:


[tolik@localhost ]$ telnet localhost 2222
Connected to localhost.
Escape character is '^]'.
test client 1
bla bla bla
^]
telnet> quit
Connection closed.

В этот момент наша программа запускает по циклу следующий accept и read, следовательно, принимает сообщение из второго соединения:


[root@localhost ]# cat /tmp/nginx_vs_apache.log
test client 1
test client 2

Наше сообщение bla bla bla нигде не появилось, мы уже закрыли сокет, и ОС очистила буфер, тем самым удалив наши данные. Нужно модернизировать программу — читать из сокета до тех пор, пока оттуда поступает информация.


Программа с бесконечным чтением из сокета differ2.c
#define PORT_NO 2222

#include <stdio.h>
#include <string.h>
#include <netinet/in.h>

int main(int argc, char *argv[])
{
        int sockfd, newsockfd;
        socklen_t clilen;
        char buffer;
        char * pointbuffer = &buffer;
        struct sockaddr_in serv_addr, cli_addr;
        FILE * resultfile;
        resultfile = fopen("/tmp/nginx_vs_apache.log","a");
        bzero((char *) &serv_addr, sizeof(serv_addr));
        serv_addr.sin_family = AF_INET;
        serv_addr.sin_addr.s_addr = INADDR_ANY;
        serv_addr.sin_port = htons(PORT_NO);
        sockfd = socket(AF_INET, SOCK_STREAM, 0);
        if (sockfd < 0) error("ERROR opening socket");
        if (bind(sockfd, (struct sockaddr *) &serv_addr, sizeof(serv_addr)) < 0) error("ERROR on binding");
        listen(sockfd,50);
        while (1) {
                newsockfd = accept(sockfd, (struct sockaddr *) &cli_addr, &clilen);
                if (newsockfd < 0) error("ERROR on accept");
                while (read(newsockfd, pointbuffer,1)) {
                                fprintf(resultfile, pointbuffer);
                                fflush (resultfile);
                }
        }
        close(sockfd);
        return 0;
}

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


Очищаем файл:


[root@localhost ]# > /tmp/nginx_vs_apache.log

Компилируем и запускаем:


[tolik@localhost ]$ gcc -o differ differ2.c
[tolik@localhost ]$ ./differ

Первый клиент:


[tolik@localhost ]$ telnet localhost 2222
Connected to localhost.
Escape character is '^]'.
client test 1
yoyoyo

Второй клиент:


[tolik@localhost ]$ telnet localhost 2222
Connected to localhost.
Escape character is '^]'.
client test 2
yooyoy

Проверяем, что получилось:


[root@localhost ]# cat /tmp/nginx_vs_apache.log
client test 1
yoyoyo

На этот раз все хорошо, мы забрали все данные, но проблема осталась: два соединения обрабатываются последовательно, по очереди, а это не подходит под наши требования. Если мы закроем первое соединение (ctrl + ]), то данные из второго соединения попадут сразу в лог:


[root@localhost ]# cat /tmp/nginx_vs_apache.log
client test 1
yoyoyo
client test 2
yooyoy

Данные пришли. Но как обработать два соединения параллельно? Тут нам на помощь приходит команда fork. Что делает системный вызов fork в linux? Правильный ответ на этот вопрос на любом собеседовании – ничего. Fork – устаревший вызов, и в linux присутствует только для обратной совместимости. На самом деле, вызывая команду fork, вы вызываете системный вызов clone. Функция clone создает копию процесса и ставит оба процесса в очередь на процессор. Разница между ними в том, что fork копирует данные (переменные, буферы и т.п.) сразу в область памяти дочернего процесса, а clone копирует данные в дочерний процесс только при попытке их изменить (смотрите ограничения прав доступа к памяти в MMU). То есть, если вы вызываете fork 10 раз, а данные используете только для чтения, то вы получите 10 одинаковых копий данных в памяти. И это явно не то, что вам нужно, особенно в мультитредовых приложениях. Clone запускает копию вашего приложения, но не копирует данные сразу. Если вы запустите clone 10 раз, то у вас будет 10 исполняемых процессов с одним блоком памяти, и память будет копироваться только при попытке ее изменить дочерним процессом. Согласитесь, второй алгоритм намного эффективней.


Программа c fork differ3.c
#define PORT_NO 2222

#include <stdio.h>
#include <string.h>
#include <netinet/in.h>

int main(int argc, char *argv[])
{
        int sockfd, newsockfd;
        socklen_t clilen;
        char buffer;
        char * pointbuffer = &buffer;
        struct sockaddr_in serv_addr, cli_addr;
        FILE * resultfile;
        int pid=1;
        resultfile = fopen("/tmp/nginx_vs_apache.log","a");
        bzero((char *) &serv_addr, sizeof(serv_addr));
        serv_addr.sin_family = AF_INET;
        serv_addr.sin_addr.s_addr = INADDR_ANY;
        serv_addr.sin_port = htons(PORT_NO);
        sockfd = socket(AF_INET, SOCK_STREAM, 0);
        if (sockfd < 0) error("ERROR opening socket");
        if (bind(sockfd, (struct sockaddr *) &serv_addr, sizeof(serv_addr)) < 0) error("ERROR on binding");
        listen(sockfd,50);
        while (pid!=0) {
                newsockfd = accept(sockfd, (struct sockaddr *) &cli_addr, &clilen);
                if (newsockfd < 0) error("ERROR on accept");
                pid=fork();
                if (pid!=0) {
                        close(newsockfd);
                        fprintf(resultfile,"New process was started with pid=%d\n",pid);
                        fflush (resultfile);
                }
        }
        while (read(newsockfd, pointbuffer,1)) {
                        fprintf(resultfile, pointbuffer);
                        fflush (resultfile);
        }
        close(sockfd);
        return 0;
}

В этой программе все то же самое — мы делаем accept, принимаем новое соединение. Далее мы запускаем fork. И если это мастер процесс (fork вернул pid созданного процесса), то мы закрываем текущее соединение в родительском процессе (оно доступно и в родителе, и в дочернем процессе). Если это дочерний процесс (fork вернул 0), то мы начинаем делать read с открытого сокета, который мы открыли командой accept в родительском процессе. По факту получается, что родительский процесс у нас только принимает соединения, а read/write мы делаем в дочерних процессах.


Компилируем и запускаем:


[tolik@localhost ]$ gcc -o differ differ3.c
[tolik@localhost ]$ ./differ

Очищаем наш лог файл:


[root@localhost ]# > /tmp/nginx_vs_apache.log

Смотрим процессы:


[root@localhost ]# ps axuf | grep [d]iffer
tolik      45643  0.0  0.0   4060   460 pts/12   S+   01:40   0:00  |   \_ ./differ

Клиент1:


[tolik@localhost ]$ telnet localhost 2222
Connected to localhost.
Escape character is '^]'.
client 1 test
megatest

Клиент2:


[tolik@localhost ]$ telnet localhost 2222
Connected to localhost.
Escape character is '^]'.
client2 test
yoyoyoy

Смотрим процессы:


[root@localhost ]# ps axuf | grep [d]iffer
tolik      45643  0.0  0.0   4060   504 pts/12   S+   01:40   0:00  |   \_ ./differ
tolik      45663  0.0  0.0   4060   156 pts/12   S+   01:41   0:00  |       \_ ./differ
tolik      45665  0.0  0.0   4060   160 pts/12   S+   01:41   0:00  |       \_ ./differ

Мы не закрываем оба соединения и можем туда еще что-то дописывать, смотрим наш лог:


[root@localhost ]# cat /tmp/nginx_vs_apache.log
New process was started with pid=44163
New process was started with pid=44165
client 1 test
megatest
client2 test
yoyoyoy

Два соединения обрабатываются одновременно — мы получили желаемый результат.


Программа работает, но недостаточно быстро. Она сначала принимает соединение, а только потом запускает команду fork, и соединение обрабатывает только один процесс. Возникает вопрос: могут ли несколько процессов в ОС Linux работать с одним и тем же tcp портом? Пробуем.


Программа c pre fork differ_prefork.c
#define PORT_NO 2222

#include <stdio.h>
#include <string.h>
#include <netinet/in.h>

int main(int argc, char *argv[])
{
        int sockfd, newsockfd, startservers, count ;
        socklen_t clilen;
        char buffer;
        char * pointbuffer = &buffer;
        struct sockaddr_in serv_addr, cli_addr;
        FILE * resultfile;
        int pid=1;
        resultfile = fopen("/tmp/nginx_vs_apache.log","a");
        bzero((char *) &serv_addr, sizeof(serv_addr));
        serv_addr.sin_family = AF_INET;
        serv_addr.sin_addr.s_addr = INADDR_ANY;
        serv_addr.sin_port = htons(PORT_NO);
        sockfd = socket(AF_INET, SOCK_STREAM, 0);
        if (sockfd < 0) error("ERROR opening socket");
        if (bind(sockfd, (struct sockaddr *) &serv_addr, sizeof(serv_addr)) < 0) error("ERROR on binding");
        listen(sockfd,50);
        startservers=2;
        count = 0;
        while (pid!=0) {
                if (count < startservers)
                {
                        pid=fork();
                                if (pid!=0) {
                                close(newsockfd);
                                fprintf(resultfile,"New process was started with pid=%d\n",pid);
                                fflush (resultfile);
                        }
                count = count + 1;
                }
                //sleep (1);
        }
        newsockfd = accept(sockfd, (struct sockaddr *) &cli_addr, &clilen);
        if (newsockfd < 0) error("ERROR on accept");
        while (read(newsockfd, pointbuffer,1)) {
                        fprintf(resultfile, pointbuffer);
                        fflush (resultfile);
        }
        close(sockfd);
        return 0;
}

Как видите, программа все еще не сильно изменилась, мы просто запускаем fork по циклу. В данном случае мы создаем два дочерних процесса, а только потом в каждом из них делаем accept на прием нового соединения. Проверяем.


Компилируем и запускаем:


[tolik@localhost ]$ gcc -o differ differ_prefork.c
[tolik@localhost ]$ ./differ

Смотрим, что у нас в процессах:


[root@localhost ]# ps axuf | grep [d]iffer
tolik      44194 98.0  0.0   4060   504 pts/12   R+   23:35   0:07  |   \_ ./differ
tolik      44195  0.0  0.0   4060   152 pts/12   S+   23:35   0:00  |       \_ ./differ
tolik      44196  0.0  0.0   4060   156 pts/12   S+   23:35   0:00  |       \_ ./differ

Мы еще не подключились ни одним клиентом, а программа уже два раза сделала fork. Что же сейчас происходит с системой? Для начала мастер процесс: он находится в замкнутом цикле и проверяет, надо ли форкать еще процессы. Если мы будем делать это без остановки, то, по сути, будем постоянно требовать от ОС процессорное время, так как наш цикл должен исполняться всегда. Это значит, что мы потребляем 100% одного ядра – в команде ps значение 98.0%. Это же можно увидеть в команде top:


[root@localhost ]# top -n 1 | head
top - 23:39:22 up 141 days, 21 min,  8 users,  load average: 1.03, 0.59, 0.23
Tasks: 195 total,   2 running, 193 sleeping,   0 stopped,   0 zombie
Cpu(s):  0.3%us,  0.2%sy,  0.0%ni, 99.3%id,  0.2%wa,  0.0%hi,  0.0%si,  0.0%st
Mem:   1896936k total,  1876280k used,    20656k free,   151208k buffers
Swap:  4194296k total,   107600k used,  4086696k free,  1003568k cached

    PID USER      PR  NI  VIRT  RES  SHR S %CPU %MEM    TIME+  COMMAND
  44194 tolik     20   0  4060  504  420 R 98.9  0.0   4:10.54 differ
  44255 root      20   0 15028 1256  884 R  3.8  0.1   0:00.03 top
      1 root      20   0 19232  548  380 S  0.0  0.0   2:17.17 init

Если мы подключимся командой strace к родителю, то ничего не увидим, так как наш процесс не вызывает никакие функции ядра:


[root@localhost ]# strace -p 44194
Process 44194 attached - interrupt to quit
^CProcess 44194 detached
[root@localhost ]#

Что делают дочерние процессы? Тут начинается самое интересное. Судя по коду, все они после форка должны висеть в состоянии accept и ожидать новых соединений с одного и того же порта, в нашем случае 2222. Проверяем:


[root@localhost ]# strace -p 44195
Process 44195 attached - interrupt to quit
accept(4, ^C <unfinished ...>
Process 44195 detached
[root@localhost ]# strace -p 44196
Process 44196 attached - interrupt to quit
accept(4, ^C <unfinished ...>
Process 44196 detached

На данный момент они не требуют от ОС процессорного времени и потребляют только память. Но вот в чем вопрос: кто из них примет мое соединение, если я сделаю telnet? Проверяем:


[tolik@localhost ]$ telnet localhost 2222
Connected to localhost.
Escape character is '^]'.
client 1 test
hhh

[root@localhost ]# strace -p 44459
Process 44459 attached - interrupt to quit
read(5, ^C <unfinished ...>
Process 44459 detached
[root@localhost ]# strace -p 44460
Process 44460 attached - interrupt to quit
accept(4, ^C <unfinished ...>
Process 44460 detached

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


Остается последний вопрос: что нам делать с родительским процессом, чтобы он не потреблял столько cpu и при этом продолжал работать? Нам нужно отдать время другим процессам в добровольном порядке, то есть «сказать» нашей ОС, что какое-то время cpu нам не нужно. Для этой задачи подойдет команда sleep 1: если вы ее раскомментируете, то увидите в strace примерно такую, повторяющуюся раз в секунду, картину:


[root@localhost ]# strace -p 44601
…..
rt_sigprocmask(SIG_BLOCK, [CHLD], [], 8) = 0
rt_sigaction(SIGCHLD, NULL, {SIG_DFL, [], 0}, 8) = 0
rt_sigprocmask(SIG_SETMASK, [], NULL, 8) = 0
nanosleep({1, 0}, 0x7fff60a15aa0)       = 0
….
rt_sigprocmask(SIG_BLOCK, [CHLD], [], 8) = 0
rt_sigaction(SIGCHLD, NULL, {SIG_DFL, [], 0}, 8) = 0
rt_sigprocmask(SIG_SETMASK, [], NULL, 8) = 0
nanosleep({1, 0}, 0x7fff60a15aa0)        = 0
…

и т.д.


Наш процесс будет получать процессор примерно раз в секунду или, по крайней мере, требовать его от ОС.


Если вы всё еще не понимаете, к чему эта длинная статья, то посмотрите на apache httpd работающий в режиме prefork:


[root@www /]# ps axuf | grep [h]ttpd
root     12730  0.0  0.5 271560 11916 ?        Ss   Feb25   3:14 /usr/sbin/httpd
apache   19832  0.0  0.3 271692  7200 ?        S    Apr17   0:00  \_ /usr/sbin/httpd
apache   19833  0.0  0.3 271692  7212 ?        S    Apr17   0:00  \_ /usr/sbin/httpd
apache   19834  0.0  0.3 271692  7204 ?        S    Apr17   0:00  \_ /usr/sbin/httpd
apache   19835  0.0  0.3 271692  7200 ?        S    Apr17   0:00  \_ /usr/sbin/httpd

Дочерние процессы в accept:


[root@www /]# strace -p 19832
Process 19832 attached
accept4(3, ^CProcess 19832 detached
 <detached ...>
[root@www /]# strace -p 19833
Process 19833 attached
accept4(3, ^CProcess 19833 detached
 <detached ...>

Мастер процесс с секундной паузой:


[root@www /]# strace -p 12730
Process 12730 attached
select(0, NULL, NULL, NULL, {0, 629715}) = 0 (Timeout)
wait4(-1, 0x7fff4c9e3fbc, WNOHANG|WSTOPPED, NULL) = 0
select(0, NULL, NULL, NULL, {1, 0})     = 0 (Timeout)
wait4(-1, 0x7fff4c9e3fbc, WNOHANG|WSTOPPED, NULL) = 0
select(0, NULL, NULL, NULL, {1, 0})     = 0 (Timeout)
wait4(-1, 0x7fff4c9e3fbc, WNOHANG|WSTOPPED, NULL) = 0
…

При старте httpd мастер процесс плодит дочерние процессы, это легко увидеть если запустить strace на мастер процесс в момент старта:
Запустим веб сервер с такими настройками:


StartServers       1
MinSpareServers    9
MaxSpareServers   10
ServerLimit       10
MaxClients        10
MaxRequestsPerChild  1

Эти настройки говорят о том, что каждый дочерний процесс будет обрабатывать только один запрос, затем процесс будет убиваться. Минимальное количество процессов в accept равно 9 и максимальное равно 10.


Если запустить strace на мастер процесс в момент старта, то мы увидим как мастер вызывает clone до тех пор, пока не достигнет MinSpareServers.


Трассировка

rt_sigaction(SIGSEGV, {0x7f9991933c20, [], SA_RESTORER|SA_RESETHAND, 0x7f99901dd500}, NULL, 8) = 0
rt_sigaction(SIGBUS, {0x7f9991933c20, [], SA_RESTORER|SA_RESETHAND, 0x7f99901dd500}, NULL, 8) = 0
rt_sigaction(SIGABRT, {0x7f9991933c20, [], SA_RESTORER|SA_RESETHAND, 0x7f99901dd500}, NULL, 8) = 0
rt_sigaction(SIGILL, {0x7f9991933c20, [], SA_RESTORER|SA_RESETHAND, 0x7f99901dd500}, NULL, 8) = 0
rt_sigaction(SIGFPE, {0x7f9991933c20, [], SA_RESTORER|SA_RESETHAND, 0x7f99901dd500}, NULL, 8) = 0
rt_sigaction(SIGTERM, {0x7f999193de50, [], SA_RESTORER, 0x7f99901dd500}, NULL, 8) = 0
rt_sigaction(SIGWINCH, {0x7f999193de50, [], SA_RESTORER, 0x7f99901dd500}, NULL, 8) = 0
rt_sigaction(SIGINT, {0x7f999193de50, [], SA_RESTORER, 0x7f99901dd500}, NULL, 8) = 0
rt_sigaction(SIGXCPU, {SIG_DFL, [], SA_RESTORER, 0x7f99901dd500}, NULL, 8) = 0
rt_sigaction(SIGXFSZ, {SIG_IGN, [], SA_RESTORER, 0x7f99901dd500}, NULL, 8) = 0
rt_sigaction(SIGPIPE, {SIG_IGN, [], SA_RESTORER, 0x7f99901dd500}, NULL, 8) = 0
rt_sigaction(SIGHUP, {0x7f999193de80, [HUP USR1], SA_RESTORER, 0x7f99901dd500}, NULL, 8) = 0
rt_sigaction(SIGUSR1, {0x7f999193de80, [HUP USR1], SA_RESTORER, 0x7f99901dd500}, NULL, 8) = 0
clone(child_stack=0, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, child_tidptr=0x7f99918eeab0) = 13098
write(2, "[Wed Jan 25 13:24:39 2017] [noti"..., 114) = 114
wait4(-1, 0x7fffae295fdc, WNOHANG|WSTOPPED, NULL) = 0
select(0, NULL, NULL, NULL, {1, 0}) = 0 (Timeout)
clone(child_stack=0, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, child_tidptr=0x7f99918eeab0) = 13099
wait4(-1, 0x7fffae295fdc, WNOHANG|WSTOPPED, NULL) = 0
select(0, NULL, NULL, NULL, {1, 0}) = 0 (Timeout)
clone(child_stack=0, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, child_tidptr=0x7f99918eeab0) = 13100
clone(child_stack=0, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, child_tidptr=0x7f99918eeab0) = 13101
wait4(-1, 0x7fffae295fdc, WNOHANG|WSTOPPED, NULL) = 0
select(0, NULL, NULL, NULL, {1, 0}) = 0 (Timeout)
clone(child_stack=0, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, child_tidptr=0x7f99918eeab0) = 13102
clone(child_stack=0, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, child_tidptr=0x7f99918eeab0) = 13103
clone(child_stack=0, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, child_tidptr=0x7f99918eeab0) = 13104
clone(child_stack=0, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, child_tidptr=0x7f99918eeab0) = 13105
wait4(-1, 0x7fffae295fdc, WNOHANG|WSTOPPED, NULL) = 0
select(0, NULL, NULL, NULL, {1, 0}) = 0 (Timeout)
clone(child_stack=0, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, child_tidptr=0x7f99918eeab0) = 13106
clone(child_stack=0, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, child_tidptr=0x7f99918eeab0) = 13107
wait4(-1, 0x7fffae295fdc, WNOHANG|WSTOPPED, NULL) = 0
select(0, NULL, NULL, NULL, {1, 0}) = 0 (Timeout)


Смотрим как стартует апач – для этого можно просто смотреть ps axuf | grep [h]ttp каждую секунду, сразу после старта.


Старт апача

[root@www /]# date; ps axuf | grep [h]ttp
Wed Jan 25 14:12:10 EST 2017
root 13342 2.5 0.4 271084 9384? Ss 14:12 0:00 /usr/sbin/httpd
apache 13344 0.0 0.2 271084 5232? S 14:12 0:00 _ /usr/sbin/httpd


[root@www /]# date; ps axuf | grep [h]ttp
Wed Jan 25 14:12:11 EST 2017
root 13342 1.6 0.4 271084 9384? Ss 14:12 0:00 /usr/sbin/httpd
apache 13344 0.0 0.2 271084 5232? S 14:12 0:00 _ /usr/sbin/httpd
apache 13348 0.0 0.2 271084 5232? S 14:12 0:00 _ /usr/sbin/httpd


[root@www /]# date; ps axuf | grep [h]ttp
Wed Jan 25 14:12:11 EST 2017
root 13342 2.0 0.4 271084 9384? Ss 14:12 0:00 /usr/sbin/httpd
apache 13344 0.0 0.2 271084 5232? S 14:12 0:00 _ /usr/sbin/httpd
apache 13348 0.0 0.2 271084 5232? S 14:12 0:00 _ /usr/sbin/httpd
apache 13352 0.0 0.2 271084 5232? S 14:12 0:00 _ /usr/sbin/httpd
apache 13353 0.0 0.2 271084 5232? S 14:12 0:00 _ /usr/sbin/httpd


[root@www /]# date; ps axuf | grep [h]ttp
Wed Jan 25 14:12:12 EST 2017
root 13342 1.7 0.4 271084 9384? Ss 14:12 0:00 /usr/sbin/httpd
apache 13344 0.0 0.2 271084 5232? S 14:12 0:00 _ /usr/sbin/httpd
apache 13348 0.0 0.2 271084 5232? S 14:12 0:00 _ /usr/sbin/httpd
apache 13352 0.0 0.2 271084 5232? S 14:12 0:00 _ /usr/sbin/httpd
apache 13353 0.0 0.2 271084 5232? S 14:12 0:00 _ /usr/sbin/httpd
apache 13357 0.0 0.2 271084 5232? S 14:12 0:00 _ /usr/sbin/httpd
apache 13358 0.0 0.2 271084 5232? S 14:12 0:00 _ /usr/sbin/httpd
apache 13359 0.0 0.2 271084 5232? S 14:12 0:00 _ /usr/sbin/httpd
apache 13360 0.0 0.2 271084 5232? S 14:12 0:00 _ /usr/sbin/httpd


[root@www /]# date; ps axuf | grep [h]ttp
Wed Jan 25 14:12:13 EST 2017
root 13342 1.4 0.4 271084 9384? Ss 14:12 0:00 /usr/sbin/httpd
apache 13344 0.0 0.2 271084 5232? S 14:12 0:00 _ /usr/sbin/httpd
apache 13348 0.0 0.2 271084 5232? S 14:12 0:00 _ /usr/sbin/httpd
apache 13352 0.0 0.2 271084 5232? S 14:12 0:00 _ /usr/sbin/httpd
apache 13353 0.0 0.2 271084 5232? S 14:12 0:00 _ /usr/sbin/httpd
apache 13357 0.0 0.2 271084 5232? S 14:12 0:00 _ /usr/sbin/httpd
apache 13358 0.0 0.2 271084 5232? S 14:12 0:00 _ /usr/sbin/httpd
apache 13359 0.0 0.2 271084 5232? S 14:12 0:00 _ /usr/sbin/httpd
apache 13360 0.0 0.2 271084 5232? S 14:12 0:00 _ /usr/sbin/httpd
apache 13364 0.0 0.2 271084 5232? S 14:12 0:00 _ /usr/sbin/httpd
apache 13365 0.0 0.2 271084 5232? S 14:12 0:00 _ /usr/sbin/httpd
[root@www /]#


Итак, у нас несколько дочерних процессов, которые готовы принять наш http запрос. Давайте попробуем послать запрос:


[root@www /]# wget -O /dev/null http://localhost
--2017-01-25 14:04:00--  http://localhost/
Resolving localhost... ::1, 127.0.0.1
Connecting to localhost|::1|:80... failed: Connection refused.
Connecting to localhost|127.0.0.1|:80... connected.
HTTP request sent, awaiting response... 403 Forbidden
2017-01-25 14:04:00 ERROR 403: Forbidden.

Апач нам ответил 403, смотрим процессы:


root     13342  0.0  0.4 271084  9384 ?        Ss   14:12   0:00 /usr/sbin/httpd
apache   13348  0.0  0.2 271084  5232 ?        S    14:12   0:00  \_ /usr/sbin/httpd
apache   13352  0.0  0.2 271084  5232 ?        S    14:12   0:00  \_ /usr/sbin/httpd
apache   13353  0.0  0.2 271084  5232 ?        S    14:12   0:00  \_ /usr/sbin/httpd
apache   13357  0.0  0.2 271084  5232 ?        S    14:12   0:00  \_ /usr/sbin/httpd
apache   13358  0.0  0.2 271084  5232 ?        S    14:12   0:00  \_ /usr/sbin/httpd
apache   13359  0.0  0.2 271084  5232 ?        S    14:12   0:00  \_ /usr/sbin/httpd
apache   13360  0.0  0.2 271084  5232 ?        S    14:12   0:00  \_ /usr/sbin/httpd
apache   13364  0.0  0.2 271084  5232 ?        S    14:12   0:00  \_ /usr/sbin/httpd
apache   13365  0.0  0.2 271084  5232 ?        S    14:12   0:00  \_ /usr/sbin/httpd

Как видим, процесс с минимальным pid обработал запрос и завершил свою работу:


apache   13344  0.0  0.2 271084  5232 ?        S    14:12   0:00  \_ /usr/sbin/httpd

Дочерних процессов у нас осталось 9, что вписывается в наш лимит MinSpareServers.


Пробуем опять отправить запрос:


[root@www /]# wget -O /dev/null http://localhost
--2017-01-25 14:15:47--  http://localhost/
Resolving localhost... ::1, 127.0.0.1
Connecting to localhost|::1|:80... failed: Connection refused.
Connecting to localhost|127.0.0.1|:80... connected.
HTTP request sent, awaiting response... 403 Forbidden
2017-01-25 14:15:47 ERROR 403: Forbidden.
[root@www /]# ps axuf | grep [h]ttp
root     13342  0.0  0.4 271084  9384 ?        Ss   14:12   0:00 /usr/sbin/httpd
apache   13352  0.0  0.2 271084  5232 ?        S    14:12   0:00  \_ /usr/sbin/httpd
apache   13353  0.0  0.2 271084  5232 ?        S    14:12   0:00  \_ /usr/sbin/httpd
apache   13357  0.0  0.2 271084  5232 ?        S    14:12   0:00  \_ /usr/sbin/httpd
apache   13358  0.0  0.2 271084  5232 ?        S    14:12   0:00  \_ /usr/sbin/httpd
apache   13359  0.0  0.2 271084  5232 ?        S    14:12   0:00  \_ /usr/sbin/httpd
apache   13360  0.0  0.2 271084  5232 ?        S    14:12   0:00  \_ /usr/sbin/httpd
apache   13364  0.0  0.2 271084  5232 ?        S    14:12   0:00  \_ /usr/sbin/httpd
apache   13365  0.0  0.2 271084  5232 ?        S    14:12   0:00  \_ /usr/sbin/httpd
apache   13373  0.0  0.2 271084  5232 ?        S    14:15   0:00  \_ /usr/sbin/httpd

На этот раз наш запрос обработал процесс


apache   13348  0.0  0.2 271084  5232 ?        S    14:12   0:00  \_ /usr/sbin/httpd

так как теперь у него минимальный pid.


Но у нас осталось 8 свободных дочерних процессов в accept, одного не хватает до MinSpareServers, поэтому мастер процесс нам создал новый процесс:


apache   13373  0.0  0.2 271084  5232 ?        S    14:15   0:00  \_ /usr/sbin/httpd

Давайте скажем нашей ОС, чтобы она не давала процессорное время мастер процессу апача:


[root@www /]# kill -SIGSTOP 13342

Смотрим:


[root@www /]# ps axuf | grep [h]ttp | grep ^root
root     13342  0.0  0.4 271084  9384 ?        Ts   14:12   0:00 /usr/sbin/httpd

Статус процесса изменился, теперь он не работает.


Проверяем, работает ли у нас веб сервер:


[root@www /]# wget -O /dev/null http://localhost
--2017-01-25 14:20:12--  http://localhost/
Resolving localhost... ::1, 127.0.0.1
Connecting to localhost|::1|:80... failed: Connection refused.
Connecting to localhost|127.0.0.1|:80... connected.
HTTP request sent, awaiting response... 403 Forbidden
2017-01-25 14:20:12 ERROR 403: Forbidden.

О да, все еще работает, веб сервер еще отвечает.


Смотрим что у нас с процессами:


root     13342  0.0  0.4 271084  9384 ?        Ts   14:12   0:00 /usr/sbin/httpd
apache   13352  0.0  0.0      0     0 ?        Z    14:12   0:00  \_ [httpd] <defunct>
apache   13353  0.0  0.2 271084  5232 ?        S    14:12   0:00  \_ /usr/sbin/httpd
apache   13357  0.0  0.2 271084  5232 ?        S    14:12   0:00  \_ /usr/sbin/httpd
apache   13358  0.0  0.2 271084  5232 ?        S    14:12   0:00  \_ /usr/sbin/httpd
apache   13359  0.0  0.2 271084  5232 ?        S    14:12   0:00  \_ /usr/sbin/httpd
apache   13360  0.0  0.2 271084  5232 ?        S    14:12   0:00  \_ /usr/sbin/httpd
apache   13364  0.0  0.2 271084  5232 ?        S    14:12   0:00  \_ /usr/sbin/httpd
apache   13365  0.0  0.2 271084  5232 ?        S    14:12   0:00  \_ /usr/sbin/httpd
apache   13373  0.0  0.2 271084  5232 ?        S    14:15   0:00  \_ /usr/sbin/httpd

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


apache   13352  0.0  0.0      0     0 ?        Z    14:12   0:00  \_ [httpd] <defunct>

Естественно дочерних процессов у нас осталось 8, так как новый 9й плодить некому, мастер остановлен.


Давайте для эксперимента отправим еще один http запрос:


[root@www /]# wget -O /dev/null http://localhost
--2017-01-25 14:25:03--  http://localhost/
Resolving localhost... ::1, 127.0.0.1
Connecting to localhost|::1|:80... failed: Connection refused.
Connecting to localhost|127.0.0.1|:80... connected.
HTTP request sent, awaiting response... 403 Forbidden
2017-01-25 14:25:03 ERROR 403: Forbidden.
[root@www /]# ps axuf | grep [h]ttp
root     13342  0.0  0.4 271084  9384 ?        Ts   14:12   0:00 /usr/sbin/httpd
apache   13352  0.0  0.0      0     0 ?        Z    14:12   0:00  \_ [httpd] <defunct>
apache   13353  0.0  0.0      0     0 ?        Z    14:12   0:00  \_ [httpd] <defunct>
apache   13357  0.0  0.2 271084  5232 ?        S    14:12   0:00  \_ /usr/sbin/httpd
apache   13358  0.0  0.2 271084  5232 ?        S    14:12   0:00  \_ /usr/sbin/httpd
apache   13359  0.0  0.2 271084  5232 ?        S    14:12   0:00  \_ /usr/sbin/httpd
apache   13360  0.0  0.2 271084  5232 ?        S    14:12   0:00  \_ /usr/sbin/httpd
apache   13364  0.0  0.2 271084  5232 ?        S    14:12   0:00  \_ /usr/sbin/httpd
apache   13365  0.0  0.2 271084  5232 ?        S    14:12   0:00  \_ /usr/sbin/httpd
apache   13373  0.0  0.2 271084  5232 ?        S    14:15   0:00  \_ /usr/sbin/httpd

Логично, что ситуация повторяется.


Давайте скажем нашей ОС, что мастер процесс может снова продолжить работу:


[root@www /]# kill -SIGCONT 13342
[root@www /]# ps axuf | grep [h]ttp
root     13342  0.0  0.4 271084  9384 ?        Ss   14:12   0:00 /usr/sbin/httpd
apache   13357  0.0  0.2 271084  5232 ?        S    14:12   0:00  \_ /usr/sbin/httpd
apache   13358  0.0  0.2 271084  5232 ?        S    14:12   0:00  \_ /usr/sbin/httpd
apache   13359  0.0  0.2 271084  5232 ?        S    14:12   0:00  \_ /usr/sbin/httpd
apache   13360  0.0  0.2 271084  5232 ?        S    14:12   0:00  \_ /usr/sbin/httpd
apache   13364  0.0  0.2 271084  5232 ?        S    14:12   0:00  \_ /usr/sbin/httpd
apache   13365  0.0  0.2 271084  5232 ?        S    14:12   0:00  \_ /usr/sbin/httpd
apache   13373  0.0  0.2 271084  5232 ?        S    14:15   0:00  \_ /usr/sbin/httpd
apache   13388  0.0  0.2 271084  5232 ?        S    14:26   0:00  \_ /usr/sbin/httpd
apache   13389  0.0  0.2 271084  5232 ?        S    14:26   0:00  \_ /usr/sbin/httpd
apache   13390  0.0  0.2 271084  5232 ?        S    14:26   0:00  \_ /usr/sbin/httpd

Мастер процесс тут же считал exit code дочерних процессов, и упоминания о них ушли из таблицы процессов, а недостающие процессы мастер процесс нам снова склонировал — теперь у нас 10 свободных процессов в accept, что уместилось в рамки наших переменных из конфигов.


Как устроен nginx? Как вы уже поняли, системный вызов accept блокирует выполнение нашей программы до тех пор, пока не придет новое соединение. Получается, что мы не можем ожидать новое соединение и обрабатывать уже открытое соединение в одном процессе. Или?


Взглянем на код:


Код с select
#define PORT 2222
#include <stdio.h>
#include <string.h>   
#include <stdlib.h>
#include <errno.h>
#include <unistd.h>   
#include <arpa/inet.h>    
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <sys/time.h>

int main(int argc , char *argv[])
{
        int opt = 1;
        int master_socket , addrlen , new_socket , client_socket[30] , max_clients = 30 , activity, i , valread , sd;
        int max_sd;
        FILE * resultfile;
        struct sockaddr_in address;
        char buffer[50];
        fd_set readfds;
        resultfile = fopen("/tmp/nginx_vs_apache.log","a");
      //Заполняем наш массив сокетов нулями
        for (i = 0; i < max_clients; i++)  client_socket[i] = 0;
        if( (master_socket = socket(AF_INET , SOCK_STREAM , 0)) == 0)  error("socket failed");
        address.sin_family = AF_INET;
        address.sin_addr.s_addr = INADDR_ANY;
        address.sin_port = htons( PORT );
        if (bind(master_socket, (struct sockaddr *)&address, sizeof(address))<0) error("bind failed");
        if (listen(master_socket, 3) < 0) error("listen");
        addrlen = sizeof(address);
        while(1) //В бесконечном цикле обрабатываем запросы
        {
            FD_ZERO(&readfds);
            FD_SET(master_socket, &readfds);
            max_sd = master_socket;
            for ( i = 0 ; i < max_clients ; i++)
            {
                sd = client_socket[i];
                if(sd > 0) FD_SET( sd , &readfds);
                if(sd > max_sd) max_sd = sd;
            }
            //Ждем событий на любом из интересующих нас сокетов
            activity = select( max_sd + 1 , &readfds , NULL , NULL , NULL);
            if ((activity < 0) && (errno!=EINTR))  printf("select error");
            //Обработка нового соединения
            if (FD_ISSET(master_socket, &readfds))
            {
                if ((new_socket = accept(master_socket, (struct sockaddr *)&address, (socklen_t*)&addrlen))<0) error("accept");
                for (i = 0; i < max_clients; i++)
                    if( client_socket[i] == 0 ) { client_socket[i] = new_socket; break; }
            }

            //Читаем данные из каждого сокета, так как не знаем какие события заставил ОС дать нам CPU
            for (i = 0; i < max_clients; i++)
            {
                sd = client_socket[i];
                if (FD_ISSET( sd , &readfds))
                {
                    if ((valread = read( sd , buffer, 1024)) == 0) { close( sd ); client_socket[i] = 0; }
                    else
                    {
                        buffer[valread] = '\0';
                        fprintf(resultfile, buffer);
                        fflush (resultfile);
                    }
                }
            }
        }

        return 0;
}

Этот код выглядит чуть сложнее, чем предыдущие, но его довольно легко объяснить. Допустим, в процессе нужно обрабатывать максимум 30 соединений. Мы создаем массив из нулей. Как только к нам придет новое соединение, мы его обрабатываем, а адрес сокета записываем в этот массив. Перебирая весь массив и все наши сокеты, мы можем последовательно считывать с них информацию. Но как нам узнать о новом соединении без использования вызова accept? В linux для этого есть как минимум 3 функции: select, poll и epoll. А в freebsd для этого есть аналог функции epoll под названием kqueue (kernel queue). Что делают эти команды? select – самая старая функция, которая до сих пор используется для того, чтобы отдавать всё процессорное время ядру, запрашивая его только при определенных условиях (по аналогии с accept). Разница в том, что ядро вернет нам cpu, когда на указанных нами сокетах начнется любая активность. Так как при запуске программы открыт только один сокет, то и в select мы указываем один. Если мы подключимся телнетом к нашему демону, то в select мы должны указывать уже два сокета: мастер сокет на порт 2222 и тот, который к нам подключился. Чтобы было понятней, продемонстрирую:


[tolik@101host nginx_vs_apache]$ ./differ &
[1] 44832
[tolik@101host nginx_vs_apache]$ ps axuf | grep [.]/differ
tolik     44832 0.0  0.0   4060   448 pts/0    S    22:47   0:00              \_ ./differ
[root@localhost ]# strace -p 44832
Process 44832 attached - interrupt to quit
select(5, [4], NULL, NULL, NULL)        = 1 (in [4])

В этот момент мы с другой консоли делаем telnet на порт 2222 в наш демон и смотрим на трейс:


accept(4, {sa_family=AF_INET, sin_port=htons(41130), sin_addr=inet_addr("127.0.0.1")}, [16]) = 5
select(6, [4 5], NULL, NULL, NULL^C <unfinished ...>
Process 44832 detached
[root@localhost ]# ls -lh /proc/44832/fd
итого 0
lrwx------ 1 tolik tolik 64 Апр 19 00:26 0 -> /dev/pts/12
lrwx------ 1 tolik tolik 64 Апр 19 00:26 1 -> /dev/pts/12
lrwx------ 1 tolik tolik 64 Апр 19 00:21 2 -> /dev/pts/12
l-wx------ 1 tolik tolik 64 Апр 19 00:26 3 -> /tmp/nginx_vs_apache.log
lrwx------ 1 tolik tolik 64 Апр 19 00:26 4 -> socket:[42651147]
lrwx------ 1 tolik tolik 64 Апр 19 00:26 5 -> socket:[42651320] 
[root@localhost ]# netstat -apeen | grep 42651147
tcp        0      0 0.0.0.0:2222                0.0.0.0:*                   LISTEN      500        42651147   44832/./differ
[root@localhost ]# netstat -apeen | grep 42651320
tcp        0      0 127.0.0.1:2222              127.0.0.1:41130             ESTABLISHED 500        42651320   44832/./differ

Сначала команде select мы указывали сокет 4 (смотрите в квадратных скобках). По /proc мы узнали, что 4й файл-дескриптор — это сокет с номером 42651147. По netstat мы узнали, что сокет с таким номером — это наш сокет в состоянии listen порта 2222. Как только мы подключились к этому сокету, ОС произвела tcp handshake с нашим telnet клиентом и установила новое соединение, о чем известила приложение через select. Наша программа получила процессорное время и начала обрабатывать пустой массив с соединениями. Увидев, что это новое соединение, мы запустили команду accept, зная, что она точно не заблокирует выполнение программы, так как соединение уже присутствует. То есть фактически мы используем тот же accept, только в неблокирующем режиме.


После того, как мы выполнили соединение, мы снова отдали управление ядру linux, но сказали ему, что теперь мы хотим получать уведомление по двум сокетам — под номером 4 и 5, что очень хорошо видно в команде strace ([4 5]). Именно так работает nginx: он способен обрабатывать большое количество сокетов одним процессом. По существующим сокетам мы можем проводить операции read/write, по новым можем вызывать accept. Select — очень старый системный вызов, имеющий ряд ограничений: например, на максимальное количество коннектов (файл дескрипторов). Ему на смену пришел сначала более совершенный системный вызов poll, лишенный этих лимитов и работающий быстрее. Впоследствии появились epoll и kqueue (в freebsd). Более современные функции позволяют более эффективно работать с коннектами.


Какие из этих функций поддерживает nginx? Nginx умеет работать со всеми этими функциями.


Ссылка на документацию. В этой статье я не буду описывать, чем отличаются все эти функции, т.к. объем текста уже достаточно большой.


Nginx использует fork для того, чтобы создавать процессы и загружать все ядра на сервере. Но каждый отдельно взятый дочерний процесс nginx работает с множеством соединений так же, как в примере с select, только использует для этого современные функции (для linux по умолчанию это epoll). Смотрим:


[root@localhost ]# ps axuf| grep [n]ginx
root      232753  0.0  0.0  96592   556 ?        Ss   Feb25   0:00 nginx: master process /usr/sbin/nginx -c /etc/nginx/nginx.conf
nginx     232754  0.0  0.0  97428  1400 ?        S    Feb25   5:20  \_ nginx: worker process
nginx     232755  0.0  0.0  97460  1364 ?        S    Feb25   5:02  \_ nginx: worker process
[root@localhost ]# strace -p 232754
Process 232754 attached - interrupt to quit
epoll_wait(12, ^C <unfinished ...>
Process 232754 detached
[root@localhost ]# strace -p 232755
Process 232755 attached - interrupt to quit
epoll_wait(14, {}, 512, 500)            = 0
epoll_wait(14, ^C <unfinished ...>
Process 232755 detached

Что делает родительский мастер процесс nginx?


[root@localhost ]# strace -p 232753
Process 232753 attached - interrupt to quit
rt_sigsuspend([]^C <unfinished ...>
Process 232753 detached

Он не принимает входящие соединения, а только ждет сигнал со стороны ОС. По сигналу nginx умеет много интересного, например, переоткрывать файл дескрипторы, что полезно при ротации логов, или же перечитывать конфигурационный файл.


Все взаимодействие между процессами nginx осуществляет через unix сокеты:


[root@localhost ]# ls -lh /proc/232754/fd
итого 0
lrwx------ 1 nginx nginx 64 Апр  8 13:20 0 -> /dev/null
lrwx------ 1 nginx nginx 64 Апр  8 13:20 1 -> /dev/null
lrwx------ 1 nginx nginx 64 Апр  8 13:20 10 -> socket:[25069547]
lrwx------ 1 nginx nginx 64 Апр  8 13:20 11 -> socket:[25069551]
lrwx------ 1 nginx nginx 64 Апр  8 13:20 12 -> anon_inode:[eventpoll]
lrwx------ 1 nginx nginx 64 Апр  8 13:20 13 -> anon_inode:[eventfd]
l-wx------ 1 nginx nginx 64 Апр  8 13:20 2 -> /var/log/nginx/error.log
lrwx------ 1 nginx nginx 64 Апр  8 13:20 3 -> socket:[25069552]
l-wx------ 1 nginx nginx 64 Апр  8 13:20 5 -> /var/log/nginx/error.log
l-wx------ 1 nginx nginx 64 Апр  8 13:20 6 -> /var/log/nginx/access.log
lrwx------ 1 nginx nginx 64 Апр  8 13:20 9 -> socket:[25069546]
[root@localhost ]# netstat -apeen | grep 25069547
tcp        0      0 172.16.0.1:80               0.0.0.0:*                   LISTEN      0          25069547   232753/nginx
[root@localhost ]# netstat -apeen | grep 25069551
unix  3      [ ]         STREAM     CONNECTED     25069551 232753/nginx

Итог


Перед тем как выбирать те или иные инструменты важно понимать, как именно они работают. Так в некоторых случаях выгоднее использовать только apache httpd без nginx – и наоборот. Но чаще всего эти продукты используются вместе, потому что распараллеливанием обработки сокетов в апаче занимается ОС (разные процессы), а распараллеливанием обработки сокетов в nginx занимается сам nginx.


P.S.


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

Поделиться с друзьями
-->

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


  1. amaksr
    28.01.2017 23:54
    +3

    Обычно администраторы не пишут код, и часто ограничены в выборе решений. Например, какое-то покупное приложение может требовать работы под апачем или IIS, а под nginx разработчик не тестировал, и гарантий не дает. В этой ситуации админ скорее должен знать из каких кубиков-компонентов и как собрать инфраструктуру, чтоб «все работало» и без явных узких мест: может nginx стоит поставить перед апачем в качестве reverse proxy, или как сконфигурировать high-availability, или какие бэкап-решения подходят под требования, и т.п.


    1. bormotov
      29.01.2017 00:42
      +2

      Поэтому, unix-way и выигрывает — стандартизация способов взаимодействия, упрощает выбор конкретного «кубика».

      И вопрос выбора IIS/apache — проверенные производителем или nginx — не проверенный, сводится к тому, понимает ли админ, что нужно программе, от httpd, и если эти «нужности» обеспечивает IIS/apache, сможет ли обеспечить nginx. А еще лучше, конечно, уметь протестировать.


      1. muon
        31.01.2017 02:07

        сможет ли обеспечить nginx
        И если сможет, то выбрать apache. Потому что иначе админ затруднит себе общение с техподдержкой, даже если проблема не будет связана с веб-сервером.


        1. bormotov
          31.01.2017 09:08
          +2

          хороший критерий, но тоже нужно взвешивать
          — как часто требуется общаться с поддержкой, насколько дороже обойдется этот процесс, против преимуществ nginx?
          — можно ли поддержку сфокусировать на проблему продукта, а не возможную проблему взаимодействия с «нетестированным компонентом инфраструктуры»?


  1. DistortNeo
    29.01.2017 05:13

    Ужаснулся от решения, использумого Apache — число одновременно выполняемых запросов ограничено числом процессов. Если каждый из запросов выполняется долго (идёт долгое обращение к удалённой БД, например), то быстро наступит DoS. Но на самом деле такое поведение — это цена за универсальность. Архитектура Apache просто не позволяет использовать кооперативную многозадачность внутри обработчика запроса.


    1. datacompboy
      29.01.2017 08:42

      Позволяет. И есть тредные исполнители для врача.


      1. NLO
        29.01.2017 10:08

        НЛО прилетело и опубликовало эту надпись здесь


        1. datacompboy
          29.01.2017 12:51

          Ну поток всё же легче процесса.
          а event так вообще фактически поток на исполнение, а не на соединение — то есть keepalive'ы не занимают потоков.

          у одного потока на всё тоже есть свои минусы, в том числе — изолязия между исполнителями. или вон недавняя хохма, что текущее время в nginx может начать отставать при недостатке нагрузки.


          1. VBart
            29.01.2017 16:52
            +2

            текущее время в nginx может начать отставать при недостатке нагрузки

            Это что-то новенькое. Можно подробнее?


            1. datacompboy
              29.01.2017 16:59

              1. VBart
                29.01.2017 17:08
                +3

                На самом деле время обновляется каждую итерацию цикла обработки событий. В нормальной ситуации события обрабатываются мгновенно. Если у вас итерация обработки событий затянулась на секунды, то скорее всего это следствие кривого стороннего модуля, который заблокировал nginx или сервер уже не справляется с нагрузкой (рабочий процесс nginx оказался выгружен в своп, ему не дают процессорного времени или заблокирован на диске).


                1. datacompboy
                  29.01.2017 17:21

                  Cпасибо, больше похоже на правду. Я еще ни разу не видел отставаний в своих задачах.

                  Интересно, в xslt модуль «кривой» или нормальный, годный?


                  1. VBart
                    29.01.2017 17:33
                    +4

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


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


                    1. SaveTheRbtz
                      05.02.2017 10:52
                      +3

                      Ну, если совсем по чесноку, то не все =)


                      На больших нагрузках nginx может блокировать eventloop на:


                      • Записи логов. В случае access_log'ов — может сильно помочь директива buffer=. Однако в идеале лучше писать логи напрямую в syslog.
                      • Записи тел запросов и ответов на диск. Тут хорошо помогает костыль в виде aio threads. Однако, насколько я понимаю, он не работает для приёма файлов, только на отдачу.
                        (Почему костыль? Потому что в идеале aio должно работать через нативный аснихронный интерфейс к файловой системе, а не эмулироваться через thread pool, но в *nix ОС с этим всё плохо).
                      • В худшем случае, nginx начинает блокироваться на TLS хендшейках: если вы используете RSA 2048 это примерно 2мс на handshake. В новом OpenSSL 1.1.0 появилось асинхронное API, но в nginx оно если и попадёт, то не скоро. (Патчи по интернетам ходят, но до продакшна я бы их не допускал пока).
                      • Были ещё сложные случаи со сжатием, когда люди пытаются максимально сжать статику (например, gzip 9 и brotli 11). в таких случаях сильно лучше статику pre-сжимать в офлайне и использовать gzip_static и brotli_static. Что делать если хочется по-максимуму сжимать динамку пока не понятно, но оно обычно того не стоит. (Можно, наверное, сжимать на backend'е(или маленьком sidecar-демоне), но это значит больше нельзя применять никакие body-filter'ы).
                      • Image Filter'ы скорее всего тоже могут блокировать eventloop, но я, если честно, код не смотрел, ибо не использовал — все конторы в которых я работал писали простенькие backend'ы для "тяжёлых" манипуляций типа resize/recompress/crop/etc.


                      1. VBart
                        05.02.2017 19:09

                        Конечно, при желании, у пользователя есть способы отстрелить себе обе ноги (как тот же gzip 9), но не надо этого делать.


                        Что касается записи, то есть директива aio_write.


    1. RPG18
      29.01.2017 09:43

      Не переживайте DoS будет, но только не на уровне httpd. httpd обычно не делает запросы к базе. Количество запущенных процессов php/python/ruby ограничено и количество одновременных соединений к базе то же.


      1. mayorovp
        30.01.2017 08:55

        Разве mod_php работает в отдельном процессе?


    1. neol
      29.01.2017 10:08
      +3

      Это не ограничение архитектуры и у apache много решений. В статье речь идёт про используемый по умолчанию prefork, есть ещё event, использующий тот же epoll/kqueue и worker, являющийся чем-то средним между предыдущими. Ну и кучка более экзотических модулей.

      Если говорить про все возможные варианты, то ИМХО проблема apache не в том, что он кривой и медленный, а в том, что в попытке объединить в себе web и application сервер, он стал слишком навороченным и сложным.

      Можно даже использовать один экземпляр apache с mpm_event/mpm_worker/mpmt_os2/mpm_netware + mod_proxy как проксирующий web сервер, второй с mpm_prefork как application сервер и возможно эта связка не сильно уступит классическим nginx+apache/php-fpm/etc.

      Я не фанат apache и предпочту использовать nginx, если такой вариант в принципе возможен, но в контексте данной статья мне показалось уместным упомянуть о том, что не prefork'ом единым жив старый индеец.


      1. NLO
        29.01.2017 10:45

        НЛО прилетело и опубликовало эту надпись здесь


  1. zhihorka
    29.01.2017 10:09

    Большое спасибо за статью. Скажите, в чем смысл регулярного выражения [h]ttp или [n]ginx?


    1. akzhan
      29.01.2017 10:34

      [a]pple эквивалентно apple. http://perldoc.perl.org/perlre.html в помощь


    1. alexkuzko
      29.01.2017 10:35
      +10

      Это очень интересный хак, я хоть и имею огромный опыт в никсах, его отчего-то пропустил. Он позволяет без дополнительных костылей убрать вывод самого грепа в выводе. Пример:

      # ps x|grep mysql
      24383 ?        S      0:00 /bin/sh /usr/bin/mysqld_safe --datadir=/var/lib/mysql --pid-file=/var/lib/mysql/mysql.pid
      25301 ?        S      0:00 /bin/sh /usr/bin/mysqld_safe --datadir=/var/lib/mysql --pid-file=/var/lib/mysql/mysql.pid
      29419 pts/2    S+     0:00 grep mysql
      # ps x|grep [m]ysql
      24383 ?        S      0:00 /bin/sh /usr/bin/mysqld_safe --datadir=/var/lib/mysql --pid-file=/var/lib/mysql/mysql.pid
      25301 ?        S      0:00 /bin/sh /usr/bin/mysqld_safe --datadir=/var/lib/mysql --pid-file=/var/lib/mysql/mysql.pid
      


      Заметили разницу? Обычно в примерах в сети видна конструкция «grep something | grep -v grep», а если сделать «grep [s]omething», то something попадет, а сам grep — уже нет, т.к. при строковом сравнении something!=[s]omething.

      Это очень поверхностно, можно подробно найти в сети (выше дали неплохую ссылку). Но очень удобно.


      1. iDm1
        30.01.2017 16:14
        +2

        А не проще использовать pgrep, вместо двух утилит и хака?


        1. alexkuzko
          30.01.2017 16:57

          В моем комментарии речь не про ps|grep, а про то, что же такое grep [m]atch

          Что же до автора статьи — он решил именно так. Я pgrep тоже использую, но привычка — страшная сила! Да и само по себе это отвечает конвейерной сути никсов: вначале просто ps -aux, потом немного отсортировать и т.п., не всегда стоит задача сразу отгрепать.

          В любом случае — спасибо за напоминание. Кто-то начнет с правильных утилит сразу.


          1. grossws
            30.01.2017 18:12
            +2

            Только не ps -aux, если вы, конечно, не хотите увидеть все процессы пользователя x. Есть у ps небольшое западло с поддержкой unix, bsd и long-gnu style options.



  1. S_A
    29.01.2017 17:22

    Дело не в том кто там что форкает или тредает, дело в архитектуре изначально: если нужно выполнить серверный код, то этот код сам по себе процесс, покуда вы «не перепилили апач» (ну или модуль к нему сделали).

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

    Особнячком node.js со своей асинхронностью.

    Конечно, поселектить можно, но любой юниксовый пайп, сокет, ммап и т.п. — это InterProcessCommunication (тот же баш форкает при | grep), то есть если надо выполнить код, отвязанный от обработки запроса, ничем кроме как треда/форка его не сделать. И то если тред, то интерпретатор нужен (как модуль возможно) встроенный. А треды хуже процессов в том плане, что у них общая память с процессом, и соответственно и корка (core dump) общая :)

    Это все на самом деле причина того, что бывает масштабируют горизонтально.


    1. lexore
      30.01.2017 01:55

      Конечно, поселектить можно, но любой юниксовый пайп, сокет, ммап и т.п. — это InterProcessCommunication

      Сейчас приложения часто запускаются в виде демонов и сами слушают tcp сокет, в который можно слать запросы по http/fastcgi.
      На самом сервере (на машине) может даже не быть веб сервера. Это я про бекенд сервера логики, естественно.


      1. S_A
        30.01.2017 02:22

        Это не только сейчас, так и 10 лет назад бывало делали (и я так делал), и еще раньше (до меня много раз). Речь-то шла про nginx и apache, и про fork против event loop, как я понял.

        Если не отвязывать код от сервера, как в том же node.js, то ту же БД всё равно не встроишь в логику, значит опять пайп, сокет, ммап и т.п.


  1. VBart
    29.01.2017 17:46
    +3

    У select есть большой недостаток — мы не можем знать, какое именно событие произошло и с каким именно сокетом. Каждый раз, когда мы получаем процессорное время, нам приходится обрабатывать все наши коннекты и проверять их на получение данных, делая с них read. Если у нас будет 1000 соединений, а данные придут только по одному из них, то мы обработаем все 1000 соединений, чтобы найти нужный.

    Тут написана неправда. Процитирую man: On exit, the sets are modified in place to indicate which file descriptors actually changed status.


    1. DistortNeo
      29.01.2017 18:08

      Тут написана неправда

      Да, неправда — read не нужно делать. Но нюанс всё же есть. В случае select на вход подаётся не список дескрипторов, а битовая маска, она же является и выходным параметром. Битовая маска имеет размер (в битах), равный максимальному количеству дескрипторов (зависит от настроек операционной системы, обычно 1024, но может быть и 8192, и больше). Соответственно, нужно пробегаться по всей маске, чтобы найти все сокеты, по которым произошли события.


      1. VBart
        29.01.2017 18:12
        +1

        По маске пробегать относительно дешево, а написано другое, я же процитировал: нам приходится обрабатывать все наши коннекты и проверять их на получение данных, делая с них read


        И код в примере именно так и делает зачем-то.


        1. VBart
          29.01.2017 18:17

          А, пардон, код в примере маску всё же проверяет. Но комментарий опять же вводит в заблуждение:


          //Читаем данные из каждого сокета, так как не знаем какие события заставил ОС дать нам CPU


      1. VBart
        29.01.2017 18:23
        +2

        Ок. Вы изменили комментарий. =)


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


        1. mechanicusilius
          29.01.2017 18:59

          Коллеги, если посмотреть тот же man fd_set, то:
          select() has no sigmask argument, and behaves as pselect() called with NULL sigmask.
          Описывал я select, а pselect использовал, так как в свежем ядре он заменил select. Суть статьи была не в этом. Но в любом случае спасибо, что читаете. Все комментарии выше относятся к pselect, который используется в исходниках.


          1. VBart
            29.01.2017 19:06
            +1

            sigmask — это вообще про обработчики сигналов. И единственное отличие pselect() от select() в наличии дополнительного аргумента, который позволяет переопределить обработку сигналов на время вызова select().


            Опять же, обратимся к man:


            sigmask is a pointer to a signal mask (see sigprocmask(2)); if it is not NULL, then pselect() first replaces the current signal mask by the one pointed to by sigmask, then does the "select" function, and then restores the original signal mask.


            Ну и man signal процитирую:


            Signal mask and pending signals


            A signal may be blocked, which means that it will not be delivered until it is later unblocked. Between the time when it is generated and when it is delivered a signal is said to be pending.


            Each thread in a process has an independent signal mask, which indicates the set of signals that the thread is currently blocking.


            1. mechanicusilius
              29.01.2017 20:20

              Да спасибо, убрал это предложение из текста.


        1. DistortNeo
          29.01.2017 21:30

          Ок. Вы изменили комментарий. =)

          Да у меня даже и в мыслях не было, что кто-то после select будет проверять каждый сокет по отдельности.


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

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


      1. mayorovp
        30.01.2017 09:10

        Хм. Впервые узнал что в Линуксе fd_set является битовой маской...


  1. gabber2k
    29.01.2017 22:58
    -1

    Можно просто libevent использовать, там уже все написано


  1. grossws
    30.01.2017 01:36

    Хороший системный администратор при выборе решения при заданных требованиях ориентируется на два условия: минимальное потребление ресурсов и их сбалансированное распределение.

    А как же всякие вещи типа стоимости поддержки, в частности, прозрачности конфигурации, удобства автоматизации управления конфигурацией и прочих подобных критериев?


  1. Loki3000
    30.01.2017 10:45
    +1

    Я совсем не системный администратор и мне в статье не хватило еще одного абзаца: вся статья подвела меня к мысли что подход апача плохой и устаревший: плодит процессы, жрет память, а вот ngnix весь такой современный и красивый. Но какова цена всей этой красивости? Или же апач настолько устарел, что во всем проигрывает nginx? Но нет, в конце статьи написано что в некоторых случаях выгоднее использовать только апач. Так в чем же преимущества prefork подхода и в каких случаях он будет уместен? И кстати, а если клонировать процессы не заранее, а по запросу — насколько просядет скорость работы?


    1. mayorovp
      30.01.2017 11:10

      Апач выигрывает тем, что это — default сервер. Любой PHP-разработчик параллельно изучает возможности mod_rewrite и сопровождает файлы .htaccess.


      При попытке перейти на nginx все эти .htaccess приходится переписывать вручную.


      1. Loki3000
        30.01.2017 11:19

        Гм… мы тут, вроде, о системных администраторах говорили. При чем тут разработчики? Все уже разработано — надо только сервер выбрать, под которым все эти разработки крутиться будут.


        1. mayorovp
          30.01.2017 11:24

          Так о том и речь. Часть конфига для Apache уже написана разработчиком. Конфиг для Nginx надо писать самому, причем важную часть документации придется получать реверс-инженерингом конфига Apache.


          1. Loki3000
            30.01.2017 11:29

            Не, ну это как-то совсем не серьезно. Это как выбирать машину исходя из того, насколько в ней удобно масло менять. Масло мы в ней меняем раз в полгода, а ездим-то каждый день. Давайте все же ездовые качества сравнивать.


            1. mayorovp
              30.01.2017 11:40

              Сомневаюсь, что вы купите вместо машины конструктор "собери машину сам", даже если отзывы обещают исключительные ездовые качества.


              Если веб-сервер не суметь настроить — он не будет работать. Не смотря на свою крутую архитектуру.


              Для настройки Nginx требуется намного большая квалификация, нежели для настройки Apache. Если для нас с вами настройка Nginx не является проблемой — это не значит что с ней справится каждый.


              1. Loki3000
                30.01.2017 12:00

                Вас в какие-то дебри понесло — «настройка», «собери сам». В статье говорилось о различиях серверов. При этом мы считаем что админы в состоянии настроить и то и другое — это их профессия, в конце концов. Я задал вполне конкретный вопрос (см. мой первый пост): в каких случаях архитектура apache будет иметь преимущества перед nginx. Трудности настройки и разработки рассматривать я не вижу смысла — будем считать что квалификация разработчиков и администраторов достаточно высока, чтобы не быть препятствием для работы серверов.


                1. VBart
                  30.01.2017 12:14

                  Я задал вполне конкретный вопрос (см. мой первый пост): в каких случаях архитектура apache будет иметь преимущества перед nginx.

                  Я более-менее разбирал этот вопрос в статье.


                  1. Loki3000
                    30.01.2017 12:39

                    Спасибо. Правильно ли я понял: если сервер практически не отдает статику и имеет мало памяти, то подход апача с распараллеливанием процессов может обставить по производительности nginx? Или пулы потоков и в этом случае не оставят апачу шансов на реванш?:)


                    1. mayorovp
                      30.01.2017 13:02

                      Когда сервер практически не отдает статику — всех делает Нода с ее сквозной асинхронностью. Nginx тут не подойдет хотя бы потому что сам по себе он отдавать динамику не умеет, нужен бакэнд — а про бакэнд см. выше.


                      Но если сравнивать Nginx и Apache по потенциальным возможностям — то пулы потоков и правда спасут nginx.


                    1. VBart
                      30.01.2017 14:19

                      Как раз наоборот. Статика + мало памяти — было единственным слабым местом nginx, которое и устранили с помощью пула потоков. =)


                      1. Loki3000
                        30.01.2017 14:25

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


                        1. mayorovp
                          30.01.2017 14:55
                          +3

                          Когда продукт прибит к Apache гвоздями.


                        1. VBart
                          30.01.2017 16:31
                          +1

                          Работа с соединениями всегда происходит в неблокирующем режиме асинхронно. Проблема могла возникнуть только с файлами из-за отсутствия аналогичной поддержки со стороны ядра.


                          В пользу Apache можно привести ряд аргументов:


                          1. Можно отдать ему предпочтение при прочих равных, если вы умеете его настраивать и знаете гораздо лучше чем nginx.
                          2. Ваш сервис опирается на некоторую функциональность Apache, которой либо нет в nginx, либо она реализована иначе. Распространенный пример в данном случае — это виртуальный хостинг с использованием .htaccess файлов вашими клиентами.
                          3. Вы используете Apache в качестве сервера приложений, как менеджер процессов для php, python, java, etc..., чем nginx самостоятельно не занимается, а работает в паре с другими, например php-fpm, gunicorn, wildfly, тем же Apache.

                          В рунете более чем 76% веб-сайтов обслуживает nginx по крайней мере в качестве фронтенда. В мировом масштабе успехи nginx скромнее, но тенденция такова, что доля Apache стабильно снижается, а доля nginx стабильно растет.


                          1. grossws
                            30.01.2017 18:17

                            Ваш сервис опирается на некоторую функциональность Apache, которой либо нет в nginx, либо она реализована иначе. Распространенный пример в данном случае — это виртуальный хостинг с использованием .htaccess файлов вашими клиентами.

                            Другой классический пример — использование mod_jk с ajp для интеграции с Apache Tomcat (прокидывание TLS-сессий в appserver).


                            1. datacompboy
                              30.01.2017 18:43

                              https://github.com/yaoweibin/nginx_ajp_module?


                              1. grossws
                                30.01.2017 19:17

                                TODO
                                • SSL

                                https://github.com/yaoweibin/nginx_ajp_module#todo, т. е. то, ради чего я бы стал связываться с ajp не реализовано.


                                Плюс оно требует пересборки nginx'а. И поддержка такого проекта без community выглядит довольно печально: некоторые тикеты висят с 14 года.


                            1. VBart
                              03.02.2017 15:06

                              Другой классический пример — использование mod_jk с ajp для интеграции с Apache Tomcat (прокидывание TLS-сессий в appserver).

                              Знаю не мало случаев перехода с ajp на nginx проксирующий по http, а вот такой хотелки как-то не припоминаю. Если чего-то очень не хватает, об этом имеет смысл пойти и написать feature request в trac.


                              1. grossws
                                04.02.2017 04:28
                                +1

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


                                Мы используем связки nginx + tomcat, nginx + jetty и nginx + wildfly (в зависимости от приложения), везде http от nginx'а до соответствующего upstream'а.


                                Когда мне нужен был аналог того что apache + tomcat-ajp даёт из коробки, я передавал данные о клиентском сертификате через заголовки и использовал простой сервлет-фильтр для обработки x509 из заголовков.


  1. Eugene_Burachevskiy
    31.01.2017 12:02
    -1

    А что насчет mpm-event в апач начиная с версии 2.4?
    По идее апач в этом режиме не должен уступать в производительности на статике если на сервере хватает памяти для всех его worker-threads и listener-threads…


    1. VBart
      31.01.2017 20:42
      +1

      Event MPM работает только для keep-alive соединений. От этого Apache не стал nginx-ом. Обработка запроса в Apache, как происходила многопоточно, так и происходит, и это не масштабируется.


      1. Eugene_Burachevskiy
        01.02.2017 10:00

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


        1. VBart
          02.02.2017 20:38
          +1

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


  1. worsediablo
    31.01.2017 12:03
    +1

    Правильный ответ на этот вопрос на любом собеседовании – ничего. Fork – устаревший вызов, и в linux присутствует только для обратной совместимости.

    А можно пруф про устаревший fork?
    man fork:
    fork() creates a new process by duplicating the calling process. The new process is referred to as the child process. The calling process is referred to as the parent process.

    The child process and the parent process run in separate memory spaces. At the time of fork() both memory spaces have the same content. Memory writes, file mappings (mmap(2)), and unmappings (mun?
    map(2)) performed by one of the processes do not affect the other.


    man clone
    clone() creates a new process, in a manner similar to fork(2)

    Unlike fork(2), clone() allows the child process to share parts of its execution context with the calling process, such as the memory space, the table of file descriptors, and the table of signal
    handlers. (Note that on this manual page, «calling process» normally corresponds to «parent process». But see the description of CLONE_PARENT below.)

    One use of clone() is to implement threads: multiple threads of control in a program that run concurrently in a shared memory space.

    А дальше ОС сама разбирается с памятью, т.к. процесс выделяет не физическую память, а виртуальную. И пока процесс не начал менять страницы в памяти они общие (shared) для parent-а и child.


    1. mayorovp
      31.01.2017 12:23

      Вы не тот фрагмент процитировали. Системный вызов fork устарел потому что он практически никогда не делает то, что нужно, делая при этом кучу лишней работы. Вместо него лучше использовать или clone, или posix_spawn.


      А в посте, конечно же, ерунда написана.


      1. mechanicusilius
        31.01.2017 14:33

        man vfork

        Historic description
        Under Linux, fork(2) is implemented using copy-on-write pages, so the
        only penalty incurred by fork(2) is the time and memory required to
        duplicate the parent's page tables, and to create a unique task
        structure for the child. However, in the bad old days a fork(2)
        would require making a complete copy of the caller's data space,
        often needlessly, since usually immediately afterward an exec(3) is
        done. Thus, for greater efficiency, BSD introduced the vfork()
        system call, which did not fully copy the address space of the parent
        process, but borrowed the parent's memory and thread of control until
        a call to execve(2) or an exit occurred. The parent process was
        suspended while the child was using its resources. The use of
        vfork() was tricky: for example, not modifying data in the parent
        process depended on knowing which variables were held in a register.


        1. mayorovp
          31.01.2017 14:41

          Вы это к чему? Если вы хотели напомнить про существование vfork — да, это тоже вариант замены устаревшего fork. Но его описание, с моей точки зрения, выглядит как костыль. Тот же posix_spawn выглядит куда красивее архитектурно.


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


      1. worsediablo
        31.01.2017 14:55

        вот отсюда http://man7.org/linux/man-pages/man3/posix_spawn.3.html

        The posix_spawn() and posix_spawnp() functions are used to create a
        new child process that executes a specified file. These functions
        were specified by POSIX to provide a standardized method of creating
        new processes on machines that lack the capability to support the
        fork(2) system call. These machines are generally small, embedded
        systems lacking MMU support.

        Если не нужно шарить память и прочее с родительским процессом, то clone() не требуется.
        C library/kernel differences
        Since version 2.3.3, rather than invoking the kernel's fork() system call, the glibc fork() wrapper that is provided as part of the NPTL threading implementation invokes clone(2) with flags that pro?
        vide the same effect as the traditional system call. (A call to fork() is equivalent to a call to clone(2) specifying flags as just SIGCHLD.)

        Т.е. GlibС-шный fork() из glibc по факту вызывает clone(2).


        1. mechanicusilius
          31.01.2017 15:02

          Этот пост не про то, как правильно писать программы плодя чайлды, а про то, что делает системный вызов fork (именно про него спрашивают чаще всего на собесах админов, а не про vfork, не про clone, и не про posix_spawn). Но я с удовольствием прочитаю вашу статью на эту тему. Тема, безусловно, интересная.


        1. mayorovp
          31.01.2017 15:14

          И что дальше? Хватит говорить цитатами.


  1. simpleadmin
    03.02.2017 15:20

    2 VBart
    Валентин, пользуясь случаем задам вопрос:

    http://nginx.org/ru/docs/http/ngx_http_core_module.html#location
    Если location задан префиксной строкой со слэшом в конце и запросы обрабатываются при помощи proxy_pass, fastcgi_pass, uwsgi_pass, scgi_pass или memcached_pass, происходит специальная обработка. В ответ на запрос с URI равным этой строке, но без завершающего слэша, будет возвращено постоянное перенаправление с кодом 301 на URI с добавленным в конец слэшом. Если такое поведение нежелательно, можно задать точное совпадение URI и location, например:


    1. VBart
      03.02.2017 16:19
      +1

      Это классическая практика добавлять слэш в конце и традиционно это делается через 301-ый редирект, который кэшируется браузером. Если у вас путь в форме или скриптах приписан неверно (а откуда ещё возьмется POST-запрос?), то нужно править код, а не затыкать проблему редиректом. Редирект на запросы с телом — это вообще плохо, вне зависимости от кода.