Bem, ou quase tudo ...

Eu acredito que o problema na Internet moderna é uma superabundância de informações de qualidade diferente. Encontrar material sobre um tópico de interesse não é um problema; o problema é distinguir material bom de material ruim se você tiver pouca experiência nesse campo. Observo uma figura quando há muitas informações gerais "no topo" (quase no nível de uma simples enumeração), muito poucos artigos detalhados e nenhum artigo de transição do simples ao complexo. No entanto, é o conhecimento das características de um mecanismo específico que nos permite fazer uma escolha informada durante o desenvolvimento.
No artigo, tentarei revelar qual é a diferença fundamental entre epoll e outros mecanismos, o que o torna único, além de citar artigos que você só precisa ler para entender melhor as possibilidades e os problemas do epoll .
Qualquer um pode usar um machado, mas é preciso um verdadeiro guerreiro para fazê-lo cantar melodia melee.
Presumo que o leitor esteja familiarizado com epoll , pelo menos leia a página de manual. Já foi escrito o suficiente sobre epoll , poll , select , para que todos que desenvolvem no Linux tenham ouvido falar sobre isso pelo menos uma vez.
Muito fd
Quando as pessoas falam sobre epoll, basicamente, ouço a tese de que "seu desempenho é melhor quando há muitos descritores de arquivos".
Só quero fazer uma pergunta - quanto é quanto? Quantas conexões são necessárias e, o mais importante, sob quais condições a epoll começará a dar ganhos tangíveis de desempenho?
Para quem estudou epoll (há muito material, incluindo artigos científicos), a resposta é óbvia - é melhor se e somente se o número de compostos "aguardando um evento" exceder significativamente o número de "prontos para processamento". A marca da quantidade, quando o ganho se torna tão significativo que simplesmente não há urina para ignorar esse fato, 10 mil compostos são considerados [4].
A suposição de que a maioria das conexões estará pendente vem da lógica do som e do monitoramento de carga dos servidores que estão em uso ativo.
Se o número de compostos ativos se esforçar para o número total, não haverá ganho não haverá ganho significativo, um ganho significativo é devido ae somente porque epoll retorna apenas descritores que requerem atenção e a pesquisa retorna todos os descritores que foram adicionados para observação.
Obviamente, no último caso, passamos um tempo percorrendo todos os descritores + sobrecarga de copiar uma matriz de eventos do kernel.
De fato, na medição de desempenho inicial, que foi anexada ao patch [9], esse ponto não está sublinhado e só podemos adivinhar pela presença do utilitário deadcon mencionado no artigo (infelizmente, o código do utilitário pipetest.c foi perdido). Por outro lado, em outras fontes [6, 8] é muito difícil não notar, porque esse fato está praticamente se destacando.
A questão surge imediatamente, mas e agora, se não estiver planejado atender a um número tão grande de descritores de arquivos epoll , por assim dizer, e não for necessário?
Apesar de o epoll ter sido originalmente criado especificamente para essas situações [5, 8, 9], isso está longe de ser a única diferença entre epoll .
EPOLLET
Para começar, entenderemos a diferença entre gatilhos de gatilho de borda e gatilhos de gatilho de nível. Há uma declaração muito boa sobre esse tópico no artigo Borda acionada contra interrupções de gatilho de nível - Venkatesh Yadav :
Interrupção de nível, é como uma criança. Se o bebê estiver chorando, você deve desistir de tudo o que fez e correr para o bebê para alimentá-lo. Então você coloca o bebê de volta no berço. Se ele chorar novamente, você não o deixará em lugar nenhum, mas tentará acalmá-lo. E enquanto a criança estiver chorando, você não a deixará nem por um momento e só retornará ao trabalho quando se acalmar. Mas digamos que saímos para o jardim (interrupção desativada) quando a criança começou a chorar; depois, quando você voltou para casa (interrupção ligada), a primeira coisa que você fez foi ir verificar a criança. Mas você nunca saberá que ele estava chorando enquanto você estava no jardim.
A interrupção na frente é como uma babá eletrônica para pais surdos. Assim que a criança começa a chorar no dispositivo, uma luz vermelha acende e acende até você pressionar o botão. Mesmo que a criança comece a chorar, mas rapidamente pare e adormeça, você ainda saberá que a criança estava chorando. Mas se ele começou a chorar e você pressionou o botão (confirmação de interrupção), a luz não acenderá, mesmo que ele continue chorando. O nível do som na sala deve cair e subir novamente para que a luz acenda.
Se o epoll (assim como pesquisar / selecionar ) for desbloqueado no comportamento acionado por nível, se o descritor estiver no estado especificado e será considerado ativo até que esse estado seja limpo, o acionado por borda será desbloqueado apenas alterando o estado ordenado atual.
Isso permite que você lide com o evento posteriormente, e não imediatamente após o recebimento (quase uma analogia direta com a metade superior e a metade inferior do manipulador de interrupções).
Exemplo específico com epoll:
Nível acionado
- identificador adicionado ao epoll com a bandeira EPOLLIN
- O epoll_wait () bloqueia enquanto aguarda um evento
- escreva no descritor de arquivo 19 bytes
- epoll_wait () é desbloqueado com o evento EPOLLIN
- nós não fazemos nada com os dados que vieram
- epoll_wait () é desbloqueado novamente com o evento EPOLLIN
E isso continuará até contarmos ou redefinirmos completamente os dados do descritor.
Borda acionada
- identificador adicionado ao epoll com sinalizadores EPOLLIN | EPOLLET
- O epoll_wait () bloqueia enquanto aguarda um evento
- escreva no descritor de arquivo 19 bytes
- epoll_wait () é desbloqueado com o evento EPOLLIN
- nós não fazemos nada com os dados que vieram
- epoll_wait () está bloqueado aguardando um novo evento
- escreva outros 19 bytes no descritor de arquivo
- epoll_wait () é desbloqueado com o novo evento EPOLLIN
- epoll_wait () está bloqueado aguardando um novo evento
exemplo simples: epollet_socket.c
Este mecanismo foi projetado para impedir o retorno de epoll_wait () devido a um evento que já está sendo processado.
Se, no caso de level, ao chamar epoll_wait (), o kernel verifica se fd está nesse estado, o edge ignora essa verificação e coloca imediatamente o processo de chamada no estado de suspensão.
O próprio EPOLLET é o que torna o epoll O (1) um multiplexador para eventos.
É necessário explicar sobre o EAGAIN e o EPOLLET - a recomendação com o EAGAIN não é tratar o fluxo de bytes, o perigo no último caso surge apenas se você não leu o descritor até o final e novos dados não chegaram. A cauda ficará no descritor, mas você não receberá uma nova notificação. Com accept (), a situação é apenas diferente; você deve continuar até o accept () retornar EAGAIN , somente neste caso a operação correta é garantida.
// TCP socket (byte stream) // fd EPOLLIN int len = read(fd, buffer, BUFFER_LEN); if(len < BUFFER_LEN) { // } else { // // - epoll_wait, // }
// accept // listenfd EPOLLIN event.events = EPOLLIN | EPOLLERR; epoll_ctl(epoll_fd, EPOLL_CTL_ADD, server_fd, &event); sleep(5); // >1 // while(epoll_wait()) { newfd = accept(listenfd, ...); // // // epoll_wait listenfd } // while(epoll_wait()) { while((newfd = accept(...)) > 0) { // - } if(newfd == -1 && errno = EAGAIN) { // // } }
Com essa propriedade, basta a fome:
- pacotes vêm para o descritor
- ler pacotes para o buffer
- outro pacote vem
- ler pacotes para o buffer
- vem uma pequena porção
- ...
Portanto , não receberemos o EAGAIN em breve, mas podemos não recebê-lo.
Assim, outros descritores de arquivo não recebem tempo para processamento e estamos ocupados lendo constantemente pequenas porções de dados que chegam constantemente.
estrondoso nerd manada
Para ir para a última flag, você precisa entender por que ela foi realmente criada e um dos problemas que surgiram para os desenvolvedores com a evolução da tecnologia e do software.
Problema do rebanho trovejante
Problema do rebanho do trovão
Imagine um grande número de processos aguardando um evento. Se um evento ocorrer, eles serão despertados e a luta por recursos começará, embora apenas um processo seja necessário para lidar com o processamento adicional do evento. O restante dos processos dormirá novamente.
Terminologia de TI - Vasily Alekseenko
Nesse caso, estamos interessados no problema de accept () e read () distribuídos por fluxos em conjunto com epoll .
aceitar
Na verdade, com uma chamada de bloqueio para accept (), não há problemas há muito tempo. O kernel cuidará para que apenas um processo tenha sido desbloqueado para este evento e todas as conexões recebidas sejam serializadas.
Mas com epoll, esse truque não vai funcionar. Se tivermos listen () feito em um soquete sem bloqueio, quando a conexão for estabelecida, todos os epoll_wait () aguardarão o evento desse descritor.
Obviamente, o accept () poderá fazer apenas um thread, o restante receberá EAGAIN , mas isso é um desperdício de recursos.
Além disso, o EPOLLET também não nos ajuda, pois não sabemos exatamente quantas conexões há na fila de conexões ( lista de pendências ). Como lembramos, ao usar o EPOLLET , o processamento do soquete deve continuar até que retorne com o código de erro EAGAIN , para que haja uma chance de que todos os accept () sejam processados por um thread e o restante não funcione.
E isso novamente nos leva a uma situação em que a corrente vizinha foi acordada em vão.
Também podemos obter um tipo diferente de inanição - teremos apenas um encadeamento carregado e o restante não receberá conexões para processamento.
EPOLLONESHOT
Antes da versão 4.5, a única maneira correta de processar o epoll distribuído em um descritor listen () sem bloqueio com a próxima chamada accept () era definir o sinalizador EPOLLONESHOT , que novamente nos levou a aceitar () sendo processado apenas em um encadeamento por vez.
Em resumo - se EPOLLONESHOT for usado , o evento associado a um descritor em particular será acionado apenas uma vez, após o que é necessário rearmar os sinalizadores usando epoll_ctl () .
EPOLLEXCLUSIVE
Aqui, o EPOLLEXCLUSIVE e acionado por nível vem em nosso auxílio.
EPOLLEXCLUSIVE desbloqueia um epoll_wait () pendente de cada vez para um evento.
O esquema é bastante simples (na verdade não):
- Temos N threads aguardando um evento de conexão
- O primeiro cliente se conecta a nós
- O segmento 0 será disperso e iniciará o processamento, outros segmentos permanecerão bloqueados
- Um segundo cliente se conecta a nós, se o segmento 0 ainda estiver ocupado com o processamento, o segmento 1 será desbloqueado
- Continuamos mais até que o pool de threads esteja esgotado (ninguém espera um evento em epoll_wait () )
- Outro cliente está se conectando a nós
- E seu processamento receberá o primeiro thread, que chamará epoll_wait ()
- O segundo thread receberá o segundo cliente, que chamará epoll_wait ()
Assim, toda a manutenção é distribuída uniformemente pelos fluxos.
$ ./epollexclusive --help -i, --ip=ADDR specify ip address -p, --port=PORT specify port -n, --threads=NUM specify number of threads to use
código de exemplo: epollexclusive.c (funcionará apenas com a versão do kernel da 4.5)
Temos um modelo pré-fork no epoll. Esse esquema é bem aplicável para conexões TCP de curto prazo .
ler
Mas com read () no caso de fluxo de bytes, EPOLLEXCLUSIVE , como EPOLLET, não nos ajudará.
Por razões óbvias, sem EPOLLEXCLUSIVE , não podemos usar, de maneira alguma, acionados por nível. Com o EPOLLEXCLUSIVE, nem tudo está melhor, pois podemos espalhar um pacote pelos fluxos, além de ter chegado uma ordem desconhecida de bytes.
Com EPOLLET, a situação é a mesma.
E aqui a EPOLLONESHOT com reinicialização após a conclusão do trabalho será a saída. Portanto, assim que um thread trabalhar com este descritor de arquivo e buffer:
- identificador adicionado ao epoll com sinalizadores EPOLLONESHOT | EPOLLET
- esperando epoll_wait ()
- ler do soquete para o buffer até read () retornar EAGAIN
- reinicializar com sinalizadores de EPOLLONESHOT | EPOLLET
struct epoll_event
typedef union epoll_data { void *ptr; int fd; uint32_t u32; uint64_t u64; } epoll_data_t; struct epoll_event { uint32_t events; epoll_data_t data; };
Este item é talvez o único no meu artigo meu IMHO pessoal. A capacidade de usar um ponteiro ou um número é útil. Por exemplo, usar um ponteiro ao usar epoll permite que você faça um truque como este:
#define container_of(ptr, type, member) ({ \ const typeof( ((type *)0)->member ) *__mptr = (ptr); \ (type *)( (char *)__mptr - offsetof(type,member) );}) struct epoll_client { struct epoll_event event; }; struct epoll_client* to_epoll_client(struct epoll_event* event) { return container_of(event, struct epoll_client, event); } struct epoll_client ec; ... epoll_ctl(efd, EPOLL_CTL_ADD, fd, &ec.e); ... epoll_wait (efd, events, 1, -1); struct epoll_client* ec_ = to_epoll_client(events[0].data.ptr);
Acho que todo mundo sabe de onde veio essa técnica.
Conclusão
Espero que possamos abrir o tópico epoll . Quem deseja usar esse mecanismo conscientemente, só precisa ler os artigos na lista de referências [1, 2, 3, 5].
Com base nesse material (ou, melhor ainda, na leitura cuidadosa dos materiais a partir das referências), você pode criar um servidor pré-fork multi-threaded (geração avançada do processo) sem bloqueio (sem bloqueio) ou revisar estratégias existentes com base nas propriedades especiais do epoll () ).
O epoll é um dos mecanismos únicos que as pessoas que escolheram seus caminhos de programação Linux precisam conhecer, pois oferecem uma vantagem séria sobre outros sistemas operacionais) e, possivelmente, recusarão a plataforma cruzada para um caso específico (deixe funcionar) somente no Linux, mas fará bem).
Raciocínio sobre a "especificidade" do problema
Antes de alguém falar sobre a especificidade desses sinalizadores e padrões de uso, quero fazer uma pergunta:
"Mas não é nada que estamos tentando discutir a especificidade do mecanismo que foi criado para tarefas específicas inicialmente [9, 11]? Ou servimos até conexões 1k é uma tarefa diária para um programador?"
Não compreendo o conceito de "especificidade da tarefa", ele me lembra todos os tipos de gritos sobre a utilidade e a futilidade das várias disciplinas ensinadas. Permitindo-nos raciocinar dessa maneira, arrogamos para nós mesmos o direito de decidir para os outros quais informações são úteis para eles e quais são inúteis, enquanto, lembre-se, você não participa do processo educacional como um todo.
Para os céticos, alguns links:
Aumentando o desempenho com SO_REUSEPORT no NGINX 1.9.1 - VBart
Aprendendo com o Unicorn: o problema não resolvido do rebanho trovejante - Chris Siebenmann
Serializing accept (), também conhecido como Thundering Herd, também conhecido como o problema de Zeeg - Roberto De Ioris
Como o modo EPOLLEXCLUSIVE do epoll interage com o acionamento de nível?
Referências
- Select é fundamentalmente quebrado - Marek
- Epoll é fundamentalmente quebrado 1/2 - Marek
- Epoll é fundamentalmente quebrado 2/2 - Marek
- O problema do C10K - Dan Kegel
- Poll vs Epoll, mais uma vez - Jacques Mattheij
- epoll - recurso de notificação de eventos de E / S - The Mann
- O método para epollar a loucura - Cindy Sridharan
Benchmarks
- https://www.kernel.org/doc/ols/2004/ols2004v1-pages-215-226.pdf
- http://lse.sourceforge.net/epoll/index.html
- https://mvitolin.wordpress.com/2015/12/05/endurox-testing-epollexclusive-flag/
A evolução do epoll
- https://lwn.net/Articles/13918/
- https://lwn.net/Articles/520012/
- https://lwn.net/Articles/520198/
- https://lwn.net/Articles/542629/
- https://lwn.net/Articles/633422/
- https://lwn.net/Articles/637435/
Postscript
Muito obrigado a Sergey ( dlinyj ) e Peter Ovchenkov por valiosas discussões, comentários e ajuda!