Compartilhar via


Latência e taxa de transferência de rede

Três problemas principais estão relacionados ao uso ideal da rede:

  • Latência de rede
  • Saturação de rede
  • Implicações de processamento de pacotes

Esta seção apresenta uma tarefa de programação que exige o uso do RPC e, em seguida, projeta duas soluções: uma mal escrita e outra bem escrita. Em seguida, ambas as soluções são examinadas e seus efeitos no desempenho da rede são discutidos.

Antes de discutir as duas soluções, as próximas seções discutem e esclarecem problemas de desempenho relacionados à rede.

Latência de rede

Largura de banda de rede e latência de rede são termos separados. Redes com alta largura de banda não garantem baixa latência. Por exemplo, um caminho de rede que atravessa um link satélite geralmente tem alta latência, embora a taxa de transferência seja muito alta. Não é incomum que uma viagem de ida e volta de rede que atravessa um link satélite tenha cinco ou mais segundos de latência. A implicação desse atraso é esta: um aplicativo projetado para enviar uma solicitação, aguardar uma resposta, enviar outra solicitação, aguardar outra resposta e assim por diante, aguardará pelo menos cinco segundos por cada troca de pacotes, independentemente da rapidez com que o servidor está. Apesar da velocidade crescente dos computadores, as transmissões por satélite e as mídias de rede são baseadas na velocidade da luz, que geralmente permanece constante. Assim, é improvável que ocorram melhorias na latência das redes satélites existentes.

Saturação de rede

Alguma saturação ocorre em muitas redes. As redes mais fáceis de saturar são links de modem lento, como modems analógicos padrão de 56k. No entanto, os links Ethernet com muitos computadores em um único segmento também podem ficar saturados. O mesmo acontece com redes de ampla área com uma largura de banda baixa ou um link sobrecarregado, como um roteador ou comutador que pode lidar com uma quantidade limitada de tráfego. Nesses casos, se a rede enviar mais pacotes do que seu link mais fraco pode manipular, ela descartará pacotes. Para evitar o congestionamento, a pilha TCP do Windows é redimensionada quando são detectados pacotes descartados, o que pode resultar em atrasos significativos.

Implicações do processamento de pacotes

Quando os programas são desenvolvidos para ambientes de nível superior, como RPC, COM e até mesmo Soquetes do Windows, os desenvolvedores tendem a esquecer o quanto de trabalho ocorre nos bastidores para cada pacote enviado ou recebido. Quando um pacote chega da rede, uma interrupção do cartão de rede é atendida pelo computador. Em seguida, uma DPC (Chamada de Procedimento Adiado) é enfileirada e deve percorrer os drivers. Se qualquer forma de segurança for usada, o pacote poderá ter que ser descriptografado ou o hash criptográfico verificado. Uma série de verificações de validade também devem ser executadas em cada estado. Só então o pacote chega ao destino final: o código do servidor. Enviar muitas pequenas partes de dados resulta em sobrecarga de processamento de pacotes para cada pequena parte dos dados. O envio de uma grande parte dos dados tende a consumir significativamente menos tempo de CPU em todo o sistema, embora o custo de execução para muitas partes pequenas em comparação com uma parte grande possa ser o mesmo para o aplicativo de servidor.

Exemplo 1: um servidor RPC mal projetado

Imagine um aplicativo que deve acessar arquivos remotos e a tarefa em questão é criar uma interface RPC para manipular o arquivo remoto. A solução mais simples é espelhar as rotinas de arquivo do estúdio para arquivos locais. Isso pode resultar em uma interface enganosamente limpa e familiar. Aqui está um arquivo .idl abreviado:

typedef [context_handle] void *remote_file;
... .
interface remote_file
{
    remote_file remote_fopen(file_name);
    void remote_fclose(remote_file ...);
    size_t remote_fread(void *, size_t, size_t, remote_file ...);
    size_t remote_fwrite(const void *, size_t, size_t, remote_file ...);
    size_t remote_fseek(remote_file ..., long, int);
}

Isso parece elegante o suficiente, mas na verdade, esta é uma receita honrada para o desastre de desempenho. Ao contrário da opinião popular, a chamada de procedimento remoto não é simplesmente uma chamada de procedimento local com uma ligação entre o chamador e o chamador.

Para ver como essa receita queima o desempenho, considere um arquivo 2K, em que 20 bytes são lidos desde o início e, em seguida, 20 bytes do final e veja como isso funciona. No lado do cliente, as seguintes chamadas são feitas (muitos caminhos de código são omitidos para fins de brevidade):

rfp = remote_fopen("c:\\sample.txt");
remote_read(...);
remote_fseek(...);
remote_read(...);
remote_fclose(rfp);

Agora imagine que o servidor esteja separado do cliente por um link satélite com um tempo de viagem de ida e volta de cinco segundos. Cada uma dessas chamadas deve aguardar uma resposta antes que ela possa continuar, o que significa um mínimo absoluto para executar essa sequência de 25 segundos. Considerando que estamos recuperando apenas 40 bytes, isso é um desempenho escandalosamente lento. Os clientes desse aplicativo ficariam furiosos.

Agora imagine que a rede está saturada, pois a capacidade de um roteador em algum lugar no caminho de rede está sobrecarregada. Esse design força o roteador a manipular pelo menos 10 pacotes se não tivermos segurança (um para cada solicitação e um para cada resposta). Isso também não é bom.

Esse design também força o servidor a receber cinco pacotes e enviar cinco pacotes. Novamente, não é uma implementação muito boa.

Exemplo 2: um servidor RPC melhor projetado

Vamos reprojetar a interface discutida no Exemplo 1 e ver se podemos torná-la melhor. É importante observar que tornar esse servidor realmente bom requer conhecimento do padrão de uso para os arquivos fornecidos: esse conhecimento não é considerado para este exemplo. Portanto, este é um servidor RPC melhor projetado, mas não um servidor RPC projetado de forma ideal.

A ideia neste exemplo é recolher o máximo possível de operações remotas em uma operação. A primeira tentativa é a seguinte:

typedef [context_handle] void *remote_file;
typedef struct
{
    long position;
    int origin;
} remote_seek_instruction;
... .
interface remote_file
{
    remote_fread(file_name, void *, size_t, size_t, [in, out] remote_file ..., BOOL CloseWhenDone, remote_seek_instruction *...);
    size_t remote_fwrite(file_name, const void *, size_t, size_t, [in, out] remote_file ..., BOOL CloseWhenDone, remote_seek_instruction *...);
}

Este exemplo recolhe todas as operações em uma leitura e gravação, o que permite uma abertura opcional na mesma operação, bem como um fechamento e busca opcionais.

Essa mesma sequência de operação, quando gravada em forma abreviada, tem esta aparência:

remote_read("c:\\sample.txt", ..., &rfp, FALSE, NULL);
remote_read(NULL, ..., &rfp, TRUE, seek_to_20_bytes_before_end);

Ao considerar o servidor RPC melhor projetado, na segunda chamada, o servidor verifica se o file_name está NULL e usa o arquivo aberto armazenado em rfp. Em seguida, ele vê que há instruções de busca e posicionará o ponteiro de arquivo 20 bytes antes do final antes de ler. Quando terminar, ele reconhecerá o sinalizador CloseWhenDone está definido como TRUE e fechará o arquivo e fechará o rfp.

Na rede de alta latência, essa versão melhor leva 10 segundos para ser concluída (2,5 vezes mais rápida) e requer o processamento de apenas quatro pacotes; dois recebimentos do servidor e dois envios do servidor. O extra se e a nãomarsalação que o servidor executa são insignificantes em comparação com todo o resto.

Se a ordenação causal for especificada corretamente, a interface poderá até mesmo ser tornada assíncrona e as duas chamadas poderão ser enviadas em paralelo. Quando a ordenação causal é usada, as chamadas ainda são enviadas em ordem, o que significa que na rede de alta latência apenas um atraso de cinco segundos é suportado, mesmo que o número de pacotes enviados e recebidos seja o mesmo.

Podemos recolher isso ainda mais criando um método que usa uma matriz de estruturas, cada membro da matriz que descreve uma operação de arquivo específica; uma variação remota de E/S de dispersão/coleta. A abordagem compensa desde que o resultado de cada operação não exija processamento adicional no cliente; em outras palavras, o aplicativo vai ler os 20 bytes no final, independentemente do que são os primeiros 20 bytes lidos.

No entanto, se algum processamento precisar ser executado nos primeiros 20 bytes depois de lê-los para determinar a próxima operação, recolher tudo em uma operação não funcionará (pelo menos não em todos os casos). A elegância do RPC é que um aplicativo pode ter ambos os métodos na interface e chamar qualquer método dependendo da necessidade.

Em geral, quando a rede está envolvida, é melhor combinar o maior número possível de chamadas em uma única chamada. Se um aplicativo tiver duas atividades independentes, use operações assíncronas e permita que elas sejam executadas em paralelo. Essencialmente, mantenha o pipeline cheio.