Разница между nginx и apache с примерами

в 20:00, , рубрики: Apache, apache httpd, nginx, Серверное администрирование

Во время собеседований на роль 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=%dn",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=%dn",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] = '';
                        fprintf(resultfile, buffer);
                        fflush (resultfile);
                    }
                }
            }
        }

        return 0;
}

Этот код выглядит чуть сложнее, чем предыдущие, но его довольно легко объяснить. Допустим, в процессе нужно обрабатывать максимум 30 соединений. Мы создаем массив из нулей. Как только к нам придет новое соединение, мы его обрабатываем, а адрес сокета записываем в этот массив. Перебирая весь массив и все наши сокеты, мы можем последовательно считывать с них информацию. Но как нам узнать о новом соединении без использования вызова accept? В linux для этого есть как минимум 3 функции: select, poll и epoll. А в freebsd для этого есть аналог функции epoll под названием kqeueu (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 есть большой недостаток — мы не можем знать, какое именно событие произошло и с каким именно сокетом. Каждый раз, когда мы получаем процессорное время, нам приходится обрабатывать все наши коннекты и проверять их на получение данных, делая с них read. Если у нас будет 1000 соединений, а данные придут только по одному из них, то мы обработаем все 1000 соединений, чтобы найти нужный. Select — очень старый системный вызов, имеющий ряд ограничений: например, на максимальное количество коннектов (файл дескрипторов). Ему на смену пришел сначала более совершенный системный вызов poll, лишенный этих лимитов и работающий быстрее. Впоследствии появились epoll и kqeueu (в freebsd). Более современные функции позволяют более эффективно работать с коннектами. Например, при использовании kqeueu вы будете точно знать, по какому сокету у вас произошло событие, и сможете сразу его обработать.

Какие из этих функций поддерживает 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. В программе я не обрабатывал ситуации по закрытию сокетов, и они могут быть какое-то время еще открыты в разных состояниях от старых демонов. Если программа не запускается, просто подождите, пока все сокеты закроются по таймауту.

Автор: mechanicusilius

Источник

Поделиться

* - обязательные к заполнению поля