BLOG | NGINX

Os pools de threads no NGINX aumentam o desempenho em 9x!

Valentin Bartenev Miniatura
Valentin Bartenev
Publicado em 19 de junho de 2015

É bem sabido que o NGINX usa uma abordagem assíncrona e orientada a eventos para lidar com conexões . Isso significa que, em vez de criar outro processo ou thread dedicado para cada solicitação (como servidores com arquitetura tradicional), ele manipula várias conexões e solicitações em um processo de trabalho. Para conseguir isso, o NGINX trabalha com soquetes em um modo não bloqueante e usa métodos eficientes como epoll e kqueue .

Como o número de processos de peso total é pequeno (geralmente apenas um por núcleo da CPU) e constante, muito menos memória é consumida e os ciclos da CPU não são desperdiçados na troca de tarefas. As vantagens dessa abordagem são bem conhecidas pelo exemplo do próprio NGINX. Ele lida com sucesso com milhões de solicitações simultâneas e é muito bem dimensionado.

Cada processo consome memória adicional e cada troca entre eles consome ciclos de CPU e destrói L-caches

Mas a abordagem assíncrona e orientada a eventos ainda tem um problema. Ou, como eu gosto de pensar, um “inimigo”. E o nome do inimigo é: bloqueio . Infelizmente, muitos módulos de terceiros usam chamadas de bloqueio, e os usuários (e às vezes até mesmo os desenvolvedores dos módulos) não estão cientes das desvantagens. Operações de bloqueio podem prejudicar o desempenho do NGINX e devem ser evitadas a todo custo.

Mesmo no código oficial atual do NGINX não é possível evitar operações de bloqueio em todos os casos e, para resolver esse problema, o novo mecanismo de “thread pools” foi implementado no NGINX versão 1.7.11 e no NGINX Plus Release 7 . O que é e como deve ser usado, abordaremos mais tarde. Agora vamos ficar cara a cara com nosso inimigo.

Editor – Para uma visão geral do NGINX Plus R7, consulte Anunciando o NGINX Plus R7 em nosso blog.

Para discussões detalhadas sobre outros novos recursos no NGINX Plus R7, consulte estas postagens de blog relacionadas:

 

O Problema

Primeiro, para melhor compreensão do problema, algumas palavras sobre como o NGINX funciona.

Em geral, o NGINX é um manipulador de eventos, um controlador que recebe informações do kernel sobre todos os eventos que ocorrem nas conexões e então fornece comandos ao sistema operacional sobre o que fazer. Na verdade, o NGINX faz todo o trabalho pesado orquestrando o sistema operacional, enquanto o sistema operacional faz o trabalho rotineiro de ler e enviar bytes. Portanto, é muito importante que o NGINX responda de forma rápida e oportuna.

NGINX-Evento-Loop2
O processo de trabalho escuta e processa eventos do kernel

Os eventos podem ser tempos limite, notificações sobre soquetes prontos para leitura ou gravação ou notificações sobre um erro ocorrido. O NGINX recebe vários eventos e os processa um por um, executando as ações necessárias. Assim, todo o processamento é feito em um loop simples em uma fila em um thread. O NGINX retira um evento da fila e reage a ele, por exemplo, gravando ou lendo um soquete. Na maioria dos casos, isso é extremamente rápido (talvez exigindo apenas alguns ciclos de CPU para copiar alguns dados na memória) e o NGINX prossegue por todos os eventos na fila em um instante.

Ciclo de processamento da fila de eventos
Todo o processamento é feito em um loop simples por um thread

Mas o que acontecerá se alguma operação longa e pesada tiver ocorrido? Todo o ciclo de processamento de eventos ficará preso esperando a conclusão desta operação.

Então, ao dizer “uma operação de bloqueio” queremos dizer qualquer operação que interrompa o ciclo de manipulação de eventos por um período de tempo significativo. As operações podem estar bloqueadas por vários motivos. Por exemplo, o NGINX pode estar ocupado com um processamento longo e intensivo de CPU, ou pode ter que esperar para acessar um recurso (como um disco rígido, ou uma chamada de função de biblioteca ou mutex que obtém respostas de um banco de dados de forma síncrona, etc.). O ponto principal é que, ao processar essas operações, o processo de trabalho não pode fazer mais nada e não pode manipular outros eventos, mesmo que haja mais recursos do sistema disponíveis e alguns eventos na fila possam utilizar esses recursos.

Imagine um vendedor em uma loja com uma longa fila na sua frente. O primeiro cara da fila pede algo que não está na loja, mas está no depósito. O vendedor vai até o depósito para entregar as mercadorias. Agora, toda a fila precisa esperar algumas horas por essa entrega e todos na fila ficam insatisfeitos. Você consegue imaginar a reação das pessoas? O tempo de espera de cada pessoa na fila aumenta com essas horas, mas os itens que elas pretendem comprar podem estar ali mesmo na loja.

Todos na fila têm que esperar pelo pedido da primeira pessoa

Quase a mesma situação acontece com o NGINX quando ele pede para ler um arquivo que não está armazenado em cache na memória, mas precisa ser lido do disco. Os discos rígidos são lentos (especialmente os que estão girando) e, embora as outras solicitações na fila possam não precisar de acesso à unidade, elas são forçadas a esperar de qualquer maneira. Como resultado, as latências aumentam e os recursos do sistema não são totalmente utilizados.

Apenas uma operação de bloqueio pode atrasar todas as operações seguintes por um tempo significativo

Alguns sistemas operacionais fornecem uma interface assíncrona para leitura e envio de arquivos e o NGINX pode usar essa interface (veja a diretiva aio ). Um bom exemplo aqui é o FreeBSD. Infelizmente, não podemos dizer o mesmo sobre o Linux. Embora o Linux forneça um tipo de interface assíncrona para leitura de arquivos, ele tem algumas desvantagens significativas. Um deles são os requisitos de alinhamento para acesso a arquivos e buffers, mas o NGINX lida bem com isso. Mas o segundo problema é pior. A interface assíncrona requer que o sinalizador O_DIRECT seja definido no descritor de arquivo, o que significa que qualquer acesso ao arquivo ignorará o cache na memória e aumentará a carga nos discos rígidos. Isso definitivamente não o torna ideal para muitos casos.

Para resolver esse problema em particular, os pools de threads foram introduzidos no NGINX 1.7.11 e no NGINX Plus Release 7.

Agora vamos mergulhar no que são os pools de threads e como eles funcionam.

Pools de Tópicos

Vamos voltar ao nosso pobre assistente de vendas que entrega mercadorias de um armazém distante. Mas ele ficou mais esperto (ou talvez tenha ficado mais esperto depois de ser espancado pela multidão de clientes furiosos?) e contratou um serviço de entrega. Agora, quando alguém pede algo de um depósito distante, em vez de ir até o depósito, ele simplesmente deixa o pedido em um serviço de entrega e eles cuidam do pedido enquanto nosso assistente de vendas continua atendendo outros clientes. Assim, apenas os clientes cujas mercadorias não estão na loja aguardam a entrega, enquanto os demais podem ser atendidos imediatamente.

Passar um pedido para o serviço de entrega desbloqueia a fila

Em termos de NGINX, o pool de threads está executando as funções do serviço de entrega. Ele consiste em uma fila de tarefas e um número de threads que manipulam a fila. Quando um processo de trabalho precisa fazer uma operação potencialmente longa, em vez de processar a operação sozinho, ele coloca uma tarefa na fila do pool, da qual ela pode ser retirada e processada por qualquer thread livre.

Os pools de threads ajudam a aumentar o desempenho do aplicativo atribuindo uma operação lenta a um conjunto separado de tarefas
O processo de trabalho descarrega operações de bloqueio para o pool de threads

Parece que temos outra fila. Certo. Mas neste caso a fila é limitada por um recurso específico. Não podemos ler de uma unidade mais rápido do que a unidade é capaz de produzir dados. Agora, pelo menos, a unidade não atrasa o processamento de outros eventos e apenas as solicitações que precisam acessar os arquivos ficam esperando.

A operação de “leitura do disco” é frequentemente usada como o exemplo mais comum de uma operação de bloqueio, mas, na verdade, a implementação de pools de threads no NGINX pode ser usada para quaisquer tarefas que não sejam apropriadas para processar no ciclo de trabalho principal.

No momento, o descarregamento para pools de threads é implementado apenas para três operações essenciais: a chamada de sistema read() na maioria dos sistemas operacionais, sendfile() no Linux e aio_write() no Linux, que é usada ao gravar alguns arquivos temporários, como os do cache. Continuaremos testando e comparando a implementação e poderemos transferir outras operações para os pools de threads em versões futuras se houver um benefício claro.

Editor – O suporte para a chamada de sistema aio_write() foi adicionado no NGINX 1.9.13 e no NGINX Plus R9 .

Avaliação comparativa

É hora de passar da teoria para a prática. Para demonstrar o efeito do uso de pools de threads, realizaremos um benchmark sintético que simula a pior combinação de operações de bloqueio e não bloqueio.

Ele requer um conjunto de dados que certamente não caberá na memória. Em uma máquina com 48 GB de RAM, geramos 256 GB de dados aleatórios em arquivos de 4 MB e então configuramos o NGINX 1.9.0 para atendê-los.

A configuração é bem simples:

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;
        }
    }
}

Como você pode ver, para obter melhor desempenho, alguns ajustes foram feitos: logging e accept_mutex foram desabilitados, sendfile foi habilitado e sendfile_max_chunk foi definido. A última diretiva pode reduzir o tempo máximo gasto no bloqueio de chamadas sendfile() , já que o NGINX não tentará enviar o arquivo inteiro de uma vez, mas o fará em blocos de 512 KB.

A máquina tem dois processadores Intel Xeon E5645 (12 núcleos, 24 HT‑threads no total) e uma interface de rede de 10‑Gbps. O subsistema de disco é representado por quatro discos rígidos Western Digital WD1003FBYX dispostos em uma matriz RAID10. Todo esse hardware é alimentado pelo Ubuntu Server 14.04.1 LTS.

Configuração de geradores de carga e NGINX para o benchmark

Os clientes são representados por duas máquinas com as mesmas especificações. Em uma dessas máquinas, o wrk cria carga usando um script Lua. O script solicita arquivos do nosso servidor em uma ordem aleatória usando 200 conexões paralelas, e cada solicitação provavelmente resultará em uma perda de cache e um bloqueio de leitura do disco. Vamos chamar essa carga de carga aleatória .

Na segunda máquina cliente, executaremos outra cópia do wrk que solicitará o mesmo arquivo várias vezes usando 50 conexões paralelas. Como esse arquivo será acessado com frequência, ele permanecerá na memória o tempo todo. Em circunstâncias normais, o NGINX atenderia a essas solicitações muito rapidamente, mas o desempenho cairia se os processos de trabalho fossem bloqueados por outras solicitações. Vamos chamar essa carga de carga constante .

O desempenho será medido monitorando a taxa de transferência da máquina servidora usando ifstat e obtendo resultados de trabalho do segundo cliente.

Agora, a primeira execução sem pools de threads não nos dá resultados muito interessantes:

% ifstat -bi eth2eth2
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

Como você pode ver, com essa configuração o servidor é capaz de produzir cerca de 1 Gbps de tráfego no total. Na saída de top , podemos ver que todos os processos de trabalho passam a maior parte do tempo em bloqueio de E/S (eles estão em um estado D ):

top - 10:40:47 up 11 days,  1:32,  1 user,  load average: 49.61, 45.77 62.89Tasks: 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

Nesse caso, a taxa de transferência é limitada pelo subsistema de disco, enquanto a CPU fica ociosa na maior parte do tempo. Os resultados do trabalho também são muito baixos:

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

E lembre-se, isso é para o arquivo que deve ser servido de memória! As latências excessivamente grandes ocorrem porque todos os processos de trabalho estão ocupados lendo arquivos das unidades para atender à carga aleatória criada por 200 conexões do primeiro cliente e não conseguem lidar com nossas solicitações em tempo hábil.

É hora de colocar nossos pools de threads em jogo. Para isso, basta adicionar a diretiva aio threads ao bloco location :

location / {    root /storage;
    aio threads;
}

e peça ao NGINX para recarregar sua configuração.

Depois disso repetimos o teste:

% ifstat -bi eth2eth2
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

Agora nosso servidor produz 9,5 Gbps , comparado a ~1 Gbps sem pools de threads!

Provavelmente poderia produzir ainda mais, mas já atingiu a capacidade máxima prática da rede, então neste teste o NGINX é limitado pela interface de rede. Os processos de trabalho passam a maior parte do tempo apenas dormindo e esperando por novos eventos (eles estão no estado S no topo ):

top - 10:43:17 up 11 days,  1:35,  1 user,  load average: 172.71, 93.84, 77.90Tasks: 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

Ainda há muitos recursos de CPU.

Os resultados do trabalho :

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

O tempo médio para atender um arquivo de 4 MB foi reduzido de 7,42 segundos para 226,32 milissegundos (33 vezes menos), e o número de solicitações por segundo aumentou em 31 vezes (250 contra 8)!

A explicação é que nossas solicitações não esperam mais na fila de eventos para processamento enquanto os processos de trabalho são bloqueados na leitura, mas são manipuladas por threads livres. Enquanto o subsistema de disco estiver fazendo seu trabalho da melhor maneira possível, atendendo à nossa carga aleatória da primeira máquina cliente, o NGINX usa o restante dos recursos da CPU e a capacidade da rede para atender às solicitações do segundo cliente da memória.

Ainda não é uma solução mágica

Depois de todos os nossos medos sobre operações de bloqueio e alguns resultados interessantes, provavelmente a maioria de vocês já vai configurar pools de threads em seus servidores. Não tenha pressa.

A verdade é que, felizmente, a maioria das operações de leitura e envio de arquivos não lida com discos rígidos lentos. Se você tiver RAM suficiente para armazenar o conjunto de dados, um sistema operacional será inteligente o suficiente para armazenar em cache os arquivos usados com frequência em um chamado “cache de página”.

O cache de página funciona muito bem e permite que o NGINX demonstre ótimo desempenho em quase todos os casos de uso comuns. A leitura do cache da página é bastante rápida e ninguém pode chamar essas operações de “bloqueio”. Por outro lado, o descarregamento para um pool de threads tem alguma sobrecarga.

Então, se você tem uma quantidade razoável de RAM e seu conjunto de dados de trabalho não é muito grande, o NGINX já funciona da maneira mais otimizada sem usar pools de threads.

Descarregar operações de leitura para o pool de threads é uma técnica aplicável a tarefas muito específicas. É mais útil quando o volume de conteúdo solicitado com frequência não cabe no cache da VM do sistema operacional. Este pode ser o caso, por exemplo, de um servidor de streaming de mídia baseado em NGINX muito carregado. Esta é a situação que simulamos em nosso benchmark.

Seria ótimo se pudéssemos melhorar o descarregamento de operações de leitura em pools de threads. Tudo o que precisamos é de uma maneira eficiente de saber se os dados do arquivo necessários estão na memória ou não, e somente neste último caso a operação de leitura deve ser transferida para um thread separado.

Voltando à nossa analogia de vendas, atualmente o vendedor não pode saber se o item solicitado está na loja e deve sempre passar todos os pedidos para o serviço de entrega ou sempre lidar com eles ele mesmo.

O culpado é que os sistemas operacionais não têm esse recurso. As primeiras tentativas de adicioná-lo ao Linux como a chamada de sistema fincore() foram em 2010, mas isso não aconteceu. Mais tarde, houve uma série de tentativas de implementá-lo como uma nova chamada de sistema preadv2() com o sinalizador RWF_NONBLOCK (consulte Operações de leitura de arquivo com buffer não bloqueante e Operações de leitura com buffer assíncrono em LWN.net para obter detalhes). O destino de todos esses patches ainda não está claro. O ponto triste aqui é que parece que a principal razão pela qual esses patches ainda não foram aceitos no kernel é o contínuo bikeshedding .

Por outro lado, os usuários do FreeBSD não precisam se preocupar. O FreeBSD já tem uma interface assíncrona suficientemente boa para leitura de arquivos, que você deve usar em vez de pools de threads.

Configurando pools de threads

Então, se você tem certeza de que pode obter algum benefício usando pools de threads em seu caso de uso, então é hora de mergulhar fundo na configuração.

A configuração é bastante fácil e flexível. A primeira coisa que você deve ter é o NGINX versão 1.7.11 ou posterior, compilado com o argumento --with-threads no comando configure . Os usuários do NGINX Plus precisam da versão 7 ou posterior. No caso mais simples, a configuração parece muito simples. Tudo o que você precisa é incluir a diretiva aio threads no contexto apropriado:

# in the 'http', 'server', or 'location' contextaio threads;

Esta é a configuração mínima possível de pools de threads. Na verdade, é uma versão curta da seguinte configuração:

# in the 'main' contextthread_pool default threads=32 max_queue=65536;
 
# in the 'http', 'server', or 'location' context
aio threads=default;

Ele define um pool de threads chamado default com 32 threads de trabalho e um comprimento máximo para a fila de tarefas de 65536 tarefas. Se a fila de tarefas estiver sobrecarregada, o NGINX rejeitará a solicitação e registrará este erro:

thread pool "NAME" queue overflow: N tasks waiting

O erro significa que é possível que os threads não consigam lidar com o trabalho tão rapidamente quanto ele é adicionado à fila. Você pode tentar aumentar o tamanho máximo da fila, mas se isso não ajudar, isso indica que seu sistema não é capaz de atender a tantas solicitações.

Como você já percebeu, com a diretiva thread_pool você pode configurar o número de threads, o comprimento máximo da fila e o nome de um pool de threads específico. O último implica que você pode configurar vários pools de threads independentes e usá-los em diferentes lugares do seu arquivo de configuração para atender a diferentes propósitos:

# in the 'main' context
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;
        }

    }
    # ...
}

Se o parâmetro max_queue não for especificado, o valor 65536 será usado por padrão. Como mostrado, é possível definir max_queue como zero. Nesse caso, o pool de threads só poderá manipular tantas tarefas quantos forem os threads configurados; nenhuma tarefa ficará na fila.

Agora vamos imaginar que você tem um servidor com três discos rígidos e quer que esse servidor funcione como um “proxy de cache” que armazena em cache todas as respostas dos seus backends. A quantidade esperada de dados armazenados em cache excede em muito a RAM disponível. Na verdade, é um nó de cache para seu CDN pessoal. É claro que neste caso o mais importante é obter o máximo desempenho dos drives.

Uma das opções é configurar um array RAID. Essa abordagem tem seus prós e contras. Agora com o NGINX você pode tirar mais uma:

# We assume that each of the hard drives is mounted on one of these directories:# /mnt/disk1, /mnt/disk2, or /mnt/disk3

# in the 'main' context
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;
        }
    }
}

Nessa configuração, as diretivas thread_pool definem um pool de threads dedicado e independente para cada disco, e as diretivas proxy_cache_path definem um cache dedicado e independente em cada disco.

O módulo split_clients é usado para balanceamento de carga entre os caches (e, consequentemente, entre os discos), o que se encaixa perfeitamente nessa tarefa.

O parâmetro use_temp_path=off da diretiva proxy_cache_path instrui o NGINX a salvar arquivos temporários nos mesmos diretórios onde os dados de cache correspondentes estão localizados. É necessário evitar a cópia de dados de resposta entre os discos rígidos ao atualizar nossos caches.

Tudo isso junto nos permite obter o máximo desempenho do subsistema de disco atual, porque o NGINX, por meio de pools de threads separados, interage com as unidades em paralelo e de forma independente. Cada uma das unidades é atendida por 16 threads independentes com uma fila de tarefas dedicada para leitura e envio de arquivos.

Aposto que seus clientes gostam dessa abordagem personalizada. Certifique-se de que seus discos rígidos também gostem.

Este exemplo é uma boa demonstração de quão flexível o NGINX pode ser ajustado especificamente para seu hardware. É como se você estivesse dando instruções ao NGINX sobre a melhor maneira de interagir com a máquina e seu conjunto de dados. E ao ajustar o NGINX no espaço do usuário, você pode garantir que seu software, sistema operacional e hardware trabalhem juntos no modo mais otimizado para utilizar todos os recursos do sistema da forma mais eficaz possível.

Conclusão

Resumindo, os pools de threads são um ótimo recurso que leva o NGINX a novos níveis de desempenho ao eliminar um de seus inimigos mais conhecidos e antigos: o bloqueio, especialmente quando falamos de grandes volumes de conteúdo.

E ainda há mais por vir. Como mencionado anteriormente, esta nova interface permite potencialmente o descarregamento de qualquer operação longa e bloqueadora sem qualquer perda de desempenho. O NGINX abre novos horizontes em termos de ter uma massa de novos módulos e funcionalidades. Muitas bibliotecas populares ainda não fornecem uma interface assíncrona não bloqueante, o que as tornava incompatíveis com o NGINX. Podemos gastar muito tempo e recursos desenvolvendo nosso próprio protótipo não bloqueante de alguma biblioteca, mas sempre valerá a pena o esforço? Agora, com pools de threads a bordo, é possível usar essas bibliotecas com relativa facilidade, criando esses módulos sem afetar o desempenho.

Fique atento.

Experimente você mesmo os pools de threads no NGINX Plus – comece seu teste gratuito de 30 dias hoje mesmo ou entre em contato conosco para discutir seus casos de uso .


"Esta postagem do blog pode fazer referência a produtos que não estão mais disponíveis e/ou não têm mais suporte. Para obter as informações mais atualizadas sobre os produtos e soluções F5 NGINX disponíveis, explore nossa família de produtos NGINX . O NGINX agora faz parte do F5. Todos os links anteriores do NGINX.com redirecionarão para conteúdo semelhante do NGINX no F5.com."