qtile에서 커스텀 위젯으로 디스크 공간 고갈 예측하기
qtile 개발자는 DiskSpaceAlert 위젯을 통합하여 선형 회귀를 활용해 디스크 공간 고갈을 미리 예측할 수 있습니다. 이 위젯은 가상 파일시스템을 제외한 실제 파일시스템의 사용 공간 추세를 분석하며, 24시간 내에 공간이 0이 되는 시점을 넘어서면 경고를 발동합니다. 위젯은 InLoopPollText를 상속받아 매 5초마다 /proc/mounts와 os.statvfs를 통해 데이터를 수집하고, deque를 사용해 과거 데이터를 유지합니다.
코드는 외부 의존성 없이 동작하므로 Prometheus가 설치되지 않은 데스크톱 환경에도 이상적입니다. 초기 경고는 /tmp (zram 기반의 ext2, 16GB)에서 발생했는데, 백그라운드 로그가 파티션을 가득 채워 Bash 도구가 조용히 실패했습니다.
마운트 포인트 필터링
위젯은 가상 파일시스템과 시스템 경로를 무시하고, 오직 물리적 디스크만 대상으로 합니다. 다음 상수는 제외 대상으로 정의됩니다:
DISK_SPACE_SKIP_FS = {
'sysfs', 'proc', 'devtmpfs', 'devpts', 'tmpfs', 'securityfs',
'cgroup', 'cgroup2', 'pstore', 'debugfs', 'hugetlbfs', 'mqueue',
'configfs', 'fusectl', 'efivarfs', 'binfmt_misc', 'autofs',
'fuse.portal', 'nsfs', 'tracefs', 'bpf', 'ramfs', 'rpc_pipefs',
'nfsd', 'overlay', 'fuse.gvfsd-fuse',
}
DISK_SPACE_SKIP_PREFIXES = ('/sys/', '/proc/', '/dev/', '/run/')
get_mountpoints 함수는 /proc/mounts를 파싱하여 파일시스템 유형이나 경로 접두사에 따라 항목을 건너뜁니다. zram 기반 tmpfs의 경우 필터 적용이 안 되어 심각한 상황도 탐지할 수 있습니다.
def get_mountpoints(self):
mounts = []
try:
with open('/proc/mounts') as f:
for line in f:
parts = line.split()
if len(parts) < 3:
continue
mountpoint, fstype = parts[1], parts[2]
if fstype in DISK_SPACE_SKIP_FS:
continue
if any(mountpoint.startswith(p)
for p in DISK_SPACE_SKIP_PREFIXES):
continue
mounts.append(mountpoint)
except OSError:
pass
return mounts
예측을 위한 선형 회귀
예측은 NumPy 없이 일반 최소제곱법(OLS)을 사용합니다. 자유 공간을 y(t) = a·t + b로 모델링하고, y=0이 되는 시점을 계산합니다. 공식은 다음과 같습니다:
a = (n∑(t_i y_i) - ∑t_i ∑y_i) / (n∑t_i² - (∑t_i)²)b = (∑y_i - a ∑t_i) / n
고갈까지 남은 시간: -b / a에서 윈도우 경과 시간을 뺍니다. 기울기가 ≥ 0이거나 seconds_left ≤ 0이면 None을 반환합니다.
@staticmethod
def _predict_exhaustion(history, now):
n = len(history)
t = [h[0] - history[0][0] for h in history]
y = [h[1] for h in history]
sum_t = sum(t)
sum_y = sum(y)
sum_tt = sum(ti * ti for ti in t)
sum_ty = sum(ti * yi for ti, yi in zip(t, y))
denom = n * sum_tt - sum_t * sum_t
if denom == 0:
return None
slope = (n * sum_ty - sum_t * sum_y) / denom
intercept = (sum_y - slope * sum_t) / n
if slope >= 0:
return None
time_to_zero = -intercept / slope
elapsed = now - history[0][0]
seconds_left = time_to_zero - elapsed
if seconds_left > 0:
return seconds_left / 3600
return None
슬라이딩 윈도우 크기 maxlen=120 (5초 간격으로 10분)을 사용해 일시적인 파일로 인한 노이즈를 완화합니다.
색상 표시 및 알림 로직
두 가지 알림 수준이 있습니다:
- 빨간색 (#FF0000): 최근 30초 동안 공간 감소 중이거나, 자유 공간이 10% 미만일 때
- 주황색 (#FF8800): 24시간 내 고갈 예측되지만, 추세가 안정화된 경우
_set_color 메서드는 self.foreground와 self.layout.colour를 효율적으로 업데이트하며 불필요한 재렌더링을 방지합니다. 레이블 텍스트는 DISK /tmp: 8%free, 2h left처럼 표시됩니다.
def _set_color(self, color):
self.foreground = color
if self.layout:
self.layout.colour = color
마운트 해제된 파일시스템의 경우 기록이 초기화됩니다.
전체 DiskSpaceAlert 클래스 코드
DISK_SPACE_CHECK_INTERVAL = 5
DISK_SPACE_WARN_PERCENT = 10
DISK_SPACE_PREDICT_HOURS = 24
# ... (SKIP_FS 및 PREFIXES는 위와 동일)
class DiskSpaceAlert(base.InLoopPollText):
COLOR_RED = '#FF0000'
COLOR_ORANGE = '#FF8800'
def __init__(self, **config):
config.setdefault('foreground', self.COLOR_RED)
config.setdefault('update_interval', DISK_SPACE_CHECK_INTERVAL)
super().__init__(**config)
self.space_history: dict[str, deque] = {}
# get_mountpoints()는 위와 동일
# _predict_exhaustion()는 위와 동일
# _set_color()는 위와 동일
def poll(self):
now = time()
alerts = []
any_active = False
for mp in self.get_mountpoints():
try:
st = os.statvfs(mp)
except OSError:
continue
if st.f_blocks == 0:
continue
free_pct = st.f_bfree / st.f_blocks * 100
free_bytes = st.f_bfree * st.f_frsize
history = self.space_history.setdefault(
mp, deque(maxlen=120))
history.append((now, free_bytes))
reasons = []
if free_pct < DISK_SPACE_WARN_PERCENT:
reasons.append(f'{free_pct:.0f}%free')
any_active = True
if len(history) >= 2 and now - history[0][0] > 60:
hours_left = self._predict_exhaustion(
history, now)
if (hours_left is not None
and hours_left < DISK_SPACE_PREDICT_HOURS):
recent = [h for h in history
if now - h[0] < 30]
if (len(recent) >= 2
and recent[-1][1] <= recent[0][1]):
any_active = True
reasons.append(f'{hours_left:.0f}h left')
if reasons:
label = mp if mp != '/' else '/'
alerts.append(f'DISK {label}: {", ".join(reasons)}')
— Editorial Team
아직 댓글이 없습니다.