Dyski VHDX, które ciągle rosną: przyczyny, kompaktowanie i zapobieganie
Dynamiczny dysk VHDX rośnie przy każdym zapisie w nowe miejsce, a usunięcie plików w gościu go nie zmniejsza. Skąd bierze się rozrost, jak odzyskać miejsce przez TRIM i kompaktowanie oraz jak skonfigurować gościa, żeby problem nie wracał.
Maszyna wirtualna z Linuksem, na której działają kontenery, ma dynamiczny dysk VHDX o pojemności 200 GB. W gościu df -h pokazuje 70 GB zajętości, a plik na hoście ma 190 GB i co tydzień jest większy. Administrator usuwa w gościu stare obrazy i logi, zwalnia 30 GB, ale plik VHDX nie zmniejsza się ani o bajt. Po kilku miesiącach na woluminie z maszynami kończy się miejsce, choć według gości dane zajmują mniej niż połowę dysków.
Dynamiczny dysk VHDX rośnie tylko w jedną stronę, dopóki gość nie poinformuje hosta, które bloki są wolne, albo dopóki ktoś nie skompaktuje pliku. Ten wpis pokazuje, skąd bierze się rozrost, jak odzyskać miejsce i jak skonfigurować gościa, żeby problem nie wracał.
Trzy rozmiary jednego dysku
Dysk stały (Fixed) od chwili utworzenia zajmuje na hoście całą swoją pojemność i nie rośnie. Dysk dynamiczny (Dynamic) startuje jako mały plik i przydziela miejsce blokami, gdy gość po raz pierwszy zapisze coś w danym obszarze.
Przy dysku dynamicznym liczą się trzy wielkości: pojemność widziana przez gościa, rozmiar pliku na hoście i zajętość danych w gościu. Dwie pierwsze pokazuje Get-VHD:
Get-VHD -Path 'D:\VM\srv-app-01\srv-app-01.vhdx' |
Select-Object VhdFormat, VhdType, Size, FileSize, MinimumSize, BlockSize
| Pole | Znaczenie | Na co uważać |
|---|---|---|
Size | pojemność logiczna widziana przez gościa | nie mówi nic o miejscu na hoście |
FileSize | rozmiar pliku na woluminie hosta | w dysku dynamicznym sam nie maleje |
MinimumSize | najmniejsza pojemność, do której Resize-VHD może zmniejszyć dysk | wynika z układu partycji, nie z ilości danych |
BlockSize | jednostka przydziału w pliku, domyślnie 32 MB | dotyczy dysków dynamicznych i różnicowych |
VhdType | Fixed, Dynamic albo Differencing | Differencing to np. plik checkpointu |
Trzecią wielkość pokazuje gość: df -h w Linuksie albo Get-Volume w Windows. Zdrowy dysk dynamiczny ma FileSize niewiele większy od zajętości w gościu. Plik bliski Size przy gościu zajętym w jednej trzeciej oznacza, że zwolnione miejsce nie wraca do hosta.
Dlaczego plik rośnie, a nie maleje
Hyper-V nie wie, co gość przechowuje w systemie plików, widzi tylko zapisy do sektorów. Pierwszy zapis w obszarze bez przydzielonego bloku powiększa plik o cały blok. Usunięcie pliku w gościu zmienia wyłącznie metadane systemu plików: host się o tym nie dowiaduje, więc blok w pliku VHDX zostaje.
Gdyby gość zapisywał zawsze w tych samych sektorach, plik zatrzymałby się na poziomie najwyższej zajętości. Systemy plików umieszczają jednak nowe dane w różnych miejscach dysku, a typowe obciążenia stale tworzą nowe pliki i kasują stare:
- obrazy kontenerów: każda nowa wersja aplikacji to nowe warstwy, a stare usuwa dopiero sprzątanie obrazów;
- logi: rotacja tworzy nowe pliki, kompresuje i usuwa stare;
- pliki tymczasowe, pamięć podręczna menedżera pakietów, katalogi robocze buildów CI;
- aktualizacje: pobrane pakiety, kolejne wersje jądra, w Windows pliki usługi aktualizacji.
Po kilku miesiącach gość zapisał coś w większości obszarów dysku i plik zbliża się do pełnej pojemności, choć zajętość w gościu jest stała.
Osobnym mechanizmem wzrostu są checkpointy. Po utworzeniu checkpointu zapisy trafiają do pliku różnicowego AVHDX, a miejsce odzyskuje się dopiero przez usunięcie checkpointu, czyli scalenie łańcucha. Szczegóły opisuje wpis Checkpoint to nie kopia zapasowa.
Rozmiar bloku: 1 MB dla gości Linux
Przy domyślnym bloku 32 MB zapis 4 KB w nowym obszarze dysku powiększa plik o 32 MB. W gościach Windows ten rozmiar się sprawdza. Systemy plików Linuksa rozkładają metadane i dane po całym dysku (grupy bloków w ext4, grupy alokacji w XFS), więc przy bloku 32 MB plik szybko rośnie mimo niewielkiej ilości danych. Microsoft w zaleceniach dla gości Linux na Hyper-V wskazuje dynamiczny VHDX z blokiem 1 MB:
New-VHD -Path 'D:\VM\srv-app-01\srv-app-01-data.vhdx' -SizeBytes 200GB -Dynamic -BlockSizeBytes 1MB
Bloku istniejącego dysku nie da się zmienić w miejscu. Dysk konwertuje się do nowego pliku i podłącza zamiast starego. Konwersja wymaga wyłączonej maszyny bez checkpointów i wolnego miejsca na nowy plik:
$old = 'D:\VM\srv-app-01\srv-app-01.vhdx'
$new = 'D:\VM\srv-app-01\srv-app-01-1mb.vhdx'
Stop-VM -Name srv-app-01
Convert-VHD -Path $old -DestinationPath $new -VHDType Dynamic -BlockSizeBytes 1MB
# Kontroler i pozycję dysku podaje Get-VMHardDiskDrive -VMName srv-app-01
Set-VMHardDiskDrive -VMName srv-app-01 -ControllerType SCSI -ControllerNumber 0 -ControllerLocation 0 -Path $new
Start-VM -Name srv-app-01
Stary plik usuwa się dopiero po sprawdzeniu, że maszyna startuje z nowego dysku i ma komplet danych.
TRIM i UNMAP: gość zgłasza wolne bloki
TRIM (ATA) i UNMAP (SCSI) to polecenia, którymi system informuje dysk, że zakres sektorów nie zawiera już danych. Hyper-V przyjmuje je od gościa, oznacza odpowiadające im bloki VHDX jako nieużywane i przekazuje informację do magazynu hosta. Warunki po stronie maszyny:
- dysk w formacie VHDX; format VHD nie obsługuje UNMAP i trzeba go przekonwertować;
- dysk na wirtualnym kontrolerze SCSI. Maszyny generacji 2 mają tylko SCSI, w generacji 1 dysk systemowy leży na kontrolerze IDE.
W gościu Linux TRIM wysyła fstrim. W wielu dystrybucjach fstrim.timer jest włączony domyślnie i działa raz w tygodniu, w innych trzeba go włączyć:
# TRIM wszystkich zamontowanych systemów plików, które go obsługują, z raportem
sudo fstrim -av
# Cotygodniowy TRIM uruchamiany przez systemd
sudo systemctl enable --now fstrim.timer
Gość Windows domyślnie wysyła powiadomienia o usuniętych danych, a zaplanowane zadanie optymalizacji dysków co tydzień ponawia TRIM. Ustawienie sprawdza fsutil behavior query DisableDeleteNotify (0 oznacza włączone powiadomienia), a ręcznie TRIM wywołuje:
Optimize-Volume -DriveLetter C -ReTrim -Verbose
TRIM zatrzymuje wzrost, bo zwolnione bloki mogą zostać użyte ponownie. Żeby zmniejszyć sam plik, trzeba go skompaktować.
Kompaktowanie: zmniejszenie pliku
Optimize-VHD zmniejsza plik dynamicznego lub różnicowego dysku, usuwając nieużywane bloki; na dyskach stałych nie działa. Dysk nie może być używany przez działającą maszynę, a w trybach Quick i Full musi być podłączony do hosta tylko do odczytu. Tryb Full dodatkowo wyszukuje bloki wypełnione zerami.
Skuteczność zależy od systemu plików w gościu. Gdy na dysku jest NTFS, host odczyta z niego informację o wolnym miejscu. Systemów ext4 i XFS Windows nie odczyta, więc przy gościu Linux odzyskuje się bloki oznaczone wcześniej przez TRIM i bloki wyzerowane. Dlatego tuż przed wyłączeniem maszyny z Linuksem wykonuje się sudo fstrim -av.
$vm = 'srv-app-01'
$vhdx = 'D:\VM\srv-app-01\srv-app-01.vhdx'
Stop-VM -Name $vm # zamknięcie systemu gościa
$before = (Get-VHD -Path $vhdx).FileSize
Mount-VHD -Path $vhdx -ReadOnly # tryb Full wymaga podłączenia tylko do odczytu
Optimize-VHD -Path $vhdx -Mode Full # usuwa nieużywane i wyzerowane bloki
Dismount-VHD -Path $vhdx
Start-VM -Name $vm
'{0:N1} GB -> {1:N1} GB' -f ($before / 1GB), ((Get-VHD -Path $vhdx).FileSize / 1GB)
Zapobieganie
Kompaktowanie odzyskuje miejsce jednorazowo. Żeby plik nie wracał do poprzedniego rozmiaru, gość musi regularnie wysyłać TRIM i nie może bez ograniczeń gromadzić danych, które i tak zostaną usunięte.
Dziennik systemowy. journald domyślnie zajmuje do 10% systemu plików, najwyżej 4 GB. Niższy limit ustawia plik drop-in, zmiana działa po sudo systemctl restart systemd-journald, a obecny rozmiar pokazuje journalctl --disk-usage:
[Journal]
SystemMaxUse=1G
Logi aplikacji. Aplikacje piszące do własnych plików potrzebują reguły logrotate z limitem liczby i rozmiaru plików:
/var/log/aplikacja/*.log {
daily
rotate 14
maxsize 200M
compress
delaycompress
missingok
notifempty
# dla aplikacji, które nie potrafią ponownie otworzyć pliku logu
copytruncate
}
Obrazy kontenerów. Kubelet usuwa nieużywane obrazy, gdy zajętość systemu plików z obrazami przekroczy imageGCHighThresholdPercent (domyślnie 85%), i sprząta do imageGCLowThresholdPercent (domyślnie 80%). Niższe progi zostawiają mniej starych warstw. W k3s ustawia się je argumentami kubeleta:
kubelet-arg:
- "image-gc-high-threshold=75"
- "image-gc-low-threshold=60"
Na hostach budujących obrazy nieużywane obrazy usuwa uruchamiane z harmonogramu podman image prune -a lub docker image prune -a, a na węźle Kubernetes jednorazowo crictl rmi --prune.
Monitorowanie. Minimum to alert na wolne miejsce na woluminie z maszynami, np. poniżej 15%. Wcześniejszy sygnał daje drugi skrypt z sekcji „Sprawdź u siebie”, uruchamiany z Harmonogramu zadań i wysyłający do monitoringu możliwy wzrost dysków na każdym woluminie. Plik, którego FileSize rośnie co tydzień przy stałej zajętości w gościu, wskazuje maszynę bez działającego TRIM.
Objaw, przyczyna, działanie
| Objaw | Przyczyna | Co zrobić |
|---|---|---|
FileSize bliski Size, a df -h w gościu pokazuje 30–40% | gość nie wysyła TRIM | włączyć fstrim.timer, wykonać fstrim -av, skompaktować |
lsblk --discard pokazuje 0 w DISC-GRAN i DISC-MAX | format VHD albo kontroler IDE | konwersja do VHDX, dysk z danymi na kontroler SCSI |
| Nowy gość Linux z kilkoma GB danych ma wielokrotnie większy plik | blok 32 MB | nowy dysk z blokiem 1 MB albo Convert-VHD |
Obok VHDX rosną pliki .avhdx | checkpoint lub pozostałość po zadaniu kopii | usunąć checkpoint przez Hyper-V, nie kasować plików ręcznie |
Optimize-VHD przy gościu Linux prawie nic nie odzyskuje | host nie odczytuje ext4 ani XFS | przed wyłączeniem maszyny fstrim -av, potem kompaktowanie |
Sprawdź u siebie
Dyski maszyn na hoście z typem, kontrolerem, blokiem i stosunkiem rozmiaru pliku do pojemności:
Get-VM | Get-VMHardDiskDrive | ForEach-Object {
$vhd = Get-VHD -Path $_.Path
[pscustomobject]@{
VM = $_.VMName
Kontroler = $_.ControllerType
Typ = $vhd.VhdType
BlokMB = $vhd.BlockSize / 1MB
PojemnoscGB = [math]::Round($vhd.Size / 1GB)
PlikGB = [math]::Round($vhd.FileSize / 1GB, 1)
PlikProcent = [math]::Round(100 * $vhd.FileSize / $vhd.Size)
}
} | Sort-Object PlikProcent -Descending | Format-Table -AutoSize
Dobry wynik: dyski gości Linux z blokiem 1 MB, na kontrolerze SCSI, z PlikProcent zbliżonym do zajętości w gościu. Zły wynik: PlikProcent powyżej 90 przy gościu zajętym w połowie, BlokMB równy 32 przy gościu Linux albo dysk z danymi na IDE. Wiersz Differencing to checkpoint, a jego PlikGB to rozmiar tylko ostatniego pliku łańcucha.
Wolne miejsce na woluminach i to, ile dyski dynamiczne mogą jeszcze urosnąć. Skrypt zakłada woluminy z literą dysku; dla woluminów CSV trzeba go dostosować:
Get-VM | Get-VMHardDiskDrive | ForEach-Object { Get-VHD -Path $_.Path } |
Group-Object { (Split-Path $_.Path -Qualifier).TrimEnd(':') } |
ForEach-Object {
$vol = Get-Volume -DriveLetter $_.Name
$growth = ($_.Group | Where-Object VhdType -ne 'Fixed' |
ForEach-Object { $_.Size - $_.FileSize } | Measure-Object -Sum).Sum
[pscustomobject]@{
Wolumin = $_.Name
WolneGB = [math]::Round($vol.SizeRemaining / 1GB)
WolneProcent = [math]::Round(100 * $vol.SizeRemaining / $vol.Size)
MozliwyWzrostGB = [math]::Round($growth / 1GB)
}
} | Format-Table -AutoSize
Dobry wynik: WolneGB większe niż MozliwyWzrostGB albo świadomy nadprzydział z alertem. Zły wynik: WolneProcent poniżej 15 albo możliwy wzrost kilkukrotnie większy od wolnego miejsca bez alertu.
W gościu Linux: czy dyski przyjmują TRIM, czy timer działa i jaka jest zajętość:
lsblk --discard # DISC-GRAN i DISC-MAX większe od 0 = dysk przyjmuje TRIM
systemctl status fstrim.timer # oczekiwany stan: active (waiting)
systemctl list-timers fstrim.timer # ostatnie i następne uruchomienie
df -h # zajętość systemów plików w gościu
Dobry wynik: niezerowe DISC-GRAN i DISC-MAX, timer active (waiting) z ostatnim uruchomieniem w ciągu tygodnia, zajętość z df -h zbliżona do PlikProcent. Zły wynik: zera w kolumnach DISC, timer nieaktywny lub nieznany, zajętość w gościu wyraźnie niższa niż PlikProcent.