Files
microdao-daarion/docs/NODA1-MEMORY-RUNBOOK.md
Apple ef3473db21 snapshot: NODE1 production state 2026-02-09
Complete snapshot of /opt/microdao-daarion/ from NODE1 (144.76.224.179).
This represents the actual running production code that has diverged
significantly from the previous main branch.

Key changes from old main:
- Gateway (http_api.py): expanded from ~40KB to 164KB with full agent support
- Router: new /v1/agents/{id}/infer endpoint with vision + DeepSeek routing
- Behavior Policy: SOWA v2.2 (3-level: FULL/ACK/SILENT)
- Agent Registry: config/agent_registry.yml as single source of truth
- 13 agents configured (was 3)
- Memory service integration
- CrewAI teams and roles

Excluded from snapshot: venv/, .env, data/, backups, .tgz archives

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-09 08:46:46 -08:00

413 lines
20 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.
# NODA1 — Runbook: Memory Stack (діагностика та відновлення)
**Контекст:** Проблеми зі стеком пам'яті на NODA1 трапляються регулярно. Цей runbook — для швидкої діагностики та безпечного перезапуску **без змін конфігурації**.
**Нода:** NODA1 — `node1-daarion` (144.76.224.179)
**Сервіси:** `dagi-memory-service-node1`, `dagi-qdrant-node1`, `postgres-backup-node1`, `render-pdf-worker-node1`, `dagi-nats-node1`, Caddy
---
## 1) Швидка фіксація стану (read-only, ~1 хв)
```bash
# Загальний стан контейнерів (імена/статуси/порти)
docker ps -a --format 'table {{.Names}}\t{{.Status}}\t{{.Ports}}' | egrep 'dagi-memory-service-node1|dagi-qdrant-node1|postgres-backup-node1|render-pdf-worker-node1|dagi-nats-node1| nats '
# Health-статус детально (memory + postgres-backup)
docker inspect dagi-memory-service-node1 --format '{{json .State.Health}}' | jq .
docker inspect postgres-backup-node1 --format '{{json .State.Health}}' | jq . 2>/dev/null || true
# Останні 100/200 рядків логів (таймаути, Qdrant)
docker logs --tail 100 dagi-memory-service-node1
docker logs --tail 200 dagi-qdrant-node1
docker logs --tail 200 postgres-backup-node1
```
---
## 2) Діагностика мережі/доступності memory-service ↔ Qdrant (read-only)
### 2.1 Qdrant "живий" з хоста
```bash
curl -s -o /dev/null -w "Qdrant /healthz HTTP=%{http_code}\n" --connect-timeout 3 http://127.0.0.1:6333/healthz
curl -s -o /dev/null -w "Qdrant /collections HTTP=%{http_code}\n" --connect-timeout 3 http://127.0.0.1:6333/collections
```
### 2.2 Qdrant доступний **з контейнера** memory-service (критично для міжконтейнерної мережі)
```bash
# DNS + TCP + HTTP всередині контейнера
docker exec dagi-memory-service-node1 sh -lc '
echo "== DNS ==";
getent hosts dagi-qdrant-node1 || true;
echo "== TCP 6333 ==";
(nc -vz -w 2 dagi-qdrant-node1 6333 && echo OK) || echo FAIL;
echo "== HTTP /healthz ==";
curl -s -o /dev/null -w "HTTP=%{http_code}\n" --connect-timeout 5 http://dagi-qdrant-node1:6333/healthz || echo FAIL
' 2>/dev/null || echo "docker exec failed"
```
### 2.3 Перевірка "де саме" таймаут: `/collections` і латентність
```bash
# /collections інколи повільний при навантаженні/IO
docker exec dagi-memory-service-node1 sh -lc '
echo "== /collections latency ==";
time curl -s -o /dev/null --connect-timeout 5 http://dagi-qdrant-node1:6333/collections || true
' 2>/dev/null
```
> Якщо `healthz` швидкий, а `/collections` "висить" або дає таймаути — проблема частіше **навантаження/IO Qdrant**, а не DNS.
---
## 0) Довести причину: bind vs iptables (read-only)
### 0.1 Bind-порти всередині Qdrant
```bash
docker exec dagi-qdrant-node1 sh -c '
ss -ltnp | egrep ":(6333|6334)\s" || netstat -ltnp | egrep ":(6333|6334)\s" || cat /proc/net/tcp | head -5
'
```
- **`127.0.0.1:6333`** → причина (A): bind на localhost; фікс: `QDRANT__SERVICE__HOST=0.0.0.0`.
- **`0.0.0.0:6333`** (або в `/proc/net/tcp`: `00000000:18BD`) → причина (B): мережеве блокування; див. 0.3.
### 0.2 Одна мережа для Qdrant і memory
```bash
docker network inspect dagi-network --format '{{json .Containers}}' | jq 'keys'
docker inspect dagi-qdrant-node1 --format '{{json .NetworkSettings.Networks}}' | jq .
docker inspect dagi-memory-service-node1 --format '{{json .NetworkSettings.Networks}}' | jq .
```
### 0.3 iptables DOCKER-USER (часта причина таймаутів між контейнерами)
```bash
iptables -S DOCKER-USER
iptables -L DOCKER-USER -n -v
```
**Перевірено 2025-01-28:** У DOCKER-USER є правило `! -s 127.0.0.1/32 -p tcp --dport 6333 -j DROP` (і аналогічно для 8000, 9500, …). Трафік з контейнерів (172.18.x.x) на порт 6333/8000 **DROP**-иться; з хоста (127.0.0.1) — працює. Qdrant при цьому слухає на **0.0.0.0:6333** (`/proc/net/tcp`: `00000000:18BD`). Висновок: причина **(B) iptables**, не bind.
**Фікс (дозволити контейнер→контейнер для 6333 і 8000):** вставити ACCEPT **перед** DROP (наприклад у `rc.local` або окремому скрипті, щоб правило пережило перезавантаження):
```bash
# Дозволити трафік з мережі контейнерів (dagi-network 172.18.0.0/16) на Qdrant і memory-service
iptables -I DOCKER-USER 1 -s 172.18.0.0/16 -p tcp -m tcp --dport 6333 -j ACCEPT
iptables -I DOCKER-USER 1 -s 172.18.0.0/16 -p tcp -m tcp --dport 8000 -j ACCEPT
```
Після цього: `docker restart dagi-memory-service-node1`, дочекатися 3060 с, перевірити health.
---
## 0.4 Збереження правил DOCKER-USER після reboot
### A) iptables-persistent (рекомендовано, Ubuntu/Debian)
**A0) Зафіксувати поточні правила:**
```bash
iptables -S DOCKER-USER
iptables -L DOCKER-USER -n -v --line-numbers
```
**A1) Встановити пакети:**
```bash
apt-get update
DEBIAN_FRONTEND=noninteractive apt-get install -y iptables-persistent netfilter-persistent
```
**A2) Зберегти правила:**
```bash
iptables-save > /etc/iptables/rules.v4
# (IPv6 за потреби) ip6tables-save > /etc/iptables/rules.v6
```
**A3) Увімкнути відновлення на старті:**
```bash
systemctl enable netfilter-persistent
systemctl restart netfilter-persistent
systemctl status netfilter-persistent --no-pager -l
```
**A3.1) Готовність без reboot (read-only)** — переконатися, що netfilter-persistent підхопив `rules.v4` і що Docker не перетре ланцюги:
```bash
# 1) netfilter-persistent читає rules.v4
sudo netfilter-persistent status
sudo systemctl status netfilter-persistent --no-pager -l
# 2) Завантажені правила і звірка з файлом
sudo iptables-save | grep -n "^\*filter"
sudo iptables-save | egrep "DOCKER-USER|172\.18\.0\.0/16|dport (6333|8000)" -n
# 3) У файлі правила є
sudo grep -n "DOCKER-USER" /etc/iptables/rules.v4
sudo egrep -n "172\.18\.0\.0/16.*dport (6333|8000).*ACCEPT" /etc/iptables/rules.v4 || echo "MISSING_IN_FILE"
# 4) ACCEPT стоять ПЕРЕД DROP
sudo iptables -L DOCKER-USER -n --line-numbers | head -n 25
```
Очікувано: `iptables -L DOCKER-USER --line-numbers` показує ACCEPT для 172.18.0.0/16 на 6333 і 8000 **вище** за DROP.
**A4) Після reboot — валідація:**
```bash
iptables -S DOCKER-USER | egrep '172\.18\.0\.0/16.*dport (6333|8000).*ACCEPT' || echo "MISSING"
docker ps --format 'table {{.Names}}\t{{.Status}}' | egrep 'dagi-qdrant-node1|dagi-memory-service-node1'
curl -s -o /dev/null -w "memory /health HTTP=%{http_code}\n" http://127.0.0.1:8000/health
```
**A4.1) Після reboot — ~30 секунд перевірок:**
```bash
sudo iptables -S DOCKER-USER | egrep '172\.18\.0\.0/16.*dport (6333|8000).*ACCEPT' || echo "MISSING"
docker ps --format 'table {{.Names}}\t{{.Status}}' | egrep 'dagi-qdrant-node1|dagi-memory-service-node1'
curl -s -o /dev/null -w "memory /health HTTP=%{http_code}\n" http://127.0.0.1:8000/health
```
### B) Альтернатива без пакетів: systemd unit на boot
**B1) Створити unit** `/etc/systemd/system/dagi-iptables-allow-dagi-network.service`:
```ini
[Unit]
Description=Allow dagi-network container traffic to Qdrant and Memory ports in DOCKER-USER
After=network-online.target docker.service
Wants=network-online.target
[Service]
Type=oneshot
ExecStart=/bin/sh -c '\
iptables -C DOCKER-USER -s 172.18.0.0/16 -p tcp --dport 6333 -j ACCEPT 2>/dev/null || \
iptables -I DOCKER-USER 1 -s 172.18.0.0/16 -p tcp --dport 6333 -j ACCEPT; \
iptables -C DOCKER-USER -s 172.18.0.0/16 -p tcp --dport 8000 -j ACCEPT 2>/dev/null || \
iptables -I DOCKER-USER 1 -s 172.18.0.0/16 -p tcp --dport 8000 -j ACCEPT; \
iptables -S DOCKER-USER'
RemainAfterExit=yes
[Install]
WantedBy=multi-user.target
```
**B2) Увімкнути і запустити:**
```bash
systemctl daemon-reload
systemctl enable --now dagi-iptables-allow-dagi-network.service
systemctl status dagi-iptables-allow-dagi-network.service --no-pager -l
```
**Страхувальний варіант (без конфлікту з A):** залишити A як є і **додати B-unit лише як fallback**. Unit ідемпотентний (`iptables -C` перед вставкою). Корисно, якщо на хості є UFW/інші інструменти, що переупорядковують правила після boot.
### Якщо після reboot правила зникнуть
**1) iptables-nft vs iptables-legacy або netfilter-persistent тягне не той бекенд:**
```bash
iptables -V
update-alternatives --display iptables 2>/dev/null || true
sudo journalctl -u netfilter-persistent -b --no-pager | tail -200
```
**2) Docker/ufw змінюють форвардинг або ланцюги після старту:**
```bash
sudo journalctl -u docker -b --no-pager | tail -200
sudo iptables -S DOCKER-USER
```
Якщо змішування legacy/nft — стандартизувати backend (окремий крок).
---
## 0.5 Перший крок при таймаутах між контейнерами
При будь-яких таймаутах memory↔Qdrant або інших контейнер↔контейнер **спочатку** перевірити DOCKER-USER:
```bash
iptables -S DOCKER-USER
iptables -L DOCKER-USER -n -v --line-numbers
```
Якщо є `! -s 127.0.0.1/32 ... --dport 6333 -j DROP` без попереднього ACCEPT для 172.18.0.0/16 — застосувати фікс з 0.3 і зберегти правила (0.4).
---
## 3) Діагностика самого memory-service (read-only)
```bash
# Health endpoint з хоста
curl -s -i --max-time 5 http://127.0.0.1:8000/health | head
# Ресурси контейнера (CPU throttling / RAM pressure)
docker stats --no-stream dagi-memory-service-node1 dagi-qdrant-node1 2>/dev/null
# Env (тільки читання) — чи не з'їхав QDRANT_HOST/PORT
docker exec dagi-memory-service-node1 sh -lc 'env | egrep "QDRANT|MEM|VECTOR|COLLECTION|TIMEOUT" | sort' 2>/dev/null || true
```
---
## 4) postgres-backup-node1 unhealthy — мінімальна діагностика (read-only)
1. Що саме перевіряє healthcheck і останній output:
```bash
docker inspect postgres-backup-node1 --format '{{json .Config.Healthcheck}}' | jq .
docker inspect postgres-backup-node1 --format '{{range .State.Health.Log}}{{println .End " exit=" .ExitCode " " .Output}}{{end}}'
```
2. Логи контейнера:
```bash
docker logs --tail 200 postgres-backup-node1
```
> Типові причини "Up, але unhealthy": healthcheck звертається до Postgres по hostname, який не резолвиться/не доступний; немає прав/пароля; або backup-процес завис і healthcheck очікує lock.
**Перевірено 2025-01-28:** Логи показують **pg_dump version mismatch**: Postgres сервер 16.11, pg_dump у контейнері 15.14. Healthcheck повертає 503, бо внутрішній HTTP-сервіс бекапу відповідає 503 при невдалому бекапі.
**Наступний фікс (зміна образу):** оновити образ postgres-backup так, щоб **pg_dump був 16.x** (базуватись на `postgres:16` або встановити `postgresql-client-16` у backup image). Після оновлення образу — перезапуск контейнера backup.
---
## 4.1 render-pdf-worker: NATS timeout
- Перевірити env: `docker inspect render-pdf-worker-node1 --format '{{json .Config.Env}}' | jq -r '.[]' | egrep -i NATS'`.
- **Перевірено 2025-01-28:** `NATS_URL=nats://nats:4222` — hostname **nats**, тоді як контейнер у мережі називається **dagi-nats-node1**. Якщо аліасу `nats` немає, воркер не з’єднається з NATS. **Наступний фікс (один із варіантів):** (1) додати **network alias** `nats` для контейнера `dagi-nats-node1` у docker-compose; (2) змінити `NATS_URL` на `nats://dagi-nats-node1:4222` у воркері; (3) аліас через compose `networks: … aliases: [nats]`.
---
## 4.2 Caddy failed: конфлікт порту
- **Перевірено 2025-01-28:** `journalctl -u caddy`: **"listen tcp :443: bind: address already in use"**. Порти 80/443 зайняті **nginx**. Caddy не стартує, поки nginx тримає ці порти.
**Наступний фікс (архітектура):** це не поломка, а конфлікт. Варіанти: (1) залишити **nginx** єдиним reverse proxy і вимкнути Caddy; (2) перейти на **Caddy** (вимкнути nginx); (3) Caddy слухає інші порти і nginx проксує до нього (рідко має сенс).
---
## 5) Точковий перезапуск БЕЗ зміни конфігурації
### 5.1 Перезапуск лише memory-service
```bash
docker restart dagi-memory-service-node1
# Дати прогрітись і перевірити health
sleep 30
docker inspect dagi-memory-service-node1 --format '{{.State.Health.Status}}'
curl -s -o /dev/null -w "memory /health HTTP=%{http_code}\n" --max-time 5 http://127.0.0.1:8000/health
docker logs --tail 80 dagi-memory-service-node1
```
### 5.2 Якщо знову таймаут до Qdrant — перезапуск Qdrant → потім memory-service
```bash
docker restart dagi-qdrant-node1
sleep 20
docker restart dagi-memory-service-node1
sleep 40
docker inspect dagi-memory-service-node1 --format '{{.State.Health.Status}}'
```
### 5.3 render-pdf-worker-node1 (Exited 1) — запустити і глянути причину падіння
```bash
docker start render-pdf-worker-node1
sleep 2
docker ps -a --filter name=render-pdf-worker-node1 --format 'table {{.Names}}\t{{.Status}}'
docker logs --tail 200 render-pdf-worker-node1
```
### 5.4 postgres-backup (unhealthy) — лише restart за потреби
```bash
docker restart postgres-backup-node1
docker logs --tail 200 postgres-backup-node1
```
---
## 6) Caddy failed (read-only + точковий restart за потреби)
```bash
systemctl status caddy --no-pager -l
journalctl -u caddy -n 200 --no-pager
```
Якщо треба просто підняти сервіс без змін конфігурації:
```bash
systemctl restart caddy
systemctl status caddy --no-pager -l
```
---
## 7) Що саме зібрати після "4-го падіння" (щоб знайти повторювану причину)
1. **Час** останнього падіння memory-service (з `docker logs --since ...`).
2. `docker inspect ...Health.Log` для memory-service та postgres-backup.
3. Латентність **Qdrant /collections** та чи проходить **nc** з контейнера memory-service на `dagi-qdrant-node1:6333`.
4. `docker stats --no-stream` для memory + qdrant у момент проблеми.
Якщо надаси ці 4 блоки в одному повідомленні — можна точніше локалізувати: **мережа між контейнерами**, **Qdrant під навантаженням/IO**, або **healthcheck memory-service надто "агресивний"** відносно реального часу відповіді Qdrant.
---
## 8) Однією командою (копіпаста на NODA1)
**Швидка діагностика (read-only):**
```bash
echo "=== Qdrant from host ===" && curl -s -o /dev/null -w "%{http_code}\n" --connect-timeout 3 http://127.0.0.1:6333/healthz
echo "=== Qdrant from memory container ===" && docker exec dagi-memory-service-node1 sh -c 'curl -s -o /dev/null -w "%{http_code}\n" --connect-timeout 5 http://dagi-qdrant-node1:6333/healthz' 2>/dev/null || echo "fail"
echo "=== Memory /health ===" && curl -s -o /dev/null -w "%{http_code}\n" --max-time 5 http://127.0.0.1:8000/health
docker ps -a --format 'table {{.Names}}\t{{.Status}}' | egrep 'dagi-memory-service-node1|dagi-qdrant-node1|postgres-backup-node1|render-pdf-worker-node1'
```
**Тільки перезапуск memory + render-pdf-worker (без Qdrant):**
```bash
docker restart dagi-memory-service-node1
docker start render-pdf-worker-node1
echo "Waiting 45s for memory health..."
sleep 45
docker ps --filter name=dagi-memory-service-node1 --filter name=render-pdf-worker-node1 --format "table {{.Names}}\t{{.Status}}"
```
---
## Рекомендація для вашого кейсу (щоб не ловити це 5-й раз)
1. **Закріпити правила DOCKER-USER** одним із способів **0.4 A або B** (A краще — iptables-persistent).
2. **Додати в runbook** перевірку DOCKER-USER як перший крок при будь-яких таймаутах між контейнерами (див. 0.5).
3. **Окремо запланувати 3 техборги:** (1) backup image з pg16; (2) NATS alias/URL для render-pdf-worker; (3) reverse-proxy single-owner (nginx або Caddy).
---
## Статус по інших знайдених проблемах (куди рухатись далі)
- **postgres-backup:** pg_dump version mismatch (сервер 16.x, клієнт 15.x) → потрібен образ з pg16.
- **render-pdf-worker:** NATS hostname `nats` vs реальний контейнер `dagi-nats-node1` → alias або зміна NATS_URL.
- **Caddy:** конфлікт портів з nginx (80/443) → один reverse proxy (nginx-only або міграція на Caddy).
| Проблема | Що потрібно |
|----------|-------------|
| **postgres-backup** | Образ з `pg_dump 16.x` (зміна образу неминуча). |
| **render-pdf-worker** | Або network alias `nats` для `dagi-nats-node1`, або зміна `NATS_URL` на `nats://dagi-nats-node1:4222` (мінімальна правка compose). |
| **Caddy** | Конфлікт портів з nginx — вибір одного reverse proxy. |
**Готові патчі:** див. **node2/docs/NODA1-TECHBORGS-PATCHES.md** — зібрані YAML-блоки з NODA1, точні diff, порядок застосування та acceptance checks для патчів #1 (postgres-backup pg16), #2 (render-pdf-worker NATS alias), #3 (Caddy vs nginx).
---
**Скрипт діагностики (на NODA1):** `node2/scripts/noda1-memory-diagnose.sh`
Запуск на сервері: скопіювати скрипт на NODA1 і виконати `bash noda1-memory-diagnose.sh`.
Або з локальної машини: `ssh root@144.76.224.179 'bash -s' < node2/scripts/noda1-memory-diagnose.sh`
**Останнє оновлення:** 2025-01-28
**Історія:** 4-й випадок; додано постійне збереження DOCKER-USER (A/B), перший крок при таймаутах (0.5), наступні фікси (backup pg16, NATS alias, Caddy vs nginx), рекомендація.