Esse artigo tem como objetivo explicar de forma resumida e direta como funciona o armazenamento de dados no Docker. Falar sobre volumes e como eles impactam na performance do contêiner.
Para entender como o Docker gerencia seus volumes, primeiro precisamos explicar como funciona ao menos um backend de armazenamento do Docker. Faremos aqui com o AUFS, que foi o primeiro e ainda é um padrão em boa parte das instalações do Docker.
Como funciona um backend do Docker (Ex. AUFS)
O backend de armazenamento é a parte da solução do Docker que cuida do gerenciamento dos dados. No Docker, temos várias possibilidades de backend de armazenamento, mas nesse texto falaremos apenas do que implementa o AUFS.
AUFS é um unification filesystem. Isso quer dizer que ele é responsável por gerenciar múltiplos diretórios, empilhá-los uns sobre os outros e fornece uma visão única e unificada. Como se todos juntos fossem apenas um diretório.
Esse único diretório é o utilizado para apresentar ao contêiner, e funciona como se fosse um único sistema de arquivo comum. Cada diretório usado nessa pilha é correspondente a uma camada, e é dessa forma que o Docker unifica as camadas e proporciona a reutilização entre contêineres, pois o mesmo diretório correspondente a uma imagem pode ser montado em várias pilhas de vários contêineres.
Com exceção da pasta (camada) correspondente ao contêiner, todas as outras são montadas com permissão de somente leitura, pois caso contrário, as mudanças de um contêiner poderia interferir em um outro, o que de fato é totalmente contra os princípios do Linux Container.
Caso seja necessário modificar um arquivo que esteja nas camadas (pastas) referentes a imagens, é utilizado a tecnologia Copy-on-write (CoW), que é responsável por copiar o arquivo necessário para a pasta (camada) do contêiner e fazer todas as modificações nesse nível. Dessa forma, o arquivo original da camada inferior é sobreposto nessa pilha, ou seja, o contêiner em questão sempre verá apenas os arquivos das camadas mais altas.
No caso da remoção, o arquivo da camada superior é marcado como whiteout file e, assim, viabilizando a visualização do arquivo de camadas inferiores.
Problema com performance
O Docker aproveita da tecnologia Copy-on-write (CoW) do AUFS para permitir o compartilhamento de imagem e minimizar o uso de espaço em disco. O AUFS funciona no nível de arquivo. Isto significa que todas as operações AUFS CoW copiarão arquivos inteiros, mesmo que apenas uma pequena parte do arquivo esteja sendo modificado. Esse comportamento pode ter um impacto notável no desempenho do contêiner, especialmente se os arquivos que estão sendo copiados são grandes e estão localizados abaixo de um monte de camadas de imagem, ou seja, nesse caso, o procedimento copy-on-write dedicará muito tempo para uma cópia interna.
Volume como solução para performance
Ao utilizar volumes, o Docker montará essa pasta (camada) no nível imediatamente inferior ao do contêiner, o que nesse caso viabilizaria que todo dado armazenado nessa camada (pasta) fosse acessível rapidamente, ou seja, resolvendo o problema de performance.
O volume também resolve questões de persistência de dados, pois as informações armazenadas na camada (pasta) do contêiner são perdidas ao remover o contêiner, ou seja, ao utilizar volumes temos uma maior garantia no armazenamento desses dados.
Usando volumes
Para utilizar volumes, temos três caminhos: mapeamento de pasta específica do host, mapeamento via contêiner de dados e mapeamento de volumes.
Mapeamento de pasta específica do host
Nesse modelo o usuário escolhe uma pasta específica do host (Ex. /var/lib/container1) e a mapea com uma pasta interna do contêiner (Ex. /var), ou seja, dessa forma tudo que é escrito na pasta /var do contêiner é escrito também na pasta /var/lib/container1 do host.
Segue abaixo o exemplo de comando usado para esse modelo de mapeamento:
docker run -v /var/lib/container1:/var ubuntu
Esse modelo não é portável, pois necessita que o host tenha uma pasta específica para que o contêiner funcione adequadamente.
Mapeamento via contêiner de dados
Nesse modelo é criado um contêiner e dentro dele é nomeado um volume a ser consumido por outros contêineres. Dessa forma não precisa criar uma pasta específica no host para persistir dados. Essa pasta será criada automaticamente dentro da pasta raiz do Docker daemon, mas você não precisa se preocupar com essa pasta, pois toda referência será feita para o contêiner detentor do volume e não a pasta diretamente.
Segue abaixo um exemplo do uso desse modelo de mapeamento:
docker create -v /dbdata --name dbdata postgres /bin/true
No comando acima criamos um contêiner de dados, onde a sua pasta /dbdata pode ser consumida por outros contêiner, ou seja, o conteúdo da pasta /dbtada poderá ser visualizado e/ou editado por outros contêineres.
Para consumir esse volume do contêiner, basta utilizar esse comando:
docker run -d --volumes-from dbdata --name db2 postgres
Agora o contêiner db2 tem uma pasta /dbdata que é a mesma do contêiner dbdata. Com isso, tornando esse modelo completamente portável.
Uma desvantagem desse modelo é a necessidade de se manter um contêiner apenas pra isso, pois em alguns ambiente os contêineres são removidos com certa regularidade e, dessa forma, é necessário ter cuidado com esses contêineres especiais. O que, de uma certa forma, é um problema adicional de gerenciamento.
Mapeamento de volumes
Na versão 1.9 do Docker foi acrescentado a possibilidade de se criar volumes isolados de contêineres, ou seja, agora é possível criar um volume portável, sem a necessidade de associá-lo a um contêiner especial.
Segue abaixo um exemplo do uso desse modelo de mapeamento:
docker volume create --name dbdata
Como podemos ver no comando acima, o Docker criou um volume que pode ser consumido por qualquer contêiner.
A associação do volume ao contêiner acontece de forma parecida a praticada no mapeamento de pasta do host, pois nesse caso você precisa associar o volume a uma pasta dentro do seu contêiner, como podemos ver abaixo:
docker run -d -v dbdata:/var/lib/data postgres
Esse modelo é o mais indicado desde seu lançamento, pois ele proporciona portabilidade, não é removido facilmente quando o contêiner é deletado e ainda assim é bastante fácil sua gestão.