Files
monitoring/EXPERIENCE.md
T

455 lines
26 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Опыт эксплуатации: мониторинг Garage v2.1 через Prometheus
> Дата: 2026-08-30
> Ситуация: кластер Garage v2.1 (RF=3) на vps01 + bigbox + vps02, WireGuard 10.8.0.0/24.
> Задача: вывести статус кластера в браузер (Grafana + Prometheus + Loki).
## Опыт: Vinograd WAN (Ростелеком) — ICMP-мониторинг внешнего канала (2026-09-08)
> Ситуация: UptimeKuma алертил про 100% потерю пингов на шлюз 83.239.50.145
> (канал «Винный город», РТК). Задача — мониторить ОБА адреса канала (шлюз +
> наше оборудование) в нашем стеке с графиками RTT каждые 30с.
### 18. ICMP-пробы через blackbox-exporter — модуль `icmp`
blackbox-exporter поддерживает ICMP-пробы (prober: icmp). Метрики:
- `probe_success` — 1/0 (успех пробы)
- `probe_icmp_duration_seconds{phase="rtt"}` — RTT в секундах
- `probe_icmp_reply_hop_limit` — TTL ответа
Нюансы:
- В контейнере (host-network, root) ICMP работает без доп. настроек — проверил
`docker exec blackbox-exporter id` → root. В не-root окружении нужен
`setcap cap_net_raw+ep` или `net.ipv4.ping_group_range`.
- **Важно про YAML:** в `static_configs` таргеты — это список, `labels` относится
к списку целиком, а НЕ к каждому элементу отдельно. Ошибка синтаксиса ловится
`promtool check config`.
### 19. Scrape job с интервалом 30s и relabel instance
```yaml
- job_name: vinograd_wan
scrape_interval: 30s
metrics_path: /probe
params:
module: [icmp]
static_configs:
- targets: [83.239.50.145, 83.239.50.146]
relabel_configs:
# __address__ → instance: человекочитаемые имена для легенд Grafana
- source_labels: [__address__]
regex: '83\.239\.50\.145.*'
target_label: instance
replacement: vinograd-gw-83.239.50.145
...
# __address__ → реальный адрес blackbox (multi-target exporter pattern)
- target_label: __address__
replacement: 127.0.0.1:9115
```
- `scrape_interval: 30s` на уровне job — работает (проверено: точки каждые 30с).
- Regex с точками надо экранировать (`\.`), иначе 83.239.50.145 совпадёт с .146.
- relabel применяется по-порядку; сначала маппим instance, потом __address__ → blackbox.
- Проверка таргетов: `curl http://127.0.0.1:9090/api/v1/targets` → vinograd_wan 2 targets UP.
### 20. Алерт на probe_success
```yaml
- name: vinograd
rules:
- alert: VinogradRostelecomDown
expr: probe_success{job="vinograd_wan"} == 0
for: 2m
labels: {severity: critical}
```
- `for: 2m` при scrape 30s ≈ 4 пробы подряд. `promtool check config` → 7 rules found.
### 21. Grafana dashboard provisioning и ретеншн
- Дашборд кладём в `grafana/dashboards/vinograd-wan.json` — provisioner
(updateIntervalSeconds: 30) сам импортирует в фолдере Garage; рестарт не нужен.
Проверка: в grafana.db появился dashboard с uid=vinograd-wan.
- **Ретеншн «неделя»:** retention в Prometheus глобальный (--storage.tsdb.retention.time=30d
в этом стеке). Для 7 дней ровно нужен отдельный инстанс — здесь оставили 30d
(перекрывает неделю с запасом). Не пытаться задать retention per-job — его нет.
### 22. Наблюдение: шлюз РТК не пингуется, но оборудование пингуется
- 83.239.50.146 (наше оборудование) — probe_success=1, RTT ~13ms.
- 83.239.50.145 (шлюз) — probe_success=0 (не отвечает на ICMP). Совпадает с
алертом UptimeKuma. Это реальная авария, а не ошибка конфига: blackbox
корректно видит недоступность шлюза.
### 23. Read-only пользователь Grafana: provisioning НЕ работает (OSS), только UI (2026-09-08)
Задача: дать сотруднику IT Винограда read-only доступ к дашбордам
(`it@vinogorod.ru`, роль Viewer). Попытки автоматизировать — провалились:
- **Файловое provisioning пользователей** (`grafana/provisioning/access-control/users.yml`)
в Grafana 11 OSS **не обрабатывается**: в логах старта только
dashboards/datasources/alerting/plugins; access-control — фича
Enterprise/Cloud (`security.provisioning`). Файл молча игнорируется, даже с
валидным YAML.
- **API create**: `POST /api/users` → 404 (в OSS недоступно). `POST /api/login`
(JSON) → 401 даже при верном пароле; а **Basic auth работает**
(`curl -u estorozhenko:пароль /api/user` → 200).
- Итог: пользователя можно создать **только в UI** (Administration → Users →
New user, роль Viewer). Пароль задаётся при создании.
Проверка после создания:
```bash
curl -s -u 'it@vinogorod.ru:1qazXSW2' http://127.0.0.1:3001/api/user # → 200
curl -s -u 'estorozhenko:ПАРОЛЬ' http://127.0.0.1:3001/api/orgs/1/users # role=Viewer
curl -s -u 'it@vinogorod.ru:1qazXSW2' http://127.0.0.1:3001/api/users # → 403 (read-only)
```
### 24. Ошибка "Datasource __grafana__ was not found" при открытии дашборда (2026-09-08)
Симптом: на `https://grafana.nixg.ru/d/vinograd-wan/vinograd-wan` выскакивало
окно "Failed to retrieve datasource / Datasource __grafana__ was not found".
Панели при этом в порядке (Prometheus uid есть), а **секция `annotations`**
в JSON дашборда ссылалась на встроенный датасорс:
```json
"annotations": { "list": [ { "builtIn": 1,
"datasource": {"type": "grafana", "uid": "__grafana__"}, ... } ] }
```
- `__grafana__` — встроенный datasource Grafana (аннотации/алерты). В нашей
БД `data_source` его НЕТ (только Prometheus и Loki) → Grafana 11 OSS не
может его найти и показывает ошибку. Панели не используют аннотации —
секция добавляется в JSON автоматически при создании (шаблон),
но в файле она бесполезна.
- **Фикс:** очистить `annotations.list` в файле дашборда:
```bash
jq '.annotations.list = []' grafana/dashboards/vinograd-wan.json > /tmp/vw.json \
&& mv /tmp/vw.json grafana/dashboards/vinograd-wan.json
```
Провайдер дашбордов перечитывает файл **каждые 30с** (рестарт не нужен),
в БД появляется version 2 с `"list": []` — ошибка исчезает.
- Эталон: рабочий `garage-cluster.json` всегда имеет `"annotations": {"list": []}`.
- Проверка из БД: `docker cp grafana:/var/lib/grafana/grafana.db /tmp/gf.db` →
`SELECT data FROM dashboard WHERE uid='vinograd-wan'` → `__grafana__` отсутствует.
## Ключевые находки / грабли
### 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 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"
```
Аналогично пришлось открыть 9100 (node-exporter):
```
sudo ufw allow from 10.8.0.0/24 to any port 9100 proto tcp
```
На 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. Docker bridge-сеть НЕ видит WireGuard-интерфейс хоста
Самое важное при разворачивании стека мониторинга: контейнеры в дефолтной
bridge-сети (docker compose networks) **не имеют доступа к WG-подсети 10.8.0.0/24**
хоста. Prometheus не мог достать 10.8.0.x:3903, blackbox — тем более.
**Решение:** prometheus и blackbox-exporter запущены с `network_mode: host`.
Они видят и WG-интерфейс, и друг друга через 127.0.0.1. Grafana, Loki, Promtail
остались в bridge-сети (им WG не нужен).
### 8. `/health` Garage возвращает ТЕКСТ, а не метрику
`curl http://node:3903/health` → `Garage is fully operational` (текст, HTTP 200).
Нельзя использовать как metrics-таргет: Prometheus пытается распарсить текст
как float → `strconv.ParseFloat: parsing "is"`.
**Решение:** health-проверка через **blackbox-exporter** (module http_2xx,
probe_success). В prometheus.yml job `garage_health` с `params: module: [http_2xx]`,
relabel `__address__` → `127.0.0.1:9115`.
### 9. node-exporter — ставится apt'ом
Ubuntu (bigbox): `prometheus-node-exporter` 1.7.0; Debian 13 (vps01/vps02):
1.9.0. Работает сразу как systemd-сервис на :9100 (*:9100, все интерфейсы — WG
виден). На vps01 дополнительно открыть 9100 в ufw (см. п.5).
### 10. Права на volume-каталоги мониторинга
Контейнеры запускаются от не-root UID, а каталоги создавались от root:
- prometheus: UID **65534** (nobody) → `chown 65534:65534 prometheus-data`
- loki: UID **10001** → `chown 10001:10001 loki-data`
- grafana: UID **472** → `chown 472:472 grafana-data`
Иначе: prometheus падает с `Unable to create mmap-ed active query log`,
loki — `mkdir /loki/rules: permission denied`.
### 11. Caddy на vps02 — docker, host network, конфиг /opt/caddy/Caddyfile
Grafana опубликована как `grafana.nixg.ru` (DNS → 87.242.100.206 = vps02):
```
grafana.nixg.ru {
reverse_proxy 10.8.0.2:3001
}
```
- Caddy в docker (`network_mode: host`), конфиг смонтирован из /opt/caddy/Caddyfile.
- Перезагрузка: `docker exec caddy caddy reload --config /etc/caddy/Caddyfile`
- Сертификат Let's Encrypt выпускается автоматически (http-01), но первые ~30с
после reload соединение может падать — это нормально.
- В логах caddy много ошибок renew для старых доменов — они имеют
уже выпущенные сертификаты в caddy_data, работает всё.
## Опыт: дашборд Grafana пустой (NO DATA) — 3 причины подряд (2026-09-02)
> Дата: 2026-09-02. Ситуация: после этапа 6 (tproxy) пользователь сообщил,
> что дашборд Garage Cluster в Grafana полностью пустой (NO DATA).
### 12. NO DATA #1: у datasource не задан `uid` — Grafana генерит случайный
В `grafana/provisioning/datasources/datasources.yml` у Prometheus-датасорса
НЕ был задан `uid`. Grafana при провиженинге присваивает датасорсу **случайный
UID** (в БД видно для Loki: `P8E80F9AEF21F6940`), а все панели дашборда
ссылались на `"uid": "Prometheus"`. Панели искали датасорс с таким uid — не
находили → NO DATA.
**Решение:** в datasources.yml добавить датасорсу явный `uid: Prometheus`
(совпадение 1-в-1 с uid в панелях). Дашборды провиженятся каждые 30с,
а datasources — **только при старте** Grafana → после правки `docker compose
restart grafana`. Проверка из БД (`docker cp grafana:/var/lib/grafana/grafana.db`):
`SELECT name, uid, url FROM data_source` → uid стал `Prometheus`.
### 13. NO DATA #2: Prometheus на host-сети, а datasource URL `http://prometheus:9090`
Prometheus и blackbox запущены с `network_mode: host` (нужно для WG 10.8.0.0/24),
а Grafana — в bridge-сети. Внутри сети Grafana имя `prometheus` **не
резолвится** (проверено `docker exec grafana getent hosts prometheus` →
NO_RESOLVE), поэтому URL `http://prometheus:9090` недостижим → панели без
данных. Loki в той же bridge-сети — резолвится нормально.
**Решение:** datasource URL заменить на `http://172.28.0.1:9090` — IP хоста
со стороны bridge-сети Grafana (шлюз сети = адрес хоста). Определяется так:
```
docker inspect grafana --format '{{range .NetworkSettings.Networks}}{{.Gateway}} {{end}}'
# → 172.28.0.1
```
Проверено из контейнера: `wget http://172.28.0.1:9090/-/healthy` → OK.
ВАЖНО: шлюз bridge-сети docker стабилен (подсеть фиксированная),
но если пересоздать сеть — IP может смениться.
### 14. NO DATA #3 (частично): панели ссылались на НЕСУЩЕСТВУЮЩИЕ метрики
После починки datasource tproxy-панели ожили, а garage-панели (blocks,
RPC node health, S3 req/sec) остались пустыми. Причина: панели были написаны
под НЕСУЩЕСТВУЮЩИЕ имена метрик, которых в Prometheus нет:
- `garage_block_count` — в реальности `block_resync_queue_length` / `block_resync_errored_blocks`
- `garage_rpc_node_health_is_up` — в реальности `cluster_layout_node_connected`
- `garage_api_s3_request_counter` — в реальности `api_s3_request_counter`
Метрики Garage из admin API (10.8.0.x:3903) **не имеют префикса `garage_`**:
`api_s3_request_counter`, `api_s3_request_duration_*`, `block_resync_*`,
`cluster_*` (connected_nodes, healthy, partitions_all_ok, layout_node_connected),
`table_*`, `rpc_*`. Префикс `garage_` есть только у `garage_build_info`,
`garage_local_disk_avail/total`, `garage_replication_factor`.
Проверка реальных метрик: `curl 'http://127.0.0.1:9090/api/v1/label/__name__/values'`
и фильтр по `job=garage`. Панели переписаны на реальные метрики:
- Garage blocks (resync): `block_resync_queue_length` + `block_resync_errored_blocks`
- Garage node health (layout): `cluster_layout_node_connected` (легенда `{{ role_zone }}`)
- S3 requests /sec: `rate(api_s3_request_counter[5m])`
### 15. Алерты ссылались на те же несуществующие метрики
В alerts.yml алерты GarageResyncErrors и GarageNodeUnstable использовали те же
фантомные имена (`garage_block_resync_error_count`, `garage_rpc_node_health_is_up`),
поэтому никогда не сработали бы. Исправлено на реальные:
`block_resync_errored_blocks > 0` и `cluster_layout_node_connected == 0`.
После правки `promtool check config` → 6 rules found, все eval.
### 16. Relabel `instance` → hostname вместо IP (читаемые легенды)
По умолчанию instance = адрес скрейпа: у tproxy это `127.0.0.1:18081`
(локальный конец SSH-туннеля), у garage `10.8.0.x:3903`, у node `10.8.0.x:9100`.
В Grafana легенды показывали IP — некрасиво и непонятно. Не путать: это НЕ
«мониторинг локального интерфейса», просто лейбл instance = транспорт туннеля.
**Решение (на уровне Prometheus, не панелей)** — relabel_configs в пром.yml:
```yaml
relabel_configs:
- target_label: instance
replacement: vps03:8081 # для job tproxy
- source_labels: [__address__] # для garage/node — маппинг IP → hostname
regex: 10.8.0.2:3903
target_label: instance
replacement: bigbox:3903
```
Теперь легенды: `vps01:3903`, `bigbox:3903`, `vps02:3903` (garage),
`vps03:8081` (tproxy), `vps01:9100` и т.д. (node). Правило: правим источник
(prom.yml relabel), а не легенды в каждой панели — консистентно везде
(панели, explore, алерты).
### 17. Grafana provisioning: дашборды — каждые 30с, datasources — только при старте
Дашборды перечитываются автоматически (updateIntervalSeconds, по умолчанию 30с),
а datasources провиженятся только при старте Grafana. После правки
datasources.yml — обязателен `docker compose restart grafana`.
## Опыт: tproxy-server (WEB Proxy для Telegram Desktop) на vps03
> Дата: 2026-08-31
> Ситуация: развернули telegramdesktop/tproxy-server на vps03 (77.67.89.154,
> Debian 13): Caddy → tproxy-server:8080 → MTProxy:2398.
> Домен vps03.nixg.ru (HTTPS), порты 80/443 открыты из интернета.
### A. Архитектура и порты
- Внешний вход: `https://vps03.nixg.ru:443` → Caddy → `127.0.0.1:8080` (tproxy-server)
→ `127.0.0.1:2398` (MTProxy).
- tproxy-server config: `/etc/tproxy-server/config.json` (600 root:tproxy).
- Профили/секреты: `/etc/tproxy-server/profiles.json` — массив профилей,
**можно несколько секретов** (max_profiles: 32 в config). systemd читает их
через `LoadCredential=profiles.json:/etc/tproxy-server/profiles.json`
(файл должен быть 600 root, иначе падает — тест
TestLoadAcceptsSystemdCredentialReadPermissions).
- Формат секрета: 16 байт = 32 hex (`openssl rand -hex 16`), либо 17 байт
с префиксом `dd` (fake-TLS-режим).
- Добавить секрет: дописать профиль в profiles.json, `chmod 600`, `chown root:tproxy`,
`systemctl restart tproxy-server` → в логе `profiles=N`, readyz 200.
- Отозвать секрет: убрать профиль из profiles.json + restart.
### B. Admin-эндпоинты (только loopback 127.0.0.1:8081)
| Эндпоинт | Ответ |
|---|---|
| /healthz | всегда `ok` 200 (процесс жив) |
| /readyz | 200 `ready` / 503 `backend unavailable` — TCP-проба ко ВСЕМ backend-профилям (у нас 127.0.0.1:2398) |
| /metrics | Prometheus-формат (text/plain version 0.0.4), 13 счётчиков |
Метрики (все с префиксом `tproxy_`): sessions_live, streams_live,
backend_dials_in_flight, pending_bytes, pending_items, sessions_created_total,
sessions_closed_total, streams_opened_total, streams_rejected_total,
backend_dial_failures_total, bytes_up_total, bytes_down_total, limit_hits_total.
Ключевые для алертов: `backend_dial_failures_total` (рост = бэкенд недоступен),
`limit_hits_total` (DDOS/перегруз), `sessions_live` (активность).
### C. Подключение к Prometheus (стек /opt/monitoring, bigbox) — РЕШЕНО через SSH-туннель
- vps03 НЕ в WG (10.8.0.0/24 = vps01/bigbox/vps02), поэтому прямого доступа
к 8081 с bigbox нет. Prometheus в стеке — `network_mode: host` (видит внешние IP).
- Изначальный план (nft-правило на vps03: разрешить TCP 8081 с publIP bigbox
178.176.197.2) **не сработал**: tproxy-server слушает `admin_listen: 127.0.0.1:8081`,
запрос с bigbox к `77.67.89.154:8081` вернул `Connection refused` (соединение
упёрлось в отсутствующего наружу слушателя, а не в файрвол).
- **Решение:** SSH-туннель bigbox → vps03 (паттерн systemd, как telegram-tunnel):
```ini
# /etc/systemd/system/tproxy-tunnel.service (bigbox)
User=estorozhenko
ExecStart=/usr/bin/ssh -i /home/estorozhenko/.ssh/hostkeyVPS \
-L 127.0.0.1:18081:127.0.0.1:8081 -N \
-o ServerAliveInterval=30 -o ServerAliveCountMax=3 \
-o ExitOnForwardFailure=yes -o StrictHostKeyChecking=accept-new \
root@77.67.89.154
Restart=always
```
Prometheus скрейпит `127.0.0.1:18081`. tproxy-server config/файрвол НЕ трогаются,
метрики остаются на loopback.
- **ВНИМАНИЕ с портом:** сначала взял `8081` — но он на bigbox уже занят ICQ-веб-чатом
(Converse.html на 0.0.0.0:8081), туннель конфликтовал и отдавал HTML чата вместо
метрик. Порт на bigbox выбирать свободный (взял 18081).
- readyz ходит ко ВСЕМ профилям: если добавить профиль с мёртвым бэкендом —
readyz станет 503 (фича, учтена при алертах).
### D. Грабли установки (уже в INSTALL_NOTES.md проекта tproxy-web)
- MTProxy-бинарь падал 203/EXEC: `chmod 755 /opt/MTProxy/objs/bin` + chown
root:mtproxy + chmod 750. (см. /opt/hermes/tproxy-web/INSTALL_NOTES.md)
- `go test ./...` флакал на TestLoadAcceptsSystemdCredentialReadPermissions
при запуске от root — обходится запуском тестов от не-root или пропуском.
```
# все три ноды отдают метрики:
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
# Prometheus: все таргеты UP (10/10):
curl http://127.0.0.1:9090/api/v1/targets
# garage x3, garage_health x3, node x3, prometheus self
# Grafana наружу:
curl -sk https://grafana.nixg.ru/api/health # → 200
# логи garage в Loki:
curl -G "http://127.0.0.1:3100/loki/api/v1/query_range" \
--data-urlencode 'query={container="garage"}' --data-urlencode 'limit=3'
```
## Что осталось сделать / TODO
- [x] Развернуть стек (docker compose up -d) в /opt/monitoring
- [x] Node-exporter на всех 3 хостах (10.8.0.x:9100)
- [x] Дашборд Garage в Grafana (provisioning + JSON)
- [x] `up{job="garage"}` в Prometheus, Grafana :3001
- [x] Логи garage через promtail → Loki → Grafana
- [x] Опубликовать Grafana: grafana.nixg.ru через caddy на vps02
- [x] Mirror репозитория мониторинга в gitea (estorozhenko/monitoring)
- [ ] Сделать разные admin_token / metrics_token (сейчас совпадают)
- [ ] Поменять пароль Grafana с дефолтного (сейчас: пользователь estorozhenko,
пароль сменён пользователем вручную через UI)