2016-03-07

Não tenha medo do ZFS Filesystem - Parte 1

Importante: Em todos os posts desse blog, estão
presentes minhas opiniões, visões e experiências
técnicas.O desenvolvimento, disponibidade de novas
funcionalidades ou quaisquer outras características
de produtos, são de única e exclusiva decisão da Oracle(r).

O filesystem ZFS é uma das inovações mais importantes que introduzimos no Oracle Solaris (ainda na versão 10) - parece que foi ontem, mas já se passaram mais de 11 anos desde seu lançamento.

Porém, mesmo depois deste tempo todo, ainda vejo pessoas com dúvidas sobre o ZFS, sua arquitetura e seu gerenciamento - especialmente considerando que operações, assim como os métodos de recuperação em caso de crises, em outros filesystems mais conservadores, como o UFS (antigo padrão do Oracle Solaris antes do ZFS),  são bem conhecidos e fazem parte do dia-a-dia do sysadmin.

A grande questão é: "por que migrar de outro filesystem para o ZFS?". A resposta é mais simples para o mundo Oracle Solaris - Porque root file system com UFS não é suportado pela Oracle a partir da versão 11. Mas além da questão de suporte oficial, existem muitas vantagens como performance, resiliência, alta disponibilidade, praticidade no gerenciamento, detecção de falhas e uma característica exclusiva do ZFS no mundo dos filesystems: self-healing (explicado mais a frente).

Com esta pergunta em foco, vou iniciar uma série de posts sobre as caraterísticas internas que fazem do ZFS o file system mais avançado disponível no mercado e como suas características podem nos ajudar nas atividades de dia a dia gerenciando ambientes críticos e complexos.

Visão geral

O ZFS pode ser dividido em duas partes principais: o ZFS pool e o ZFS dataset.

A base do ZFS é o pool - ele é o "alicerce" que suporta os datasets. Quando um dataset precisa de mais espaço, a alocação que o ZFS realizará é a partir dos blocos disponíveis no pool onde o dataset se encontra.

O gerenciamento de espaço do ZFS pool (zpool) pode ser comparado ao sistema de memória virtual - quando mais memória é adicionada a um sistema, o Solaris não requer que a memória seja configurada e assinalada para os processos. O mesmo acontece com o zpool. Os datasets são criados no topo do zpool e não usam nenhuma camada intermediária para virtualizar volumes. Isso significa que você pode facilmente adicionar espaço com poucos comandos ZFS.

Ainda, todos os datasets compartilham o espaço em um mesmo zpool. Não é necessário distribuir o espaço entre os datasets, pois eles podem crescer automaticamente dentro do espaço que lhe foi alocado no zpool. Quando um novo disco é adicionado ao zpool, todos os datasets dentro deste zpool podem utilizar o espaço extra disponibilizado e quando um arquivo é apagado, o espaço livre retorna para o zpool.

São 3 os datasets possíveis na arquitetura do ZFS:

ZFS file systems: Um file system que é montado para uso geral
Volumes: raw devices que podem ser usados como swap, dump devices em base de dados, etc
Clones: Uma copia de um file system ou volume
Snapshots: Uma copia de um file system ou volume, como o clone, mas read-only

Comparando o modelo tradicional de file system/volume versus o ZFS (baseado em zpools - chamado de pooled storage), podemos citar:

Tradicional:

Cada file system reside em uma partição/volume

Atividades de crescimento (growth) ou encolhimento (shrinking) de filesystems feitas de forma manual

Largura de banda limitada para cada file system

O storage é fragmentado

ZFS Pooled Storage:

Sem partições para gerenciar

growth/shrinking de File system são automáticos

Toda a largura de banda sempre disponível

Todo o storage no pool é compartilhado



Outra grande vantagem, que facilita o gerenciamento do ZFS: ele apresenta, apenas, dois comandos de administração: o comando zpool, para criar, modificar e destruir zpools. E o comando zfs, para criar, modificar e destruir ZFS datasets.

A criação de um zpool é bastante simples, basta determinar o nome do pool, o layout de formatação dos discos envolvidos (mirror, raidz, stripped, etc...) e opcionalmente log devices, cache devices e spare devices.

O exemplo abaixo, efetua a criação de um zpool chamado ORACLE, formado por dois mirrors compostos por dois discos cada:

# zpool create ORACLE mirror c0t1d0 c1t1d0 mirror c0t2d0 c1t2d0

Para expandir o pool, basta adicionar novos devices:

# zpool add ORACLE mirror c0t3d0 c1t3d0

A situação atual do zpool, incluindo seus objetos lógicos e físicos, pode ser visualizada com a saída do sub-comando status:

# zpool status ORACLE
pool: ORACLE
state: ONLINE
scrub: none requested
config:

NAME        STATE     READ WRITE CKSUM
ORACLE      ONLINE       0     0     0
mirror-0  ONLINE       0     0     0
c0t1d0  ONLINE       0     0     0
c1t1d0  ONLINE       0     0     0
mirror-1  ONLINE       0     0     0
c0t2d0  ONLINE       0     0     0
c1t2d0  ONLINE       0     0     0
mirror-2  ONLINE       0     0     0
c0t3d0  ONLINE       0     0     0
c1t3d0  ONLINE       0     0     0

errors: No known data errors
#

A criação de um dataset é tão simples quanto criar um simples diretório em um ambiente Oracle Solaris. No exemplo abaixo, estamos criando o filesystem /ORACLE/data01, parte do zpool ORACLE:

# zfs create ORACLE/data01
# zfs list
NAME                   USED  AVAIL  REFER  MOUNTPOINT
ORACLE                92.0K  87.0G   9.5K  /ORACLE
ORACLE/data01      24.0K  87.0G   8K  /ORACLE/data01

Um lab completo usando Oracle Solaris 11 para x86 e Virtualbox está disponível neste link.

Olhando mais de perto



A figura acima mostra como a estrutura interna do ZFS é organizada.

Os principais componentes do código estão na área de kernel (ou kernel land) do Solaris, divididos em três camadas:

Pooled Storage

Transactional Object

Interface

E na área de usuários (ou user land) podemos encontrar os seguintes componentes:

Consumers (file system e dispositivos)

Aplicações (interagindo com os ZFS datasets)

Comandos (zfs e zpool)

libzfs (a biblioteca de integração de outros components com o ZFS)

Então para entender como a orquestra funciona, vamos conhecer os instrumentos individualmente, ou seja, vamos analisar a estrutura da base ao topo.

Virtual Devices e Physical Devices



O pool é uma coleção de dispositivos virtuais, chamados de vdev, que são organizados em forma de árvore (vdev tree). Um vdev pode ser um arquivo (até mesmo um sparse file), um slice de um disco, um disco local (ou uma LUN de um storage externo), podendo-se habilita automaticamente o write cache do disco.

O acesso ao vdev (quando este é um disco) é feito através do LDI (layered driver interface). O LDI, que é um conjunto de device driver interfaces (ddi) e device kernel interfaces (dki), possibilita que um módulo de kernel acesse outros dispositivos no sistema, além de auxiliar na definição de quais dispositivos estão sendo usados pelos módulos do kernel.

Um vdev descreve
um único dispositivo, ou uma coleção de dispositivos organizados de
acordo com certas características de performance e tolerância a falhas.
Todas as operações do zpool passam pelo framework do vdev, que fornece
funções como:

Replicação de dados

I/O scheduling

Caching

As configurações suportadas no
ZFS são stripped (sem nenhum tipo de alta disponibilidade de dados, mirror (espelhamento, similar ao RAID-1), RAID-Z[1] (similar ao RAID-5), e RAID-Z2 com
double parity (similar ao RAID-6).

Aqui vale lembrar que o algoritmos tradicionais baseados em paridade de dados (como implementado em (RAID-4, RAID-5, RAID-6, RDP, e EVEN-ODD, por exemplo) sofrem de um problema
conhecido como "write hole". Funciona assim: se apenas parte de um stripe em RAID-5 (por exemplo) é
escrita e, durante esta operação, há uma perda de energia antes de todos os blocos irem para o
disco, a paridade vai continuar fora de sincronia com os dados (ou seja, é inutilizada para sempre), a não ser que depois uma escrita sobrescreva o
stripe todo.

No RAID-Z, o ZFS usa stripes de tamanhos variáveis, então todas
as escritas são "full stripe". Este design só é possível porque o ZFS
integra file system e gerenciamento de dispositivo de tal forma que o
metadata do file system tem informação suficiente sobre o modelo de
redundância de dados para lidar com estes stripes de tamanho variado.
RAID-Z é a primeira solução apenas em software que endereça e soluciona os write holes.

A raiz da estrutura do zpool é o root vdev, e os vdevs diretamente conectados a ele (children) são chamados de top level vdevs. Nas operações de escrita os dados são distribuídos através dos top level vdevs e quando novos top level devices são adicionados eles juntam-se automaticamente à estrutura do zpool.

Existem diferentes tipos de vdevs:

raidz, raidz2, raidz3 (lógico)

root, mirror, replacing, spare (lógico)

disco (físico)

arquivo (físico)

hole, missing (top-level)

Um grupo raidz pode ter paridade simples, dupla ou tripla o que significa que o raidz group pode garantir a integridade de dados, mesmo com a perda de 1 (paridade simples), 2 (paridade dupla) ou 3 (paridade tripla) discos parte do mesmo zpool.

raidz ou raidz1 - paridade simples (single parity)

raidz2 - paridade dupla (double parity)

raidz3 - paridade tripla (triple parity)

O layout dos vdev

O ZFS reserva 1 megabyte no dispositivo para ser usado como label do vdev. São quatro cópias (256 kilobytes cada) deste label, distribuídas no início e no fim de cada disco, aumentando a possibilidade de recuperação em caso de falha: uma área reservada para proteção da VTOC, outra para o cabeçalho do boot block e as duas restantes são "vdev configuration" e "uberblocks array".

A "vdev configuration" possui, entre outras coisas, o GUID (global unique identifier) do root vdev, o número de top level vdevs e detalhes da vdev tree, como o tipo (file, disk, mirror, raidz, spare, replacing), path para o dispositivo físico e flags (degraded, removed, etc).

O uberblock é a porção do label que contém informação necessária para accessar o conteúdo do pool, sendo somente um uberblock ativo por vez - para definir qual o uberblock será o ativo, o ZFS considera: o uberblock com o mais alto transaction group e checksum válidos.

Para garantir acesso constante ao uberblock ativo, ele nunca é sobrescrito. Todos os updates de um uberblock são feitos modificando outro elemento do array de uberblocks (No "uber block array" cada uberblock tem 1 kilobyte de tamanho). Após escrever o novo uberblock, o número do transaction group e os timestamps são incrementados, tornando-o assim o novo uberblock ativo "atomicamente". Uberblocks são escritos usando um algoritmo round-robin entre os vários vdevs dentro do pool.

ZIO

Falando de modo geral, o ZIO fornece um framework linear para todas as transações de I/O no ZFS (síncronas e assíncronas). Traduz DVA (Data Virtual Addresses) para posições lógicas nos vdevs, cuida da geração e verificação do checksum, cuida da compressão e encriptação de dados, deduplicação de blocos (data deduplication) e tentativas de operações de I/O que possam ter falhado (I/O retry).

Para fornecer verificações de integridade end-to-end, os checksums são habilitados por padrão para todos os blocos, mas só pode ser desabilitado para dados, não para metadados (ZFS metadata).

Já a compressão de dados pode ser habilitada pelo comando zfs para dados, ativando uma flag especial no block pointer, e atualmente é possível usar três algoritmos, lzjb, gzip (todos os níveis) e ZLE (zero length encoding). A compressão é automaticamente feita para os metadados.

Deduplicação de Dados (data deduplication - dedup)

Data Deduplication é o processo de eliminação de copias duplicadas de dados. O dedup pode ocorrer em vários níveis, como arquivo, bloco or byte.

Os conjuntos de dados (arquivos, blocos ou uma série de bytes) passam por uma função hash para gerar o checksum, isso cria uma identificação exclusiva daquele dado (a probabilidade é muito alta de que o hash seja único). Se for usado um hash seguro como SHA256, a probabilidade de um erro (hash collision) é aproximadamente 2^256, ou 10^77, ou ainda em uma notação mais familiar 0.00000000000000000000000000000000000000000000000000000000000000000000000000001. Para referência, isto é 50 vezes menos provável de ocorrer do que um erro ECC indetectável e incorrigível na memória dos hardwares mais confiáveis nos dias de hoje.

Blocos de dados são "lembrados" em uma tabela que faz o cruzamento do checksum dos dados com a localização no disco e o contador de referência (ref count como em hard links). Quando você armazena uma nova copia de um dado existente, ao invés de alocar um novo espaço no disco, o código do dedup apenas incrementa o ref count no dado existente.

No ZFS, o dedup é em nívem de locos, pois esta é a menor granularidade que faz sentido para um storage system de uso geral. Como o bloco de checksum do ZFS tem 256 bits, O dedup fornece assinaturas únicas para todos os bloco em um zpool, desde que a função de checksum seja criptográficamente forte, exemplo SHA256.

ARC

ARC significa "Adaptative Replacement Cache" e a implementação no ZFS foi inspirada no trabalho de Nimrod Meggido e Dharmendra Modha "ARC: A Self-Tuning, Low Overhead Replacement Cache" apresentado no FAST 2003 (Usenix Conference of File and Storage Technologies). O ARC fornece cache para os buffers de SPA, ou seja, o ZFS usa ARC para fazer cache de data blocks. Ele usa um algoritmo alto-ajustável combinando métricas de acesso á blocos chamadas MRU (Most Recently Used), MFU (Most Frequently Used) e LRU (Least Recently Used).

O ARC alcança uma alta taxa de acerto (high cache hit rate) por usar múltiplos algoritmos de cache ao mesmo tempo: MRU e MFU. A memória principal é equilibrada entre esses algoritmos com base no seu desempenho, o que favorece manter extra metadata (na memória principal) para ver como seria o desempenho de cada algoritmo se ele dominasse toda a memoria.

ARC é usado pelo layer do DMU para fazer cache de data buffers
- ZFS usa o ARC ao invés do page cache do sistema

Uma hash-table visivel por todo o sistema é mantida para todos os "cached buffers"

O DVA é usado como chave para o hash

Os buffers vem do kmem caches (kernel memory caches) criados pelo ZFS

L2ARC

Para melhorar o desempenho foi adicionado um segundo nível de cache (Level 2 ARC - L2ARC). A memória disponível no sistema é finita, portanto o ARC possui um mecanismo para liberar blocos para novas entradas. É neste ponto que entra o L2ARC - ele é um cache adicional entre o ARC e o disco, criado para impulsionar a performance das leituras aleatórias (random reads). Para isso são usados dispositivos com latência de leitura menor do que discos convencionais (solid state drives - SSD, por exemplo), caso contrário o resultado é o mesmo de ter apenas o ARC.

O funcionamento é relativamente simples: uma thread do ZFS percorre a lista de blocos que serão liberados das listas do MFU/MRU e copia os blocos para os devices do L2ARC (se ainda não estiverem presentes). Não existe uma ligação entre os 2 caches (cascade), portanto não há garantia de que os blocos liberados no ARC estarão no L2ARC.

SPA

O SPA fornece as interfaces para criar, destruir, importar, exportar e modificar storage pools.

Os zpools são alocados em estruturas no kernel chamadas spa_t e armazenados em uma árvore AVL (árvore de busca binária autobalanceada) global. A estrutura guarda, em áreas distintas, a configuração do zpool, informações de spares, cache devices (l2arc).

O zpool history é um ring buffer de 1% do tamanho do pool (mínimo 128KB e máximo de 32MB), implementado e mantido para registrar as ações dos comandos zpool e zfs, além de eventos internos do ZFS. Embora seja um ring buffer, o registro de criação do zpool nunca é sobrescrito.

Metaslab/Spacemap

Todo file system precisa manter o controle de 2 coisas básicas: onde há dados, e onde está o espaço livre.

A princípio, em uma estrutura que gerencia discos, filesystems e dispositivos que armazenem dados, manter o controle do espaço livre não é essencial. Cada bloco só pode ter um dos dois status, alocado ou livre, então o espaço livre pode ser calculado assumindo que tudo é livre e depois subtraindo tudo que esteja alocado. Além disso, o espaço usado pode ser encontrado por um busca na árvore de diretórios (tree traversal). Qualquer bloco que não for encontrado pela busca à partir do root dir do zpool, por definição, é livre (simples não? nem tanto).

Na prática, encontrar espaço desta forma pode ser insuportável, porque pode levar muito tempo para terminar em file systems de tamanho não trivial. Para fazer a alocação e liberação de blocos de maneira rápida, o file system precisa de uma maneira eficiente de manter o controle do espaço livre.

O ZFS utiliza spacemaps para controlar o espaço livre. Ele divide o espaço existente em cada device virtual em uma região chamada metaslab. Cada metaslab tem um mapa associado, que descreve o espaço livre daquele metaslab. O spacemap é apenas um log de alocações e liberações de blocos, em ordem cronológica.

Quando o ZFS decide alocar blocos de um metaslab em particular, ele primeiro lê o space map daquele metaslab no disco e depois aplica as alocações/liberações em uma árvore AVL na memória.

...continua

Autor deste post:

Diogo Padovani
Principal Systems Engineer
Oracle Systems, Revenue Product Engineering (RPE)

Show more