Stas Afanasyev. Juno. Pipelines baseados em io.Reader / io.Writer. Parte 2

No relatório, falaremos sobre o conceito de io.Reader / io.Writer, por que eles são necessários, como implementá-los corretamente e que armadilhas existem a esse respeito, bem como sobre a construção de pipelines com base em implementações padrão e personalizadas de io.Reader / io.Writer .



Stas Afanasyev. Juno. Pipelines baseados em io.Reader / io.Writer. Parte 1

Bug "na confiança"


Outra nuance: nesta implementação, há um "bagul". Este bug é confirmado pelos desenvolvedores (escrevi a eles sobre isso). Talvez alguém saiba o que é esse "bagul"? No slide, está a penúltima linha:



está associada a muita confiança no Reader empacotado: se o Reader retornar um número negativo de bytes, o limite que gostaríamos de obter pelo número de bytes subtraídos aumenta. E, em alguns casos, esse é um bug muito sério que você não consegue entender imediatamente.

Eu escrevi na edição: vamos fazer alguma coisa, vamos consertar! E então uma camada de problemas foi revelada ... Primeiro, eles me disseram que se você adicionar essa verificação agora aqui, precisará adicioná-la em todos os lugares, e há uma dúzia desses lugares. Se queremos mudar isso para o lado do cliente, precisamos determinar um número de regras pelas quais o cliente validará os dados (e também podem haver cinco ou dois). Acontece que tudo isso precisa ser copiado.

Eu concordo que isso não é o ideal. Então vamos a uma versão consistente! Por que temos uma implementação da biblioteca padrão que não confia em nada, enquanto outros confiam em absolutamente tudo?

Em geral, enquanto eu escrevia minha opinião cívica, refletindo sobre o assunto, encerramos a questão com comentários: “Não faremos nada. Tchau"! Eles me fizeram parecer um tipo de idiota ... Educadamente, é claro, você não encontra falhas.

Em geral, agora temos um problema. Consiste no fato de que não está claro quem deve validar os dados do Reader empacotado. Ou o cliente, ou confiamos totalmente no contrato ... Temos uma solução! Se ainda houver tempo, eu falarei sobre isso.

Vamos para o próximo caso.

Teereader


Vimos um exemplo de como agrupar dados do Reader. O próximo exemplo de canal é ultrapassar os dados do Reader no Writer. Existem duas situações.

Primeira situação Precisamos ler os dados do Reader, copiá-los para o Writer (de forma transparente) e trabalhar com eles como no Reader. Existe uma implementação do TeeReader para isso. É apresentado no trecho de implementação superior:



Funciona como a equipe de Tee no Unix. Eu acho que muitos de vocês já ouviram falar disso.
Observe que esta implementação verifica o número de bytes que lê do Reader empacotado. Veja as condições na segunda linha? Porque quando você escreve essa implementação, fica intuitivamente claro: no caso de um número negativo, você entrará em pânico. E este é outro lugar em que confiamos no leitor embrulhado! Lembro que essas são todas as bibliotecas padrão.

Vamos passar para um caso, por exemplo, como usá-lo. O que faremos no trecho inferior? Baixaremos o arquivo robot.txt do golang.org usando o cliente http padrão.

Como você sabe, o cliente http nos retorna uma estrutura de resposta, na qual o campo Corpo é uma implementação da interface do Reader. Deve ser esclarecido dizendo que esta é uma implementação da interface ReadCloser. Mas o ReadCloser é apenas uma interface criada a partir do Reader e do Closer. Ou seja, este é um Reader, que pode, em geral, ser fechado.

Neste exemplo (no snippet inferior), coletamos o TeeReader, que lerá os dados deste corpo e os gravará em um arquivo. A criação do arquivo hoje, infelizmente, permaneceu nos bastidores, porque tudo não se encaixava. Mas, novamente, se você olhar para o dendograma, o tipo de arquivo implementa a interface do Writer, ou seja, podemos escrever para ele. É óbvio.

Montamos nosso TeeReader e o lemos usando ReadAll. Tudo funciona como esperado: subtraímos o corpo resultante, escrevemos em um arquivo e o vemos em Assad.

Maneira iniciante


A segunda situação. Só precisamos ler os dados do Reader e gravá-los no Writer. A solução é óbvia ...

Quando comecei a trabalhar com o Go, resolvi problemas como em um slide:



localizei o buffer, preenchi-o com dados do Reader e transferi a fatia preenchida para o Writer. Tudo é simples.

Dois pontos. Em primeiro lugar, não há garantia de que todo o Reader seja subtraído em uma chamada para o método Read, pois pode haver dados restantes (no bom sentido, isso deve ser feito em loop).

O segundo ponto é que esse caminho não é o ideal. Aqui está um código bastante padronizado, escrito antes de nós.

Para isso, há uma família especial de ajudantes na biblioteca padrão - são Copy, CopyN e CopyBuffer.

io.Copy. WriterTo e ReaderFrom


io.Copy basicamente faz o que era no slide anterior: ele aloca um buffer padrão de 32 KB e grava dados do Reader no Writer (a assinatura desta cópia é mostrada no snippet superior):



Além dessa rotina de modelo, ela também contém uma série de otimizações complicadas. E antes de falarmos sobre essas otimizações, precisamos nos familiarizar com mais duas interfaces:

  • WriterTo;
  • ReadFrom.

Situação hipotética. O seu Reader trabalha com um buffer de memória. Ele já o mudou, escreve, lê algo a partir daí, ou seja, um lugar sob ele já foi realocado. Você quer ler este leitor em algum lugar do lado de fora.

Já vimos como isso acontece: um buffer é criado, o buffer é passado, que é passado para o método Read; O Reader, que trabalha com memória, joga-o fora da peça replicada ... Mas isso não é mais o ideal - o local foi reposicionado. Por que fazer de novo?



Em algum lugar há 5 a 6 anos (há um link para a lista de alterações), foram feitas duas interfaces: WriteTo e ReadFrom, que são implementadas localmente. O Reader implementa WriteTo e o Writer implementa ReadFrom. Acontece que o Reader, com uma fatia com os dados já replicados, pode evitar um local adicional e aceitar os métodos Write To Writer e passar um buffer disponível no interior.

É assim que a implementação de bytes.Buffer e bufio funciona. E se você olhar o dendrograma novamente, verá que essas duas interfaces não são muito populares. Eles são implementados apenas para os tipos que trabalham com o buffer interno - onde a memória já foi realocada. Isso não ajudará você a evitar eloqüência todas as vezes, mas apenas se você já estiver trabalhando com uma peça realocada.

O ReaderFrom funciona de maneira semelhante (é implementado apenas pelo Writer). O ReaderFrom lê o Reader inteiro, que é um argumento para ele (antes do EOF) e grava em algum lugar na implementação interna do Writer.

Implementação CopyBuffer


Este trecho mostra a implementação do auxiliar copyBuffer. Este copyBuffer não exportável é usado sob o capô de io.Copy, CopyN e CopyBuffer.

E aqui há uma pequena nuance que vale a pena mencionar. CopyN foi recentemente otimizado - desamarrado dessa lógica. Essa é exatamente a otimização sobre a qual falei anteriormente: antes de criar um buffer adicional de 32 KB, é feita uma verificação - talvez a fonte de dados implemente a interface WriterTo e esse buffer adicional não seja necessário?

Se isso não acontecer, verificamos: talvez o Writer implemente o ReaderFrom para conectá-los sem esse intermediário? Se isso não acontecer, a última esperança permanece: talvez tenhamos recebido algum tipo de buffer realocado que poderíamos usar?



É assim que a io.Copy funciona.

Há uma questão, que é uma semi-proposta, um semi-bug - não está claro o que. Está pendurado há um ano e meio. Parece assim: CopyBuffer está semanticamente incorreto.

Infelizmente, não há assinatura para este copyBuffer, mas ele se parece exatamente com esse método não exportável.

Quando você chama copyBuffer na esperança de evitar um local adicional, transfere algum tipo de byte de fatia realocado para lá, a seguinte lógica funciona: se o Reader ou o Writer implementarem as interfaces WriterTo e ReaderFrom, não haverá garantia de que você poderá evitar esse local. Isso foi aceito como uma proposta e prometeu pensar nisso no Go 2.0. Por enquanto, você só precisa saber.

Trabalhe com io.Pipe. PipeReader e pipeWriter


Outro caso: você precisa obter dados do Writer de alguma forma no Reader. Caso de vida bonito.

Imagine que você já possui alguns dados, eles implementam a interface do Reader - tudo fica claro com isso. Você precisa compactar esses dados, “ajustá-los” e enviá-los ao S3. Qual é a nuance? ..
Quem trabalhou com o tipo gzip no pacote compess sabe que o próprio gzip'er é apenas um proxy: ele coleta dados, implementa a interface Writer, grava os dados, algo fará com eles e então eu tenho que deixá-los em algum lugar. No construtor, é necessária uma implementação da interface do Writer.

Portanto, aqui precisamos de algum tipo de gravador intermediário, onde descartamos os dados já compactados que são arquivados no primeiro estágio. Nosso próximo passo é fazer o upload desses dados para o S3. E o cliente padrão da AWS aceita a interface io.Reader como fonte de dados.



O slide mostra o pipeline - mostra como fica: precisamos ultrapassar os dados do Reader para o Writer, do Writer para o Reader. Como fazer isso?

A biblioteca padrão possui um recurso interessante - io.Pipe. Ele retorna dois valores: pipeReader e pipeWriter. Este par está inextricavelmente vinculado. Imagine um “telefone para bebês” em copos com cordas: não faz sentido falar em um copo enquanto ninguém está ouvindo do outro lado ...



O que esse io.Pipe faz? Ele não será lido até que ninguém grave os dados. E vice-versa, ele não escreverá nada até que ninguém leia esses dados do outro lado. Aqui está um exemplo de implementação:



Faremos o mesmo aqui. Vamos ler o arquivo robot.txt, que foi lido antes, compactá-lo usando nosso gzip e enviá-lo para o S3.

  • Na primeira linha, um par é criado - pipeReader, pipeWriter. Em seguida, precisamos executar pelo menos uma goroutine, que lerá os dados de uma extremidade (um tipo de tubo). Neste gorutin, execute o uploader com uma fonte de dados (source - pipeReader).
  • Na próxima etapa, precisamos compactar os dados. Nós compactamos os dados e os escrevemos no pipeWriter (será a outra extremidade do pipe), e a goroutine já em execução recebe os dados na outra extremidade do pipe e os lê. Quando todo esse sanduíche estiver pronto, tudo o que resta é atear fogo ao pavio ...
  • Copiar na última linha grava dados do corpo no gzip que criamos (ou seja, do Reader para o Writer). Tudo isso funciona como esperado.

Este exemplo pode ser resolvido de outra maneira. Se você usar qualquer implementação que implemente o Reader e o Writer. Você primeiro grava dados nele e depois os lê.
Foi uma demonstração clara de como trabalhar com o io.Pipe.

Outras implementações


Isso é basicamente tudo para mim. Chegamos a implementações interessantes sobre as quais gostaria de falar.



Eu não disse nada sobre o MultiReader, nem sobre o MultiWriter. E essa é outra implementação interessante da biblioteca padrão, que permite conectar diferentes implementações. Por exemplo, o MultiWriter grava em todos os Writers simultaneamente e o MultiReader lê os Leitores sequencialmente.

Outra implementação é chamada limio. Permite definir um limite para subtração. Você pode definir a velocidade em bytes por segundo na qual seu Reader precisa ser lido.

Outra implementação interessante é apenas uma visualização do progresso da leitura - a barra Progress (de algum cara). É chamado ioprogress.

Por que eu disse tudo isso? O que eu quis dizer com isso?



  • Se você precisar implementar de repente as interfaces Reader e Writer, faça-o corretamente. Ainda não existe uma decisão única sobre quem é responsável pela implementação - assumiremos que todos confiam no contrato. Então você precisa cumpri-lo impecavelmente.
  • Se o seu caso estiver trabalhando com um buffer reposicionado, não se esqueça das interfaces ReaderFrom e WriterTo.
  • Se você estiver em um beco sem saída e precisar de exemplos - consulte a biblioteca padrão, existem muitas implementações interessantes nas quais você pode confiar. Há documentação lá.
  • Se algo é completamente incompreensível para você, fique à vontade para escrever problemas. Os caras lá são adequados, respondem rapidamente, muito educadamente e com competência ajudam você.



Isso é tudo para mim. Obrigado por vir!

Questões


Pergunta da platéia (B): - Eu tenho uma pergunta simples, eu acho. Conte-nos sobre alguns casos de uso da vida: quais foram usados ​​e por quê? Você disse que o Reader / Writer retorna o tamanho que leu. Você já teve algum problema com isso? quando você exigiu a leitura (não existe apenas ReadAll), mas algo não funcionou?

SA: - Devo admitir honestamente que nunca tive casos assim, porque sempre trabalhei com implementações da biblioteca padrão. Mas hipoteticamente, essa situação, é claro, é possível. Em casos específicos, geralmente coletamos tubos multicamadas e, se você permitir um erro desse tipo, o tubo inteiro desmoronará ...

P:- Isso não é um bug. Vamos falar sobre minha pequena experiência. Eu tive um problema com o Booking.com: eles usaram o driver que eu escrevi e eles tiveram um problema - algo não estava funcionando. Existe um protocolo binário padrão que fizemos; localmente, tudo funciona bem, todo mundo está bem, mas descobriu-se que eles têm uma rede muito ruim com um data center. O Reader realmente não retornou tudo (placas de rede ruins, outra coisa).

CA: - Mas se ele não devolveu tudo, não deveria ter retornado o sinal do fim (fim) e o cliente deveria voltar. Sob o contrato descrito, o Reader não deve ... Digamos que o Reader, é claro, decide quando ele quer vir, quando ele não quer, no entanto, se ele quiser ler tudo, ele deve esperar pelo EOF.

AT:"Mas isso é precisamente por causa da conexão." Esse é exatamente o problema que ocorreu no pacote de rede padrão.

CA: - E ele devolveu o EOF?

P: - Ele não devolveu tudo - ele simplesmente não leu tudo. Eu disse a ele: "Leia os próximos 20 bytes". Ele lê. E eu não leio tudo.

SA: - Hipoteticamente, isso é possível, porque é apenas uma interface que descreve um protocolo de comunicação. É necessário assistir e desmontar especificamente o estojo. Aqui só posso responder que o cliente, em teoria, deveria ter voltado novamente se não recebesse tudo o que queria. Você pediu uma fatia de 20 bytes, ele subtraiu 15 para você, mas o EOF não veio - você deve voltar ...

P: - Existe um io.ReadFull para esta situação. Foi especialmente projetado para ler a fatia até o fim.

CA:- Sim. Eu não disse nada sobre o ReadFull.

P: - Essa é uma situação completamente normal quando o Read não preenche toda a fatia. Você precisa estar preparado para isso.

SA: - Este é um caso muito esperado!

P: - Obrigado pelo relatório - foi interessante. Eu uso os leitores em um proxy pequeno e simples que lê http e grava de outra maneira. Uso o Close Reader para resolver um problema - para fechar o que leio o tempo todo. Preciso confiar cegamente em um contrato? Você disse que pode haver problemas. Ou adicionar verificações adicionais? É teoricamente possível que algo não aconteça completamente neste site. Preciso fazer essas verificações adicionais e não confiar no contrato?

CA:- Eu diria o seguinte: se o seu aplicativo tolerar esses erros (por exemplo, se você confiar totalmente no contrato), talvez não. Mas se você não gostaria de ter um "pânico" em si mesmo (como mostrei na leitura negativa no byte.Buffer), ainda assim verificaria.
Mas isso depende de você. O que posso recomendar para você? Eu acho que apenas pesar os prós e contras. O que acontece se você repentinamente receber um número negativo de bytes?

P: - Obrigado pelo relatório. Infelizmente, não sei nada no Go. Se um "pânico" ocorreu, existe alguma maneira de interceptar essas informações e obter informações sobre o que, onde, como ser tendencioso, para evitar problemas na sexta à noite?

CA: - Sim. O mecanismo de recuperação permite "pegar" um pânico e evitá-lo sem cair, relativamente falando.



AT:- Como suas recomendações para o uso de implementações do Writer e Reader são consistentes com os erros retornados ao implementar soquetes da web. Não vou dar um exemplo concreto, mas o fim do arquivo sempre é usado lá? Tanto quanto me lembro, a mensagem termina com alguns outros significados ...

SA: - Essa é uma boa pergunta, porque simplesmente não tenho nada a responder. Deve observar! Se o EOF não vier, o cliente, se ele quiser obter tudo, deverá ir novamente.

P: - Quanto tempo o tubo foi capaz de montar? Existe alguma crença interna de que não vale a pena coletar mais de cinco participantes ou com filiais? Quanto tempo você conseguiu construir uma árvore a partir desses tubos (leitura, gravação)?

CA:- Na minha prática, cerca de cinco chamadas consecutivas são ótimas, porque é mais difícil depurar, lembre-se do que flui e para onde vai. Estrutura bastante ramificada é obtida. Mas eu diria em algum lugar 5-7 no máximo.

Q: - 5-7 - nesse caso?

SA: - Está lendo, por exemplo, alguns dados. Você precisa prometer, e o que faz login, precisa aparar. Prometido - depois de ler esses dados - você precisará enviá-los de volta para algum armazenamento (bem, hipoteticamente). Em qualquer armazenamento implementado pela interface do Writer. Com esse canal, ocorrem de 5 a 6 etapas, embora em uma delas ainda se ramifique para o lado e você continue trabalhando com o Reader.

AT:- De acordo com a maneira Iniciante, você teve um slide interessante. Você pode indicar outros 2-3 pontos interessantes que estavam lá, mas agora é melhor não fazê-los, mas fazê-lo de maneira diferente agora?

SA: - Com esse slide, eu queria mostrar exatamente como fazê-lo sem a necessidade de ler o Reader. Nunca me passou pela cabeça que algo parecido com o jeito Iniciante ... Este é provavelmente o principal erro, o principal padrão que deve ser evitado ao trabalhar com Leitores.
Apresentador: - Gostaria de acrescentar que é muito importante para um iniciante ler toda a documentação do pacote io, todas as interfaces existentes e compreendê-las. Porque, na verdade, existem muitos deles, e você frequentemente começa a fazer algo por conta própria, embora ele já exista lá e seja implementado corretamente (“certo” - levando em consideração todos os recursos).
Pergunta do líder: - Como viver mais?

CA: - Boa pergunta! Prometi dizer se temos tempo. Como resultado da discussão sobre o bug, o LimitedReader teve a seguinte decisão: criar um preservativo de leitor em algum sentido, que proteja contra ameaças externas, envolver um leitor em que você não confia - para impedir que qualquer infecção entre no sistema.

E neste Reader, você implementa todas as verificações que não pode fazer: por exemplo, leitura negativa, experimentos com o número de bytes (digamos que você enviou uma fatia de 10 bytes e recuperou 15 - como reagir a isso?) ... Reader e você pode implementar um conjunto dessas verificações. Eu disse: “Talvez vamos adicionar à biblioteca padrão, porque seria útil para todos usarem”?

Recebi a resposta de que parece não haver sentido nisso - isso é algo simples que você pode implementar. Todos. Nós vivemos. Confiamos no pessoal do contrato. Mas eu não confiaria.



P: - Quando trabalhamos com leitores, gravadores e há uma oportunidade de encontrar uma "bomba" gzip ... Quanto confiamos nos fluxos ReadAll e WriteAll? Ou, no entanto, implementar a leitura do buffer e trabalhar apenas com o buffer?

CA:- O ReadAll em si usa apenas bytes. Bufer sob o capô. Quando você quiser usar isso ou aquilo, é aconselhável entrar e ver como essas "tripas" são implementadas. Novamente, isso depende dos seus requisitos: se você é intolerante com esses erros que mostrei, precisa verificar se o que vem do Reader empacotado está marcado. Se não estiver marcado, use, por exemplo, bufio (lá está tudo marcado). Ou faça o que acabei de dizer: um certo proxy Reader, que, de acordo com sua lista de requisitos, verificará esses dados e os devolverá ao cliente ou ao cliente.




Um pouco de publicidade :)


Obrigado por ficar com a gente. Você gosta dos nossos artigos? Deseja ver materiais mais interessantes? Ajude-nos fazendo um pedido ou recomendando aos seus amigos o VPS na nuvem para desenvolvedores a partir de US $ 4,99 , um analógico exclusivo de servidores de nível básico que foi inventado por nós para você: Toda a verdade sobre o VPS (KVM) E5-2697 v3 (6 núcleos) 10 GB DDR4 480 GB SSD 1 Gbps de US $ 19 ou como dividir o servidor? (as opções estão disponíveis com RAID1 e RAID10, até 24 núcleos e até 40GB DDR4).

Dell R730xd 2 vezes mais barato no data center Equinix Tier IV em Amsterdã? Somente nós temos 2 TVs Intel TetraDeca-Core Xeon 2x E5-2697v3 2.6GHz 14C 64GB DDR4 4x960GB SSD 1Gbps 100 TV a partir de US $ 199 na Holanda!Dell R420 - 2x E5-2430 2.2Ghz 6C 128GB DDR3 2x960GB SSD 1Gbps 100TB - a partir de $ 99! Leia sobre Como criar um prédio de infraestrutura. classe c usando servidores Dell R730xd E5-2650 v4 que custam 9.000 euros por um centavo?

All Articles