De repente, um sistema de coleta de lixo por si só não é suficiente

Aqui está uma pequena história sobre falhas misteriosas de servidor que tive que depurar há um ano (artigo de 5 de dezembro de 2018, aprox. Por) Os servidores funcionaram bem por um tempo e, em algum momento, começaram a travar. Depois disso, as tentativas de executar quase todos os programas que estavam nos servidores falharam com os erros "Não há espaço no dispositivo", embora o sistema de arquivos tenha relatado apenas alguns gigabytes ocupados em discos de ~ 20 GB.

Aconteceu que o problema foi causado pelo sistema de log. Este era um aplicativo Ruby que pega arquivos de log, envia dados para um servidor remoto e exclui arquivos antigos. O erro foi que os arquivos de log abertos não foram explicitamente fechados. Em vez disso, o aplicativo permitiu que o coletor de lixo automático do Ruby limpasse os objetos File. O problema é que os objetos File não usam muita memória; portanto, o sistema de log pode teoricamente manter milhões de logs abertos antes que a coleta de lixo seja necessária.

* Os sistemas de arquivos Nix separam nomes e dados de arquivos em arquivos. Os dados em um disco podem ter vários nomes de arquivos apontando para eles (ou seja, links físicos) e os dados são excluídos apenas quando o último link é excluído. Um descritor de arquivo aberto é considerado um link; portanto, se o arquivo for excluído enquanto o programa estiver lendo, o nome do arquivo desaparecerá do diretório, mas os dados do arquivo permanecerão ativos até que o programa o feche. Foi o que aconteceu com o criador de logs. O comando du ("uso do disco") procura arquivos usando uma listagem de diretório, portanto, não viu gigabytes de dados do arquivo para os milhares de arquivos de log que ainda estavam abertos. Esses arquivos foram descobertos somente após a execução de lsof ("listar arquivos abertos").

Obviamente, um erro semelhante ocorre em outros casos semelhantes. Há alguns meses, tive que encontrar um aplicativo Java que foi interrompido após alguns dias devido a um vazamento nas conexões de rede.

Eu costumava escrever a maior parte do meu código em C e depois em C ++. Naquela época, eu achava que o gerenciamento manual de recursos era suficiente. Quão complicado foi isso? Cada malloc () precisa da função free () e cada open () precisa fechar (). Simplesmente. Exceto que nem todos os programas são simples, portanto, o gerenciamento manual de recursos ao longo do tempo se tornou uma camisa de força. Então, um dia, descobri a contagem de links e a coleta de lixo. Eu pensei que resolve todos os meus problemas e parei completamente de me preocupar com o gerenciamento de recursos. Novamente, para programas simples, isso era normal, mas nem todos os programas são simples.

Você não pode contar com a coleta de lixo, porque apenas resolve o problema do gerenciamento de memória, e programas complexos precisam lidar com muito mais do que apenas memória. Há um meme popular que responde a isso com o fato de que a memória é 95% dos problemas de recursos . Você pode até dizer que todos os recursos são 0% dos seus problemas - até você ficar sem um deles. Então esse recurso se torna 100% dos seus problemas.

Mas esse pensamento ainda percebe os recursos como um caso especial. Um problema mais profundo é que, à medida que os programas se tornam mais complexos, tudo tende a se tornar um recurso. Por exemplo, pegue um programa de calendário. O sofisticado programa de calendário permite que vários usuários gerenciem vários calendários compartilhados e com eventos que podem ser compartilhados em vários calendários. Qualquer parte dos dados afetará várias partes do programa e deve ser relevante e correta. Portanto, para todos os dados dinâmicos, você precisa de um proprietário, e não apenas para o gerenciamento de memória. À medida que novos recursos são adicionados, mais e mais partes do programa precisam ser atualizadas. Se você é sensato, você só permite atualizar dados de uma parte do programa por vez,para que o direito e a responsabilidade de atualizar os dados se tornem um recurso limitado. Modelar dados mutados usando estruturas imutáveis ​​não leva ao desaparecimento desses problemas, mas apenas os traduz em outro paradigma.

Planejar a propriedade e a vida útil dos recursos é uma parte inevitável do design de software complexo. Isso é mais fácil se você usar alguns padrões comuns. Um dos padrões é de recursos intercambiáveis. Um exemplo é a sequência imutável "foo", que é semanticamente igual a qualquer outra "foo" imutável. Esse tipo de recurso não precisa de uma vida útil ou posse predeterminada. De fato, para tornar o sistema o mais simples possível, é melhor não ter uma vida útil ou propriedade predeterminadas (oi Rust, aprox. Por). Outro padrão são os recursos que não são intercambiáveis, mas têm uma vida útil determinada. Isso inclui conexões de rede, além de conceitos mais abstratos, como o direito de controlar parte dos dados.O mais razoável é garantir explicitamente a vida útil dessas coisas ao codificar.

Observe que a coleta automática de lixo é realmente boa para implementar o primeiro padrão, mas não o segundo, enquanto as técnicas manuais de gerenciamento de recursos (como RAII) são ótimas para implementar o segundo padrão, mas péssimas para o primeiro. Essas duas abordagens se tornam complementares em programas complexos.

Source: https://habr.com/ru/post/undefined/


All Articles