Explicações · dois
Um descarregamento que corre na perfeição é o caso fácil, e todos os clientes o tratam com competência. As diferenças interessantes surgem quando falta parte do post, o que acontece muito mais vezes do que o caso limpo faria supor. É isto que se passa de facto, e porque custa tanto tempo.
A forma do problema
Um post da Usenet não é um ficheiro. São milhares de pequenos artigos, cada um guardado de forma independente nos servidores de cada fornecedor, e o NZB que descarregas é a lista dos seus nomes. Um lançamento de 6.5 GB são cerca de 9,000 deles.
Os artigos desaparecem por razões comuns. Os fornecedores guardam-nos durante um período de retenção fixo e depois apagam-nos. Alguns fornecedores nunca chegaram a receber determinado artigo, porque a propagação entre servidores é imperfeita. Por vezes um envio estava incompleto quando foi publicado, e parte dele nunca existiu em lado nenhum. Seja qual for a causa, o efeito é o mesmo: o teu cliente pede um artigo, e a resposta é uma recusa em vez de dados.
Isto é suficientemente comum para que quem publica na Usenet conte com ele. Quase todos os lançamentos são acompanhados de dados de paridade, normalmente ficheiros PAR2, ou seja, informação redundante adicional calculada a partir do original. Se faltar parte do conteúdo, a paridade consegue reconstruí-lo, desde que tenhas o suficiente. Um post típico traz cerca de 10% de paridade, portanto pode perder uma fatia apreciável de si mesmo e continuar perfeitamente recuperável.
Assim, um post danificado não costuma ser um descarregamento partido. É um descarregamento que exige alguma aritmética antes de estar completo. A única questão é quanto tempo um cliente leva a perceber isso, e é aí que diferem por um fator de dois ou mais.
A parte cara
Quando um servidor não tem um artigo devolve uma recusa, e o cliente tenta então o servidor seguinte da tua lista. É a coisa certa a fazer, porque o segundo fornecedor muitas vezes tem-no. O custo aparece quando ninguém o tem.
Nesse caso o cliente percorre toda a tua lista de fornecedores, um servidor de cada vez, e é recusado por cada um deles antes de poder concluir que o artigo desapareceu mesmo. O percurso é sequencial, e as recusas são mais lentas do que se supõe. Medimo-las diretamente, numa linha em repouso, sem qualquer conteúdo em movimento:
| fornecedor | tempo para responder a um pedido normal | tempo para recusar um artigo em falta |
|---|---|---|
| Fornecedor A | 77.5 ms | 78.8 ms |
| Fornecedor B | 10.5 ms | 454 ms |
| Fornecedor C | 10.8 ms | 871 ms |
| Fornecedor D | 9.9 ms | 1,227 ms |
| Fornecedor E | 10.6 ms | 2,239 ms |
Duas coisas saltam à vista. Primeiro, a amplitude é enorme: uma recusa custa entre 79 ms e 2.2 segundos consoante o fornecedor que responde, uma diferença de quase trinta vezes. Segundo, um fornecedor recusa praticamente no tempo que leva a perguntar, enquanto outro leva mais de duzentas vezes o seu próprio tempo de resposta normal para dizer que não. Isso não é latência de rede, é trabalho que decorre do lado deles, e perguntar em menos idas e voltas pouco ajuda.
Soma tudo: um percurso completo por cinco fornecedores, por um único artigo que já não existe, custa cerca de 5 segundos. Agora considera que um post danificado tem muitos artigos assim, e que um cliente que os processa em sequência paga esse custo repetidamente enquanto a tua ligação fica parada. O descarregamento não está lento porque os dados chegam devagar. Deixaram de chegar de todo, e o cliente está à espera que lhe digam aquilo que já tem informação suficiente para deduzir.
A solução
A ideia não é tornar as recusas mais rápidas, porque não controlamos os fornecedores. É reparar no momento em que a resposta deixa de importar.
Em qualquer momento de um descarregamento o cliente sabe duas coisas: quantas peças continuam por aparecer e quantos dados de paridade já tem. Se a paridade em mão chegar para reconstruir tudo o que falta, então a resposta a «alguém tem este artigo?» já não pode mudar o desfecho. Quer seja um sim lento quer um não lento, o ficheiro é reconstruído na mesma. Continuar o percurso não compra nada a não ser atraso.
Portanto a compilação atual para de pedir nesse ponto e vai direta à reparação. A reconstrução em si é rápida e sempre foi: cerca de 2.3 segundos num post de 6.5 GB, e esse valor é o mesmo em todas as versões que medimos. O tempo poupado é inteiramente a espera que deixa de acontecer.
| post de 6.5 GB, 60 artigos em falta | tempo até um ficheiro verificado |
|---|---|
| esperar que cada fornecedor recuse | 23 s |
| parar assim que a paridade cobre a falha | 13 s |
| do qual, a aritmética de reparação propriamente dita | 2.3 s |
A mesma alteração não faz absolutamente nada num post intacto, o que é a prova mais clara de que faz o que julgamos: sem artigos em falta não há percurso a encurtar, e as duas compilações terminam uma tarefa limpa de 6.5 GB em idênticos 7 segundos.
É uma troca, não um ganho gratuito. Reparar cedo significa manter os dados de paridade em memória em vez de os descartar e ir buscar mais depois, portanto a memória de pico (RSS) num post muito danificado sobe de cerca de 0.8 GB para cerca de 1.3 GB, e puxamos aproximadamente mais 400 MB dos fornecedores. Num post intacto a memória mantém-se em 0.24 GB. Achamos que é a troca certa para a maioria das pessoas, já que a memória só é gasta no caso em que te poupa dez segundos, mas é um custo real e existe uma definição para isso.
Os nomes dos fornecedores são omitidos de propósito na tabela de recusas. Estes valores são propriedade de uma medição, numa noite, a partir de um só local, e um fornecedor que indexe de outra maneira, ou que por acaso apanhámos numa noite má, não deve ser rotulado de lento com base numa única ronda. O que importa para o argumento é a forma: as recusas variam enormemente, e o total é grande o suficiente para dominar um descarregamento danificado. Os valores por fornecedor e a saída bruta das sondas estão no nosso registo interno, e os números de ponta a ponta que eles explicam estão na página de benchmarks.