2022-09-09
Одна из систем хранения данных, используемых на работе, выдаёт отчёт о latency входящих в неё дисков. Формат отчета следующий (часть строк я намеренно заменил):
$ cat device_latency_meters.yml
Some service info
Some service info
Host ha304s:
Device: /dev/sdb Average latency read: 20072 write: 66 (microseconds)
Device: /dev/sdc Average latency read: 17410 write: 66 (microseconds)
Device: /dev/sdd Average latency read: 21246 write: 66 (microseconds)
Device: /dev/sde Average latency read: 23616 write: 69 (microseconds)
Device: /dev/sdf Average latency read: 29485 write: 69 (microseconds)
Host ha303s:
Device: /dev/sdb Average latency read: 5679 write: 66 (microseconds)
Device: /dev/sdc Average latency read: 7069 write: 74 (microseconds)
Device: /dev/sdd Average latency read: 2778 write: 70 (microseconds)
Device: /dev/sde Average latency read: 20095 write: 73 (microseconds)
Device: /dev/sdf Average latency read: 7031 write: 69 (microseconds)
Host ha301s:
Device: /dev/sdb Average latency read: 22080 write: 76 (microseconds)
Device: /dev/sdc Average latency read: 12534 write: 70 (microseconds)
Device: /dev/sdd Average latency read: 12531 write: 66 (microseconds)
Device: /dev/sde Average latency read: 14592 write: 72 (microseconds)
Device: /dev/sdf Average latency read: 10905 write: 73 (microseconds)
Devices with maximal read latency:
Host: ha303s Device: /dev/sdl Latency: 33239 (microseconds)
Host: ha304s Device: /dev/sdf Latency: 29485 (microseconds)
Host: ha324s Device: /dev/sdh Latency: 27012 (microseconds)
Devices with maximal write latency:
Host: ha320s Device: /dev/sds Latency: 95 (microseconds)
Host: ha320s Device: /dev/sdc Latency: 93 (microseconds)
Host: ha320s Device: /dev/sdl Latency: 93 (microseconds)
Необходимо переформатировать отчёт в CSV-таблицу, полями которой будет имя хоста (без постфикса s
), путь устройства, и значения read и write latency, вот так:
ha304,/dev/sdb,20072,66
Для решения задачи я использовал AWK – язык программирования, созданный специально для обработки файлов, с переменными, условными операторами, циклами, и набором разных полезных функций. Я использую GNU Awk (GAWK) версии 5.1.0.
Вот моё решение:
$ cat device_latency_meters.yml | awk 'BEGIN {a=0} /ha[0-9]{3}s:/{a=1;host=substr($0,8,5);next} /^$/{a=0;next} {if(a==1){gsub(/[^\S\n]*Device:\s/,host",",$0);print $1","$5","$7}}'
ha304,/dev/sdb,20072,66
ha304,/dev/sdc,17410,66
ha304,/dev/sdd,21246,66
ha304,/dev/sde,23616,69
...
Для начала опишу алгоритм:
Host ha304s:
, то вычленяем имя хоста, записываем его в переменную, поднимаем флаг, переходим на следующую строку.Теперь разберу решение подробно. Скрипты AWK можно запускать из командной стройки, как в примере выше, или из файла. Для наглядности представлю скрипт в файловой записи:
#!/bin/awk -f
BEGIN {a=0}
/ha[0-9]{3}s:/ {
a=1;
host=substr($0,8,5);
next
}
/^$/ {
a=0;
next
}
{
if(a==1){
gsub(/[^\S\n]*Device:\s/,host",",$0);
print $1","$5","$7
}
}
Структура программы в AWK состоит из блоков BEGIN
и END
, выполняющихся единожды перед и после прочтения файла, и тела программы, которая выполняется на каждой строке файла. То есть AWK читает строку, применяет к ней инструкции, после чего переходит на следующую строку. Значения переменных, которые могут быть двух типов – строковыми и числовыми, при переходе на новую строку сохраняются. Приведение типов AWK выполняет автоматически.
Блок BEGIN
подходит для инициализации переменных. В нём я создаю переменную для флага и устанавливаю его значение в 0.
Тело программы AWK состоит из блоков, заключенных в фигурные скобки {}
. Если перед блоком имеется запись вида /<pattern>/
, то его команды выполнятся только для строки, которой соответствует прописанный паттерн (регулярное выражение).
Выражение ha[0-9]{3}s:
можно описать так: найди строки, в которых есть последовательность из букв h
и a
, цифр от 0 до 9 ([0-9]
), повторяющихся три раза ({3}
), буквы s
и двоеточия. Ему соответствуют строки:
Host ha304s:
Host ha303s:
Host ha301s:
В соответствующем блоке мы поднимаем флаг, затем с помощью функции substr
вычленяем имя хоста и записываем в переменную host
. Функция substr
возвращает подстроку, и принимает три параметра: изначальную строку, позицию символа – начала подстроки, количество символов в подстроке. Здесь в качестве первого аргумента мы указываем $0
– внутреннюю переменную AWK, содержащую текущую обрабатываемую строку целиком. Ключевое слово next
указывает AWK перейти на следующую строку файла.
Следующий блок начинается с выражения ^$
. Символ ^
в данном случае означает начало строки, а $
– её конец. Таким образом последовательность начала и конца строки задаёт поиск пустой строки. В соответствующем блоке мы опускаем флаг и переходим на следующую строку.
Третий блок выполняется на тех строках, которые не подходят под два описанных выражения. Условный оператор проверяет значение флага, и если он поднят, вызывает функцию gsub
. gsub
– функция, добавленная в GAWK, которая заменяет подстроку в строке. Первым аргументом передается последовательность символов под замену (в нашем случае – регулярное выражение в символах //
), вторым – последовательность, на которую производится замена (в нашем случае – результат конкатенации строки в переменной host
и символа запятой), третим – сама строка, в которой нужно произвести замену (в нашем случае – текущая строка).
Выражение [^\S\n]*Device:\s
можно разложить так. Символ ^
внутри перечисления означает “любой символ, кроме тех, что перечислены”. \S
означает любой непробельный символ, \n
– перенос строки. Таким образом в сборе начало регулярного выражения [^\S\n]*
подразумевает последовательность любой длины, не состоящую из непробельных символов и переносов строки – то есть любое количество пробелов. Затем идёт последовательность символов Device:
, заканчивающаяся пробелом \s
.
В начале предыдущего выражения можно было бы использовать более простую конструкцию \s*
, то есть любое количество пробелов. Но так как нужная там строка начинается с пробелов и слова Device:
, то такая конструкция распознает не только пробелы перед этим словом, но и предшествующий символ переноса строки. Так как мы заменяем эту подстроку, то \s*
нам не подходит, ведь перенос строки нам трогать не нужно.
AWK автоматически разбивает текст на колонки по определенному разделителю. По умолчанию таким разделителем является пробел, но его можно изменить, перечислив свои разделители после флага -F
при вызове AWK. Например в качестве разделителя можно задать последовательность из нескольких пробелов или двоеточие:
$ cat device_latency_meters.yml | awk -F ' +|:' '...'
Значения каждой из колонок в текущей строке записываются во внутренние переменные AWK $1
, $2
и т.д. В нашем случае инструкция print $1","$5","$7
говорит AWK вывести содержимое первой (ha304,/dev/sdb
), пятой (значение read latency) и седьмой (write latency) колонок через запятую (","
).
Результат:
$ cat device_latency_meters.yml | awk 'BEGIN {a=0} /ha[0-9]{3}s:/{a=1;host=substr($0,8,5);next} /^$/{a=0;next} {if(a==1){gsub(/[^\S\n]*Device:\s/,host",",$0);print $1","$5","$7}}'
ha304,/dev/sdb,20072,66
ha304,/dev/sdc,17410,66
ha304,/dev/sdd,21246,66
ha304,/dev/sde,23616,69
...
Возможно есть более оптимальное решение для задачи.