Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ cargo build --release

- **[docs/updating.md](docs/updating.md)** — hostmover 업데이트 절차, "고쳤는데 런처에서 안 바뀐다" 해결
- **[docs/bulk-update.md](docs/bulk-update.md)** — 일괄 업데이트의 안정 릴리스 원칙, 결과 보고서/메일, 플러그인 골라 업데이트
- **[docs/disk-check.md](docs/disk-check.md)** — 매일 아침 디스크 자동 점검, 결과 기록 보존과 추이 읽는 법
- **[docs/rx-cli.md](docs/rx-cli.md)** — Rhymix 터미널 도구 rx-cli 배포와 사용

### macOS
Expand Down
161 changes: 161 additions & 0 deletions docs/disk-check.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
# 매일 아침 디스크 점검 — 기록 보존

서버 디스크가 죽어가는 것을 **미리** 잡기 위한 자동 점검. 매일 아침 07:30 에 돌고,
**이상이 없는 날에도 결과를 남긴다.**

설정 > 디스크 점검 탭.

---

## 1. 왜 "이상 없는 날도" 기록하나

디스크 고장은 대부분 어느 날 갑자기가 아니라 **수치가 조금씩 늘면서** 온다.
재할당섹터 3개는 그 자체로는 위험하지 않지만, 3 → 7 → 15 로 늘고 있다면 교체 신호다.

그래서 "지금 나쁜가"보다 **"어제보다 나빠졌나"**가 중요하고, 비교하려면 정상인 날의 값도
남아 있어야 한다. 경보만 남기면 비교 기준이 사라진다.

한 가지 더: 이상이 없을 때 아무 기록도 안 남으면 **감시가 죽어도 똑같이 조용하다.**
정상 기록이 매일 쌓여야 "3일째 기록이 없네 = cron 이 멈췄네"를 알 수 있다.

---

## 2. 점검 항목

| 항목 | 보는 것 | 경보 조건 |
|---|---|---|
| 파일시스템 | `tune2fs` 상태·누적 FS 에러카운트 | `clean` 아님 / 에러카운트 **증가** |
| RO 재마운트 | 읽기전용으로 강등됐는지 | 감지되면 즉시 (심각) |
| 커널 I/O 에러 | `dmesg` 의 I/O error·EXT4-fs error·hard resetting | 건수 **증가** |
| SMART | overall-health, 재할당섹터, 대기중 불량섹터 | PASSED 아님 / 섹터수 **증가** |
| 용량 | `df` | 95% 이상 |
| inode | `df -i` | 90% 이상 |
| 무결성(주간) | write → 캐시드롭 → read-back 해시 비교 | 해시 불일치 = silent corruption |

무결성 스크럽은 매주 일요일 04:10 에 2GB 로 돈다. SMART 가 PASSED 여도 못 잡는
**갓 쓴 데이터가 읽을 때 바뀌는** 유형을 확정하는 유일한 방법이다.

---

## 3. 어디에 남나

| 경로 | 내용 | 보존 |
|---|---|---|
| `/var/log/hostmover/disk/<날짜>.log` | 그날 점검 결과 **전문** (정상이어도 생성) | 설정한 일수 (기본 90일) |
| `/var/lib/hm-disk-monitor/history.tsv` | 하루 한 줄 요약 — 추이 비교용 | 계속 누적 |
| `/var/log/hm-disk-monitor.log` | 경보만 모은 이력 | logrotate 월간 · 12개월 |
| `/var/lib/hm-disk-monitor/scrub.tsv` | 주간 무결성 검사 결과 | 계속 누적 |
| `/var/lib/hm-disk-monitor/last-run` | 마지막 점검 시각 | — |

일자별 전문만 보존일수로 정리되고, 추이 요약(`history.tsv`)은 지우지 않는다.
한 줄짜리라 몇 년을 쌓아도 부담이 없고, 장기 추이가 이 기능의 핵심이기 때문이다.

같은 날 두 번 돌면 그날 기록은 새로 쓰이고 추이도 한 줄로 유지된다.

---

## 4. 보는 법

**hostmover**: 설정 > 디스크 점검 > "점검 기록 보기" > `최근 14일` / `최근 90일` / `전체`

보여주는 것:

1. **감시 설치 상태** — cron 등록 여부, 마지막 점검 시각.
36시간 넘게 안 돌았으면 경고한다. (이게 제일 중요하다. 감시가 죽은 걸 모르는 게 최악)
2. **일자별 추이** — 아래 표
3. **가장 최근 점검 결과 전문**
4. **경보 이력** 최근 30줄
5. **스크럽 이력** 최근 10건
6. **보존 현황**

추이 표는 이렇게 나온다:

```
date result alerts fserr dmesg use% realloc pending
2026-07-24 OK 0 0 0 51% 0 0
2026-07-25 OK 0 0 0 52% 0 0
2026-07-27 WARN 1 0 0 96% 0 0
2026-07-29 OK 0 0 0 54% 0 0

표시 6건(전체 6건) 중 이상(WARN): 1건
```

**읽는 법: 값의 크기가 아니라 변화를 본다.**

- `fserr` 가 0 → 7 로 뛰었다 : 진행형 파일시스템 손상. 즉시 점검.
- `realloc` 이 12 로 몇 달째 그대로 : 과거 이력. 당장은 위험하지 않다.
- `realloc` 이 12 → 19 : 열화 진행. 교체 계획을 세운다.
- `use%` 가 계속 오른다 : 용량 문제. 95% 에서 경보가 오지만 그 전에 보인다.

서버에서 직접 볼 수도 있다:

```bash
cat /var/lib/hm-disk-monitor/history.tsv | column -t
cat /var/log/hostmover/disk/2026-07-29.log
/usr/local/sbin/hm-disk-monitor.sh # 수동 실행 (기록도 정상적으로 남는다)
```

---

## 5. 메일

기본은 **이상이 감지된 날에만** 발송한다. 매일 오는 정상 메일은 며칠 지나면 아무도 안 읽게 되고,
그러면 정작 진짜 경보도 같이 묻힌다.

매일 받고 싶으면 "이상이 없어도 매일 결과 메일 받기" 를 켠다. 이 경우 점검 결과 전문이 온다.

설치 마지막에 테스트 메일을 한 번 보내고 어떤 전송수단(`mail` / `sendmail`)이 쓰였는지 로그에
표시한다. 둘 다 없으면 그 사실을 알려준다 — 이 경우 경보가 발송되지 않으므로 MTA 를 설치해야 한다.

```bash
apt-get install -y bsd-mailx # HestiaCP 면 exim4 가 이미 있는 경우가 많다: which sendmail
exim -bp # 큐가 막혀 있는지
```

---

## 6. 설치 / 변경 / 제거

설정 > 디스크 점검 > "매일 아침 자동 점검" 카드에서 알림 이메일·기록 보존일수·매일 메일 여부를
정하고 `자동 감시 설치`. **설정을 바꾸려면 값만 고쳐서 다시 설치**하면 덮어쓴다.

설치되는 것:

```
/etc/cron.d/hm-disk-monitor 매일 07:30 점검 · 매주 일 04:10 스크럽
/usr/local/sbin/hm-disk-monitor.sh 매일 점검 본체
/usr/local/sbin/hm-disk-scrub.sh 주간 무결성 검사
/etc/logrotate.d/hm-disk-monitor 경보 로그 로테이션
smartd (DEVICESCAN + 이메일) 설치돼 있으면 활성화
```

`감시 제거` 는 cron·스크립트·logrotate 만 지운다. **쌓인 점검 기록은 지우지 않는다.**
기록까지 지우려면 직접:

```bash
rm -rf /var/log/hostmover/disk /var/lib/hm-disk-monitor /var/log/hm-disk-monitor.log
```

---

## 7. 이상이 잡혔을 때

| 신호 | 해석 | 조치 |
|---|---|---|
| dmesg I/O error · hard resetting | 하드웨어(디스크·케이블·포트) | 백업 확보 후 교체 검토 |
| FS 에러카운트만 증가, SMART 정상 | 논리 손상일 수 있음 | `fsck` — 반복 재발하면 하드웨어 |
| 스크럽 해시 불일치 | silent corruption 확정 | **즉시 교체.** SMART 무관 |
| RO 재마운트 | 커널이 손상을 감지해 쓰기 차단 | 즉시 점검. 무리한 재마운트 금지 |
| 재할당/대기섹터 증가 | 디스크 열화 진행 | 교체 계획 |
| 용량 95%+ | 서비스 장애 임박 | 정리 또는 증설 |

즉시 확인이 필요하면 같은 탭의 `디스크 건강 진단`(읽기 전용)과
`무결성 검사`(512MB / 4GB)를 수동으로 돌릴 수 있다.

---

## 8. 관련

- 일괄 업데이트: [bulk-update.md](bulk-update.md)
- hostmover 자체 업데이트: [updating.md](updating.md)
- rx-cli: [rx-cli.md](rx-cli.md)
64 changes: 60 additions & 4 deletions src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -415,6 +415,10 @@ pub struct App {
bulk_mail: String,
disk_path: String,
disk_alert_email: String,
/// 일자별 점검기록 보존일수 (문자열 입력 → 파싱, 빈 값이면 기본 90)
disk_keep_days: String,
/// 이상이 없어도 매일 결과 메일을 받을지
disk_daily_mail: bool,
/// WordPress 플러그인/테마 버전비교 스캔 결과 캐시 — (도메인id, 종류) → 행들.
/// 재방문·플러그인↔테마 전환 시 네트워크 재조회 없이 즉시 표시(스캔 버튼=강제 새로고침).
wp_cache: std::collections::HashMap<(u64, ops::WpAssetKind), Vec<ops::WpPluginRow>>,
Expand Down Expand Up @@ -510,6 +514,8 @@ impl App {
bulk_mail: String::new(),
disk_path: "/backup".to_string(),
disk_alert_email: "eond@eond.com".to_string(),
disk_keep_days: "90".to_string(),
disk_daily_mail: false,
wp_cache: std::collections::HashMap::new(),
wp_sel: std::collections::HashSet::new(),
wp_kind: ops::WpAssetKind::Plugin,
Expand Down Expand Up @@ -754,6 +760,7 @@ impl App {
let mut do_disk_health = false;
let mut do_disk_scrub: Option<u32> = None;
let mut do_disk_monitor: Option<bool> = None;
let mut do_disk_history: Option<u32> = None;
let mut do_bulk_update: Option<bool> = None;
// (실행할지, 점검만인지) — 점검은 읽기 전용이라 확인 모달 없이 바로 돈다
let mut do_wp_plugin: Option<(bool, bool)> = None;
Expand Down Expand Up @@ -1032,24 +1039,50 @@ impl App {
});
ui.add_space(8.0);
card(ui, |ui| {
ui.strong(format!("{} 자동 감시 (사전 경보)", ph::BELL_RINGING));
ui.label(egui::RichText::new("서버에 cron 감시를 설치합니다: smartd + 매일 FS에러카운트/dmesg/SMART/용량 점검 + 매주 무결성 스크럽(위 경로). 이상 시 이메일 경보.").weak());
ui.strong(format!("{} 매일 아침 자동 점검 (사전 경보 + 기록 보존)", ph::BELL_RINGING));
ui.label(egui::RichText::new("서버에 cron 감시를 설치합니다. 매일 07:30 에 FS에러카운트·dmesg I/O에러·SMART·용량·inode 를 점검하고, 매주 일요일 04:10 에 무결성 스크럽(위 경로)을 돌립니다. 이상이 생기면 이메일로 경보합니다.").weak());
ui.label(egui::RichText::new("점검 결과는 이상이 없는 날에도 매일 서버에 보존됩니다 — /var/log/hostmover/disk/<날짜>.log (전문) 와 history.tsv (추이 한 줄 요약). 아래 '점검 기록 보기' 로 조회합니다.").weak());
ui.add_space(6.0);
egui::Grid::new("disk_monitor_grid").num_columns(2).spacing([8.0, 6.0]).show(ui, |ui| {
grid_label(ui, "알림 이메일");
ui.add(egui::TextEdit::singleline(&mut self.disk_alert_email).hint_text("eond@eond.com").desired_width(260.0).margin(FIELD_MARGIN));
ui.end_row();
grid_label(ui, "기록 보존일수");
ui.horizontal(|ui| {
ui.add(egui::TextEdit::singleline(&mut self.disk_keep_days).hint_text("90").desired_width(70.0).margin(FIELD_MARGIN));
ui.label(egui::RichText::new("일 (일자별 전문 기준 · 추이 요약은 계속 누적)").weak());
});
ui.end_row();
});
ui.checkbox(&mut self.disk_daily_mail, "이상이 없어도 매일 결과 메일 받기");
ui.label(egui::RichText::new("끄면 이상이 감지된 날만 메일이 옵니다(권장). 켜면 매일 아침 점검 결과 전문이 메일로 옵니다.").weak());
ui.add_space(6.0);
ui.horizontal(|ui| {
if ui.add_enabled(!running, btn_primary(format!("{} 자동 감시 설치", ph::BELL_RINGING)))
.on_hover_text("확인 후 서버에 cron/smartd 설치")
.on_hover_text("확인 후 서버에 cron/smartd 설치 (기존 설치 위에 다시 눌러 설정 변경 가능)")
.clicked() { do_disk_monitor = Some(true); }
if ui.add_enabled(!running, egui::Button::new(format!("{} 감시 제거", ph::BELL_SLASH)))
.on_hover_text("cron/스크립트만 제거 — 쌓인 점검 기록은 보존됩니다")
.clicked() { do_disk_monitor = Some(false); }
});
ui.label(egui::RichText::new("설치 마지막에 위 주소로 테스트 메일을 자동 발송하고, 사용된 전송수단(mail/sendmail)을 로그에 표시합니다. 메일이 안 오면 서버 MTA(exim/postfix) 큐·스팸함을 점검하세요.").weak());
});
ui.add_space(8.0);
card(ui, |ui| {
ui.strong(format!("{} 점검 기록 보기", ph::CLOCK_COUNTER_CLOCKWISE));
ui.label(egui::RichText::new("서버에 쌓인 매일 점검 결과를 조회합니다(읽기 전용). 자동 점검이 실제로 돌고 있는지(마지막 실행 시각), 일자별 추이, 가장 최근 점검 결과 전문, 경보·스크럽 이력을 함께 보여줍니다.").weak());
ui.label(egui::RichText::new("※ 누적 FS 에러나 재할당섹터는 '값이 큰 것'보다 '늘어나는 것'이 위험 신호입니다. 추이 표에서 증가 여부를 보세요.").weak());
ui.add_space(6.0);
ui.horizontal(|ui| {
if ui.add_enabled(!running, btn_primary(format!("{} 최근 14일", ph::CLOCK_COUNTER_CLOCKWISE)))
.clicked() { do_disk_history = Some(14); }
if ui.add_enabled(!running, egui::Button::new("최근 90일"))
.clicked() { do_disk_history = Some(90); }
if ui.add_enabled(!running, egui::Button::new("전체"))
.on_hover_text("보존된 추이 전부")
.clicked() { do_disk_history = Some(3650); }
});
});
}
SettingsTab::Backup => {
card(ui, |ui| {
Expand Down Expand Up @@ -1224,9 +1257,32 @@ impl App {
}
}
}
if let Some(days) = do_disk_history {
match ops::build_disk_history(&self.store.settings, days) {
Ok(job) => {
self.running = true;
self.last_ok = None;
self.status = "점검 기록 조회 중...".into();
let ctx2 = ctx.clone();
ops::spawn(job, self.tx.clone(), move || ctx2.request_repaint());
}
Err(e) => {
self.last_ok = Some(false);
self.status = format!("점검 기록 조회 실패: {e}");
self.log.push(format!("디스크 점검 기록: {e}"));
}
}
}
if let Some(install) = do_disk_monitor {
let built = if install {
ops::build_disk_monitor_install(&self.store.settings, &self.disk_alert_email, &self.disk_path)
let keep = self.disk_keep_days.trim().parse::<u32>().unwrap_or(90);
ops::build_disk_monitor_install(
&self.store.settings,
&self.disk_alert_email,
&self.disk_path,
keep,
self.disk_daily_mail,
)
} else {
ops::build_disk_monitor_uninstall(&self.store.settings)
};
Expand Down
Loading