Explicações · dois

Porque é que os posts danificados importam

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

O que é um artigo em falta

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

Pedir algo que ninguém tem

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:

fornecedortempo para responder a um pedido normaltempo para recusar um artigo em falta
Fornecedor A77.5 ms78.8 ms
Fornecedor B10.5 ms454 ms
Fornecedor C10.8 ms871 ms
Fornecedor D9.9 ms1,227 ms
Fornecedor E10.6 ms2,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

Parar de pedir e reparar em vez disso

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 faltatempo até um ficheiro verificado
esperar que cada fornecedor recuse23 s
parar assim que a paridade cobre a falha13 s
do qual, a aritmética de reparação propriamente dita2.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.