Удалить ведущие нулевые блоки из разреженного файла

1190
BobC

Я использую logrotateс copytruncateопцией. Это хорошо работает, создавая разреженный файл, который начинается с растущего числа «виртуальных» нулевых блоков, которые не занимают места на диске.

Проблема заключается в скопированных файлах: хотя они занимают мало места на диске, попытка изучить их с помощью lessзанимает вечность, поскольку «виртуальные» нулевые блоки расширяются до реальных нулей. Я действительно хотел бы удалить начальные разреженные нулевые блоки с начала скопированного файла.

Вот что я знаю до сих пор: ls -lsи duмогу сказать, какая часть файла «настоящая». И я думаю, что ddможно использовать для создания копии без ведущих пустых блоков. Но у меня не получается собрать все это во что-то, что я могу поместить в postrotateраздел моего logrotate.confфайла.

Я нашел методы, которые используют trили sedдля удаления пустых значений, но для этого необходимо расширить файл (сделать виртуальные нулевые значения физическими), и со временем размер файла может превысить терабайт! Мне нужен более «хирургический» подход, который работает без расширения файла. Это должно потребовать только возиться с инодами, поскольку именно там живут разреженные блоки (не в фактически выделенной области).

Конечно, «реальное» исправление состоит в том, чтобы заставить генерирующую программу SIGHUPповторно открыть свой выходной файл, но в этом случае это невозможно.

Какой самый простой и быстрый способ напрямую удалить ведущие нулевые блоки из разреженного файла?


Приложение: Вот как можно создать свой собственный разреженный файл для воспроизведения:

$ 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 

Этот «огромный» файл почти не занимает места на диске. Теперь попробуй less sparse.txt. Вам нужно будет пройти через 8G нулей, чтобы добраться до персонажей в конце. Даже tail -n 1 sparse.txtзанимает много времени.

0
Я никогда не слышал о программе, которая изменяет файл путаясь с инодами. Для этого нет системного вызова, это должно было бы быть сделано путем изменения необработанного диска, и это было бы безопасно, только если вы сначала размонтировали файловую систему. Barmar 9 лет назад 0
Что вы могли бы сделать, так это выяснить количество пустых блоков, сравнив дисковое пространство файла с его длиной. Затем используйте `dd` с опцией` seek = n`, чтобы пропустить пустые блоки. Здесь используется `lseek`, поэтому ему не нужно читать виртуальные блоки. Barmar 9 лет назад 0
@barmar Я пытался, но не смог: Можете ли вы поделиться примером, который работает с любым файлом? Я использую `stat -c"% o% B% b% s "`, чтобы получить информацию, которая мне нужна. BobC 9 лет назад 0
Это кажется сложнее, чем я думал. stat% b sparse.txt` сообщает о 32 блоках для файла 8,1 ГБ. Я думаю, что это должно быть в том числе косвенных блоков. Если я создаю файл с нулями 16K или 32K в начале, они оба говорят 8 блоков. Barmar 9 лет назад 0
И 1М и 2М говорят 16 блоков. Barmar 9 лет назад 0
Возможно, вам следует взглянуть на исходный код `cp`, чтобы увидеть, как он обнаруживает разреженные файлы Barmar 9 лет назад 0
Несколько лет назад было предложено, чтобы API облегчил поиск дыр в разреженных файлах: http://lwn.net/Articles/260795/ Barmar 9 лет назад 0

2 ответа на вопрос

0
BobC

Вот моя первая попытка «работать», использующая statи dd, которая работает только для ведущих разреженных файлов:

#! /bin/bash for f in $@; do echo -n "$f : " fields=( `stat -c "%o %B %b %s" $f` ) xfer_block_size=$ alloc_block_size=$ blocks_alloc=$ size_bytes=$  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 

Как вы думаете?

Я только что понял, что приведенный выше скрипт обрабатывает только полные блоки ведущих нулей. Это должна быть простая настройка, чтобы он правильно обрабатывал любое количество начальных нулевых байтов. BobC 9 лет назад 0
Да, вам просто нужно округлить до кратного размера блока. Barmar 9 лет назад 0
Обратите внимание на округление в настройке * _in_xfer_blocks. Но это все еще не показывает, сколько нулей может быть в первом физическом блоке, так как файл может начинаться с частичного блока нулей. Таким образом, «лучшим» решением может быть создание файла, как указано выше, затем чтение первого блока, подсчет начальных нулей и повторное копирование, чтобы избавиться от них. BobC 9 лет назад 0
Оооо! Просто разобрался. По крайней мере в ext3 (и, вероятно, в других файловых системах), индекс для разреженной дыры имеет разрешение байтов, поэтому первый байт первого физического блока также будет первым байтом после дыры. Таким образом, приведенный выше код может быть правильным при любых обстоятельствах. Тестирование необходимо! BobC 9 лет назад 0
Я не думал, что вы слишком сильно переживали из-за небольшого количества нулей в начале; главное не искать мимо пробега нулей. Я не заметил `- 1` в расчетах. Barmar 9 лет назад 0
0
Michael Matthews

I created an account here so I could thank @BobC for his answer (and his question.) It was the catalyst I needed to solve our longstanding issue with Solr logs.

I modified BobC's script to optimize it a bit for the logrotate use case (using the $xfer_block_size for ibs, and an arbitrarily large (8M) obs, followed by a tr -d "\000" to eliminate the remaining nulls) and then used it in the firstaction section of my logrotate config.

My solution is slightly hacky, I guess, but it's much better than having to bounce critical production services when an 80+ GB log file threatens to fill up the disk...

This is what I ended up with:

#! /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=$ alloc_block_size=$ blocks_alloc=$ size_bytes=$ 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 "\000" else cat $f fi done 

Using larger blocks makes dd orders of magnitude faster. dd makes a first cut, then tr trims the rest of the nulls. As a point of reference, for an 87 GiB sparse file (containing 392 MiB data):

# 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 

When I let logrotate process this using copytruncate, it took most of an hour, and resulted in a fully-materialized non-sparse file -- which then took over an hour to gzip.

Here's my final logrotate solution:

/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 } 

The hacky bit is that when you first set this up, you have to create an empty rotated.start.log file, otherwise logrotate will never pick it up and run the firstaction script.

I did see your logrotate bug ticket for which a fix was released in logrotate 3.9.0. Unfortunately, if I'm reading it correctly, the implemented fix only addresses part of the problem. It correctly copies the sparse log file to create another sparse file. But as you observed, that's not really what we want; we want the copy to exclude all the irrelevant null blocks and retain just the log entries. After the copytruncate, logrotate still has to gzip the file, and gzip does not handle sparse files efficiently (it reads and processes every null byte.)

Our solution is better than the copytruncate fix in logrotate 3.9.x because it results in clean logs that can be easily compressed.

Похожие вопросы