first_commit

This commit is contained in:
kpa39l
2026-08-30 13:38:29 +00:00
commit 7a41e7f32c
10 changed files with 594 additions and 0 deletions
+14
View File
@@ -0,0 +1,14 @@
# Данные и кэши — не в git
prometheus-data/
grafana-data/
loki-data/
promtail-data/
# env с паролями
.env
.env.*
# временное
*.tmp
*.log
.DS_Store
+103
View File
@@ -0,0 +1,103 @@
# Опыт эксплуатации: мониторинг Garage v2.1 через Prometheus
> Дата: 2026-08-30
> Ситуация: кластер Garage v2.1 (RF=3) на vps01 + bigbox + vps02, WireGuard 10.8.0.0/24.
> Задача: вывести статус кластера в браузер (Grafana + Prometheus + Loki).
## Ключевые находки / грабли
### 1. Admin API Garage v2.1 слушает ОТДЕЛЬНЫЙ порт (`[admin] api_bind_addr`)
Самое важное. Если `api_bind_addr` НЕ задан в `[admin]`, Garage слушает admin
(включая `/metrics`) на **том же порту, что S3 API** (3900). В этом случае
`Authorization: Bearer <token>` к `/metrics` на 3900 отвечает
`400 Bad request: Unsupported authorization method` — авторизация S3 не понимает
Bearer, а admin-роутер не подключён.
**Решение:** добавить в `[admin]` отдельный адрес:
```toml
[admin]
api_bind_addr = "10.8.0.2:3903" # свой WG-IP на каждой ноде
metrics_token = "..."
admin_token = "..."
```
После рестарта ноды `/metrics` и `/health` доступны на `:3903`.
### 2. `metrics_token` НЕ обязателен
Если `metrics_token` не задан — `/metrics` доступен **без авторизации**.
Если задан — используется как обычный Bearer token:
```
curl -H "Authorization: Bearer <metrics_token>" http://node:3903/metrics
```
(совпадает с синтаксисом admin_token; для метрик достаточно metrics_token).
### 3. Docker-образ `dxflrs/garage:v2.1.0` — scratch без shell
- Нет `/bin/sh`, нет `ls`, `docker exec garage ls` — не работает.
- CLI-бинарь лежит по пути `/garage` внутри образа, а НЕ в PATH.
- Поэтому проверка статуса через docker run:
```
docker run --rm \
-v /opt/garage/garage.toml:/etc/garage.toml:ro \
-v /opt/garage/meta:/var/lib/garage/meta \
--entrypoint /garage dxflrs/garage:v2.1.0 status
```
(без монтирования `meta` будет ошибка "Unable to read node key").
### 4. Перезапуск нод кластера — безопасно, но по очереди
`docker compose restart garage` на всех трёх нодах прошёл **без потери кворума**
и без пересборки layout — кластер остался HEALTHY (layout version 1 сохранился).
Порядок: внешние (vps02, vps01) → bigbox. Пауза ~8с между рестартами.
### 5. UFW на vps01 режет новые порты
Порт 3903 на vps01 был закрыт UFW (в отличие от 3901, открытого для Anywhere).
Пришлось:
```
sudo ufw allow from 10.8.0.0/24 to any port 3903 proto tcp comment "garage admin metrics"
```
На vps02 файрвол — nftables с policy ACCEPT, порт не блокировался.
### 6. Конфиг garage.toml на vps01/vps02 принадлежит root
Правка через SSH требует sudo (python не может писать напрямую):
```
ssh vps01 'sudo sed -i "s|^\[admin\]$|[admin]\napi_bind_addr = \"10.8.0.1:3903\"|" /opt/garage/garage.toml'
```
### 7. Авторизация в веб-поиске не работает напрямую с bigbox
SearXNG (127.0.0.1:11436) возвращает пустые результаты из-за блокировок
движков (brave rate-limit, duckduckgo CAPTCHA, startpage timeout).
Документация Garage достаётся напрямую с git.deuxfleurs.fr:
```
https://git.deuxfleurs.fr/Deuxfleurs/garage/raw/branch/main-v1/doc/book/reference-manual/configuration.md
https://git.deuxfleurs.fr/Deuxfleurs/garage/raw/branch/main-v1/doc/book/reference-manual/admin-api.md
```
Ветка по умолчанию для v2.x — `main-v1` (не `main`).
## Проверка результата
```
# все три ноды отдают метрики:
curl -s -H "Authorization: Bearer <token>" http://10.8.0.1:3903/metrics | head
curl -s -H "Authorization: Bearer <token>" http://10.8.0.2:3903/metrics | head
curl -s -H "Authorization: Bearer <token>" http://10.8.0.4:3903/metrics | head
# → HTTP 200, ~90-110 КБ текста в Prometheus-формате
# кластер HEALTHY:
docker run --rm -v /opt/garage/garage.toml:/etc/garage.toml:ro -v /opt/garage/meta:/var/lib/garage/meta --entrypoint /garage dxflrs/garage:v2.1.0 status
```
## Что осталось сделать / TODO
- [ ] Поднять `docker compose up -d` в /opt/monitoring (после первого коммита)
- [ ] Node-exporter на всех 3 хостах (10.8.0.x:9100)
- [ ] Дашборд Garage в Grafana (provisioning уже настроен, нужен JSON)
- [ ] Проверить `up{job="garage"}` в Prometheus и открыть Grafana :3001
- [ ] Логи garage через promtail → Loki → Grafana
- [ ] Сделать разные admin_token / metrics_token (сейчас совпадают)
+61
View File
@@ -0,0 +1,61 @@
# План работ: мониторинг Garage-кластера
## Цель
Вывести статус кластера Garage (vps01, bigbox, vps02) в браузер через Grafana,
с Prometheus-метриками и сбором логов (Loki). Без трейсов (до них «не доросли»).
## Этап 1. Метрики Garage (выполнено ✅)
1. Понять, как Garage v2.1 отдаёт метрики.
→ Найдено: admin API на отдельном порту `[admin] api_bind_addr`; `/metrics` в
Prometheus-формате; `metrics_token` опционален.
2. Добавить в `/opt/garage/garage.toml` на **трёх** нодах:
```toml
[admin]
api_bind_addr = "<WG-IP>:3903"
metrics_token = "<token>"
```
- bigbox: 10.8.0.2:3903 (файл — estorozhenko)
- vps01: 10.8.0.1:3903 (файл — root, sudo)
- vps02: 10.8.0.4:3903 (файл — root, sudo)
3. Открыть порт 3903 для WG-подсети на vps01 (ufw); на vps02/bigbox не требуется.
4. Перезапустить ноды по очереди: vps02 → vps01 → bigbox (пауза ~8с).
5. Проверить: `curl -H "Authorization: Bearer <token>" http://<ip>:3903/metrics` → 200.
**Результат:** все 3 ноды отдают метрики, кластер HEALTHY.
## Этап 2. Стек мониторинга на bigbox (в работе 🔄)
- [x] Создать `/opt/monitoring/` с docker-compose.yml, prometheus.yml, alerts.yml
- [x] Prometheus: таргеты garage x3 (:3903), health x3, node-exporters, self
- [ ] Node-exporter на 3 хостах (10.8.0.x:9100)
- [ ] Запустить `docker compose up -d`
- [ ] Проверить Prometheus :9090 (`up{job="garage"}`, targets)
- [ ] Grafana :3001: datasource Prometheus, дашборд Garage, alerts
## Этап 3. Логи (после метрик 🔄)
- [ ] Promtail (docker.sock) → Loki :3100 → Grafana
- [ ] Логи garage со всех нод (сейчас promtail только на bigbox)
## Этап 4. Git + катастрофоустойчивость (в работе 🔄)
- [x] MD-файлы: README.md, EXPERIENCE.md, PLAN.md
- [ ] git init в /opt/monitoring, первый коммит
- [ ] remote: git@gitverse.ru:kpa39l/monitoring.git (master)
- [ ] `git push -u origin master`
- [ ] В gitea: pull mirror репозитория из gitverse
- [ ] В gitea: добавить estorozhenko с полными правами (на случай поломки bigbox)
## Этап 5. Полировка (TODO)
- [ ] Разные admin_token / metrics_token (`openssl rand -base64 32`)
- [ ] Дашборд: статус нод, занятость диска, блоки, репликация
- [ ] Уведомления алертов (например, в Telegram/почту)
## Риски / заметки
- Перезапуск нод кластера безопасен, но делать по очереди.
- Scratch-образ garage → для CLI-команд нужен `--entrypoint /garage` + монтирование meta.
- Интернет с bigbox частично ограничен; документация — через git.deuxfleurs.fr (ветка main-v1).
- Port 3000 занят gitea → Grafana на 3001.
+93
View File
@@ -0,0 +1,93 @@
# Monitoring stack — Garage cluster (vps01 + bigbox + vps02)
Стек мониторинга для S3-кластера Garage (репликация RF=3, WireGuard 10.8.0.0/24).
Расположен на **bigbox** в `/opt/monitoring`.
## Архитектура
```
bigbox (10.8.0.2)
┌─────────────────────────────────────────────────┐
│ prometheus :9090 ── scrape ──► garage x3 :3903 │
│ │ (WG admin API) │
│ ├──► node-exporter :9100 (bigbox) │
│ └──► 10.8.0.1:3903 (vps01) │
│ 10.8.0.4:3903 (vps02) │
│ │
│ grafana :3001 (dashboards, alerts) │
│ loki :3100 (logs) │
│ promtail ── docker.sock ──► docker logs garage │
└─────────────────────────────────────────────────┘
│
▼ WireGuard 10.8.0.0/24
vps01 (10.8.0.1) vps02 (10.8.0.4)
```
## Сервисы и порты
| Сервис | Порт | Доступ | Примечание |
|-----------|-----------|--------------------|--------------------------------|
| Prometheus| 9090 | локально/WG | retention 30d (TSDB) |
| Grafana | 3001 | браузер bigbox | 3000 занят gitea → 3001 |
| Loki | 3100 | локально | сбор логов |
| Promtail | — | docker.sock | docker logs garage |
| node-exp | 9100 | WG | на каждой ноде (системные метрики) |
## Garage admin API (метрики)
В Garage v2.1 admin API (включая `/metrics` в Prometheus-формате) слушает
**отдельный порт** `[admin] api_bind_addr`. На всех трёх нодах прописано:
```toml
[admin]
api_bind_addr = "<WG-IP>:3903" # bigbox 10.8.0.2, vps01 10.8.0.1, vps02 10.8.0.4
admin_token = "c213de..."
metrics_token = "c213de..."
```
Проверка метрик:
```
curl -H "Authorization: Bearer c213de..." http://10.8.0.2:3903/metrics
```
`/health` возвращает 200 при кворуме (≥2 нод), 503 иначе.
Порты 3903 открыты только для WG-подсети:
- vps01: `ufw allow from 10.8.0.0/24 to any port 3903`
- vps02/bigbox: файрвол не блокирует (policy ACCEPT)
## Запуск
```
cd /opt/monitoring
docker compose up -d
```
Пароль Grafana — переменная `GRAFANA_ADMIN_PASSWORD` в `.env` (по умолчанию `admin`).
## Полезные команды
```
# статус кластера
docker run --rm -v /opt/garage/garage.toml:/etc/garage.toml:ro \
-v /opt/garage/meta:/var/lib/garage/meta \
--entrypoint /garage dxflrs/garage:v2.1.0 status
# метрики одной ноды (Prometheus-формат)
curl -s -H "Authorization: Bearer <token>" http://10.8.0.2:3903/metrics | head
# health
curl -s -o /dev/null -w "%{http_code}" http://10.8.0.2:3903/health
```
## Алерты (prometheus/alerts.yml)
| Алерт | Условие | Severity |
|--------------------|--------------------------------|----------|
| GarageNodeDown | `up{job="garage"} == 0` (2м) | critical |
| GarageNoQuorum | `<2` нод up (2м) | critical |
| GarageResyncErrors | `garage_block_resync_error_count > 0` (10м) | warning |
| GarageNodeUnstable | RPC health down (5м) | warning |
## Катастрофоустойчивость
Проект хранится в **двух** git-репозиториях (см. ниже) — на случай поломки bigbox.
+34
View File
@@ -0,0 +1,34 @@
groups:
- name: garage
rules:
- alert: GarageNodeDown
expr: up{job="garage"} == 0
for: 2m
labels:
severity: critical
annotations:
summary: "Garage node {{ $labels.instance }} is down"
- alert: GarageNoQuorum
expr: (count(up{job="garage"} == 1) < 2)
for: 2m
labels:
severity: critical
annotations:
summary: "Garage cluster lost quorum (<2 nodes up)"
- alert: GarageResyncErrors
expr: garage_block_resync_error_count > 0
for: 10m
labels:
severity: warning
annotations:
summary: "Garage resync errors on {{ $labels.instance }}"
- alert: GarageNodeUnstable
expr: garage_rpc_node_health_is_up == 0
for: 5m
labels:
severity: warning
annotations:
summary: "Garage RPC health of some node is down"
+70
View File
@@ -0,0 +1,70 @@
# Monitoring stack: Prometheus + Grafana + Loki + Promtail
# Garage cluster (vps01, bigbox, vps02) metrics via WG admin API :3903
# Logs: docker logs of garage containers via promtail (docker driver)
services:
prometheus:
image: prom/prometheus:v2.53.0
container_name: prometheus
restart: unless-stopped
volumes:
- ./prometheus.yml:/etc/prometheus/prometheus.yml:ro
- ./prometheus-data:/prometheus
command:
- '--config.file=/etc/prometheus/prometheus.yml'
- '--storage.tsdb.retention.time=30d'
- '--storage.tsdb.path=/prometheus'
ports:
- "9090:9090"
networks:
- monitoring
grafana:
image: grafana/grafana:11.1.0
container_name: grafana
restart: unless-stopped
environment:
- GF_SECURITY_ADMIN_USER=admin
- GF_SECURITY_ADMIN_PASSWORD=${GRAFANA_ADMIN_PASSWORD:-admin}
- GF_USERS_ALLOW_SIGN_UP=false
- GF_SERVER_ROOT_URL=http://localhost:3001
volumes:
- ./grafana-data:/var/lib/grafana
- ./grafana/provisioning:/etc/grafana/provisioning:ro
- ./grafana/dashboards:/var/lib/grafana/dashboards:ro
ports:
- "3001:3000"
depends_on:
- prometheus
networks:
- monitoring
loki:
image: grafana/loki:3.1.0
container_name: loki
restart: unless-stopped
command: -config.file=/etc/loki/local-config.yaml
volumes:
- ./loki-config.yaml:/etc/loki/local-config.yaml:ro
- ./loki-data:/loki
ports:
- "3100:3100"
networks:
- monitoring
promtail:
image: grafana/promtail:3.1.0
container_name: promtail
restart: unless-stopped
volumes:
- ./promtail-config.yaml:/etc/promtail/config.yaml:ro
- /var/run/docker.sock:/var/run/docker.sock
command: -config.file=/etc/promtail/config.yaml
depends_on:
- loki
networks:
- monitoring
networks:
monitoring:
driver: bridge
+144
View File
@@ -0,0 +1,144 @@
{
"annotations": {
"list": []
},
"editable": true,
"fiscalYearStartMonth": 0,
"graphTooltip": 1,
"id": null,
"links": [],
"panels": [
{
"datasource": { "type": "prometheus", "uid": "Prometheus" },
"fieldConfig": {
"defaults": {
"color": { "mode": "thresholds" },
"mappings": [
{ "options": { "0": { "color": "red", "text": "DOWN" }, "1": { "color": "green", "text": "UP" } }, "type": "value" }
],
"thresholds": { "mode": "absolute", "steps": [ { "color": "red", "value": null }, { "color": "green", "value": 1 } ] }
},
"overrides": []
},
"gridPos": { "h": 8, "w": 6, "x": 0, "y": 0 },
"id": 1,
"options": {
"colorMode": "background",
"graphMode": "none",
"justifyMode": "auto",
"orientation": "auto",
"reduceOptions": { "calcs": [ "lastNotNull" ], "fields": "", "values": false },
"textMode": "auto"
},
"pluginVersion": "11.1.0",
"targets": [
{ "datasource": { "type": "prometheus", "uid": "Prometheus" }, "expr": "up{job=\"garage\"}", "legendFormat": "{{ instance }}", "refId": "A" }
],
"title": "Garage nodes up",
"type": "stat"
},
{
"datasource": { "type": "prometheus", "uid": "Prometheus" },
"fieldConfig": {
"defaults": {
"color": { "mode": "palette-classic" },
"custom": { "axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "", "axisPlacement": "auto", "drawStyle": "line", "fillOpacity": 10, "gradientMode": "none", "hideFrom": { "legend": false, "tooltip": false, "viz": false }, "lineInterpolation": "linear", "lineWidth": 1, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "never", "spanNulls": false, "stacking": { "group": "A", "mode": "none" }, "thresholdsStyle": { "mode": "off" } },
"mappings": [],
"thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null } ] }
},
"overrides": []
},
"gridPos": { "h": 8, "w": 6, "x": 6, "y": 0 },
"id": 2,
"options": {
"legend": { "calcs": [], "displayMode": "list", "placement": "bottom", "showLegend": true },
"tooltip": { "mode": "multi", "sort": "none" }
},
"targets": [
{ "datasource": { "type": "prometheus", "uid": "Prometheus" }, "expr": "node_filesystem_avail_bytes{fstype!~\"tmpfs|overlay|squashfs\"} / 1024 / 1024 / 1024", "legendFormat": "{{ instance }}", "refId": "A" }
],
"title": "Node disk free (GB)",
"type": "timeseries"
},
{
"datasource": { "type": "prometheus", "uid": "Prometheus" },
"fieldConfig": {
"defaults": {
"color": { "mode": "palette-classic" },
"custom": { "axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "", "axisPlacement": "auto", "drawStyle": "line", "fillOpacity": 10, "gradientMode": "none", "hideFrom": { "legend": false, "tooltip": false, "viz": false }, "lineInterpolation": "linear", "lineWidth": 1, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "never", "spanNulls": false, "stacking": { "group": "A", "mode": "none" }, "thresholdsStyle": { "mode": "off" } },
"mappings": [],
"thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null } ] }
},
"overrides": []
},
"gridPos": { "h": 8, "w": 12, "x": 12, "y": 0 },
"id": 3,
"options": {
"legend": { "calcs": [], "displayMode": "list", "placement": "bottom", "showLegend": true },
"tooltip": { "mode": "multi", "sort": "none" }
},
"targets": [
{ "datasource": { "type": "prometheus", "uid": "Prometheus" }, "expr": "garage_block_count", "legendFormat": "blocks {{ instance }}", "refId": "A" },
{ "datasource": { "type": "prometheus", "uid": "Prometheus" }, "expr": "garage_block_resync_queue_length", "legendFormat": "resync queue {{ instance }}", "refId": "B" }
],
"title": "Garage blocks",
"type": "timeseries"
},
{
"datasource": { "type": "prometheus", "uid": "Prometheus" },
"fieldConfig": {
"defaults": {
"color": { "mode": "thresholds" },
"mappings": [],
"thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null }, { "color": "red", "value": 1 } ] }
},
"overrides": []
},
"gridPos": { "h": 8, "w": 12, "x": 0, "y": 8 },
"id": 4,
"options": {
"legend": { "calcs": [], "displayMode": "list", "placement": "bottom", "showLegend": true },
"tooltip": { "mode": "multi", "sort": "none" }
},
"targets": [
{ "datasource": { "type": "prometheus", "uid": "Prometheus" }, "expr": "garage_rpc_node_health_is_up", "legendFormat": "{{ instance }}", "refId": "A" }
],
"title": "Garage RPC node health",
"type": "timeseries"
},
{
"datasource": { "type": "prometheus", "uid": "Prometheus" },
"fieldConfig": {
"defaults": {
"color": { "mode": "palette-classic" },
"custom": { "axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "", "axisPlacement": "auto", "drawStyle": "line", "fillOpacity": 10, "gradientMode": "none", "hideFrom": { "legend": false, "tooltip": false, "viz": false }, "lineInterpolation": "linear", "lineWidth": 1, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "never", "spanNulls": false, "stacking": { "group": "A", "mode": "none" }, "thresholdsStyle": { "mode": "off" } },
"mappings": [],
"thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null } ] }
},
"overrides": []
},
"gridPos": { "h": 8, "w": 12, "x": 12, "y": 8 },
"id": 5,
"options": {
"legend": { "calcs": [], "displayMode": "list", "placement": "bottom", "showLegend": true },
"tooltip": { "mode": "multi", "sort": "none" }
},
"targets": [
{ "datasource": { "type": "prometheus", "uid": "Prometheus" }, "expr": "rate(garage_api_s3_request_counter[5m])", "legendFormat": "{{ instance }} {{ api_endpoint }}", "refId": "A" }
],
"title": "S3 requests /sec",
"type": "timeseries"
}
],
"refresh": "30s",
"schemaVersion": 39,
"tags": [ "garage" ],
"templating": { "list": [] },
"time": { "from": "now-6h", "to": "now" },
"timepicker": {},
"timezone": "browser",
"title": "Garage Cluster",
"uid": "garage-cluster",
"version": 1,
"weekStart": ""
}
@@ -0,0 +1,11 @@
apiVersion: 1
providers:
- name: 'garage-dashboards'
orgId: 1
folder: 'Garage'
type: file
disableDeletion: false
updateIntervalSeconds: 30
options:
path: /var/lib/grafana/dashboards
@@ -0,0 +1,16 @@
apiVersion: 1
datasources:
- name: Prometheus
type: prometheus
access: proxy
url: http://prometheus:9090
isDefault: true
editable: true
- name: Loki
type: loki
access: proxy
url: http://loki:3100
isDefault: false
editable: true
+48
View File
@@ -0,0 +1,48 @@
global:
scrape_interval: 15s
evaluation_interval: 15s
external_labels:
monitor: 'garage-cluster'
rule_files:
- /etc/prometheus/alerts.yml
scrape_configs:
# Garage node metrics (admin API on WG)
- job_name: 'garage'
metrics_path: /metrics
scheme: http
authorization:
type: Bearer
credentials: c213debe864061cefe501f7791d557b6dba701710b41791b4a4a0fb036690d1d
static_configs:
- targets: ['10.8.0.1:3903', '10.8.0.2:3903', '10.8.0.4:3903']
labels:
cluster: garage
# Health check for each node (returns 200 if quorum OK)
- job_name: 'garage_health'
metrics_path: /health
scheme: http
static_configs:
- targets: ['10.8.0.1:3903', '10.8.0.2:3903', '10.8.0.4:3903']
labels:
cluster: garage
# Node exporter on each host (system metrics)
- job_name: 'node'
static_configs:
- targets: ['10.8.0.2:9100']
labels:
host: bigbox
- targets: ['10.8.0.1:9100']
labels:
host: vps01
- targets: ['10.8.0.4:9100']
labels:
host: vps02
# Prometheus self
- job_name: 'prometheus'
static_configs:
- targets: ['localhost:9090']