Во время собеседований на роль 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).
//Порт, который мы слушаем
#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 нигде не появилось, мы уже закрыли сокет, и ОС очистила буфер, тем самым удалив наши данные. Нужно модернизировать программу — читать из сокета до тех пор, пока оттуда поступает информация.
#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 исполняемых процессов с одним блоком памяти, и память будет копироваться только при попытке ее изменить дочерним процессом. Согласитесь, второй алгоритм намного эффективней.
#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 портом? Пробуем.
#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 блокирует выполнение нашей программы до тех пор, пока не придет новое соединение. Получается, что мы не можем ожидать новое соединение и обрабатывать уже открытое соединение в одном процессе. Или?
Взглянем на код:
#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)
DistortNeo
29.01.2017 05:13Ужаснулся от решения, использумого Apache — число одновременно выполняемых запросов ограничено числом процессов. Если каждый из запросов выполняется долго (идёт долгое обращение к удалённой БД, например), то быстро наступит DoS. Но на самом деле такое поведение — это цена за универсальность. Архитектура Apache просто не позволяет использовать кооперативную многозадачность внутри обработчика запроса.
datacompboy
29.01.2017 08:42Позволяет. И есть тредные исполнители для врача.
NLO
29.01.2017 10:08НЛО прилетело и опубликовало эту надпись здесь
datacompboy
29.01.2017 12:51Ну поток всё же легче процесса.
а event так вообще фактически поток на исполнение, а не на соединение — то есть keepalive'ы не занимают потоков.
у одного потока на всё тоже есть свои минусы, в том числе — изолязия между исполнителями. или вон недавняя хохма, что текущее время в nginx может начать отставать при недостатке нагрузки.VBart
29.01.2017 16:52+2текущее время в nginx может начать отставать при недостатке нагрузки
Это что-то новенькое. Можно подробнее?
datacompboy
29.01.2017 16:59VBart
29.01.2017 17:08+3На самом деле время обновляется каждую итерацию цикла обработки событий. В нормальной ситуации события обрабатываются мгновенно. Если у вас итерация обработки событий затянулась на секунды, то скорее всего это следствие кривого стороннего модуля, который заблокировал nginx или сервер уже не справляется с нагрузкой (рабочий процесс nginx оказался выгружен в своп, ему не дают процессорного времени или заблокирован на диске).
datacompboy
29.01.2017 17:21Cпасибо, больше похоже на правду. Я еще ни разу не видел отставаний в своих задачах.
Интересно, в xslt модуль «кривой» или нормальный, годный?VBart
29.01.2017 17:33+4Все родные модули написаны с учетом асинхронной архитектуры. С XSLT должно быть всё нормально, за исключением каких-то экстремальных случаев, когда у вас такой шаблон, что требует несколько секунд процессорного времени на обработку.
Просто многие авторы сторонних модулей не понимают как устроен nginx и в своих модулях могут блокировать обработку событий на долгое время (что недопустимо), например, подключаясь и запрашивая базу данных в блокирующем режиме.
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.
VBart
05.02.2017 19:09Конечно, при желании, у пользователя есть способы отстрелить себе обе ноги (как тот же gzip 9), но не надо этого делать.
Что касается записи, то есть директива aio_write.
- Записи логов. В случае
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'ом единым жив старый индеец.
zhihorka
29.01.2017 10:09Большое спасибо за статью. Скажите, в чем смысл регулярного выражения [h]ttp или [n]ginx?
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.
Это очень поверхностно, можно подробно найти в сети (выше дали неплохую ссылку). Но очень удобно.iDm1
30.01.2017 16:14+2А не проще использовать pgrep, вместо двух утилит и хака?
alexkuzko
30.01.2017 16:57В моем комментарии речь не про ps|grep, а про то, что же такое grep [m]atch
Что же до автора статьи — он решил именно так. Я pgrep тоже использую, но привычка — страшная сила! Да и само по себе это отвечает конвейерной сути никсов: вначале просто ps -aux, потом немного отсортировать и т.п., не всегда стоит задача сразу отгрепать.
В любом случае — спасибо за напоминание. Кто-то начнет с правильных утилит сразу.grossws
30.01.2017 18:12+2Только не
ps -aux
, если вы, конечно, не хотите увидеть все процессы пользователяx
. Есть уps
небольшое западло с поддержкой unix, bsd и long-gnu style options.
S_A
29.01.2017 17:22Дело не в том кто там что форкает или тредает, дело в архитектуре изначально: если нужно выполнить серверный код, то этот код сам по себе процесс, покуда вы «не перепилили апач» (ну или модуль к нему сделали).
Nginx ставят на статику потому что он не содержит серверной прикладной логики.
Apache форкает потому что ему надо выполнить логику, выполнение которой процесс.
Особнячком node.js со своей асинхронностью.
Конечно, поселектить можно, но любой юниксовый пайп, сокет, ммап и т.п. — это InterProcessCommunication (тот же баш форкает при | grep), то есть если надо выполнить код, отвязанный от обработки запроса, ничем кроме как треда/форка его не сделать. И то если тред, то интерпретатор нужен (как модуль возможно) встроенный. А треды хуже процессов в том плане, что у них общая память с процессом, и соответственно и корка (core dump) общая :)
Это все на самом деле причина того, что бывает масштабируют горизонтально.lexore
30.01.2017 01:55Конечно, поселектить можно, но любой юниксовый пайп, сокет, ммап и т.п. — это InterProcessCommunication
Сейчас приложения часто запускаются в виде демонов и сами слушают tcp сокет, в который можно слать запросы по http/fastcgi.
На самом сервере (на машине) может даже не быть веб сервера. Это я про бекенд сервера логики, естественно.S_A
30.01.2017 02:22Это не только сейчас, так и 10 лет назад бывало делали (и я так делал), и еще раньше (до меня много раз). Речь-то шла про nginx и apache, и про fork против event loop, как я понял.
Если не отвязывать код от сервера, как в том же node.js, то ту же БД всё равно не встроишь в логику, значит опять пайп, сокет, ммап и т.п.
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.
DistortNeo
29.01.2017 18:08Тут написана неправда
Да, неправда —
read
не нужно делать. Но нюанс всё же есть. В случаеselect
на вход подаётся не список дескрипторов, а битовая маска, она же является и выходным параметром. Битовая маска имеет размер (в битах), равный максимальному количеству дескрипторов (зависит от настроек операционной системы, обычно 1024, но может быть и 8192, и больше). Соответственно, нужно пробегаться по всей маске, чтобы найти все сокеты, по которым произошли события.VBart
29.01.2017 18:12+1По маске пробегать относительно дешево, а написано другое, я же процитировал: нам приходится обрабатывать все наши коннекты и проверять их на получение данных, делая с них read
И код в примере именно так и делает зачем-то.
VBart
29.01.2017 18:17А, пардон, код в примере маску всё же проверяет. Но комментарий опять же вводит в заблуждение:
//Читаем данные из каждого сокета, так как не знаем какие события заставил ОС дать нам CPU
VBart
29.01.2017 18:23+2Ок. Вы изменили комментарий. =)
Но замечу, что пробегать по всей маске не нужно. А только по дескрипторам, на которых мы ожидаем получение события.
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, который используется в исходниках.VBart
29.01.2017 19:06+1sigmask — это вообще про обработчики сигналов. И единственное отличие 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.
DistortNeo
29.01.2017 21:30Ок. Вы изменили комментарий. =)
Да у меня даже и в мыслях не было, что кто-то после
select
будет проверять каждый сокет по отдельности.
Но замечу, что пробегать по всей маске не нужно. А только по дескрипторам, на которых мы ожидаем получение события.
Если мы обрабатываем сразу несколько сотен соединений, накладные расходы будут высоки. Правда, ещё больше накладных расходов будет у операционной системы. Из-за этого вызовы
select
получаются довольно дорогими.
grossws
30.01.2017 01:36Хороший системный администратор при выборе решения при заданных требованиях ориентируется на два условия: минимальное потребление ресурсов и их сбалансированное распределение.
А как же всякие вещи типа стоимости поддержки, в частности, прозрачности конфигурации, удобства автоматизации управления конфигурацией и прочих подобных критериев?
Loki3000
30.01.2017 10:45+1Я совсем не системный администратор и мне в статье не хватило еще одного абзаца: вся статья подвела меня к мысли что подход апача плохой и устаревший: плодит процессы, жрет память, а вот ngnix весь такой современный и красивый. Но какова цена всей этой красивости? Или же апач настолько устарел, что во всем проигрывает nginx? Но нет, в конце статьи написано что в некоторых случаях выгоднее использовать только апач. Так в чем же преимущества prefork подхода и в каких случаях он будет уместен? И кстати, а если клонировать процессы не заранее, а по запросу — насколько просядет скорость работы?
mayorovp
30.01.2017 11:10Апач выигрывает тем, что это — default сервер. Любой PHP-разработчик параллельно изучает возможности mod_rewrite и сопровождает файлы .htaccess.
При попытке перейти на nginx все эти .htaccess приходится переписывать вручную.
Loki3000
30.01.2017 11:19Гм… мы тут, вроде, о системных администраторах говорили. При чем тут разработчики? Все уже разработано — надо только сервер выбрать, под которым все эти разработки крутиться будут.
mayorovp
30.01.2017 11:24Так о том и речь. Часть конфига для Apache уже написана разработчиком. Конфиг для Nginx надо писать самому, причем важную часть документации придется получать реверс-инженерингом конфига Apache.
Loki3000
30.01.2017 11:29Не, ну это как-то совсем не серьезно. Это как выбирать машину исходя из того, насколько в ней удобно масло менять. Масло мы в ней меняем раз в полгода, а ездим-то каждый день. Давайте все же ездовые качества сравнивать.
mayorovp
30.01.2017 11:40Сомневаюсь, что вы купите вместо машины конструктор "собери машину сам", даже если отзывы обещают исключительные ездовые качества.
Если веб-сервер не суметь настроить — он не будет работать. Не смотря на свою крутую архитектуру.
Для настройки Nginx требуется намного большая квалификация, нежели для настройки Apache. Если для нас с вами настройка Nginx не является проблемой — это не значит что с ней справится каждый.
Loki3000
30.01.2017 12:00Вас в какие-то дебри понесло — «настройка», «собери сам». В статье говорилось о различиях серверов. При этом мы считаем что админы в состоянии настроить и то и другое — это их профессия, в конце концов. Я задал вполне конкретный вопрос (см. мой первый пост): в каких случаях архитектура apache будет иметь преимущества перед nginx. Трудности настройки и разработки рассматривать я не вижу смысла — будем считать что квалификация разработчиков и администраторов достаточно высока, чтобы не быть препятствием для работы серверов.
VBart
30.01.2017 12:14Я задал вполне конкретный вопрос (см. мой первый пост): в каких случаях архитектура apache будет иметь преимущества перед nginx.
Я более-менее разбирал этот вопрос в статье.
Loki3000
30.01.2017 12:39Спасибо. Правильно ли я понял: если сервер практически не отдает статику и имеет мало памяти, то подход апача с распараллеливанием процессов может обставить по производительности nginx? Или пулы потоков и в этом случае не оставят апачу шансов на реванш?:)
mayorovp
30.01.2017 13:02Когда сервер практически не отдает статику — всех делает Нода с ее сквозной асинхронностью. Nginx тут не подойдет хотя бы потому что сам по себе он отдавать динамику не умеет, нужен бакэнд — а про бакэнд см. выше.
Но если сравнивать Nginx и Apache по потенциальным возможностям — то пулы потоков и правда спасут nginx.
VBart
30.01.2017 14:19Как раз наоборот. Статика + мало памяти — было единственным слабым местом nginx, которое и устранили с помощью пула потоков. =)
Loki3000
30.01.2017 14:25Тогда я окончательно запутался — разве медленный бекэнд не приводит к блокированию очереди?
И в каких же тогда случаях имеет смысл использовать apache?VBart
30.01.2017 16:31+1Работа с соединениями всегда происходит в неблокирующем режиме асинхронно. Проблема могла возникнуть только с файлами из-за отсутствия аналогичной поддержки со стороны ядра.
В пользу Apache можно привести ряд аргументов:
- Можно отдать ему предпочтение при прочих равных, если вы умеете его настраивать и знаете гораздо лучше чем nginx.
- Ваш сервис опирается на некоторую функциональность Apache, которой либо нет в nginx, либо она реализована иначе. Распространенный пример в данном случае — это виртуальный хостинг с использованием .htaccess файлов вашими клиентами.
- Вы используете Apache в качестве сервера приложений, как менеджер процессов для php, python, java, etc..., чем nginx самостоятельно не занимается, а работает в паре с другими, например php-fpm, gunicorn, wildfly, тем же Apache.
В рунете более чем 76% веб-сайтов обслуживает nginx по крайней мере в качестве фронтенда. В мировом масштабе успехи nginx скромнее, но тенденция такова, что доля Apache стабильно снижается, а доля nginx стабильно растет.
grossws
30.01.2017 18:17Ваш сервис опирается на некоторую функциональность Apache, которой либо нет в nginx, либо она реализована иначе. Распространенный пример в данном случае — это виртуальный хостинг с использованием .htaccess файлов вашими клиентами.
Другой классический пример — использование
mod_jk
сajp
для интеграции с Apache Tomcat (прокидывание TLS-сессий в appserver).datacompboy
30.01.2017 18:43https://github.com/yaoweibin/nginx_ajp_module?
grossws
30.01.2017 19:17TODO
- SSL
https://github.com/yaoweibin/nginx_ajp_module#todo, т. е. то, ради чего я бы стал связываться с ajp не реализовано.
Плюс оно требует пересборки nginx'а. И поддержка такого проекта без community выглядит довольно печально: некоторые тикеты висят с 14 года.
VBart
03.02.2017 15:06Другой классический пример — использование mod_jk с ajp для интеграции с Apache Tomcat (прокидывание TLS-сессий в appserver).
Знаю не мало случаев перехода с ajp на nginx проксирующий по http, а вот такой хотелки как-то не припоминаю. Если чего-то очень не хватает, об этом имеет смысл пойти и написать feature request в trac.
grossws
04.02.2017 04:28+1Не вижу большого смысла отвлекать разработчиков nginx на такую задачу, которая не принесёт счастья большинству пользователей.
Мы используем связки nginx + tomcat, nginx + jetty и nginx + wildfly (в зависимости от приложения), везде http от nginx'а до соответствующего upstream'а.
Когда мне нужен был аналог того что apache + tomcat-ajp даёт из коробки, я передавал данные о клиентском сертификате через заголовки и использовал простой сервлет-фильтр для обработки x509 из заголовков.
Eugene_Burachevskiy
31.01.2017 12:02-1А что насчет mpm-event в апач начиная с версии 2.4?
По идее апач в этом режиме не должен уступать в производительности на статике если на сервере хватает памяти для всех его worker-threads и listener-threads…VBart
31.01.2017 20:42+1Event MPM работает только для keep-alive соединений. От этого Apache не стал nginx-ом. Обработка запроса в Apache, как происходила многопоточно, так и происходит, и это не масштабируется.
Eugene_Burachevskiy
01.02.2017 10:00спасибо за пояснение! а подскажите, если допустим к одному процессу апач в поточном режиме подключено два пользователя. и на один поток приходит длительный запрос, допустим большая выборка из базы, то пока на этом потоке не уйдет ответ, то второй поток процесс не будет обрабатывать?
VBart
02.02.2017 20:38+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.mayorovp
31.01.2017 12:23Вы не тот фрагмент процитировали. Системный вызов fork устарел потому что он практически никогда не делает то, что нужно, делая при этом кучу лишней работы. Вместо него лучше использовать или clone, или posix_spawn.
А в посте, конечно же, ерунда написана.
mechanicusilius
31.01.2017 14:33man 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.mayorovp
31.01.2017 14:41Вы это к чему? Если вы хотели напомнить про существование vfork — да, это тоже вариант замены устаревшего fork. Но его описание, с моей точки зрения, выглядит как костыль. Тот же posix_spawn выглядит куда красивее архитектурно.
Если же вы хотели напомнить, что fork не копирует адресное пространство — а копирует только таблицы страниц — я про это помню и именно это копирование и называю лишней работой. Тот факт, что оно делается куда быстрее полного копирования адресного пространства, еще не означает что оно делается бесплатно с точки зрения производительности.
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).mechanicusilius
31.01.2017 15:02Этот пост не про то, как правильно писать программы плодя чайлды, а про то, что делает системный вызов fork (именно про него спрашивают чаще всего на собесах админов, а не про vfork, не про clone, и не про posix_spawn). Но я с удовольствием прочитаю вашу статью на эту тему. Тема, безусловно, интересная.
simpleadmin
03.02.2017 15:202 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, например:VBart
03.02.2017 16:19+1Это классическая практика добавлять слэш в конце и традиционно это делается через 301-ый редирект, который кэшируется браузером. Если у вас путь в форме или скриптах приписан неверно (а откуда ещё возьмется POST-запрос?), то нужно править код, а не затыкать проблему редиректом. Редирект на запросы с телом — это вообще плохо, вне зависимости от кода.
amaksr
Обычно администраторы не пишут код, и часто ограничены в выборе решений. Например, какое-то покупное приложение может требовать работы под апачем или IIS, а под nginx разработчик не тестировал, и гарантий не дает. В этой ситуации админ скорее должен знать из каких кубиков-компонентов и как собрать инфраструктуру, чтоб «все работало» и без явных узких мест: может nginx стоит поставить перед апачем в качестве reverse proxy, или как сконфигурировать high-availability, или какие бэкап-решения подходят под требования, и т.п.
bormotov
Поэтому, unix-way и выигрывает — стандартизация способов взаимодействия, упрощает выбор конкретного «кубика».
И вопрос выбора IIS/apache — проверенные производителем или nginx — не проверенный, сводится к тому, понимает ли админ, что нужно программе, от httpd, и если эти «нужности» обеспечивает IIS/apache, сможет ли обеспечить nginx. А еще лучше, конечно, уметь протестировать.
muon
bormotov
хороший критерий, но тоже нужно взвешивать
— как часто требуется общаться с поддержкой, насколько дороже обойдется этот процесс, против преимуществ nginx?
— можно ли поддержку сфокусировать на проблему продукта, а не возможную проблему взаимодействия с «нетестированным компонентом инфраструктуры»?