홈으로 돌아가기

qtile 위젯: 디스크 공간 예측

qtile용 DiskSpaceAlert 위젯이 선형 회귀에 의해 디스크 공간 고갈 예측. 가상 FS 필터링, deque의 히스토리 사용, 활성 및 안정화 위협 구분. 의존성 없는 전체 코드.

qtile 위젯이 /tmp 종료를 미리 예측
Advertisement 728x90

qtile에서 커스텀 위젯으로 디스크 공간 고갈 예측하기

qtile 개발자는 DiskSpaceAlert 위젯을 통합하여 선형 회귀를 활용해 디스크 공간 고갈을 미리 예측할 수 있습니다. 이 위젯은 가상 파일시스템을 제외한 실제 파일시스템의 사용 공간 추세를 분석하며, 24시간 내에 공간이 0이 되는 시점을 넘어서면 경고를 발동합니다. 위젯은 InLoopPollText를 상속받아 매 5초마다 /proc/mountsos.statvfs를 통해 데이터를 수집하고, deque를 사용해 과거 데이터를 유지합니다.

코드는 외부 의존성 없이 동작하므로 Prometheus가 설치되지 않은 데스크톱 환경에도 이상적입니다. 초기 경고는 /tmp (zram 기반의 ext2, 16GB)에서 발생했는데, 백그라운드 로그가 파티션을 가득 채워 Bash 도구가 조용히 실패했습니다.

마운트 포인트 필터링

위젯은 가상 파일시스템과 시스템 경로를 무시하고, 오직 물리적 디스크만 대상으로 합니다. 다음 상수는 제외 대상으로 정의됩니다:

Google AdInline article slot
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을 반환합니다.

Google AdInline article slot
@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.foregroundself.layout.colour를 효율적으로 업데이트하며 불필요한 재렌더링을 방지합니다. 레이블 텍스트는 DISK /tmp: 8%free, 2h left처럼 표시됩니다.

Google AdInline article slot
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

Advertisement 728x90

다음 읽기