Remove os blocos nulos iniciais de um arquivo esparso

0

Estou usando logrotate com a opção copytruncate . Isso funciona bem, criando um arquivo esparso que começa com um número crescente de blocos nulos "virtuais" que não ocupam espaço no disco.

O problema é com os arquivos copiados: Embora ocupem pouco espaço no disco, tentar examiná-los usando less leva uma eternidade, já que os blocos nulos "virtuais" são expandidos para nulos reais. Eu realmente gostaria de eliminar os blocos nulos iniciais esparsos desde o início do arquivo copiado.

Aqui está o que eu sei até agora: ls -ls e du podem me dizer quanto do arquivo é "real". E acho que dd pode ser usado para fazer uma cópia sem os blocos vazios iniciais. Mas estou tendo problemas para juntar tudo em algo que posso colocar na seção postrotate do meu arquivo logrotate.conf .

Eu encontrei métodos que usam tr ou sed para excluir os nulos, mas isso requer a expansão do arquivo (tornando os nulos virtuais físicos) e, com o tempo, o arquivo pode crescer para mais de um terabyte! Eu preciso de uma abordagem mais 'cirúrgica' que funcione sem expandir o arquivo. Ele deve exigir apenas mexer com os inodes, já que é onde os blocos esparsos vivem (não na área real alocada).

Naturalmente, a correção "real" é fazer com que o programa gerador use SIGHUP para reabrir seu arquivo de saída, mas isso não é possível neste caso.

Qual é a maneira mais simples e rápida de remover diretamente os blocos nulos iniciais de um arquivo esparso?

Adendo: veja como criar seu próprio arquivo esparso para brincar:

$ dd if=/dev/zero of=sparse.txt bs=1 count=0 seek=8G
0+0 records in
0+0 records out
0 bytes (0 B) copied, 0.000226785 s, 0.0 kB/s

$ echo 'Hello, World!' >>sparse.txt

$ ls -ls sparse.txt
4 -rwxrwxrwx 1 me me 8589934606 Nov  6 10:20 sparse.txt

$ ls -lsh sparse.txt 
4.0K -rwxrwxrwx 1 me me 8.1G Nov  6 10:20 sparse.txt

Esse arquivo "enorme" ocupa quase nenhum espaço no disco. Agora tente less sparse.txt . Você terá que percorrer 8G de nulos para chegar aos personagens no final. Mesmo tail -n 1 sparse.txt leva um bom tempo.

    
por BobC 06.11.2014 / 18:57

2 respostas

0

Esta é minha primeira tentativa do tipo parece funcionar, usando stat e dd , que funciona somente para arquivos esparsos:

#! /bin/bash
for f in $@; do
  echo -n "$f : "
  fields=( 'stat -c "%o %B %b %s" $f' )
  xfer_block_size=${fields[0]}
  alloc_block_size=${fields[1]}
  blocks_alloc=${fields[2]}
  size_bytes=${fields[3]}

  bytes_alloc=$(( $blocks_alloc * $alloc_block_size ))

  alloc_in_xfer_blocks=$(( ($bytes_alloc + ($xfer_block_size - 1))/$xfer_block_size ))
  size_in_xfer_blocks=$(( ($size_bytes + ($xfer_block_size - 1))/$xfer_block_size ))
  null_xfer_blocks=$(( $size_in_xfer_blocks - $alloc_in_xfer_blocks ))
  null_xfer_bytes=$(( $null_xfer_blocks * $xfer_block_size ))
  non_null_bytes=$(( $size_bytes - $null_xfer_bytes ))

  if [ "$non_null_bytes" -gt "0" -a "$non_null_bytes" -lt "$size_bytes" ]; then
    cmd="dd if=$f of=$f.new bs=1 skip=$null_xfer_bytes count=$non_null_bytes"
    echo $cmd
    exec $cmd
  else
    echo "Nothing to do: File is not sparse."
  fi
done

O que você acha?

    
por 07.11.2014 / 00:38
0

Eu criei uma conta aqui para que eu pudesse agradecer à @BobC por sua resposta (e sua pergunta). Foi o catalisador que eu precisava para resolver nosso problema de longa data com os logs do Solr.

Modifiquei o script do BobC para otimizá-lo um pouco para o caso de uso de logrotate (usando $xfer_block_size para ibs e um arbitrariamente grande (8M) obs , seguido por um tr -d "firstaction0" para eliminar os nulos restantes ) e, em seguida, usei na seção logrotate do meu dd config.

Minha solução é um pouco hacky, eu acho, mas é muito melhor do que ter que devolver serviços de produção críticos quando um arquivo de log de mais de 80 GB ameaça preencher o disco ...

Foi com isso que acabei:

#! /bin/bash
# truncat.sh
# Adapted from @BobC's script http://superuser.com/a/836950/539429
#
# Efficiently cat log files that have been previously truncated.  
# They are sparse -- many null blocks before the interesting content.
# This script skips the null blocks in bulk (except for the last) 
# and then uses tr to filter the remaining nulls.
#
for f in $@; do
  fields=( 'stat -c "%o %B %b %s" $f' )
  xfer_block_size=${fields[0]}
  alloc_block_size=${fields[1]}
  blocks_alloc=${fields[2]}
  size_bytes=${fields[3]}

  bytes_alloc=$(( $blocks_alloc * $alloc_block_size ))

  alloc_in_xfer_blocks=$(( ($bytes_alloc + ($xfer_block_size - 1))/$xfer_block_size ))
  size_in_xfer_blocks=$(( ($size_bytes + ($xfer_block_size - 1))/$xfer_block_size ))
  null_xfer_blocks=$(( $size_in_xfer_blocks - $alloc_in_xfer_blocks ))
  null_xfer_bytes=$(( $null_xfer_blocks * $xfer_block_size ))
  non_null_bytes=$(( $size_bytes - $null_xfer_bytes ))

  if [ "$non_null_bytes" -gt "0" -a "$non_null_bytes" -lt "$size_bytes" ]; then
    cmd="dd if=$f ibs=$xfer_block_size obs=8M skip=$null_xfer_blocks "
    $cmd | tr -d "
# ls -l 2015_10_12-025600113.start.log
-rw-r--r-- 1 solr solr 93153627360 Dec 31 10:34 2015_10_12-025600113.start.log
# du -shx 2015_10_12-025600113.start.log
392M    2015_10_12-025600113.start.log
#
# time truncat.sh 2015_10_12-025600113.start.log > test1
93275+1 records in
45+1 records out
382055799 bytes (382 MB) copied, 1.53881 seconds, 248 MB/s

real    0m1.545s
user    0m0.677s
sys 0m1.076s

# time cp --sparse=always 2015_10_12-025600113.start.log test2

real    1m37.057s
user    0m8.309s
sys 1m18.926s

# ls -l test1 test2
-rw-r--r-- 1 root root   381670701 Dec 31 10:07 test1
-rw-r--r-- 1 root root 93129872210 Dec 31 10:11 test2
# du -shx test1 test2
365M    test1
369M    test2
0" else cat $f fi done

O uso de blocos maiores torna dd ordens de magnitude mais rápidas. tr faz um primeiro corte, então logrotate apara o restante dos nulos. Como ponto de referência, para um arquivo esparso de 87 GiB (contendo 392 dados MiB):

/var/log/solr/rotated.start.log {
    rotate 14
    daily
    missingok
    dateext
    compress
    create
    firstaction
        # this actually does the rotation.  At this point we expect 
        # an empty rotated.start.log file.
        rm -f /var/log/solr/rotated.start.log
        # Now, cat the contents of the log file (skipping leading nulls) 
        # onto the new rotated.start.log
        for i in /var/log/solr/20[0-9][0-9]_*.start.log ; do
           /usr/local/bin/truncat.sh $i >> /var/log/solr/rotated.start.log
           > $i  # truncate the real log
        done
     endscript
}

Quando deixei que copytruncate processasse isso usando gzip , demorou mais de uma hora e resultou em um arquivo não esparso totalmente materializado - que levou uma hora para logrotate .

Aqui está minha solução rotated.start.log final:

#! /bin/bash
# truncat.sh
# Adapted from @BobC's script http://superuser.com/a/836950/539429
#
# Efficiently cat log files that have been previously truncated.  
# They are sparse -- many null blocks before the interesting content.
# This script skips the null blocks in bulk (except for the last) 
# and then uses tr to filter the remaining nulls.
#
for f in $@; do
  fields=( 'stat -c "%o %B %b %s" $f' )
  xfer_block_size=${fields[0]}
  alloc_block_size=${fields[1]}
  blocks_alloc=${fields[2]}
  size_bytes=${fields[3]}

  bytes_alloc=$(( $blocks_alloc * $alloc_block_size ))

  alloc_in_xfer_blocks=$(( ($bytes_alloc + ($xfer_block_size - 1))/$xfer_block_size ))
  size_in_xfer_blocks=$(( ($size_bytes + ($xfer_block_size - 1))/$xfer_block_size ))
  null_xfer_blocks=$(( $size_in_xfer_blocks - $alloc_in_xfer_blocks ))
  null_xfer_bytes=$(( $null_xfer_blocks * $xfer_block_size ))
  non_null_bytes=$(( $size_bytes - $null_xfer_bytes ))

  if [ "$non_null_bytes" -gt "0" -a "$non_null_bytes" -lt "$size_bytes" ]; then
    cmd="dd if=$f ibs=$xfer_block_size obs=8M skip=$null_xfer_blocks "
    $cmd | tr -d "
# ls -l 2015_10_12-025600113.start.log
-rw-r--r-- 1 solr solr 93153627360 Dec 31 10:34 2015_10_12-025600113.start.log
# du -shx 2015_10_12-025600113.start.log
392M    2015_10_12-025600113.start.log
#
# time truncat.sh 2015_10_12-025600113.start.log > test1
93275+1 records in
45+1 records out
382055799 bytes (382 MB) copied, 1.53881 seconds, 248 MB/s

real    0m1.545s
user    0m0.677s
sys 0m1.076s

# time cp --sparse=always 2015_10_12-025600113.start.log test2

real    1m37.057s
user    0m8.309s
sys 1m18.926s

# ls -l test1 test2
-rw-r--r-- 1 root root   381670701 Dec 31 10:07 test1
-rw-r--r-- 1 root root 93129872210 Dec 31 10:11 test2
# du -shx test1 test2
365M    test1
369M    test2
0" else cat $f fi done

O bit hacky é que, quando você o configura pela primeira vez, é necessário criar um arquivo logrotate vazio; caso contrário, firstaction nunca o pegará e executará o script logrotate .

Eu vi que o your logrotate 3.9.0 ticket de bug para o qual um correção foi liberada em copytruncate . Infelizmente, se eu estiver lendo corretamente, a correção implementada aborda apenas parte do problema. Copia corretamente o arquivo de log esparso para criar outro arquivo esparso. Mas, como você observou, isso não é realmente o que queremos; queremos que a cópia exclua todos os blocos nulos irrelevantes e retenha apenas as entradas de log. Após o logrotate , gzip ainda tem que gzip do arquivo, e copytruncate não lida com arquivos esparsos eficientemente (lê e processa cada byte nulo).

Nossa solução é melhor que a correção logrotate 3.9.x em %code% , pois resulta em logs limpos que podem ser facilmente compactados.

    
por 31.12.2015 / 18:21