From 6b44066843eff8cc934e8e3067e3337dd35d2d2b Mon Sep 17 00:00:00 2001 From: Liosha84 Date: Wed, 4 Nov 2026 00:53:46 +0100 Subject: [PATCH] Added ARTICLE.md --- docs/ARTICLE.md | 637 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 637 insertions(+) create mode 100644 docs/ARTICLE.md diff --git a/docs/ARTICLE.md b/docs/ARTICLE.md new file mode 100644 index 0000000..4d6a068 --- /dev/null +++ b/docs/ARTICLE.md @@ -0,0 +1,637 @@ +# htop-web: Веб-интерфейс мониторинга системных ресурсов на базе Spring Boot WebFlux и Angular + +--- + +## Аннотация + +В статье рассматривается архитектура и реализация **htop-web** — самохостимого веб-приложения для мониторинга системных ресурсов Linux-серверов в реальном времени. Приложение объединяет реактивный бэкенд на Spring Boot WebFlux с фронтендом на Angular 22, упаковывая оба компонента в единый Docker-контейнер. Ключевая особенность решения — сбор метрик хост-системы из контейнеризованной среды через монтирование файловых систем `/proc` и `/sys` хоста, что позволяет получить точные данные о CPU, памяти, дисках и сетевых интерфейсах реального сервера, а не виртуальной среды контейнера. + +**Ключевые слова:** мониторинг, Spring Boot, WebFlux, Angular, Docker, контейнеризация, OSHI, системные метрики. + +--- + +## 1. Введение + +Мониторинг системных ресурсов является критически важной задачей при администрировании серверной инфраструктуры. Классические инструменты, такие как `htop`, `top`, `vmstat` и `iostat`, предоставляют детальную информацию, однако требуют доступа к терминалу и не подходят для удалённого мониторинга через веб-браузер. + +Существующие веб-решения мониторинга (Grafana + Prometheus, Netdata, Zabbix) предлагают мощные возможности визуализации и агрегации, но часто требуют значительных ресурсов и сложной настройки. Для небольших инфраструктур и индивидуальных серверов возникает потребность в лёгком, самодостаточном решении, которое: + +- Разворачивается за несколько команд; +- Не требует внешних баз данных или брокеров сообщений; +- Предоставляет информацию в реальном времени; +- Занимает минимальные ресурсы хоста. + +**htop-web** отвечает этим требованиям, предоставляя компактный веб-дашборд, который собирает метрики напрямую из файловых систем Linux и отображает их через REST API и реактивный фронтенд. + +--- + +## 2. Обзор существующих решений + +| Решение | Тип | Ресурсы | Настройка | Особенности | +|---|---|---|---|---| +| **htop / top** | CLI | Минимальные | Нет | Требует терминала, нет веб-интерфейса | +| **Grafana + Prometheus** | Веб | Высокие | Сложная | Агрегация, алерты, требует Node Exporter | +| **Netdata** | Веб | Средние | Средняя | Real-time, но тяжёлый агент | +| **Zabbix** | Веб | Высокие | Сложная | Корпоративный мониторинг, алертинг | +| **Glances** | Веб/CLI | Низкие | Простая | Python-зависимости, ограниченный UI | +| **htop-web** | Веб | Низкие | Простая | Один контейнер, нет внешних зависимостей | + +htop-web занимает нишу между CLI-инструментами и тяжёлыми системами мониторинга, предлагая минималистичный подход с фокусом на простоту развёртывания. + +--- + +## 3. Архитектура системы + +### 3.1 Общая схема + +``` +┌─────────────────────────────────────────────────────┐ +│ Веб-браузер │ +│ (Angular 22, TypeScript) │ +└──────────────────────┬──────────────────────────────┘ + │ HTTP (polling, 1 сек) + ▼ +┌─────────────────────────────────────────────────────┐ +│ Docker-контейнер │ +│ │ +│ ┌─────────────────────────────────────────────┐ │ +│ │ Spring Boot WebFlux (Java 21) │ │ +│ │ │ │ +│ │ REST API: /api/system/* │ │ +│ │ ├─ /metrics — CPU, RAM, ОС │ │ +│ │ ├─ /processes — список процессов │ │ +│ │ ├─ /linux — uptime, load average │ │ +│ │ ├─ /cpu — детали по ядрам │ │ +│ │ ├─ /linux/memory — детальная память │ │ +│ │ ├─ /linux/disks — диски и файловые сист. │ │ +│ │ └─ /linux/network — сетевые интерфейсы │ │ +│ │ │ │ +│ │ Сервисы сбора метрик: │ │ +│ │ ├─ SystemMetricsService │ │ +│ │ ├─ SystemProcessesService │ │ +│ │ ├─ LinuxDetailsService │ │ +│ │ ├─ CpuDetailsService │ │ +│ │ ├─ LinuxMemoryService │ │ +│ │ ├─ LinuxDiskService │ │ +│ │ └─ LinuxNetworkService │ │ +│ │ │ │ +│ │ Статические ресурсы Angular │ │ +│ └─────────────────────────────────────────────┘ │ +│ │ +│ Монтированные тома хоста: │ +│ /host/root ← / (корень хоста, read-only) │ +│ /host/sys ← /sys (системные данные, read-only) │ +└─────────────────────────────────────────────────────┘ +``` + +### 3.2 Ключевые принципы архитектуры + +1. **Single-container deployment** — фронтенд и бэкенд упакованы в один JAR-файл и один Docker-контейнер. +2. **Host filesystem mounting** — метрики хоста получаются через монтирование `/proc` и `/sys` хоста в контейнер. +3. **Reactive backend** — Spring WebFlux обеспечивает неблокирующую обработку запросов. +4. **RxJS polling** — фронтенд использует реактивные потоки для периодического опроса API. +5. **Fallback mechanism** — при недоступности файлов хоста используется библиотека OSHI как запасной вариант. + +--- + +## 4. Реализация бэкенда + +### 4.1 Технологический стек + +| Компонент | Технология | Версия | +|---|---|---| +| Язык | Java | 21 | +| Фреймворк | Spring Boot | 4.1.2 | +| Реактивность | Spring WebFlux | 4.1.2 | +| Системные метрики | OSHI | 7.7.2 | +| Сборка | Gradle | 8.x | + +### 4.2 REST API контроллер + +Контроллер `SystemMetricsController` экспонирует шесть REST-эндпоинтов, каждый из которых делегирует сбор данных соответствующему сервису: + +```java +@RestController +@RequestMapping("/api/system") +public class SystemMetricsController { + + private final SystemMetricsService _systemMetricsService; + private final LinuxDetailsService _linuxDetailsService; + private final CpuDetailsService _cpuDetailsService; + private final LinuxMemoryService _linuxMemoryService; + private final LinuxDiskService _linuxDiskService; + private final LinuxNetworkService _linuxNetworkService; + + @GetMapping("/metrics") + public Mono getMetrics() { + return Mono.fromCallable(_systemMetricsService::getMetrics) + .subscribeOn(Schedulers.boundedElastic()); + } + + @GetMapping("/linux") + public Mono getLinuxDetails() { + return Mono.fromCallable(_linuxDetailsService::getDetails) + .subscribeOn(Schedulers.boundedElastic()); + } + // ... остальные эндпоинты +} +``` + +Каждый метод оборачивает синхронный вызов сервиса в `Mono` и выполняет его на `Schedulers.boundedElastic()`, что предотвращает блокировку реактивного пула потоков при чтении файловых систем. + +### 4.3 Сбор метрик CPU и памяти + +Сервис `SystemMetricsService` демонстрирует ключевой паттерн проекта — чтение метрик хоста из смонтированной файловой системы с fallback на OSHI: + +```java +@Service +public class SystemMetricsService { + + private static final Path HOST_ROOT = Path.of("/host/root"); + private static final Path HOST_PROC = HOST_ROOT.resolve("proc"); + + public synchronized SystemMetricsResponse getMetrics() { + CpuTicks currentTicks = readHostCpuTicks(); + double cpuUsage = 0.0; + + if (currentTicks != null && _previousHostCpuTicks != null) { + long totalDelta = currentTicks.total() - _previousHostCpuTicks.total(); + long idleDelta = currentTicks.idle() - _previousHostCpuTicks.idle(); + + if (totalDelta > 0 && idleDelta >= 0 && idleDelta <= totalDelta) { + cpuUsage = (double) (totalDelta - idleDelta) / totalDelta; + } + } + // ... расчёт памяти и формирование ответа + } + + private CpuTicks readHostCpuTicks() { + Path statFile = HOST_PROC.resolve("stat"); + // Чтение /host/root/proc/stat и парсинг строки "cpu ..." + // Возвращает null при ошибке — триггер для fallback на OSHI + } +} +``` + +Алгоритм расчёта загрузки CPU основан на сравнении тиков процессора между двумя последовательными чтениями файла `/proc/stat`. Формула: + +$$CPU_{usage} = \frac{\Delta_{total} - \Delta_{idle}}{\Delta_{total}}$$ + +где $\Delta_{total}$ — разница суммарных тиков всех состояний процессора, а $\Delta_{idle}$ — разница тиков в состоянии idle. + +### 4.4 Сбор информации о процессах + +Сервис `SystemProcessesService` парсит `/proc/[pid]/stat` для каждого процесса, вычисляя загрузку CPU на основе разницы тиков между двумя снимками: + +```java +@Service +public class SystemProcessesService { + + public synchronized List getProcesses() { + Path procRoot = Files.isDirectory(HOST_PROC) ? HOST_PROC : PROC; + + long nowNanos = System.nanoTime(); + double elapsedSeconds = + (nowNanos - _previousSampleNanos) / 1_000_000_000.0; + + try (var entries = Files.list(procRoot)) { + entries.filter(path -> + path.getFileName().toString().matches("\\d+")) + .forEach(path -> { + int pid = Integer.parseInt(path.getFileName().toString()); + ProcessSnapshot current = readProcess(path, pid, bootTimeSeconds); + // Расчёт CPU usage на основе delta тиков + // ... + }); + } + // Fallback: getProcessesFromOshi() при недоступности /proc + } +} +``` + +Расчёт CPU usage процесса: + +$$CPU_{process} = \frac{\Delta_{ticks}}{CLK\_TCK \times \Delta_{seconds}} \times \frac{100}{N_{cores}}$$ + +где $CLK\_TCK$ — количество тиков системы в секунду (обычно 100), а $N_{cores}$ — количество логических ядер. + +### 4.5 Детали Linux-системы + +Сервис `LinuxDetailsService` предоставляет информацию о системе через OSHI: + +```java +@Service +public class LinuxDetailsService { + + public LinuxDetailsResponse getDetails() { + String hostname = _operatingSystem.getNetworkParams().getHostName(); + String distribution = _operatingSystem.getFamily(); + String kernelVersion = _operatingSystem.getVersionInfo().getVersion(); + long uptimeSeconds = _operatingSystem.getSystemUptime(); + double[] loadAverages = _processor.getSystemLoadAverage(3); + + return new LinuxDetailsResponse( + linux, hostname, distribution, kernelVersion, + uptimeSeconds, + safeLoad(loadAverages, 0), // 1 мин + safeLoad(loadAverages, 1), // 5 мин + safeLoad(loadAverages, 2), // 15 мин + _logicalProcessors + ); + } +} +``` + +--- + +## 5. Реализация фронтенда + +### 5.1 Технологический стек + +| Компонент | Технология | Версия | +|---|---|---| +| Фреймворк | Angular | 22 | +| Язык | TypeScript | 5.x | +| Реактивность | RxJS | 7.x | +| Стили | SCSS | — | + +### 5.2 Сервис опроса метрик + +Фронтенд использует RxJS `timer` для периодического опроса API с интервалом 1 секуннда: + +```typescript +@Injectable({ providedIn: 'root' }) +export class SystemMetricsService { + private readonly http = inject(HttpClient); + private readonly refreshInterval = 1000; + + readonly metrics$: Observable = timer(0, this.refreshInterval).pipe( + switchMap(() => + this.http.get('/api/system/metrics').pipe( + catchError((error: unknown) => { + console.error('Failed to load system metrics', error); + return of(null); + }), + ), + ), + shareReplay({ bufferSize: 1, refCount: true }), + ); + + readonly processes$: Observable = timer(0, this.refreshInterval).pipe( + switchMap(() => + this.http.get('/api/system/processes').pipe( + catchError((error: unknown) => of([] as SystemProcess[])), + ), + ), + shareReplay({ bufferSize: 1, refCount: true }), + ); + // ... остальные потоки: linux$, cpu$, memory$, disks$, network$ +} +``` + +Оператор `shareReplay` обеспечивает многоподписку: все компоненты, подписанные на один поток, получают одни и те же данные без дублирования HTTP-запросов. + +### 5.3 Компонент дашборда + +Компонент `Dashboard` инъецирует сервис метрик и предоставляет данные для шаблона: + +```typescript +@Component({ + selector: 'app-dashboard', + standalone: true, + imports: [AsyncPipe, DecimalPipe, JsonPipe], + templateUrl: './dashboard.html', + styleUrl: './dashboard.scss', +}) +export class Dashboard { + private readonly _systemMetricsService = inject(SystemMetricsService); + + readonly metrics$ = this._systemMetricsService.metrics$; + readonly processes$ = this._systemMetricsService.processes$; + readonly linux$ = this._systemMetricsService.linux$; + readonly cpu$ = this._systemMetricsService.cpu$; + readonly memory$ = this._systemMetricsService.memory$; + readonly disks$ = this._systemMetricsService.disks$; + readonly network$ = this._systemMetricsService.network$; + + getSortedProcesses(processes: SystemProcess[]): SystemProcess[] { + return processes + .filter(p => p.name.toLowerCase().includes(this.processFilter)) + .sort((a, b) => { + switch (this.processSort) { + case 'cpu': return a.cpuUsagePercent - b.cpuUsagePercent; + case 'memory': return a.residentMemoryBytes - b.residentMemoryBytes; + case 'name': return a.name.localeCompare(b.name); + case 'pid': return a.pid - b.pid; + } + }); + } +} +``` + +### 5.4 Три режима отображения + +Дашборд поддерживает три режима визуализации: + +1. **Monitor** — карточный интерфейс с метриками CPU, памяти, дисков и сети. +2. **htop** — табличное представление процессов, стилизованное под классический htop. +3. **htop-original** — имитация терминального интерфейса htop с функциональными клавишами (F3 — поиск, F4 — фильтр, F6 — сортировка, F10 — выход). + +```html + +``` + +--- + +## 6. Сборка и упаковка + +### 6.1 Gradle-сборка + +Процесс сборки автоматически строит фронтенд и упаковывает его в бэкенд: + +```groovy +tasks.register('buildFrontend', Exec) { + dependsOn 'installFrontendDependencies' + workingDir file('../frontend') + commandLine 'npm', 'run', 'build', '--', '--configuration', 'production' +} + +tasks.register('copyFrontend', Copy) { + dependsOn 'buildFrontend' + from(file('../frontend/dist/frontend/browser')) + into(file('src/main/resources/static')) +} + +tasks.named('processResources') { + dependsOn 'copyFrontend' +} +``` + +Порядок выполнения: +1. `npm ci` — установка зависимостей фронтенда; +2. `npm run build` — production-сборка Angular; +3. `copyFrontend` — копирование статических файлов в `src/main/resources/static`; +4. `bootJar` — упаковка JAR с фронтендом внутри. + +### 6.2 Docker-образ + +Dockerfile минимален и использует официальный образ Eclipse Temurin: + +```dockerfile +FROM eclipse-temurin:21-jre +WORKDIR /app +COPY backend/build/libs/backend-0.0.1-SNAPSHOT.jar app.jar +EXPOSE 8080 +ENTRYPOINT ["java", "-jar", "app.jar"] +``` + +Образ весит около 300 МБ и не содержит инструментов разработки. + +--- + +## 7. Развёртывание через Docker Compose + +### 7.1 Базовая конфигурация + +Файл `docker-compose.yml` определяет сервис с доступом к файловой системе хоста: + +```yaml +services: + htop-web: + image: htop-web:latest + build: + context: . + dockerfile: Dockerfile + container_name: htop-web + restart: unless-stopped + pid: host + volumes: + - /sys:/host/sys:ro + - /:/host/root:ro + ports: + - "8088:8080" +``` + +**Ключевые параметры:** + +| Параметр | Назначение | +|---|---| +| `pid: host` | Разделение PID-пространства хоста для видимости процессов | +| `/:/host/root:ro` | Чтение `/proc` хоста для метрик CPU, памяти и процессов | +| `/sys:/host/sys:ro` | Доступ к системным данным ядра | +| `restart: unless-stopped` | Автоматический перезапуск при сбое | + +### 7.2 Пошаговая инструкция + +#### Шаг 1: Клонирование репозитория + +```bash +git clone https://gitea.jambotron.com/liosha84/htop-web.git +cd htop-web +``` + +#### Шаг 2: Сборка приложения + +```bash +cd backend +./gradlew clean build +cd .. +``` + +Gradle автоматически соберёт Angular-фронтенд и упакует его в JAR-файл бэкенда. + +#### Шаг 3: Запуск контейнера + +```bash +docker compose up -d --build +``` + +Приложение доступно по адресу `http://localhost:8088`. + +#### Шаг 4: Проверка статуса + +```bash +# Статус контейнера +docker compose ps + +# Логи в реальном времени +docker compose logs -f htop-web +``` + +#### Шаг 5: Остановка + +```bash +docker compose down +``` + +### 7.3 Продвинутое развёртывание с Caddy + +Для продакшн-развёртывания рекомендуется использовать обратный прокси с автоматическим HTTPS: + +```yaml +services: + htop-web: + image: gitea.jambotron.com/liosha84/htop-web:latest + container_name: htop-web + restart: unless-stopped + pid: host + volumes: + - /sys:/host/sys:ro + - /:/host/root:ro + labels: + caddy: htop-web.example.com + caddy.reverse_proxy: "{{upstreams 8080}}" + networks: + - jambotron-net + +networks: + jambotron-net: + external: true +``` + +Caddy автоматически выдаёт TLS-сертификат через Let's Encrypt и маршрутизирует запросы на контейнер. + +### 7.4 Развёртывание на различных платформах + +#### Linux-сервер + +На Linux-сервере контейнер получает прямой доступ к `/proc` и `/sys` хоста, что обеспечивает точные метрики: + +```bash +docker compose up -d --build +``` + +#### Docker Desktop на Windows + +Docker Desktop на Windows запускает Linux-контейнеры в виртуальной машине (WSL2 или Hyper-V). В этом случае метрики отражают состояние виртуальной машины, а не хостовой Windows-системы: + +> **Важно:** Метрики из контейнера не представляют полную картину Windows-хоста. Для мониторинга Windows рекомендуется использовать нативные инструменты или развёртывание на Linux. + +#### Raspberry Pi и ARM-устройства + +htop-web поддерживает ARM-архитектуру благодаря кроссплатформенности Java и OSHI. Сборка выполняется аналогично: + +```bash +docker compose up -d --build +``` + +Образ `eclipse-temurin:21-jre` доступен для `arm64` и `arm/v7`. + +--- + +## 8. Безопасность + +Мониторинг системы раскрывает конфиденциальную информацию. При развёртывании необходимо учитывать: + +1. **Аутентификация** — не размещайте htop-web в открытом доступе без защиты. Используйте обратный прокси с базовой аутентификацией или OAuth. +2. **HTTPS** — всегда используйте TLS при доступе из внешних сетей. +3. **Минимальные привилегии** — контейнер запускается с read-only доступом к файловой системе хоста, что снижает риски. +4. **Сетевая изоляция** — ограничьте доступ к порту 8088 через firewall или Docker-сети. + +--- + +## 9. Сравнение с аналогами + +### 9.1 Функциональное сравнение + +| Функция | htop-web | Netdata | Glances | Grafana+Prometheus | +|---|---|---|---|---| +| Веб-интерфейс | ✓ | ✓ | ✓ | ✓ | +| CPU-метрики | ✓ | ✓ | ✓ | ✓ | +| Память | ✓ | ✓ | ✓ | ✓ | +| Процессы | ✓ | ✓ | ✓ | ✗ | +| Диски | ✓ | ✓ | ✓ | ✓ | +| Сеть | ✓ | ✓ | ✓ | ✓ | +| Развёртывание | 1 контейнер | 1 контейнер | pip install | 3+ компонента | +| Ресурсы | ~100 МБ RAM | ~200 МБ RAM | ~50 МБ RAM | ~500+ МБ RAM | +| История данных | ✗ | ✓ | ✗ | ✓ | +| Алертинг | ✗ | ✓ | ✓ | ✓ | +| Графики | Прогресс-бары | Графики | Прогресс-бары | Графики | + +### 9.2 Позиционирование + +htop-web ориентирован на сценарии, где важны: + +- **Простота** — одна команда для запуска; +- **Минимальные ресурсы** — контейнер потребляет ~100 МБ RAM; +- **Отсутствие внешних зависимостей** — нет баз данных, брокеров, агентов; +- **Визуальное сходство с htop** — привычный интерфейс для системных администраторов. + +Для задач, требующих исторических данных, алертинга и мультихостного мониторинга, рекомендуется использовать Grafana + Prometheus или Netdata. + +--- + +## 10. Заключение + +htop-web демонстрирует, что эффективный инструмент мониторинга может быть реализован как компактное самодостаточное приложение. Ключевые достижения проекта: + +1. **Единый контейнер** — Angular-фронтенд и Spring Boot-бэкенд упакованы в один JAR, что упрощает развёртывание и обновление. +2. **Точные метрики хоста** — монтирование `/proc` и `/sys` хоста позволяет контейнеризованному приложению получать реальные данные сервера, а не метрики виртуальной среды контейнера. +3. **Реактивная архитектура** — Spring WebFlux на бэкенде и RxJS на фронтенде обеспечивают эффективную обработку данных в реальном времени. +4. **Fallback-механизм** — библиотека OSHI служит запасным источником метрик при недоступности файлов хоста. +5. **Минимальные требования** — приложение работает на устройствах с 512 МБ RAM, включая Raspberry Pi. + +Перспективы развития включают добавление исторического хранения метрик, алертинга по пороговым значениям и поддержки WebSocket для push-обновлений вместо polling. + +--- + +## Список литературы + +1. Spring Boot Documentation. — https://docs.spring.io/spring-boot/docs/current/reference/html/ +2. Spring WebFlux Reactive Web. — https://docs.spring.io/spring-framework/reference/web/reactive.html +3. Angular Documentation. — https://angular.dev/ +4. RxJS Documentation. — https://rxjs.dev/ +5. OSHI — Operating System and Hardware Information. — https://github.com/oshi/oshi +6. Docker Documentation. — https://docs.docker.com/ +7. Linux `/proc` filesystem. — man 5 proc +8. The htop source code. — https://github.com/htop-dev/htop +9. Netdata — Distributed real-time monitoring. — https://github.com/netdata/netdata +10. Grafana — The open observability platform. — https://github.com/grafana/grafana + +--- + +## 11. Скачивание и демо + +### Живое демо + +Попробовать htop-web в действии можно по адресу: + +**https://htop-web.jambotron.com/** + +Демо-инстанс развёрнут на Linux-сервере с доступом к реальным системным метрикам. + +### Репозиторий + +Исходный код проекта доступен на Gitea: + +**https://gitea.jambotron.com/liosha84/htop-web** + +Для клонирования: + +```bash +git clone https://gitea.jambotron.com/liosha84/htop-web.git +cd htop-web +``` + +### Docker-образ + +Готовый Docker-образ опубликован в контейнерном реестре Gitea: + +**gitea.jambotron.com/liosha84/htop-web:latest** + +Быстрый запуск одним командой: + +```bash +docker run -d --name htop-web --pid host \ + -v /:/host/root:ro -v /sys:/host/sys:ro \ + -p 8088:8080 \ + gitea.jambotron.com/liosha84/htop-web:latest +``` + +--- + +*Статья подготовлена на основе исходного кода проекта htop-web, версия 0.0.1-SNAPSHOT.*