Latência e taxa de transferência de rede
Três problemas principais estão relacionados ao uso ideal da rede:
- Latência da rede
- Saturação de rede
- Implicações do processamento de pacotes
Esta seção apresenta uma tarefa de programação que exige o uso de RPC e, em seguida, projeta duas soluções: uma mal escrita e outra bem escrita. Ambas as soluções são então examinadas e seu efeito no desempenho da rede é discutido.
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 de satélite tenha cinco ou mais segundos de latência. A implicação desse atraso é a seguinte: 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 longa distância com baixa largura de banda ou 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 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 Windows Sockets, os desenvolvedores tendem a esquecer quanto trabalho ocorre nos bastidores para cada pacote enviado ou recebido. Quando um pacote chega da rede, uma interrupção da rede cartão é 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á precisar ser descriptografado ou o hash criptográfico verificado. Várias verificações de validade também devem ser executadas em cada estado. Somente então o pacote chega ao destino final: o código do servidor. O envio de 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 de 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 é espelho as rotinas de arquivo do estúdio para arquivos locais. Isso pode resultar em uma interface enganosamente limpo 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 um fio entre o chamador e o receptor.
Para ver como essa receita queima o desempenho, considere um arquivo de 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 outro 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, esse é um servidor RPC melhor projetado, mas não um servidor RPC projetado de maneira 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 uma busca e fechamento opcionais.
Essa mesma sequência de operação, quando escrita 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 é NULL e usa o arquivo aberto armazenado em rfp. Em seguida, ele verá que há instruções de busca e posicionará o ponteiro do arquivo 20 bytes antes do final antes de ler. Quando terminar, ele reconhecerá que o sinalizador CloseWhenDone está definido como TRUE e fechará o arquivo e fechará 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 recebem do servidor e dois são enviados do servidor. Os ses extras 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 de quais 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 máximo 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.