Пулы потоков: ускоряем NGINX в 9 и более раз

http://nginx.com/blog/thread-pools-boost-performance-9x/
  • Перевод
Как известно, для обработки соединений NGINX использует асинхронный событийный подход. Вместо того, чтобы выделять на каждый запрос отдельный поток или процесс (как это делают серверы с традиционной архитектурой), NGINX мультиплексирует обработку множества соединений и запросов в одном рабочем процессе. Для этого применяются сокеты в неблокирующем режиме и такие эффективные методы работы с событиями, как epoll и kqueue.

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

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

У медали есть и обратная сторона. Главной проблемой асинхронного подхода, а лучше даже сказать «врагом» — являются блокирующие операции. И, к сожалению, многие авторы сторонних модулей, не понимая принципов функционирования NGINX, пытаются выполнять блокирующие операции в своих модулях. Такие операции способны полностью убить производительность NGINX и их следует избегать любой ценой.

Но даже в текущей реализации NGINX не всегда возможно избежать блокировок. И для решения данной проблемы в NGINX версии 1.7.11 был представлен новый механизм «пулов потоков». Что это такое и как его применять разберем далее, а для начала познакомимся с нашим врагом в лицо.

Проблема


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

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

Рабочий процесс получает события из ядра и обрабатывает их.

Такими событиями могут быть: события таймера, поступление новых данных или отправка ответа и освобождение места в буфере, уведомления об ошибках в соединении или его закрытие. NGINX получает пачку таких событий и начинает их по очереди обрабатывать, выполняя необходимые действия. Так вся обработка очереди событий происходит в простом цикле в одном потоке. NGINX извлекает из очереди события одно за другим и производит какие-то действия, например, пишет в сокет данные или читает. В большинстве случаев это происходит настолько быстро (чаще всего это просто копирования небольших объемов данных в памяти), что можно считать обработку всех событий мгновенной.

Вся обработка происходит простым циклом в одном потоке.

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

Так, под блокирующей операцией мы подразумеваем любую операцию, которая задерживает цикл обработки событий на существенное время. Операции можно назвать блокирующими по разным причинам. Например, NGINX может быть занят долгой ресурсоемкой вычислительной операцией, либо он может ожидать доступа к какому-то ресурсу (жесткому диску, мьютексу, библиотечному вызову, ожидающему ответа от базы данных в синхронном режиме, и т. д.). Ключевым моментом тут является то, что во время выполнения этих операций рабочий процесс не может делать более ничего полезного, не может обрабатывать другие события, хотя у нас зачастую есть еще свободные ресурсы, и события, ожидающие далее в очереди, могут их использовать.

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

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

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

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

Некоторые операционные системы предоставляют интерфейсы для асинхронного чтения файлов и NGINX умеет эффективно использовать их (см. описание директивы aio). Хорошим примером такой системы является FreeBSD. К сожалению, нельзя сказать того же о Linux. Хотя в Linux и существует некий асинхронный интерфейс для чтения файлов, но он обладает рядом существенных недостатков. Одним из таких является требования к выравниванию чтений и буферов. С этим NGINX успешно с справляется, но вторая проблема хуже. Для асинхронного чтения требуется установка флага O_DIRECT на файловом дескрипторе. Это означает, что все данные будут читаться с диска минуя кэш страниц операционной системы (т. н. page cache), что во многих случаях не является оптимальным и существенно увеличивает нагрузку на дисковую подсистему.

В частности для решения данной проблемы в NGINX 1.7.11 и был представлен новый механизм пулов потоков. Они пока не включены в NGINX Plus, но вы можете связаться с отделом продаж, если желаете испытать сборку NGINX Plus R6 с пулами потоков.

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

Пулы потоков


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

Обработка заказов курьерской службой не блокирует очередь.

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

Рабочий процесс отдает обработку блокирующих операций в пул потоков.

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

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

В настоящий момент выгрузка операций в пул потоков реализована только для системного вызова read() на большинстве операционных систем, а также для sendfile() на Linux. Мы продолжим исследования данного вопроса и, вероятно, в будущем реализуем выполнение и других операций пулом потоков, если это даст выигрыш в производительности.

Тестируем производительность


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

Для это требуется набор данных, который гарантированно не поместится в кэш операционной системы. На машине с объемом оперативной памяти в 48 Гб было сгенерировано 256 Гб файлов по 4 Мб каждый, содержащих рандомные данные и запущен NGINX версии 1.9.0 для их раздачи.

Конфигурация достаточно проста:

worker_processes 16;

events {
    accept_mutex off;
}

http {
    include mime.types;
    default_type application/octet-stream;

    access_log off;
    sendfile on;
    sendfile_max_chunk 512k;

    server {
        listen 8000;

        location / {
            root /storage;
        }
    }
}

Как вы можете заметить, для получения лучших показателей произведен небольшой тюнинг: отключено логирование, отключен accept_mutex, включен sendfile и настроено значение sendfile_max_chunk. Последнее позволяет сократить время блокировки на вызове sendfile(), поскольку в этом случае NGINX не станет пытаться прочесть и отправить весь файл за раз, а будет это делать частями по 512 килобайт.

Машина снабжена двумя процессорами Intel Xeon E5645 (всего 12 ядер, 24 HyperThreading потока) и сетевым интерфейсом 10 ГБит. Дисковая подсистема представляет из себя 4 жестких диска Western Digital WD1003FBYX объединенных в RAID10 массив. Все это управляется операционной системой Ubuntu Server 14.04.1 LTS.
Конфигурация тестового стенда.

В качестве клиентов выступают две аналогичные по характеристикам машины. На одной из них запущен wrk, создающий постоянную нагрузку Lua-скриптом. Скрипт запрашивает файлы из хранилища в случайном порядке используя 200 параллельных соединений. Назовем данную нагрузку паразитной.

С другой машины-клиента мы будем запускать wrk, который будет запрашивать один и тот же файл в 50 потоков. Поскольку к данному файлу идет постоянное обращение, то, в отличие от файлов, запрашиваемых в случайном порядке, он не будет успевать вымываться из кэша операционной системы и его чтение всегда будет происходить из памяти. Назовем такую нагрузку тестовой.

Производительность мы будем измерять по показателям ifstat на сервере и статистике wrk со второй машины-клиента.

Итак, первый запуск без использования пулов потоков показывает очень скромные результаты:

% ifstat -bi eth2
eth2
Kbps in  Kbps out
5531.24  1.03e+06
4855.23  812922.7
5994.66  1.07e+06
5476.27  981529.3
6353.62  1.12e+06
5166.17  892770.3
5522.81  978540.8
6208.10  985466.7
6370.79  1.12e+06
6123.33  1.07e+06

Как видно с данной конфигурацией и под такой нагрузкой сервер способен выдавать порядка одного гигабита в секунду. При этом в top-е можно наблюдать, что все рабочие процессы NGINX находятся большую часть времени в состоянии блокировки на I/O (помечены буквой D):

top - 10:40:47 up 11 days,  1:32,  1 user,  load average: 49.61, 45.77 62.89
Tasks: 375 total,  2 running, 373 sleeping,  0 stopped,  0 zombie
%Cpu(s):  0.0 us,  0.3 sy,  0.0 ni, 67.7 id, 31.9 wa,  0.0 hi,  0.0 si,  0.0 st
KiB Mem:  49453440 total, 49149308 used,   304132 free,    98780 buffers
KiB Swap: 10474236 total,    20124 used, 10454112 free, 46903412 cached Mem

  PID USER     PR  NI    VIRT    RES     SHR S  %CPU %MEM    TIME+ COMMAND
 4639 vbart    20   0   47180  28152     496 D   0.7  0.1  0:00.17 nginx
 4632 vbart    20   0   47180  28196     536 D   0.3  0.1  0:00.11 nginx
 4633 vbart    20   0   47180  28324     540 D   0.3  0.1  0:00.11 nginx
 4635 vbart    20   0   47180  28136     480 D   0.3  0.1  0:00.12 nginx
 4636 vbart    20   0   47180  28208     536 D   0.3  0.1  0:00.14 nginx
 4637 vbart    20   0   47180  28208     536 D   0.3  0.1  0:00.10 nginx
 4638 vbart    20   0   47180  28204     536 D   0.3  0.1  0:00.12 nginx
 4640 vbart    20   0   47180  28324     540 D   0.3  0.1  0:00.13 nginx
 4641 vbart    20   0   47180  28324     540 D   0.3  0.1  0:00.13 nginx
 4642 vbart    20   0   47180  28208     536 D   0.3  0.1  0:00.11 nginx
 4643 vbart    20   0   47180  28276     536 D   0.3  0.1  0:00.29 nginx
 4644 vbart    20   0   47180  28204     536 D   0.3  0.1  0:00.11 nginx
 4645 vbart    20   0   47180  28204     536 D   0.3  0.1  0:00.17 nginx
 4646 vbart    20   0   47180  28204     536 D   0.3  0.1  0:00.12 nginx
 4647 vbart    20   0   47180  28208     532 D   0.3  0.1  0:00.17 nginx
 4631 vbart    20   0   47180    756     252 S   0.0  0.1  0:00.00 nginx
 4634 vbart    20   0   47180  28208     536 D   0.0  0.1  0:00.11 nginx
 4648 vbart    20   0   25232   1956    1160 R   0.0  0.0  0:00.08 top
25921 vbart    20   0  121956   2232    1056 S   0.0  0.0  0:01.97 sshd
25923 vbart    20   0   40304   4160    2208 S   0.0  0.0  0:00.53 zsh

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

Running 1m test @ http://192.0.2.1:8000/1/1/1
  12 threads and 50 connections
  Thread Stats   Avg    Stdev     Max  +/- Stdev
    Latency     7.42s  5.31s   24.41s   74.73%
    Req/Sec     0.15    0.36     1.00    84.62%
  488 requests in 1.01m, 2.01GB read
Requests/sec:      8.08
Transfer/sec:     34.07MB

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

А теперь подключим пул потоков, для чего добавим директиву aio threads в блок location с хранилищем:

location / {
    root /storage;
    aio threads;
}

и попросим наш NGINX перезагрузить конфигурацию.

Повторим тест:

% ifstat -bi eth2
eth2
Kbps in  Kbps out
60915.19  9.51e+06
59978.89  9.51e+06
60122.38  9.51e+06
61179.06  9.51e+06
61798.40  9.51e+06
57072.97  9.50e+06
56072.61  9.51e+06
61279.63  9.51e+06
61243.54  9.51e+06
59632.50  9.50e+06

Теперь наш сервер выдает 9,5 ГБит/сек (против ~1 ГБит/сек без пулов потоков)!

Вероятно он мог бы отдавать и больше, но это является практическим пределом для данного сетевого интерфейса и NGINX упирается в пропускную способность сети. Рабочие процессы большую часть времени спят в ожидании событий (находятся в состоянии S):

top - 10:43:17 up 11 days,  1:35,  1 user,  load average: 172.71, 93.84, 77.90
Tasks: 376 total,  1 running, 375 sleeping,  0 stopped,  0 zombie
%Cpu(s):  0.2 us,  1.2 sy,  0.0 ni, 34.8 id, 61.5 wa,  0.0 hi,  2.3 si,  0.0 st
KiB Mem:  49453440 total, 49096836 used,   356604 free,    97236 buffers
KiB Swap: 10474236 total,    22860 used, 10451376 free, 46836580 cached Mem

  PID USER     PR  NI    VIRT    RES     SHR S  %CPU %MEM    TIME+ COMMAND
 4654 vbart    20   0  309708  28844     596 S   9.0  0.1  0:08.65 nginx
 4660 vbart    20   0  309748  28920     596 S   6.6  0.1  0:14.82 nginx
 4658 vbart    20   0  309452  28424     520 S   4.3  0.1  0:01.40 nginx
 4663 vbart    20   0  309452  28476     572 S   4.3  0.1  0:01.32 nginx
 4667 vbart    20   0  309584  28712     588 S   3.7  0.1  0:05.19 nginx
 4656 vbart    20   0  309452  28476     572 S   3.3  0.1  0:01.84 nginx
 4664 vbart    20   0  309452  28428     524 S   3.3  0.1  0:01.29 nginx
 4652 vbart    20   0  309452  28476     572 S   3.0  0.1  0:01.46 nginx
 4662 vbart    20   0  309552  28700     596 S   2.7  0.1  0:05.92 nginx
 4661 vbart    20   0  309464  28636     596 S   2.3  0.1  0:01.59 nginx
 4653 vbart    20   0  309452  28476     572 S   1.7  0.1  0:01.70 nginx
 4666 vbart    20   0  309452  28428     524 S   1.3  0.1  0:01.63 nginx
 4657 vbart    20   0  309584  28696     592 S   1.0  0.1  0:00.64 nginx
 4655 vbart    20   0  30958   28476     572 S   0.7  0.1  0:02.81 nginx
 4659 vbart    20   0  309452  28468     564 S   0.3  0.1  0:01.20 nginx
 4665 vbart    20   0  309452  28476     572 S   0.3  0.1  0:00.71 nginx
 5180 vbart    20   0   25232   1952    1156 R   0.0  0.0  0:00.45 top
 4651 vbart    20   0   20032    752     252 S   0.0  0.0  0:00.00 nginx
25921 vbart    20   0  121956   2176    1000 S   0.0  0.0  0:01.98 sshd
25923 vbart    20   0   40304   3840    2208 S   0.0  0.0  0:00.54 zsh

И у нас еще есть солидный запас по ресурсам процессора.

Результаты wrk со второй машины:

Running 1m test @ http://192.0.2.1:8000/1/1/1
  12 threads and 50 connections
  Thread Stats   Avg      Stdev     Max  +/- Stdev
    Latency   226.32ms  392.76ms   1.72s   93.48%
    Req/Sec    20.02     10.84    59.00    65.91%
  15045 requests in 1.00m, 58.86GB read
Requests/sec:    250.57
Transfer/sec:      0.98GB

Среднее время отдачи 4 Мб файла сократилось с 7.42 секунд до 226.32 миллисекунд, т.е. в ~33 раза, а количество обрабатываемых запросов в секунду возросло в 31 раз (250 против 8)!

Объясняется все это тем, что теперь запросы более не ждут в очереди на обработку, пока рабочие процессы заблокированы на чтении с диска, а обслуживаются свободными потоками. И пока дисковая подсистема делает свою работу как может, обслуживая наш “паразитный” трафик с первой машины, NGINX использует оставшиеся ресурсы процессора и пропускную способность сети, чтобы обслужить второго клиента из памяти.

Серебряной пули не существует


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

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

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

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

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

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

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

Дело в том, что отсутствует соответствующая поддержка со стороны ядра операционной системы. Первые попытки добавить такую возможность в Linux в виде системного вызова fincore() относятся к 2010 году, но «воз и ныне там». Позже были попытки в виде системного вызова preadv2() и флага RWF_NONBLOCK (подробности можно узнать из статей Non-blocking buffered file read operations и Asynchronous buffered read operations на LWN.net) — но судьба и этих патчей по прежнему под вопросом. Печально, что виной всему этому похоже является пресловутый байкшединг (споры о том, какого цвета фломастеры лучше пахнут).

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

Конфигурация


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

Конфигурация достаточно простая и вместе с тем очень гибкая. Для начала вам потребуется NGINX версии 1.7.11 или выше, собранный с флагом --with-threads. В простейшим случае настройка выглядит элементарно. Все, что необходимо для включения выгрузки операций чтения и отправки файлов в пул потоков, это директива aio на уровне http, server или location, установленная в значение threads:

aio threads;

Это минимально возможный вариант настройки пулов потоков. На самом деле он является сокращенной версией такой конфигурации:

thread_pool default threads=32 max_queue=65536;
aio threads=default;

Она задает пул потоков default, в котором будут работать 32 потока и максимально допустимый размер очереди заданий составляет 65536. Если очередь заданий переполняется, то NGINX отклоняет запрос и логирует ошибку:

thread pool "NAME" queue overflow: N tasks waiting

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

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

thread_pool one threads=128 max_queue=0;
thread_pool two threads=32;

http {
    server {
        location /one {
            aio threads=one;
        }

        location /two {
            aio threads=two;
        }
    }
…
}

Если параметр max_queue не указан явно, как в пуле two, то используется значение по умолчанию, равное 65536. Как видно из примера, можно задать нулевой размер очереди. Тогда пул сможет одновременно принимать в обработку только такое количество заданий, сколько у него имеется свободных потоков и не будет ожидающих в очереди заданий.

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

Одним из решений тут может стать организация RAID массива. У такого подхода конечно же есть свои плюсы и минусы. Но сегодня NGINX готов предложить вам другой подход:

# В нашей системе каждый из жестких дисков примонтирован в одну из следующих директорий:
# /mnt/disk1, /mnt/disk2 или /mnt/disk3 соответственно

thread_pool pool_1 threads=16;
thread_pool pool_2 threads=16;
thread_pool pool_3 threads=16;

http {
    proxy_cache_path /mnt/disk1 levels=1:2 keys_zone=cache_1:256m max_size=1024G use_temp_path=off;
    proxy_cache_path /mnt/disk2 levels=1:2 keys_zone=cache_2:256m max_size=1024G use_temp_path=off;
    proxy_cache_path /mnt/disk3 levels=1:2 keys_zone=cache_3:256m max_size=1024G use_temp_path=off;

    split_clients $request_uri $disk {
        33.3%     1;
        33.3%     2;
        *         3;
    }
    
    server {
        …
        location / {
            proxy_pass http://backend;
            proxy_cache_key $request_uri;
            proxy_cache cache_$disk;
            aio threads=pool_$disk;
            sendfile on;
        }
    }
}

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

Для равномерного распределения нагрузки между кэшами (а соответственно и жесткими дисками) используется модуль split_clients, который прекрасно для этого подходит.

Параметр use_temp_path=off у директивы proxy_cache_path инструктирует NGINX сохранять временные файлы в той же директории, где находятся данные кэша. Это необходимо во избежание копирования данных с одного диска на другой при сохранении ответа в кэш.

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

Ведь ваши клиенты любят индивидуальных подход? Будьте уверены ваши жесткие диски тоже. ;)

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

Выводы


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

Как уже упоминалось ранее, пулы потоков могут быть использованы и для любых других операций и работы с библиотеками, не имеющими асинхронного интерфейса. Потенциально это открывает новые возможности для реализации модулей и функциональности, реализация которой без ущерба для производительности ранее была неосуществимой в разумные сроки. Можно потратить много усилий и времени на написание асинхронного варианта имеющийся библиотеки или в попытках добавить такой интерфейс, но возникал вопрос: «стоит ли игра свеч»? С пулами потоков данную задачу можно решить гораздо проще, создавая модули, работающие с блокирующими вызовами, и при этом не мешая NGINX выполнять свою основную задачу обрабатывая остальные запросы.

Так что много нового и интересного ждет NGINX в будущем. Оставайтесь с нами!
Поделиться публикацией
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Реклама
Комментарии 58
  • 0
    Погодите, а что, nginx не использует aio_* при общении с диском?!
    • +7
      Я об этом писал в статье. На линукс все плохо. См. lse.sourceforge.net/io/aio.html — с тех пор не продвинулись.
      aio_*() просто обертка над всем этим.
      • 0
        … вот вроде ж читал статью, а этот абзац как-то проглядел.

        Спасибо! Шок, это по-нашему. Я использовал асинхронный ввод-вывод в ТойЧтоНельзяУпоминать — там, конечно, кода вагон, но работало весьма стабильно…
        • 0
          Наверное я тут перепутал с libaio. То POSIX AIO, что сейчас в glibc — это тоже обертка, но уже над тредами и довольно примитивная, чтобы её использовать для серьезных задач.
          • 0
            А вы не в курсе, под OSX тут как дело обстоит? Я сходу попробовал погуглить, но, видимо, т.к. я не совсем в теме, ничего полезного не нашлось :(
            • 0
              OSX не серверная система. Не стоит пытаться использовать её в этом качестве, можно наступить на огромное количество различных граблей.
              • 0
                Есть хостеры, которые продают серверы с OSX server.
                • 0
                  Что не отменяет написанного мной выше. =)

                  Мы поддерживаем OSX как платформу для разработки, поскольку многие программисты пользуются макбуками. Но граблей там порядочно. Вот из последнего, не так давно натыкались на ту же проблему, что и разработчик Rust: erickt.github.io/blog/2014/11/19/adventures-in-debugging-a-potential-osx-kernel-bug — банально баг в ядре, связанный с race-condition, который приводит к тому, что попытка записи в сокет может вызывать ошибку EPROTOTYPE, которую никто в этом месте не ожидает увидеть.

                  И подобного там достаточно, и подозреваю достаточно того, о чем еще неизвестно, но сразу повылезает при попытке использовать OSX под более-менее серьезной нагрузкой.
                  • 0
                    Ну так то да, но сервера с OSX таки попадаются у клиентов.
                    Теперь есть еще один повод советовать сменить, спасибо :)
        • +21
          Офигительный перевод Бартенева, сделаный Бартеневым )) Неделя nginx продолжается… Ура!
          • 0
            Я так понял, что теперь будет проще на IOCP переписать под Windows?
            • +2
              Нет. IOCP тут ортогонально. Прелесть данного механизма пулов потоков заключается в том, что он абсолютно независим от основного процесса обработки событий, не использует с ним разделяемых ресурсов, кроме двух очередей, что позволяет практически избежать локов и вмешательств в основные структуры.

              Кроме того он реализован только под *nix.
              • 0
                Так через асинхронные функции с OVERLAPPED (под Windows) не будет достигнут такой же эффект?
                • 0
                  Почитал про overlapped. Кажется это скорее про реализацию нормального ядерного AIO и отдельные потоки там не нужны. Но я могу ошибаться, поскольку плохо знаком с windows даже на уровне пользователя.

                  Возможно sebres сможет ответить.

                  Важно понимать, что текущая реализация пулов не делает nginx многопоточным в плане обработки событий, он по-прежнему обрабатывает события на дескрипторах асинхронно и в одном потоке на рабочий процесс. А отдельные потоки из пулов используются исключительно для выгрузки долгих блокирующих операций, при этом они изолированы от основного потока через две сериализованные очереди (очередь ожидающих обработки заданий и очередь выполненных заданий).
                  • 0
                    Да, на Windows OVERLAPPED это корректный ядерный AIO.
                    • +2
                      Так то оно так. И это правда реактор на AIO. Просто тонкости реализации и «тонкие» настройки там играют очень заметную роль. Много и много фрагильнее линуксовых. Например в свое время была такая библиотека как ACE, так там до какой-то версии реактор acceptor-ов «конфликтовал» с реактором worker-ов. Ужасно бажная была реализация.
                      Проблема еще в том, что под win GetOverlappedResult (или WSAGetOverlappedResult) многопоточно, это не тоже самое что GetOverlappedResult многопроцессно. Вся разница в том, что оно изначально заточено под пул потоков и там более менее оптимально «ядерно» работает. В случае же много процессов есть небольшой конфликт в ядре в генераторе события и обертка над наследуемыми сокетами там в одном месте как-бы настолько синхронна (и насколько помню даже не атомарно), что не просто context switch, а процессы тупо «распараллеливаются» — это не то, чтобы узкое горлышко — по моему такое называют порогами (как на реке), т.е. потоки разных процессов их проходят друг за другом.
                      Кто погоняет такой реактор SoftICE-ом с дебажным win-kernel, откроет для себя много нового по поводу thread != process под windows.
                      • 0
                        при этом они изолированы от основного потока через две сериализованные очереди (очередь ожидающих обработки заданий и очередь выполненных заданий).
                        А что если их на динамических ринг-буферах переписать (возможно чанками), я имею ввиду очереди задач, т.е. ngx_thread_task_post, ngx_thread_pool_cycle и иже с ними.
                        Один писатель же… Ну а задачи забирать в пуле атомарно проставляя start/end.

                        Я бы уже повозился с этим, только последнее время все винда — а там нет notify (ну event method не тот — т.е. пул не работает от слова совсем, а жаль). Хотя непонятно почему, ведь и прослушка т.е. листенер организована polling-ом, и цикл как в *ксах крутится, почему бы в том цикле не сделать и вызов событий из ngx_notify, непонятно…
                        Не думаю что сложно должно быть (потирая руки).
                        • +1
                          У меня изначально там были безлоковые очереди с семафорами, но благодаря стараниям всем известного персонажа их пришлось выкинуть, дескать слишком сложно. А с condition variables у нас там все равно уже лок есть.

                          Так что не рекомендую даже пытаться — зарежет.
                          • 0
                            Это да — зверь человек!
                            Но у меня чукчи жеж своя форка имеется :).

                            А можно поподробнее про безлоковые-семафорные, можно PR-ом сюда :) Ну или куском кода в личку бросить (просительно-умалятельный смайл).
                            • 0
                              у нас там все равно уже лок есть
                              Тот другой всем локам-лок:
                              • во первых он на пуле (1+N конкурентов), а на «condition variables» будет если не ошибаюсь 1+1
                              • ну а во вторых один или два лока — две большие разницы (с)

                              • +1
                                Это не со мной спорить нужно. ;-) Всем известный персонаж глубоко уверен, что контеншн там не грозит почти никогда и с точки зрения производительности абсолютно некритично, а вот с точки зрения сложности кода любая оптимизация — усложнение.
                                • +1
                                  Глубокая и непоколебимая уверенность в своей правоте есть по сути убийца многих больших начинаний.

                                  Я даже аргументы искать не буду, согласен с тобой — как минимум бесполезно, да и нервы жалко. У меня по работе своих таких сладкая парочка имеется. Больно надо мне еще и с вашим бодаться ;)
                                  • +1
                                    Про нервы — это очень верно подмечено. Очень много их на это уходит.
                  • 0
                    Пулы потоков это хорошо, но проблема еще и в том, что нет возможности узнать чем занимается конкретный поток. Быть может он ждет на блокирующей операции, а процессор в результате простаивает. К сожалению, системного вызова который бы сообщил состояние thread в Linux нет.
                    • +4
                      Так задача потока в пуле и заключается в том, чтобы ждать на блокирующей операции.
                      • 0
                        Если использовать их только для этого то все ок (как у вас указано — для обслуживания read()) но если добавить что-то еще то возможны варианты.

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

                        Разумеется, пока вы используете пулы потоков только для ожидания — все более-менее ок.
                        • +2
                          Для интенсивного счета можно будет настроить отдельный пул потоков. NGINX позволяет создать их сколько угодно. Зачем смешивать все операции в одну кучу? У него будет своя очередь и необходимое количество тредов.
                          • 0
                            Возможно вы и правы, и я хочу странного.
                            Просто хочется меньше задумываться над спецификой.
                    • +1
                      Спасибо за статью. А какие блоги можете посоветовать nginx internals с точки зрения разработки/доработки модулей? Из того, что находил хорошего — Evan Miller, agentzh и что-то у Joshua Zhu.
                      • +4
                        Пожалуй к этому списку нечего больше добавить. Самый лучший nginx internals постигается чтением исходников.

                        Есть ещё выступление Валерия Холодкова на HL++ 2008 и статья по мотивам: www.grid.net.ru/nginx/nginx-modules.html и его же блог, посвященный исследованиям внутренностей nginx: www.nginxguts.com
                      • +1
                        lkml.org/lkml/2015/3/16/891 — Уже 7 версию пытаются… Andrew Morton конечно бухтит но и смысл в его словах есть.
                        Нам для Postgres это то же нужно, что бы адекватнее косты подсчитывать (fincore в смысле). Надо как то их всех пнуть.
                        • 0
                          Да, я пару месяцев назад общался с Милошем. С его патчами нам будет не хватать sendfile(). В этом отношении подход Мортона чуть более универсален, хотя менее эффективен и порождает маловероятный на практике, но всё же существующий race-condition между вызовом fincore() и соответвующим read() или sendfile(). С другой стороны fincore() нам не поможет при записи. При определенных условиях write() блокируется и хорошо бы об этом узнать заранее, чтобы отправить соответвующий таск в пул.

                          Милош настроен твердо: twitter.com/mtanski/status/611932316865298433
                        • +2
                          Чудо! Не могу поверить, что в самом популярном сервере поддержка пула потоков для длительных операций появилась только в 2015 году. При том, что паттерн Proactor известен науке по меньшей мере с 90-х.

                          Забавно, что автор винит ОС в том, что нет возможности узнать, какие данные закешированы, а какие нет.
                          Кешируйте сами — делов-то!

                          Именно так работает, например, video download сервер известной социальной сети. То, что закешировано в памяти, он отдаёт сразу из потока-селектора. А за тем, чего в кеше нет, обращается асинхронно из отдельного пула. В результате один сервер отдаёт наружу до 40 Гбит/с, причём сам сервер написан даже не на C, а на «тормозной» Java.
                          • +5
                            Не могу поверить, что в самом популярном сервере поддержка пула потоков для длительных операций появилась только в 2015 году. При том, что паттерн Proactor известен науке по меньшей мере с 90-х.
                            Не очень понял, какая логическая связь между двумя процитированными предложениями. Мне кажется её нет, но отвечу. Не было необходимости, да и сейчас в большинстве случаев включать пул потоков нет необходимости. Для большинства типичных задач он не нужен. И даже в тех случаях, когда он нужен, есть и другие варианты решения проблемы.

                            Забавно, что автор винит ОС в том, что нет возможности узнать, какие данные закешированы, а какие нет.
                            Кешируйте сами — делов-то!
                            И файловую систему пишите сами, и tcp-стек, и т.д. по списку. Как только вы начинаете кэшировать данные сами, то натыкаетесь на необходимость копировать огромные объемы данных из ядра в пользовательское пространство и обратно. Такой системный вызов, как sendfile() был создан специально для того, чтобы этим не заниматься. Вы наверное не сталкивались, когда производительность начинает упираться в память и приходится делать всё возможное, чтобы уменьшать количество копирований.

                            Именно так работает, например, video download сервер известной социальной сети. То, что закешировано в памяти, он отдаёт сразу из потока-селектора. А за тем, чего в кеше нет, обращается асинхронно из отдельного пула. В результате один сервер отдаёт наружу до 40 Гбит/с, причём сам сервер написан даже не на C, а на «тормозной» Java.
                            Безусловно есть разные подходы к решению задачи. Каждый подход имеет свои плюсы и минусы. Инженеры из известной социальной сети выбрали такой подход и мы не знаем всех факторов, которые повлияли на их решение, поэтому я не возьмусь, например, давать оценки.

                            Информация про 40 Гбит/с особо не говорит ни о чем, ибо число сильно далеко от того, чтобы производить впечатление в 2015 году. И если уж мериться, то нужно делить этот показатель на стоимость сервера. Одно дело раздавать 40 Гбит/c с одного сервера, а другое дело раздавать треть всего интернет трафика в США и значительную долю всего мирового трафика с серверов, собранных из недорогих комплектующих. Когда количество серверов исчесляется десятками и сотнями тысяч, то их стоимость начинает иметь существенное значение. Ну и т. д. ;)
                            • 0
                              Прошу прощения, писал ночью — грубовато получилось. Просто искренне удивился, что лежащая на поверхности идея, которую я и сам не раз упоминал в докладах про архитектуру ОК, оказывается, до недавнего времени, в NGINX не была реализована. Думал, может, какие-то подводные камни были. Ну, раз просто было не нужно — то ладно.

                              Ручное кеширование не отменяет sendfile. Я ж не зря ссылку на презентацию дал — там всё наглядно рассказано и показано. Создаёте один большой файл в /dev/shm, мапите в адресное пространство процесса. Работаете как с обычной памятью, разбиваете на блоки, вытесняемые по принципу LRU. А в сеть отдаёте блоки через sendfile, минуя user space.

                              40 Гбит/с, поверьте, у нас тоже не суперкомпьютеры раздают. Понятно, что это ближе к верхушке нашей линейки, но я про то, что это не экспериментальный стенд, а commodity оборудование, стоящее на эксплуатации.
                              • +1
                                На 2002 год, когда Игорь начал разработку nginx, он пробовал разные подходы и смотрел на треды. На тот момент с поддержкой потоков в интересующих операционных системах всё было очень и очень плохо. Поэтому да, подводные камни были.
                                • +1
                                  С предложенным подходом возникает несколько вопросов. Как минимум оно не будет нормально работать на FreeBSD, поскольку там с tmpfs всё плохо в этом месте или было плохо до недавнего времени (могу уточнить этот момент у наших FreeBSD разработчиков, поскольку сам я на Linux специализируюсь).

                                  Второй момент в том, что всё это сильно напоминает попытку реализации кэша страниц, но в пользовательском простанстве. Я вот не уверен, что это будет сильно лучше работать. Зато точно уверен, что потребует трудозатрат на более-менее универсальную реализацию и последующую поддержку. Тем более, что большинство проблем связаны как раз с тем случаем, когда горячие данные никак в память не помещаются и любое кэширование малоэффективно. Докладчик упоминает, что данные в LRU кэше в среднем живут 40 секунд. Возникает вопрос, что будете делать, когда данные там будут жить 40 микросекунд?

                                  Решая отдельную специфическую задачу можно не наступить на ряд граблей, которые всплывут при попытке реализовать универсальное решение. Например, очень скоро обнаружится, что сильно не хватает умного readahead'а, и его тоже потребуется реализоваывать в юзерспейсе.
                                  • 0
                                    Решение, конечно же, должно быть опциональным и конфигурируемым. Я ведь не говорю, что нужно по умолчанию его использовать везде, включая FreeBSD, с которым, как вы утверждаете, и так всё хорошо. Или на системах, где кеш вообще бесполезен (у нас есть и такие — мы в это случае просто ставим в сервер дюжину SSD накопителей, и с кешами даже не заморачиваемся). Но в ряде случаев, как раз для раздачи тяжёлого медиаконтента с характерным профилем популярности, это и будет той серебряной пулей для Linux, о которой вы писали.

                                    Про затраты на разработку никто не спорит. Я лишь делюсь опытом, что этот подход: а) востребован, б) относительно просто реализуем, в) приносит пользу.

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

                                    Дисковый же кеш очень непредсказуем. Скажем, Linux может вдруг решить, что в данный момент кеш важнее хипа приложения, и выгрузить хип в своп, несмотря на любые настройки /proc/sys/vm.

                                    А что насчёт HTTPS? Как вам тут поможет дисковый кеш и sendfile? В нашем же случае мы без лишних копирований отправляем данные из того же кеша, заменив только sendfile на SSL_write. Конечно, 40 Гбит/с тут уже не будет, но 25 Гбит шифрованного трафика тоже немало.
                                    • +4
                                      Я знаю другой опыт, который подсказывает, что серебряной пулей это не будет, если вообще будет лучше. Кстати, так работает Varnish Cache например, он полностью управляет своим кэшом, но особого выигрыша там не видно.

                                      Есть две крайности — одна запихивать всё в ядро (аля TUX), а другая реализовывать всё в юзерспейсе. Последний подход чреват тем, что в итоге всё равно прослойка в виде ядра остается и проблемы ядра никуда не уходят, а только множаться. Ядро работает с сетью, ядро работает с диском, и от этого никуда не деться, если только не выкинуть ядро совсем, но это уже другая история. Я согласен с тем, что у приложения есть больше информации о том, как лучше кэшировать данные, но это не является причиной приложению брать на себя функции ядра. Это является поводом улучшить интерфейс ядра в этом месте.

                                      Пример одной социальной сети очень неубедительный. Его не с чем сравнить. В докладе я не услышал о том, что пытались сделать так-то, столкнулись с такими-то проблемами, их можно было преодолеть только таким вот решением. Скорее звучит, как мы решили, что так будет эффективнее, реализовали и вроде как работает, наши задачи выполняет. Но из этого нельзя сделать вывод, что решение получилось эффективным. Где сравнение с другими подходами?

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

                                      Улучшение, о котором вы говорите — скорее не улучшение, а наоборот ещё один механизм, который уже реализован ядром и его потребуется реализовывать заново. Сейчас всё это можно настроить с помощью dm-cache или bcache достаточно гибко и эффективно.

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

                                      В свою очередь с за'map'ленными в память файлами есть серьезная проблема. Сейчас если у нас происходит ошибка чтения с диска, то read() или sendfile() возвращает ошибку, приложение может эту ошибку обработать и продолжить работать дальше. С map() приложение упадет, что для асинхронного сервера, обрабатывающего тысячи запросов в одном процессе очень больно.

                                      Если мы будем копировать с диска в /dev/shm, то будет дополнительная работа, во-первых. Во-вторых, возникает проблема со свопингом, если наш кэш начнет свопиться, то всё «превратиться в тыкву».

                                      Из памяти можно отдавать столько, сколько позволяет шина памяти и сетевая подсистема, 40 Гбит/сек тут не предел. Сложности наступают тогда, когда нужно раздавать 150 терабайт с одного сервера и упираешься во все подряд так, что приходится пробовать различные подходы, работать с вендорами жестких дисков, оптимизировать прошивку, драйвера, различные подсистемы ядра и сетевой стек, изобретать новый алгоритм для congestion control в TCP. Всего этого в докладе одной социальной сети не наблюдается.

                                      В свою очередь советую послушать:
                                      events.yandex.ru/lib/talks/2682 — там же есть ответ на ваш вопрос о HTTPS ;-)
                                      events.yandex.ru/lib/talks/2396
                                      • 0
                                        По-моему, вы сейчас пытаетесь на ходу придумать оправдания, почему кеширование в /dev/shm плохо. Не хотите так делать — не надо, кто-нибудь другой сделает. Я ж не принуждаю, а просто делюсь положительным опытом.

                                        При этом столкнулись с проблемой вытеснения данных из кэша, до того, как отработает sendfile() и никак эту проблему не решили.
                                        Решили тривиальным reference counter'ом.

                                        И ещё ни разу не видел, чтобы при наличии свободных страниц ядро начинало бы свопить если не преодолен рубеж swappiness.
                                        А я, вот, видел, как при swappiness=0 Linux вместо освобождения page cache иногда начинает свопить полезную память. В тоже время со свопом кеша из /dev/shm проблем нет. Тем более, что кеш этот — одна непрерывная область, которая легко лочится через mlock. Так что метафора про «тыкву» неуместна.

                                        В свою очередь с за'map'ленными в память файлами есть серьезная проблема. Сейчас если у нас происходит ошибка чтения с диска
                                        С какого диска? Мы мапим файл из tmpfs, он целиком в памяти.

                                        Если мы будем копировать с диска в /dev/shm, то будет дополнительная работа
                                        Серьёзно? Если данные востребованы, и мы хотим их закешировать в памяти, как это можно сделать проще, не делая read?

                                        приходится пробовать различные подходы, работать с вендорами жестких дисков, оптимизировать прошивку, драйвера, различные подсистемы ядра и сетевой стек, изобретать новый алгоритм для congestion control в TCP
                                        Вот, как раз про всё это вас было бы очень интересно послушать :)
                                        • +1
                                          По-моему, вы сейчас пытаетесь на ходу придумать оправдания, почему кеширование в /dev/shm плохо. Не хотите так делать — не надо, кто-нибудь другой сделает. Я ж не принуждаю, а просто делюсь положительным опытом.
                                          У меня свое мнение на этот счет и свой опыт, поэтому я привожу аргументы в пользу того, что не стоит один положительный опыт обощать в качестве универсального решения.

                                          Решение с fincore() или RWF_NONBLOCK оно универсально. Поскольку его можно включить и оно у всех будет работать, не важно сколько у человека памяти и как она используется, не важно, что у человека на том же сервере ещё и база данных крутится, php-процессы и log rotate периодически запускается сжимая большие логи. А главное, ничего отдельно настраивать не придется, не придется подбирать размер кэша.

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

                                          Тем более, что кеш этот — одна непрерывная область, которая легко лочится через mlock. Так что метафора про «тыкву» неуместна.

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

                                          В вашей системе — кэш есть центр вселенной и всё вращается вокруг него, у множества пользователей nginx это не так. И завтра человек залочит 3 Гб памяти из имеющихся у него 4 на его VPS-ке, а в пик нагрузки у него php-fpm демоны улетят в своп.

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

                                          Я не исключаю, что подобный кэш будет когда-нибудь реализован в nginx для отдельных случаев, но сегодня нужны fincore() или RWF_NONBLOCK.

                                          С какого диска? Мы мапим файл из tmpfs, он целиком в памяти.
                                          И при этом файл пустой.

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

                                          Я понимаю, что вы делаете иначе. Следующий абзац и был про ваш случай, на что вы вполне резонно заметили:
                                          Серьёзно? Если данные востребованы, и мы хотим их закешировать в памяти, как это можно сделать проще, не делая read?
                                          Всё верно, изобретая промежуточный слой в виде tmpfs, вы вынуждены делать сперва read() и копировать в него данные.

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

                                          Вот, как раз про всё это вас было бы очень интересно послушать :)
                                          Я ссылку и дал ниже на пару докладов Глеба Смирнова, который непосредственно работает с Netflix-ом и может гораздо больше и лучше меня об этом рассказывать.
                                          • 0
                                            Спутанно получилось. Третий час ночи, пора спать.

                                            Если коротко, то:

                                            1. Есть различные подходы.

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

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

                                            4. У каждого из этих подходов есть приемущества и недостатки. И рассматривать их нужно в зависимости от решаемой задачи.

                                            5. Как бы то ни было, fincore() или RWF_NONBLOCK нам с текущим подходом поможет.
                                        • 0
                                          Кстати, к вопросу о других решениях. NGINX мы тоже тестировали, хотели использовать в качестве прокси. Не вышло: уже при трафике в 3 Гбит/с CPU usage улетал в 100%. Деталей, к сожалению, не знаю, могу потом у коллег уточнить. Просто о неудачах редко когда рассказывают на конференциях :) Хотя, согласен, отрицательный опыт тоже порой очень интересен.
                                          • +5
                                            Польза большинства отрицательных опытов гораздо выше большинства положительных.
                                        • +1
                                          В православной ОС идут работы по переносу части SSL в ядро как раз, чтобы не копировать лишниый раз.
                                    • 0
                                      Мда, я так выразился, что вы, наверное, подумали, будто у нас всего один download сервер :) Не, их много. И они генерируют львиную долю трафика, пусть и не Штатов, но хотя бы Рунета.
                                  • +1
                                    [удалено, промахнулся]
                                    • 0
                                      CentOS 7 x64

                                      $ uname -a
                                      Linux 3.10.0-229.1.2.el7.x86_64 #1 SMP x86_64 GNU/Linux
                                      
                                      $ nginx -V
                                      nginx version: nginx/1.8.0
                                      built by gcc 4.8.2 20140120 (Red Hat 4.8.2-16) (GCC) 
                                      ...
                                      --with-file-aio
                                      


                                      При вставке в /etc/nginx/nginx.conf строчки aio threads; получаю ошибку:

                                      [emerg] «aio threads» is unsupported on this platform

                                      PS: nginx из репозитория nginx.org/packages/centos/7

                                      Что я делаю не так?
                                      • 0
                                        Опция --with-file-aio не имеет отношения к тредам, она про файловое AIO в ядре. NGINX должен быть собран с опцией --with-threads. В нашем репозитории на данный момент с ней собирается только mainline версия пакетов и для тех дистрибутивов, где glibc не слишком старый и в нём есть поддержка eventfd(). Попробуйте поставить 1.9.2.
                                      • 0
                                        >>если бы существовал эффективный способ узнать заранее, находятся ли необходимые данные в памяти или нет, и только в последнем случае выгружать операцию в отдельный поток.
                                        То есть второй поток awk из примера всегда заново читает данные с диска, именно поэтому всего 250rps?
                                        • –1
                                          Всего 250?! Вы хоть раз такую нагрузку симулировали?
                                          А именно воссоздадим наиболее тяжелые условия, заставив NGINX выполнять смесь блокирующих и неблокирующих чтений, когда проблема блокировок на обращениях к диску проявит себя в полной мере.
                                          Вы через строчку читаете?
                                          И пока дисковая подсистема делает свою работу как может, обслуживая наш “паразитный” трафик с первой машины, NGINX использует оставшиеся ресурсы процессора и пропускную способность сети, чтобы обслужить второго клиента из памяти.

                                          Грубо говоря, есть огромный disk load (паразитный ли нет, есть не суть важно). Кэш вымывается, скорость чтения падает и т.д. Но из за того что имеем теперь пул потоков, а не один воркер, этот пул может отдать в 31 раз больше (т.е. целых 250 вместо всего 8 rps) например то, что еще или уже лежит в памяти.
                                          • 0
                                            Я так и не понял, кто следит за тем, чтобы эти вот эти небольшие данные всегда лежали в памяти и не вымавались оттуда, несмотря на огромный фоновый disk load?
                                            Можно ли например на location с сотнями тысяч картинок включить aio threads, а для location со статическими файлами дизайна сайта, которые всегда должны быть в памяти и отдаваться максимально быстро, не включать aio threads, не теряя в этом случае скорость на помещение задания в очередь?

                                            Nginx с aio threads теперь так же быстр например для тех же фотографий, как решение elliptics от yandex или backpack от habrahabr.ru/post/184652?
                                            • 0
                                              Я так и не понял, кто следит за тем, чтобы эти вот эти небольшие данные всегда лежали в памяти и не вымавались оттуда, несмотря на огромный фоновый disk load?
                                              Этим занимается операционная система. Если к данным регулярно обращаются, то они всегда всплывают наверх очереди в кэше страниц и не успевают из него вымываться. Пулы потоков тут не причем.

                                              Можно ли например на location с сотнями тысяч картинок включить aio threads, а для location со статическими файлами дизайна сайта, которые всегда должны быть в памяти и отдаваться максимально быстро, не включать aio threads, не теряя в этом случае скорость на помещение задания в очередь?
                                              Можно.
                                          • +1
                                            250rps по 4 мегабайта каждый — это чуть менее 1 гигабайта в секунду или ~8.4 гигабит в секунду. Поскольку данные запрашивались по 10 гигабитной сети с двух клиентов, то это просто та эффективная полоса, которая осталась свободной для запросов со второй машины, создающей тестовую нагрузку к одному файлу. Как я и написал в статье, в данном случае nginx уперся в максимальную пропускную способность сети на тестовом стенде.

                                            Именно поэтому 250rps.

                                            Как явно было указано, wrk со второй машины всегда получает данные из памяти. Благодаря этому он разогнался до ~8.4 гигабит и загрузил сеть по полной.

                                            Читается ли данные в потоке или в основном процессе — никак не влият на то, будут данные отданы из кэша страниц или прочитанны с диска. Просто если бы мы знали заранее, что данные находятся в памяти, то мы бы могли прочитать их в основном процессе, что было бы немного более эффективно.

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

                                            В случае, когда данные находятся в памяти, отправляя задание в поток nginx тратит больше ресурсов, чем если бы он выполнил чтение и отправку в рамках основного процесса. В данном тесте эти траты ничтожны по сравнению с выигрышем от избавления от блокировок на чтении с диска.
                                          • 0
                                            Есть ли смысл в установке ненулевого значения sendfile_max_chunks при использовании пула потоков?
                                            До пула потоков sendfile мог заблокировать рабочий процесс, но с потоками этой проблемы уже нет, не так ли?
                                            • +1

                                              Всё так. Смысла особого нет.

                                              • +1

                                                Подумал ещё и понял, что смысл всё-таки есть. Если sendfile() подолгу блокируется, то может возникнуть ситуация, когда все потоки из пула потоков окажутся заняты такими долгими вызовами, а в очереди на обработку будут накапливаться запросы от других клиентов. Чтобы все они как-то ротировались, сменяя друг друга, а не отваливались по таймауту, и может пригодится sendfile_max_chunks

                                            • 0
                                              При включённом в nginx'e aio_threads — системный Load Average вырастает на порядок и перестаёт быть объективным показателем, потому что даже при LA > 10 система остаётся отзывчивой и быстрой.

                                              Какие параметры посоветуете использовать вместо LA для контроля состояния системы?

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