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 "firstaction
0"
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.