8 Commits
Author SHA1 Message Date
Liosha84 46cd7c39c0 Merge remote-tracking branch 'origin/master'
continuous-integration/drone/push Build is passing
2026-11-04 00:54:10 +01:00
Liosha84 6b44066843 Added ARTICLE.md 2026-11-04 00:53:46 +01:00
Liosha84 ffc1026ce1 Merge pull request 'fix(drone): use 'master' branch instead of 'main' for build-and-push trigger' (#6) from develop into master
continuous-integration/drone/push Build is passing
Reviewed-on: #6
2026-10-29 01:14:03 +01:00
Liosha84 ea453bd409 Merge pull request 'fix(docker): use correct Debian adduser syntax for non-interactive user creation' (#5) from develop into master
Reviewed-on: #5
2026-10-29 01:06:38 +01:00
Liosha84 6666fca194 Merge pull request 'fix(ci): split drone into two separate pipelines for PR and main' (#4) from develop into master
Reviewed-on: #4
2026-10-29 00:56:56 +01:00
Liosha84 3a0c22abf0 Merge pull request 'Develop' (#3) from develop into master
continuous-integration/drone/push Build is failing
Reviewed-on: #3
2026-10-29 00:52:19 +01:00
Liosha84 8369b2a6ba Merge pull request 'Develop->main' (#2) from develop into master
continuous-integration/drone/push Build is failing
Reviewed-on: #2
2026-10-29 00:43:36 +01:00
Liosha84 3e78dcff0f Merge pull request 'feat(drone): add pull request trigger for CI/CD testing' (#1) from develop into master
Reviewed-on: #1
2026-10-29 00:20:20 +01:00
+637
View File
@@ -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<SystemMetricsResponse> getMetrics() {
return Mono.fromCallable(_systemMetricsService::getMetrics)
.subscribeOn(Schedulers.boundedElastic());
}
@GetMapping("/linux")
public Mono<LinuxDetailsResponse> 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<ProcessInfoResponse> 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<SystemMetrics> = timer(0, this.refreshInterval).pipe(
switchMap(() =>
this.http.get<SystemMetrics>('/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<SystemProcess[]> = timer(0, this.refreshInterval).pipe(
switchMap(() =>
this.http.get<SystemProcess[]>('/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
<nav class="view-switcher">
<button (click)="setViewMode('monitor')">Monitor</button>
<button (click)="setViewMode('htop')">htop</button>
<button (click)="setViewMode('htop-original')">htop-original</button>
</nav>
```
---
## 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.*