Nota
O acesso a esta página requer autorização. Podes tentar iniciar sessão ou mudar de diretório.
O acesso a esta página requer autorização. Podes tentar mudar de diretório.
O termo, escalabilidade, é muitas vezes mal utilizado. Para esta secção, é fornecida uma definição dupla:
- 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 são comumente referidas como ampliação. O final deste tópico fornece dicas sobre como expandir .
Esta discussão se concentra exclusivamente em escrever servidores escaláveis, não clientes escaláveis, porque servidores escaláveis são requisitos mais comuns. Esta seção também aborda a escalabilidade apenas no contexto de servidores RPC e RPC. As práticas recomendadas para escalabilidade, como reduzir a contenção, evitar falhas frequentes de cache em locais de memória global ou evitar 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 ficarem bloqueados. Ele opera com base na presunção de que o bloqueio é raro, e se um thread é bloqueado, esta é uma condição temporária que é rapidamente resolvida. Essa abordagem permite a eficiência para servidores de baixa contenção. Por exemplo, um servidor RPC de chamada nula operando em um servidor de 550MHz de oito processadores acessado por uma rede de área do sistema (SAN) de alta velocidade atende mais de 30.000 chamadas vazias por segundo de mais de 200 clientes remotos. Isto representa mais de 108 milhões de chamadas por hora.
O resultado é que o pool de threads agressivo realmente atrapalha quando a contenção no servidor é alta. Para ilustrar, imagine um servidor pesado usado para acessar arquivos remotamente. Suponha que o servidor adote a abordagem mais direta: 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 muitos clientes.
O servidor começará com cinco threads (isso realmente varia, mas cinco threads é usado para simplificar). Depois que o RPC pega a primeira chamada RPC, ele despacha a chamada para a rotina do servidor e a rotina do servidor emite a E/S. Raramente, ele perde o cache de arquivos e, em seguida, bloqueia a espera pelo resultado. Assim que ele bloqueia, o quinto thread é liberado para pegar uma solicitação, e um sexto thread é criado como um hot standby. 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 cerca de 20.000 chamadas por segundo (5.000 chamadas por processador), uma modelagem simplista preveria 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 é 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á opções de contexto extras que atrasam o servidor e também diminuem 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 esperar por algo (seja uma E/S ou acesso a um recurso).
O RPC e a porta de conclusão que atende as solicitações de entrada tentarão manter o número de threads RPC utilizáveis no servidor igual ao número de processadores na máquina. 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 tem permissão para pegar uma nova solicitação e, em vez disso, ficará em um estado de espera ativa no caso de um dos blocos de threads atualmente utilizáveis. Se o quinto thread esperar tempo suficiente como um hot standby sem que o número de threads RPC utilizáveis caia 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 apenas se os threads bloquearem com frequência. Em um servidor onde os threads geralmente bloqueiam, um thread que retorna ao RPC é logo retirado da lista de espera ativa, porque todos os threads atualmente utilizáveis bloqueiam e recebe uma solicitação para processar. Quando um thread é bloqueado, 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, acessando diferentes estruturas de dados e terá uma pilha diferente, o que significa que a taxa de acerto do cache de memória (os caches L1 e L2) será muito menor, resultando em execução mais lenta. Os vários threads executados simultaneamente aumentam a contenção para 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 comboios sobre recursos se formam. Se a memória estiver 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 mais baixo de desempenho com uma alta taxa de comutação de contexto, ou pode se deteriorar a ponto de estar apenas acessando repetidamente o disco rígido e a troca de contexto sem executar nenhum trabalho real. Esta situação não se mostrará sob carga de trabalho leve, é claro, mas uma carga de trabalho pesada rapidamente traz o problema à tona.
Como é que isto pode ser evitado? Se se espera que os threads bloqueiem, declare as chamadas como assíncronas e, assim que a solicitação entrar na rotina do servidor, enfileirá-la em um pool de threads de trabalho que usam os recursos assíncronos do sistema de E/S e/ou RPC. Se o servidor, por sua vez, estiver fazendo chamadas RPC, torne-as assíncronas e certifique-se de que a fila não cresça muito. Se a rotina do servidor estiver executando E/S de arquivos, use E/S de arquivo assíncrona para enfileirar várias solicitações para o sistema de E/S e ter apenas alguns threads enfileirando-as e coletando 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 use o menor número possível de threads. Quando a E/S estiver concluída ou a chamada RPC feita pelo servidor estiver 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.
Dimensionamento
O RPC pode ser configurado para funcionar com o Balanceamento de Carga de Rede (NLB) se o NLB estiver configurado de forma que todas as solicitações de um determinado endereço de cliente vão para o mesmo servidor. Como cada cliente RPC abre um pool de conexões (para obter mais informações, consulte RPC e ode Rede), é essencial que todas as conexões do pool de determinado cliente acabem no mesmo computador servidor. Desde que essa condição seja atendida, um cluster NLB pode ser configurado para funcionar como um grande servidor RPC com escalabilidade potencialmente excelente.