Compartilhar via


Escalabilidade

O termo, escalabilidade, geralmente é usado incorretamente. Para esta seção, uma definição dupla é fornecida:

  • A escalabilidade é a capacidade de utilizar totalmente o poder de processamento disponível em um sistema multiprocessador (2, 4, 8, 32 ou mais processadores).
  • Escalabilidade é a capacidade de atender a um grande número de clientes.

Essas duas definições relacionadas geralmente são conhecidas como escalar verticalmente. O final deste tópico fornece dicas sobre como escalar horizontalmente.

Essa discussão se concentra exclusivamente na gravação de servidores escalonáveis, não clientes escalonáveis, pois os servidores escalonáveis são requisitos mais comuns. Esta seção também aborda a escalabilidade somente no contexto de servidores RPC e RPC. As práticas recomendadas para escalabilidade, como reduzir a contenção, evitar erros frequentes de cache em locais de memória global ou evitar o compartilhamento falso, não são discutidas aqui.

Modelo de threading RPC

Quando uma chamada RPC é recebida por um servidor, a rotina do servidor (rotina do gerente) é chamada em um thread fornecido pelo RPC. O RPC usa um pool de threads adaptável que aumenta e diminui à medida que a carga de trabalho flutua. A partir do Windows 2000, o núcleo do pool de threads RPC é uma porta de conclusão. A porta de conclusão e seu uso por RPC são ajustados para rotinas de servidor de contenção zero a baixa. Isso significa que o pool de threads RPC aumenta agressivamente o número de threads de manutenção se alguns forem bloqueados. Ele opera com a presunção de que o bloqueio é raro e, se um thread for bloqueado, essa é uma condição temporária que é resolvida rapidamente. Essa abordagem permite eficiência para servidores de baixa contenção. Por exemplo, um servidor RPC de chamada nula que opera em um servidor de oito processadores de 550MHz acessado em uma SAN (rede de área do sistema) de alta velocidade atende mais de 30.000 chamadas nulas por segundo de mais de 200 clientes remotos. Isso representa mais de 108 milhões de chamadas por hora.

O resultado é que o pool de threads agressivo realmente fica no caminho quando a contenção no servidor é alta. Para ilustrar, imagine um servidor de trabalho pesado usado para acessar remotamente arquivos. Suponha que o servidor adote a abordagem mais simples: ele simplesmente lê/grava o arquivo de forma síncrona no thread no qual esse RPC invoca a rotina do servidor. Além disso, suponha que temos um servidor de quatro processadores que atende a muitos clientes.

O servidor começará com cinco threads (isso realmente varia, mas cinco threads são usados para simplificar). Depois que o RPC atende a primeira chamada RPC, ele envia a chamada para a rotina do servidor e a rotina do servidor emite a E/S. Raramente, ele perde o cache de arquivos e bloqueia a espera pelo resultado. Assim que ele é bloqueado, o quinto thread é liberado para pegar uma solicitação e um sexto thread é criado como um modo de espera frequente. Supondo que cada décima operação de E/S perca o cache e bloqueie por 100 milissegundos (um valor de tempo arbitrário) e supondo que o servidor de quatro processadores atenda a cerca de 20.000 chamadas por segundo (5.000 chamadas por processador), uma modelagem simplista prevê que cada processador gerará aproximadamente 50 threads. Isso pressupõe que uma chamada que bloqueará vem a cada 2 milissegundos e, após 100 milissegundos, o primeiro thread será liberado novamente para que o pool se estabilize em cerca de 200 threads (50 por processador).

O comportamento real é mais complicado, pois o alto número de threads causará comutadores de contexto extras que atrasam o servidor e também reduzem a taxa de criação de novos threads, mas a ideia básica é clara. O número de threads aumenta rapidamente à medida que os threads no servidor começam a bloquear e aguardar algo (seja uma E/S ou acesso a um recurso).

O RPC e a porta de conclusão que portam as solicitações de entrada tentarão manter o número de threads RPC utilizáveis no servidor para serem iguais ao número de processadores no computador. Isso significa que, em um servidor de quatro processadores, uma vez que um thread retorna ao RPC, se houver quatro ou mais threads RPC utilizáveis, o quinto thread não terá permissão para pegar uma nova solicitação e, em vez disso, ficará em um estado de espera quente no caso de um dos blocos de threads atualmente utilizáveis. Se o quinto thread aguardar tempo suficiente como uma espera ativa sem o número de threads RPC utilizáveis caindo abaixo do número de processadores, ele será liberado, ou seja, o pool de threads diminuirá.

Imagine um servidor com muitos threads. Como explicado anteriormente, um servidor RPC acaba com muitos threads, mas somente se os threads bloquearem com frequência. Em um servidor em que os threads geralmente são bloqueados, um thread que retorna ao RPC logo é retirado da lista de espera ativa, pois todos os threads atualmente utilizáveis bloqueiam e recebe uma solicitação para processar. Quando um thread bloqueia, o dispatcher de thread no kernel alterna o contexto para outro thread. Essa opção de contexto por si só consome ciclos de CPU. O próximo thread executará código diferente, acessará diferentes estruturas de dados e terá uma pilha diferente, o que significa que a taxa de ocorrência do cache de memória (os caches L1 e L2) será muito menor, resultando em uma execução mais lenta. Os vários threads em execução simultaneamente aumentam a contenção de recursos existentes, como heap, seções críticas no código do servidor e assim por diante. Isso aumenta ainda mais a contenção à medida que os comboios nos recursos se formam. Se a memória for baixa, a pressão de memória exercida pelo grande e crescente número de threads causará falhas de página, o que aumentará ainda mais a taxa na qual os threads bloqueiam e fará com que ainda mais threads sejam criados. Dependendo da frequência com que bloqueia e da quantidade de memória física disponível, o servidor pode estabilizar em algum nível inferior de desempenho com uma alta taxa de alternância de contexto ou pode se deteriorar ao ponto em que está acessando repetidamente o disco rígido e a troca de contexto sem executar nenhum trabalho real. Essa situação não será exibida sob carga de trabalho leve, é claro, mas uma carga de trabalho pesada rapidamente traz o problema à superfície.

Como isso pode ser evitado? Se espera-se que os threads bloqueiem, declare chamadas como assíncronas e, depois que a solicitação entrar na rotina do servidor, enfileira-a em um pool de threads de trabalho que usam os recursos assíncronos do sistema de E/S e/ou RPC. Se o servidor estiver fazendo chamadas RPC, faça essas chamadas assíncronas e verifique se a fila não cresce muito. Se a rotina do servidor estiver executando E/S de arquivo, use E/S de arquivo assíncrono para enfileirar várias solicitações para o sistema de E/S e ter apenas alguns threads enfileirando-os e obtendo os resultados. Se a rotina do servidor estiver fazendo E/S de rede, novamente, use os recursos assíncronos do sistema para emitir as solicitações e pegar as respostas de forma assíncrona e usar o menor número possível de threads. Quando a E/S for concluída ou a chamada RPC feita pelo servidor for concluída, conclua a chamada RPC assíncrona que entregou a solicitação. Isso permitirá que o servidor seja executado com o menor número possível de threads, o que aumenta o desempenho e o número de clientes que um servidor pode atender.

Escalonamento horizontal

O RPC poderá ser configurado para funcionar com o NLB (Balanceamento de Carga de Rede) se o NLB estiver configurado de modo que todas as solicitações de um determinado endereço de cliente acessem o mesmo servidor. Como cada cliente RPC abre um pool de conexões (para obter mais informações, consulte RPC e a Rede), é essencial que todas as conexões do pool do cliente determinado acabem no mesmo computador servidor. Desde que essa condição seja atendida, um cluster NLB pode ser configurado para funcionar como um servidor RPC grande com escalabilidade potencialmente excelente.