Всем привет!


В этой статье я хочу рассказать о том, как выбирал архиватор для сжатия логов нашей фронт-офисной системы.


Подразделение, в котором я работаю, занимается разработкой и сопровождением единой фронт офисной системы Банка. Я отвечаю за ее сопровождение, мониторинг и DevOps.


Наша Система — это высоконагруженное приложение, ежедневно обслуживающее более 5 000 уникальных пользователей. На сегодняшний день — это «монолит» со всеми своими достоинствами и недостатками. Но сейчас активно идет процесс выноса функционала в микросервисы.


Ежедневно наша система генерирует более 130 ГБ «сырых» логов и, несмотря на то, что мы используем ENG стек (Elasticsearch Nxlog Graylog), файловые логи содержат гораздо больше информации (например, stack trace ошибок), поэтому требуют архивирования и хранения.


Так как место хранения ограничено, встаёт вопрос: «А какой архиватор лучше всего справится с этой задачей».


Для решения этого вопроса я написал скрипт на языке PowerShell, который произвел анализ за меня.


Задача скрипта вызвать архиваторы rar, 7z и zip с разными параметрами компрессии, посчитать скорость формирования архива, а также занимаемое место на диске.


ArchSearch.ps1
#Requires -Version 4.0

# Очищаем экран, чтобы не мешало ничего лишнего
Clear-Host

# Переходим в каталог, где базируется исполняемый файл
Set-Location $PSScriptRoot

# Объявляем переменные, которые будем использовать в работе
$Archive = "Archive"
$ArchFileName = "ArchFileName"
[array]$path = (Get-ChildItem '.\logs').DirectoryName|Select-Object -Unique

# Для архивных файлов нам нужна папка-архив. Создаём ее, если она еще не создана.
if ((Test-Path -Path ".\$Archive") -ne $true){
    New-Item -Path .\ -Name $Archive -ItemType Directory -Force
} else {
    # Если папка существует - удаляем ее содержимое
    Get-ChildItem .\$Archive|Remove-Item -Recurse -Force
}

# Объявляем переменную для создания таблицы
[array]$table=@()

# Выполняем архивацию Rar
#rar 
#  m<0..5>       Set compression level (0-store...3-default...5-maximal)
1..5|foreach{
$CompressionLevel = $("-m" + $_)
$mc = Measure-Command {cmd /c .\rar.exe a -ep1 -ed  $CompressionLevel -o+ -tsc .\$Archive\$($ArchFileName + $_) "$path"}
[math]::Round(($mc.TotalMilliseconds), 0)
$ArchFileNamePath = ".\$Archive\$($ArchFileName + $_ + ".rar")"
$table += ""|Select-Object -Property @{name="ArchFileName"; expression={$ArchFileNamePath -split "\\"|Select-Object -Last 1}},`
@{name="CompressionLevel"; expression={$CompressionLevel}},@{name="Extension"; expression={"rar"}},@{name="Size"; expression={(Get-ChildItem $ArchFileNamePath).Length}},`
@{name="Time"; expression={[math]::Round(($mc.TotalMilliseconds), 0)}},`
@{name="Size %"; expression={0}},@{name="Time %"; expression={0}},@{name="Result %"; expression={0}}
}

# Выполняем архивацию 7z
#7z
# -mx[N] : set compression level: -mx1 (fastest) ... -mx9 (ultra)
#cmd /c "$env:ProgramFiles\7-Zip\7z.exe" a -mx="$MX" -mmt="$MMT" -t7z -ssw -spf $($ArchFileName + "Fastest") "$path"
1..9|foreach{
$CompressionLevel = $("-mx=" + $_)
$mc = Measure-Command {cmd /c "$env:ProgramFiles\7-Zip\7z.exe" a $CompressionLevel -t7z -ssw -spf .\$Archive\$($ArchFileName + $_) $path} #-mmt="$MMT"
[math]::Round(($mc.TotalMilliseconds), 0)
$ArchFileNamePath = ".\$Archive\$($ArchFileName + $_ + ".7z")"
$table += ""|Select-Object -Property @{name="ArchFileName"; expression={$ArchFileNamePath -split "\\"|Select-Object -Last 1}},`
@{name="CompressionLevel"; expression={$CompressionLevel}},@{name="Extension"; expression={"7z"}},@{name="Size"; expression={(Get-ChildItem $ArchFileNamePath).Length}},`
@{name="Time"; expression={[math]::Round(($mc.TotalMilliseconds), 0)}},`
@{name="Size %"; expression={0}},@{name="Time %"; expression={0}},@{name="Result %"; expression={0}}
}

# Выполняем архивацию zip (Встроенная функция PS "Compress-Archive")
#zip 
1..2|foreach{

    Switch ($_){

        1{$CompressionLevel = "Fastest"}

        2{$CompressionLevel = "Optimal"}

    }
$mc = Measure-Command {Compress-Archive -Path $path -DestinationPath .\$Archive\$($ArchFileName + $_) -CompressionLevel $CompressionLevel -Force}
[math]::Round(($mc.TotalMilliseconds), 0)
$ArchFileNamePath = ".\$Archive\$($ArchFileName + $_ + ".zip")"
$table += ""|Select-Object -Property @{name="ArchFileName"; expression={$ArchFileNamePath -split "\\"|Select-Object -Last 1}},`
@{name="CompressionLevel"; expression={$CompressionLevel}},@{name="Extension"; expression={"zip"}},@{name="Size"; expression={(Get-ChildItem $ArchFileNamePath).Length}},`
@{name="Time"; expression={[math]::Round(($mc.TotalMilliseconds), 0)}},`
@{name="Size %"; expression={0}},@{name="Time %"; expression={0}},@{name="Result %"; expression={0}}
}

# Выполняем сортировку по полю Size и берем первое значение [0] - это самый сжатый файл
$Size = ($table|Sort-Object -Property Size)[0].Size / 100
# Выполняем сортировку по полю Time и берем первое значение [0] - это самый быстро сжатый файл
$Time = ($table|Sort-Object -Property Time)[0].Time / 100

# Формируем итоговую таблицу
$table|foreach {
$_.time
$_."Size %" = [math]::Round(($_.Size / $Size), 0)
$_."Time %" = [math]::Round(($_.Time / $Time), 0)
    if ($_."Size %" -ge $_."Time %"){
        $_."Result %" = $_."Size %" - $_."Time %"
    } else {
        $_."Result %" = $_."Time %" - $_."Size %"
    }

}

# Выводим на экран таблицу с сортировкой по "Size %" - это самый сжатый файл
$table|Sort-Object -Property "Size %","Result %"|Select-Object -First 1|Format-Table -AutoSize
# Выводим на экран таблицу с сортировкой по "Result %" - оптимальный вариант между скоростью и занимаемым местом на диске
$table|Sort-Object -Property "Result %","Size %","Time %"|Select-Object -First 1|Format-Table -AutoSize

$table|Sort-Object -Property "Size %","Result %"|Select-Object -First 1|Format-Table -AutoSize
$table|Sort-Object -Property "Result %","Size %","Time %"|Select-Object -First 1|Format-Table -AutoSize

# Работа сделана! Удаляем созданные архивы, чтобы не занимали место
Get-ChildItem .\$Archive|Remove-Item -Force

Подготовительные работы:


# Запрещаем запускать скрипт, если версия PowerShell меньше 4.0 ($PSVersionTable):
#Requires -Version 4.0

# Очищаем экран, чтобы не мешало ничего лишнего
Clear-Host

# Переходим в каталог, где базируется исполняемый файл (возможность использовать путь .\)
Set-Location $PSScriptRoot

# Объявляем переменные, которые будем использовать в работе
$Archive = "Archive"
$ArchFileName = "ArchFileName"
[array]$path = (Get-ChildItem '.\logs').DirectoryName|Select-Object -Unique

# Для архивных файлов нам нужна папка-архив. Создаём ее, если она еще не создана
if ((Test-Path -Path ".\$Archive") -ne $true){
    New-Item -Path .\ -Name $Archive -ItemType Directory -Force
} else {
    # Если папка существует, то удаляем ее содержимое
    Get-ChildItem .\$Archive|Remove-Item -Recurse -Force
}

# Объявляем переменную для создания таблицы
[array]$table=@()

Разбираем подробнее


# Формируем массив данных от 1 до 5 и передаем через конвейер в цикл foreach
1..5|foreach{

# Передаем в переменную $CompressionLevel значение от 1 до 5($_), это необходимо для формирования уровня компрессии
$CompressionLevel = $("-m" + $_)

# Командлет Measure-Command позволяет посчитать время выполнения команды, заключенной в {}
$mc = Measure-Command {cmd /c .\rar.exe a -ep1 -ed  $CompressionLevel -o+ -tsc .\$Archive\$($ArchFileName + $_) "$path"}

# Выводим время выполнения запроса на экран
[math]::Round(($mc.TotalMilliseconds), 0)
# Формируем переменную вида $ArchFileName + $_ + ".rar": Archive1.rar
$ArchFileNamePath = ".\$Archive\$($ArchFileName + $_ + ".rar")"
# Формируем таблицу $table построчно(+=) добавляя данные
$table += ""|Select-Object -Property @{name="ArchFileName"; expression={$ArchFileNamePath -split "\\"|Select-Object -Last 1}},`
@{name="CompressionLevel"; expression={$CompressionLevel}},@{name="Extension"; expression={"rar"}},@{name="Size"; expression={(Get-ChildItem $ArchFileNamePath).Length}},`
@{name="SizeAVD"; expression={0}},@{name="Time"; expression={[math]::Round(($mc.TotalMilliseconds), 0)}},`
@{name="Size %"; expression={0}},@{name="Time %"; expression={0}},@{name="Result %"; expression={0}}
}

# Выполняем сортировку по полю Size и берем первое значение [0] - это самый сжатый файл
$Size = ($table|Sort-Object -Property Size)[0].Size / 100
# Выполняем сортировку по полю Time и берем первое значение [0] - это самый быстро сжатый файл
$Time = ($table|Sort-Object -Property Time)[0].Time / 100

# Формируем итоговую таблицу
$table|foreach {
$_.time
$_."Size %" = [math]::Round(($_.Size / $Size), 0)
$_."Time %" = [math]::Round(($_.Time / $Time), 0)
    if ($_."Size %" -ge $_."Time %"){
        $_."Result %" = $_."Size %" - $_."Time %"
    } else {
        $_."Result %" = $_."Time %" - $_."Size %"
    }

}

# Выводим на экран таблицу с сортировкой по "Size %" - это самый сжатый файл
$table|Sort-Object -Property "Size %","Result %"|Select-Object -First 1|Format-Table -AutoSize
# Выводим на экран таблицу с сортировкой по "Result %" - оптимальный вариант между скоростью и занимаемым местом на диске
$table|Sort-Object -Property "Result %","Size %","Time %"|Select-Object -First 1|Format-Table -AutoSize

# Работа сделана! Удаляем созданные архивы, чтобы не занимали место.
Get-ChildItem .\$Archive|Remove-Item -Force

$table|Sort-Object -Property "Size %","Result %"|Format-Table -AutoSize

ArchFileName Compression Level Extension Size Time Size % Time % Result %
ArchFileName8.7z -mx=8 7z 26511520 62404 100 1674 1574
ArchFileName9.7z -mx=9 7z 26511520 64614 100 1733 1633
ArchFileName7.7z -mx=7 7z 28948176 52832 109 1417 1308
ArchFileName6.7z -mx=6 7z 30051742 37339 113 1002 889
ArchFileName5.7z -mx=5 7z 31239355 35169 118 943 825
ArchFileName4.rar -m4 rar 33514693 11426 126 306 180
ArchFileName5.rar -m5 rar 33465152 12894 126 346 220
ArchFileName3.rar -m3 rar 33698079 9835 127 264 137
ArchFileName2.rar -m2 rar 34399885 8352 130 224 94
ArchFileName4.7z -mx=4 7z 38926348 6470 147 174 27
ArchFileName3.7z -mx=3 7z 44545819 5889 168 158 10
ArchFileName2.7z -mx=2 7z 51690114 4754 195 128 67
ArchFileName1.rar -m1 rar 53605833 4600 202 123 79
ArchFileName1.7z -mx=1 7z 57472172 3728 217 100 117
ArchFileName2.zip Optimal zip 65733242 14025 248 376 128
ArchFileName1.zip Fastest zip 81556824 9031 308 242 66

$table|Sort-Object -Property "Result %","Size %","Time %"|Format-Table -AutoSize

ArchFileName Compression Level Extension Size Time Size % Time % Result %
ArchFileName3.7z -mx=3 7z 44545819 5889 168 158 10
ArchFileName4.7z -mx=4 7z 38926348 6470 147 174 27
ArchFileName1.zip Fastest zip 81556824 9031 308 242 66
ArchFileName2.7z -mx=2 7z 51690114 4754 195 128 67
ArchFileName1.rar -m1 rar 53605833 4600 202 123 79
ArchFileName2.rar -m2 rar 34399885 8352 130 224 94
ArchFileName1.7z -mx=1 7z 57472172 3728 217 100 117
ArchFileName2.zip Optimal zip 65733242 14025 248 376 128
ArchFileName3.rar -m3 rar 33698079 9835 127 264 137
ArchFileName4.rar -m4 rar 33514693 11426 126 306 180
ArchFileName5.rar -m5 rar 33465152 12894 126 346 220
ArchFileName5.7z -mx=5 7z 31239355 35169 118 943 825
ArchFileName6.7z -mx=6 7z 30051742 37339 113 1002 889
ArchFileName7.7z -mx=7 7z 28948176 52832 109 1417 1308
ArchFileName8.7z -mx=8 7z 26511520 62404 100 1674 1574
ArchFileName9.7z -mx=9 7z 26511520 64614 100 1733 1633

# Работа сделана! Удаляем созданные архивы, чтобы не занимали место.
Get-ChildItem .\$Archive|Remove-Item -Force

Итог:
Самым экономичным по месту на диске оказался 7z со степенью сжатия -mx=8 и -mx=9



Самым быстрым по времени создания архива оказался 7z со степенью сжатия -mx=1



Самым оптимальным по скорости и занимаемому месту оказался 7z со степенью сжатия -mx=3



Мы выбираем 7z со степенью сжатия -mx=8, так как по размеру архива он равен -mx 9, но при этом работает чуть быстрее.


Отлично, архиватор и степень сжатия выбраны, теперь давайте заархивируем логи!


Нам нужно решить следующие задачи:


  1. Избегать высокой нагрузки на сервер во время работы.
  2. Обрабатывать все папки с вложенной папкой Logs.
  3. Удалять архивы старше 30 дней (чтобы не заканчивалось место на диске).
  4. Создавать архивы по дням в зависимости от времени изменения файлов.

ArchLogs.ps1
#Requires -Version 4.0

# Добавляем в планировщик заданий если необходимо задачу архивации логов
#SCHTASKS /Create /SC DAILY /ST 22:00 /TN ArchLogs /TR "C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe C:\Scripts\ArchLogs.ps1" /RU "NT AUTHORITY\NETWORKSERVICE" /F /RL HIGHEST

# Очищаем экран, чтобы не мешало ничего лишнего
Clear-Host

# Переходим в каталог, где базируется исполняемый файл
Set-Location $PSScriptRoot

# Описываем функцию получения значения потоков в зависимости от количества ядер процессора.  
# Условие 1
Function Set-MMTCore {

$Core = (Get-WmiObject –class Win32_processor|Measure-Object NumberOfLogicalProcessors -sum).Sum / 2
$CoreTMP = $Core / 4
IF ($Core -lt 4)
    {
    $Core = $Core -1
    }
    Else
    {
    $Core = $Core - $CoreTMP
    }
[math]::Round($Core)
}

# Задаем степень сжатия
$MX = 8
# Получаем количество потоков работы
$MMT = Set-MMTCore

# Объявляем переменные, которые будем использовать в работе
$SearchFolder = "C:\inetpub"
# Получаем объекты из папки $SearchFolder
$InetpubFolder = Get-ChildItem $SearchFolder
# Ищем папки с подпапкой Logs
# Условие 2
$LogsFolder = $InetpubFolder|foreach {Get-ChildItem $_.Fullname|Where-Object{$_.PSIsContainer -eq $true}|Where-Object{$_.name -eq "logs"}}

# Объявляем переменные, которые будем использовать в работе
$Archive = "Archive"
$ArchiveFile = "ArchFiles.txt"

# Перебираем по одной паке из $LogsFolder передавая в $LogsFolderName полный путь
foreach($LogsFolderName in $LogsFolder.Fullname){
$LogsFolderName
# Создаем папку $Archive, если она не была создана ранее
IF ((Test-Path -Path "$LogsFolderName\$Archive") -ne $true){New-Item -Path $LogsFolderName -Name $Archive -ItemType Directory -Force}
# Удаляем файлы старше 30 дней
# Условие 3
Get-ChildItem "$LogsFolderName\$Archive"|Where-Object {$_.LastWriteTime -le (Get-Date).AddDays(-30)}| Remove-Item -Force
# Получаем файлы для архивирования
[Array]$ArchiveItems = Get-ChildItem -Path $LogsFolderName -Exclude $Archive
    # Если файлы есть, продолжаем. В другом случае - выходим
    IF ($ArchiveItems.Count -ne ^_^quot�quot^_^){
    # Нас интересуют файлы изменённые не сегодня
    $AllLogsFiles = Get-ChildItem $ArchiveItems -Recurse <#-Filter *.log#>|Where-Object {$_.LastWriteTime -lt (Get-Date).Date}
    # Получаем дату изменения файлов
    # Условие 4
    $AllData = ($AllLogsFiles|Sort-Object -Property LastWriteTime).LastWriteTime.Date|Select-Object -Unique
    $AllData
        foreach ($Data in $AllData){
            IF ($ArchiveItems.Count -ne ^_^quot�quot^_^){
            # Формируем список файлов для архивации за определенную дату
            ($AllLogsFiles|Where-Object {$_.LastWriteTime -lt (Get-Date).Date}|Where-Object {$_.LastWriteTime -ge (Get-Date $Data) -and $_.LastWriteTime -lt (Get-Date $Data).AddDays(1)}).FullName|Out-File .\$ArchiveFile -Force -Encoding default
            Write-Host "==="
            $Data
            Write-Host "==="
                # Если файлы существуют архивируем их
                IF ($(Get-Content .\ArchFiles.txt -Encoding default) -ne $null){
                $ArchiveFileName =$(($LogsFolderName.Remove($LogsFolderName.LastIndexOf("\"))) -split "\\"|Select-Object -Last 1) + "_" + $(Get-Date $Data -Format dd-MM-yyyy)
                cmd /c "$env:ProgramFiles\7-Zip\7z.exe" a -mx="$MX" -mmt="$MMT" -t7z -ssw -sdel "$LogsFolderName\$Archive\$ArchiveFileName" "@$ArchiveFile"
                # Удаляем файл со списокм файлов для архивирования
                IF(Test-Path ".\$ArchiveFile"){Remove-Item ".\$ArchiveFile" -Force}
                # Удаляем пустые папки внутри папки Logs
                $LogsFolderName|foreach {Get-ChildItem $_|Where-Object {(Get-ChildItem -Path $_.FullName) -eq $null}|Remove-Item -Force}
                }
            }
        }
    }
}

Здесь я не буду расписывать каждую строчку скрипта(описано в комментариях), а остановлюсь лишь на функции Set-MMTCore, которая позволяет нам вычислить количество потоков для 7z, чтобы не грузить процессор на нашем сервере:


Function Set-MMTCore {
# Получаем сумму ядер процессора и делим на 2
$Core = (Get-WmiObject –class Win32_processor|Measure-Object NumberOfLogicalProcessors -sum).Sum / 2
# Получившееся значение делим еще на 4
$CoreTMP = $Core / 4
IF ($Core -lt 4)
    {
    $Core = $Core -1
    }
    Else
    {
    $Core = $Core - $CoreTMP
    }
#Округляем значение до целого
[math]::Round($Core)
}

Без использования функции Set-MMTCore

Видно, что CPU упирается в 100%. Это значит, что мы неизбежно вызовем проблемы на сервере, а также получим алерт от системы мониторинга.



При использовании функции Set-MMTCore

Видно, что CPU равен 30-35%. Это значит, использование функции Set-MMTCore позволяет архивировать файлы без влияния на работу сервера.



Итог работы скрипта архивации:


Папка до архивации:


Папка после архивации:


Файлы созданные в процессе архивации:


Файлы в архиве: