1148 lines
88 KiB
Markdown
1148 lines
88 KiB
Markdown
# Журнал сессии — 2026-08-14 (продолжение миграции shared-sqs)
|
||
|
||
**Рабочая папка:** /home/naeel/nubes/SQS-service
|
||
|
||
---
|
||
|
||
## Текущее состояние
|
||
|
||
- Сервис ЗАДЕПЛОЕН и работает: `https://sqs.containerk8s.dev.nubes.ru`
|
||
- `/health` → `OK` (200)
|
||
- Образ: Docker Hub `naeel/shared-sqs:latest`
|
||
- Репа: Gitea `gitea.services.ngcloud.ru/Nail/SQS-service`
|
||
|
||
---
|
||
|
||
## Что сделано сегодня
|
||
|
||
1. Добавлен инфо-блок «Информация о сервисе» на страницу входа `/ui` (`app/ui/index.html`).
|
||
Коммит `032ca66`. Образ пересобран, запушен (`digest 4bb4f20d`).
|
||
|
||
2. **Проблема:** вход в UI с реальным JWT → `Ошибка: token rejected by cloud API (HTTP 403)`.
|
||
Причина: контейнер валидировал JWT на `deck-api-test.ngcloud.ru/api/v1`
|
||
(default в `app/admin/admin.go`), а production JWT туда не подходит.
|
||
|
||
3. **Поиск правильного endpoint** (в `~/tf_provider`, по указанию пользователя):
|
||
- `deck-api*.ngcloud.ru` — **ЛЕГАСИ** (пользователь явно запретил этот формат)
|
||
- Правильный: **API Gateway** `lk-api-gateway.ngcloud.ru`
|
||
|
||
4. Найдено в `~/tf_provider/TOOLS/config/prod/profile.env`:
|
||
```
|
||
NUBES_API_ENDPOINT="https://lk-api-gateway.ngcloud.ru/api/v1/svc"
|
||
```
|
||
(test: `...-test...`, dev: `...-dev...`)
|
||
Также default в `TOOLS/yaml-generator/internal/config/config.go`:
|
||
`getenvDefault("NUBES_API_ENDPOINT", "https://lk-api-gateway.ngcloud.ru/api/v1/svc")`
|
||
|
||
5. **Тест токенов против gateway** (токены из `/home/naeel/nubes/tokens_all.txt`):
|
||
- БЕЗ User-Agent → **все 403** (DDoS-Guard блокирует)
|
||
- С `User-Agent: Mozilla/5.0`:
|
||
| Токен | gateway-PROD | gateway-TEST |
|
||
|---|---|---|
|
||
| tazetdinovn@gmail.com (TEST) | 401 | **200** |
|
||
| tazet@narod.ru (PROD) | **200** | 401 |
|
||
| ntazetdinov@nubes.ru (PROD) | **200** | 401 |
|
||
|
||
---
|
||
|
||
## Выводы (факты)
|
||
|
||
1. Правильный `NUBES_ENDPOINT` = **`https://lk-api-gateway.ngcloud.ru/api/v1/svc`**
|
||
2. **КРИТИЧНО:** наш код `PingNubesAPI` (`app/auth/jwt.go`) **НЕ шлёт User-Agent** →
|
||
gateway возвращает 403 → токен всегда отклоняется.
|
||
Нужна правка кода: добавить `User-Agent: Mozilla/5.0`.
|
||
|
||
---
|
||
|
||
## Предстоящие правки (СОГЛАСОВАНЫ? — нет, ждут «делай»)
|
||
|
||
1. `Dockerfile`: `ENV NUBES_ENDPOINT=https://lk-api-gateway.ngcloud.ru/api/v1/svc`
|
||
2. `app/auth/jwt.go`: в `PingNubesAPI` добавить `User-Agent: Mozilla/5.0`
|
||
3. `app/admin/admin.go`: сменить default endpoint на gateway PROD
|
||
|
||
После правок: пересборка → push Docker Hub → redeploy → проверка входа в UI.
|
||
|
||
---
|
||
|
||
## Правила, зафиксированные в этой сессии
|
||
|
||
- Код менять ТОЛЬКО после согласования с пользователем.
|
||
- deck-api*.ngcloud.ru — ЛЕГАСИ, НЕ использовать (только lk-api-gateway).
|
||
- ngcloud за DDoS-Guard: curl-запросы всегда с `User-Agent: Mozilla/5.0`.
|
||
- Всё документировать в HISTORY.
|
||
|
||
---
|
||
|
||
## v0.1.26 — вывод версии на экран + фикс схемы тегов (14.08.2026, утро)
|
||
|
||
**Проблема**: пользователь заметил, что версии не повышаются — всё время пушился только `latest`,
|
||
под мог кэшировать `latest` и redeploy подтягивал старый образ. Плюс запрос: выводить версию на экран.
|
||
|
||
**Что сделано**:
|
||
1. `app/models/constants.go`: `var Version = "dev"` (переопределяется через ldflags).
|
||
2. `app/router/router.go`: публичный `/health` теперь отдаёт JSON `{"status":"ok","version":"v0.1.26"}`.
|
||
3. `app/admin/admin.go`: `adminHealthDetail` дополнен полем `version` (в /admin/health и /ui/api/health).
|
||
4. `app/ui/index.html`:
|
||
- блок «Информация о сервисе» на странице входа: строка «Версия» (fetch публичного /health);
|
||
- образ в инфо-блоке теперь `naeel/shared-sqs:v0.1.26`;
|
||
- дашборд: карточка «Версия» (health.version).
|
||
5. `Makefile`: `VERSION=v0.1.26`, `LDFLAGS=-X shared-sqs/app/models.Version=$(VERSION)`,
|
||
`docker-build` тегает `$(VERSION)` + `latest`; исправлена цель `build` (`./app/cmd/` вместо одного файла goaws.go).
|
||
6. `Dockerfile`: `ARG VERSION=dev`, ldflags при сборке.
|
||
|
||
**Проверки**: make build OK; `/health` → `{"status":"ok","version":"v0.1.26"}`; go test router/admin/models — ok.
|
||
|
||
**Git**: коммит `3a105de` «v0.1.26: …», пуш в Gitea (`166baa9..3a105de`).
|
||
|
||
**Образ**: `naeel/shared-sqs:v0.1.26` собран и запушен на Docker Hub,
|
||
digest `sha256:b4d1dccda75a67e1a5db3887576e1112993d6485504e683d0fa82a2374bca99b`,
|
||
`latest` указывает на него же.
|
||
|
||
**Следующий шаг**: в deck указать путь образа `naeel/shared-sqs:v0.1.26` и сделать redeploy —
|
||
тогда под гарантированно стянет новый образ (код с перебором стендов NUBES_ENDPOINT + версия на экране).
|
||
|
||
---
|
||
|
||
## РАЗВЯЗКА: образ обновлён через kubectl, вход в UI работает (14.08.2026, утро)
|
||
|
||
**Как выяснилось**:
|
||
- Наш deck-контейнер — это deployment `containerk8s` в namespace `f1ffb134-7d16-45bd-8bef-69f6ec8ab33c`
|
||
(ns = UUID инстанса), образ `naeel/shared-sqs:latest`, pullPolicy=Always.
|
||
- Старый апрельский Helm-релиз `shared-sqs` (ns `shared-sqs`, v0.1.24, 122 дня) — ЛЕГАСИ,
|
||
не имел отношения к живому сервису. УДАЛЁН по команде пользователя: `helm uninstall shared-sqs`
|
||
+ `kubectl delete namespace shared-sqs` (кластер подтвердил NotFound).
|
||
- Причина «redeploy не помогает»: под с pullPolicy=Always получал из кэша платформенного
|
||
зеркала `docker.io` (в kube-system всё на mirror.k8s.ngcloud.ru) старый digest `be2108fd…`,
|
||
хотя в Docker Hub `latest` = `b4d1dccd…` (v0.1.26). Кэш зеркала по тегу `latest` не протухал
|
||
из-за частых пушей.
|
||
|
||
**Что сделано**:
|
||
1. `kubectl -n f1ffb134-… set image deployment/containerk8s app=naeel/shared-sqs:v0.1.26`
|
||
(внимание: контейнер в поде называется `app`, а не `containerk8s`).
|
||
Уникальный тег обошёл кэш зеркала → под `containerk8s-cc5849c-8rx6l` с digest `b4d1dccd…`.
|
||
2. Проверка: `GET /health` → `{"status":"ok","version":"v0.1.26"}`.
|
||
3. Вход в UI с JWT `tazetdinovn@gmail.com` — УСПЕШНО: тенант auto-provisioned
|
||
(`SSAK-0f00384dc3cdc9ca18a50e3a10`), на дашборде карточка «Версия: v0.1.26».
|
||
|
||
**Наблюдения по gateway (важно на будущее)**:
|
||
- TEST-токен: с рабочей станции (python urllib) TEST-стенд = 200; с ВМ (curl) = 401;
|
||
из пода (wget) = 401. При этом Go-клиент сервиса (crypto/tls) проходит и получает 200 —
|
||
DDoS-Guard/шлюз различает клиентов по TLS-фингерпринту/IP. Тесты curl/wget из пода
|
||
НЕ показательны для Go-клиента сервиса.
|
||
- Перебор стендов PROD→TEST→DEV в `PingNubesAPI` работает как задумано.
|
||
|
||
**Правила этой сессии** (дополнено):
|
||
- Deck-контейнер «Простой HTTP контейнер» = deployment `containerk8s`, контейнер `app`,
|
||
ns = UUID инстанса. Правки образа через kubectl НЕ откатываются (ownerReferences нет).
|
||
- `latest` в этом кластере кэшируется зеркалом → обновлять только уникальным тегом
|
||
через `kubectl set image`.
|
||
|
||
---
|
||
|
||
## v0.1.27 — страница-описание сервиса на корне `/` (14.08.2026)
|
||
|
||
**Задача**: на `https://sqs.containerk8s.dev.nubes.ru/` разместить описание сервиса и список команд
|
||
в дизайне Nubes (`~/nubes/design/nubes-design-system.md`).
|
||
|
||
**Что сделано**:
|
||
1. `app/ui/info.html` — публичная страница: топбар с лого, hero с бейджем «тестирование»,
|
||
карточка «Подключение» (endpoint, SigV4, регион us-east-1, /ui, /health, /metrics, Redis),
|
||
таблица всех 17 Actions, примеры AWS CLI. Версия подставляется через `{{VERSION}}`.
|
||
2. `app/ui/static/{logo, favicon}.svg` — SVG-файлы из design-системы (лого ТОЛЬКО файлом, не inline).
|
||
3. `app/ui/embed.go`: `InfoHandler(version)`, `StaticHandler()`; embed расширен (`info.html static`).
|
||
4. `app/router/router.go`: `GET /` без Action → info-страница; POST/GET с Action → SQS API как раньше.
|
||
Маршрут `/static` — публично. SQS API не задет (проверено: POST Action=ListQueues → 403 без сигнатуры, как было).
|
||
|
||
**Инцидент rsync (важно!)**: rsync с ДВУМЯ источниками (`app/` + `Makefile`) в dest `~/terra/SQS-service/`
|
||
высыпал содержимое `app/` в КОРЕНЬ репо на ВМ (`~/terra/SQS-service/ui/`, `router/` и т.д.), а настоящий
|
||
`app/` остался старым → первый образ v0.1.27 (digest e69807fe) собрался из СТАРОГО кода (v0.1.26).
|
||
Исправлено: повторный rsync с ОДНИМ источником `app/` → `~/terra/SQS-service/app/`, пересборка.
|
||
|
||
**Итог**: правильный образ v0.1.27, digest `sha256:51c94894…`. В кластер поставлен ПО DIGEST
|
||
(`set image app=naeel/shared-sqs@sha256:51c94894…`) — digest обходит кэш зеркала по тегу.
|
||
Под `containerk8s-7985cd4985-gwkc6`, Ready. Проверки на проде: `/health` → v0.1.27,
|
||
`GET /` → 200 text/html (страница), `/static/logo.svg` → 200, SQS POST → 403 (норма без сигнатуры).
|
||
|
||
**Незакрытый хвост**: на ВМ в корне `~/terra/SQS-service/` остался мусор от ошибочного rsync
|
||
(топ-уровневые `ui/`, `router/` и др. с новым содержимым). На сборку не влияет (Dockerfile берёт
|
||
`./app/`). Удаление — по команде пользователя.
|
||
|
||
## Зачистка мусора на ВМ (14.08.2026)
|
||
По команде пользователя удалены untracked топ-уровневые каталоги в `~/terra/SQS-service/`,
|
||
оставшиеся от ошибочного rsync (дубли содержимого `app/`):
|
||
`admin auth billing conf gosqs interfaces metrics models persistence router tenant ui utils`.
|
||
`app/` не тронут (проверено: rootHandler на месте, app/ui/static и info.html целы).
|
||
Оставлено: app, cmd, doc, Dockerfile, go.mod, go.sum, HISTORY, Makefile, README.md, secrets.
|
||
|
||
---
|
||
|
||
## Архитектурный разбор (Claude Sonnet) — переход идентичности на email (14.08.2026)
|
||
|
||
**Запрос к Sonnet**: файлы только `app/auth/jwt.go`, `app/tenant/tenant_store.go`, `app/admin/admin.go`,
|
||
`app/gosqs/tenant_helpers.go`, `app/persistence/redis.go`, `app/cmd/goaws.go`,
|
||
`app/auth/auth_middleware.go`, `app/ui/index.html`.
|
||
|
||
**Ответ Sonnet (выводы)**:
|
||
1. Сломается: `TenantIDFromSub()` в jwt.go; вызов в `jwtAuth` (admin.go ~236); `access_key/secret_key`
|
||
в ответе jwtAuth; `GetBySub` в `jwtMiddleware` (~288); `bySub` → `byEmail` в TenantStore
|
||
(NewTenantStore, CreateFromJWT, Delete, LoadTenant, GetBySub).
|
||
2. Пересчёт легаси при 0 очередях безопасен, но ОБЯЗАТЕЛЬНО: удалить старый тенант из Redis
|
||
(`ssq:tenants` по старому ID) и сохранить новый — иначе после рестарта старые рандомные ключи оживут.
|
||
Весь пересчёт — под `s.mu.Lock()`.
|
||
3. Детерминированные функции — в jwt.go; byEmail-индекс — в tenant_store.go; LoadTenant индексирует
|
||
по Email только при `Email != ""` (защита от коллизии `byEmail[""]` с демо-тенантом CreateFixed).
|
||
4. Дыра — orphan AccessKey в byAccessKey: при пересчёте удалять из byAccessKey ДО добавления нового.
|
||
5. Критичное неучтённое: (a) email может быть пустым → проверка в jwtAuth (400), не в ParseJWTClaims;
|
||
(b) QueueUrl содержит TenantID — при смене ID URL меняется, у клиентов захардкоженные URL сломаются
|
||
(сейчас очередей нет — не критично); (c) демо-тенант без Email не должен попадать в byEmail.
|
||
|
||
**Решение (принято)**: все пункты включаем в v0.1.28. Токен — только аутентификация;
|
||
email из JWT детерминированно задаёт TenantID/AccessKey/SecretKey; рандома нет;
|
||
ключи на экране не светятся — только по кнопке в модалке с подсказкой;
|
||
`POST /ui/api/auth` перестаёт отдавать ключи; новый `GET /ui/api/credentials` под JWT-сессией.
|
||
|
||
---
|
||
|
||
## v0.1.28 — email-идентичность, детерминированные креды, credentials-эндпоинт (14.08.2026)
|
||
|
||
**Что сделано**:
|
||
1. `app/tenant/tenant_store.go`: индекс `bySub` → `byEmail`; `GetByEmail` вместо `GetBySub`;
|
||
`CreateFromJWT(sub, email, maxQueues)` — ID/AccessKey/SecretKey детерминированы из email
|
||
(SHA256, соль `shared-sqs:secret-key:v1` для секрета). Рандом остался только для manual-тенантов.
|
||
Легаси-тенант (старые ключи/ID) при входе полностью пересчитывается: старые индексы
|
||
и Redis-запись удаляются под мьютексом.
|
||
2. `app/auth/jwt.go`: `TenantIDFromSub` (sless) УДАЛЁН — связей с внешними сервисами больше нет.
|
||
3. `app/admin/admin.go`: `jwtAuth` требует email (400 без него), НЕ возвращает access/secret key;
|
||
новый `GET /ui/api/credentials` отдаёт ключи под JWT-сессией; jwtMiddleware по email.
|
||
4. `app/ui/index.html`: Access Key убран из таблицы и карточки тенанта; кнопки «Credentials»
|
||
(дашборд + тенант) открывают модалку с ключами и подсказкой (зачем ключи, export-команды,
|
||
пример aws cli). Логин-страница: образ v0.1.28.
|
||
5. `Makefile`: VERSION=v0.1.28.
|
||
|
||
**Проверки**: build OK, vet OK, тесты admin/models OK. Локально через мок-gateway:
|
||
auth без ключей ✓, credentials отдаёт ключи ✓, без токена 401 ✓. Хеши сверены с python:
|
||
tenant_id `t-fec713a719ad0b33`, AK `SSAK-fec713a719ad0b33c91fa54a`, SK `e898ed410a…`.
|
||
|
||
**Деплой**: образ v0.1.28 digest `sha256:c6b63f4c…`, поставлен по digest
|
||
(`set image … app=naeel/shared-sqs@sha256:c6b63f4c…`), под `containerk8s-86df69bfb-drv9x` Ready.
|
||
`/health` на проде → `{"status":"ok","version":"v0.1.28"}`.
|
||
|
||
**⚠ ПРОБЛЕМА (не наш код)**: TEST-gateway теперь отдаёт 401 на запросы с датацентровых IP
|
||
(кластер iot-naeel И ВМ 5.172.178.213) — с рабочей станции тот же токен даёт 200.
|
||
Ранее (~09:00) под успешно валидировался. Похоже на rate-limit/бан DDoS-Guard по egress-IP
|
||
платформы после сегодняшних частых запросов. Вход в UI будет 403, пока шлюз не отпустит.
|
||
SQS API, /health, /, /metrics не затронуты.
|
||
|
||
---
|
||
|
||
## v0.1.29 — светлый Nubes-дизайн консоли, читаемая модалка Credentials (14.08.2026)
|
||
|
||
**Проблема (жалоба юзера)**: консоль была на тёмной палитре старого iot-дизайна
|
||
(чёрный фон, мелкий шрифт), модалка Credentials нечитаемая. Требование: Nubes-дизайн, читаемо.
|
||
|
||
**Что сделано** (`app/ui/index.html`):
|
||
1. Палитра → светлая design-system Nubes: bg #fafafa, поверхности #ffffff, border #d1d5db,
|
||
primary #2563eb, muted #6b7280, карточки белые с radius 12 + тень.
|
||
2. Логотип без invert-фильтра (тёмный лого на белом), шрифт system-ui 14px, заголовки 20-22px,
|
||
бейджи/кнопки в светлых тонах, textarea/input белые.
|
||
3. Модалка Credentials переделана: читаемый блок подсказки — зачем ключи + ДВА способа
|
||
(env-export И файл ~/.aws/credentials) + пример с --profile sqs. Ширина модалки 560px, скролл.
|
||
|
||
**Деплой**: v0.1.29 digest `sha256:d1ccb53d…`, `set image` по digest, под `containerk8s-75f766b9d7-82wl7`.
|
||
Проверки: /health → v0.1.29; /ui отдаёт светлую тему (#fafafa/#ffffff/#2563eb, тёмных цветов нет).
|
||
|
||
**Остаётся открытым**: gateway 401 с датацентровых IP (см. раздел v0.1.28) — вход в UI
|
||
заработает, когда шлюз отпустит egress платформы.
|
||
|
||
---
|
||
|
||
## v0.1.30 — локальные логотип и favicon в консоли (14.08.2026)
|
||
|
||
В консоли /ui были внешние ассеты `terra.k8c.ru/.../logo.svg` и `favicon.png` — не грузились
|
||
(битый лого в шапке, нет фавикона). Заменены на локальные вшитые `/static/logo.svg`
|
||
и `/static/favicon.svg` (embed в образ, раздаются с корня роутером).
|
||
Деплой: digest `sha256:deab2979…`, под готов. Проверки: /health → v0.1.30, /ui отдаёт
|
||
2× static/logo.svg + 1× static/favicon.svg, оба файла отвечают 200.
|
||
|
||
---
|
||
|
||
## v0.1.31 — админский UI /admin + юзерский UI без слова «тенант» (14.08.2026)
|
||
|
||
**Сделано**:
|
||
1. Новый `app/ui/admin.html` — админская консоль: вход по admin-токену, таблица тенантов
|
||
(имя, ID, Access Key, лимит, статус, создан), создание тенанта с показом ключей в модалке,
|
||
удаление с подтверждением. Светлый Nubes-дизайн, /static/logo.svg + favicon, версия через {{VERSION}}.
|
||
2. `app/ui/embed.go`: embed admin.html + `AdminHandler(version)`.
|
||
3. `app/router/router.go`: GET /admin и /admin/ → админская страница (зарегистрированы ДО admin subrouter,
|
||
чтобы PathPrefix("/admin") с bearer-auth их не перехватил).
|
||
4. Юзерский UI: из видимого текста убрано слово «тенант» — «Мой аккаунт», «Аккаунт не найден»,
|
||
убрана карточка-счётчик «Тенанты», «креды вашего аккаунта», «Создать аккаунт»,
|
||
ссылка на /admin в инфо-блоке логина.
|
||
|
||
**Проверки**: build OK, vet OK, тесты admin OK. Локально: /admin → 200 (страница), /admin/tenants
|
||
без токена/с неверным токеном → 401, с верным → []. Прод: /health → v0.1.31, /admin → 200
|
||
(SQS Admin, v0.1.31), /ui содержит «аккаунт» 3× и не содержит «Тенанты».
|
||
|
||
**Инциденты по пути**:
|
||
- Пуш на Docker Hub спотыкался об IPv6 (network unreachable) и 502 — добил ретраями,
|
||
digest v0.1.31 = `sha256:34c3619d…`.
|
||
- Токен kubeconfig на ВМ истёк в 09:14 MSK (24ч) — юзер обновил, деплой добит:
|
||
`set image …@sha256:34c3619d…`, под `containerk8s-768668b9d4-stcgv` Ready.
|
||
|
||
---
|
||
|
||
## v0.1.32 — главная с пошаговым стартом, примеры на /examples (14.08.2026)
|
||
|
||
**Жалобы юзера**: главная не объясняет с первого раза, как войти/где креды/что делать;
|
||
примеры засоряли главную мелким шрифтом; в консоли лишняя хлебная крошка и таблица из одной строки.
|
||
|
||
**Сделано**:
|
||
1. `app/ui/info.html`: блок «Как начать — 3 шага» (1. Открыть консоль → кнопка,
|
||
2. Получить ключи через Credentials, 3. Подключить клиент с быстрым примером + ссылка на /examples).
|
||
Таблица команд и примеры убраны с главной, вместо них — карточка-ссылка на /examples.
|
||
Слово «тенант» убрано из текста главной.
|
||
2. Новый `app/ui/examples.html` (GET /examples): крупный шрифт 15px, команды 14px,
|
||
пошагово: ключи (env И файл ~/.aws/credentials) → создать очередь → отправить →
|
||
получить/удалить, плюс таблица всех 17 Actions.
|
||
3. `app/ui/embed.go`: ExamplesHandler; `app/router/router.go`: маршрут /examples.
|
||
4. `app/ui/index.html`: enterApp сразу открывает аккаунт (showTenant первого тенанта),
|
||
дашборд с таблицей — только запасной вариант; хлебные крошки «Мой аккаунт › email» удалены.
|
||
|
||
**Проверки**: локально root → «Как начать/Открыть консоль», /examples → 200 (Шаг 1..4, таблица).
|
||
Прод: /health → v0.1.32, / → 200, /examples → 200. digest `sha256:4834b999…`.
|
||
|
||
---
|
||
|
||
## v0.1.33 — строгий нейтральный стиль текстов (14.08.2026)
|
||
|
||
Жалоба: фамильярный тон на главной («Войди», «Получи», «Подключи свой клиент»).
|
||
Заменено на нейтральный официальный стиль:
|
||
- «Порядок действий — 3 шага»: «Вход в консоль», «Получение ключей доступа», «Подключение клиента».
|
||
- Примеры (/examples): «Шаг 1. Указание ключей доступа», «Шаг 2. Создание очереди»,
|
||
«Шаг 3. Отправка сообщения», «Шаг 4. Получение и удаление сообщения»; формулировки без повелительных форм.
|
||
Деплой: v0.1.33 digest `sha256:196283c1…`, проверено: /health → v0.1.33, корень отдаёт новые заголовки.
|
||
|
||
---
|
||
|
||
## ✅ Полный цикл подтверждён: email-креды работают на проде (14.08.2026)
|
||
|
||
Юзер залогинился (gateway отпустил egress платформы), легаси-тенант пересчитан,
|
||
в модалке Credentials показаны детерминированные ключи:
|
||
- Access Key: `SSAK-fec713a719ad0b33c91fa54a`
|
||
- Secret Key: `e898ed410a20ff166f51a52ba39a2954fb87e2da200b1648c3975772bfe9e2b4`
|
||
|
||
Совпадают с расчётным SHA256(email) — email-идентичность v0.1.28 работает.
|
||
Smoke-тест: `aws sqs list-queues --endpoint-url https://sqs.containerk8s.dev.nubes.ru`
|
||
с этими ключами → 200, `{"QueueUrls": []}`. SigV4-аутентификация на проде подтверждена.
|
||
|
||
---
|
||
|
||
## Q&A с Claude Sonnet — кэш валидации JWT (14.08.2026)
|
||
|
||
**Запрос**: файлы только `app/auth/jwt.go`, `app/admin/admin.go`; задача — кэш успешной валидации
|
||
токена (TTL 5 мин) из-за нестабильного шлюза Nubes.
|
||
|
||
**Ответ Sonnet**:
|
||
1. Кэш — в auth (новый `app/auth/cache.go`): `PingNubesAPICached`, `WarmTokenCache`, `StartTokenCacheCleanup`.
|
||
В admin.go: jwtMiddleware → PingNubesAPICached; jwtAuth → свежая проверка + WarmTokenCache; NewHandler → cleanup.
|
||
2. Ключ `hex(sha256(token))`; `map[string]tokenCacheEntry{validUntil}` + `sync.RWMutex`
|
||
(не sync.Map — нужна итерация при очистке); уборщик — ticker 5 мин.
|
||
3. `ErrTokenRejected` (sentinel, `%w`) — отличать 401/403 от сетевых ошибок; сигнатура PingNubesAPI не меняется.
|
||
4. Таблица: HIT → nil; MISS+2xx → записать; MISS+401/403 → удалить, вернуть ошибку;
|
||
MISS+сетевая → ошибка; протухший+сетевая → опция grace +2 мин (принято).
|
||
5. Гонки: параллельные MISS безвредны (singleflight избыточен); запись в кэш до проверки ctx;
|
||
рестарт пода сбрасывает кэш (норма); exp проверяется локально ДО кэша/шлюза.
|
||
|
||
**Решение (принято)**: всё выше + grace-продление 2 мин при сетевой ошибке для ранее валидного токена.
|
||
TTL 5 мин; отозванный токен умирает максимум через 5 мин; изоляция тенантов не затрагивается.
|
||
|
||
---
|
||
|
||
## v0.1.34 — кэш валидации JWT (14.08.2026)
|
||
|
||
**Проблема**: jwtMiddleware валидировал токен через Nubes-шлюз на КАЖДЫЙ запрос консоли;
|
||
шлюз нестабильно отвечает с IP платформы (то 200, то 401) → консоль периодически падала.
|
||
|
||
**Реализовано** (по плану Sonnet):
|
||
1. `app/auth/cache.go`: кэш успешной валидации в памяти: `map[hex(sha256(token))]{validUntil}` +
|
||
`sync.RWMutex`, TTL 5 мин, grace +2 мин при сетевой ошибке для ранее валидного токена,
|
||
фоновая очистка (ticker 5 мин, sync.Once).
|
||
2. `app/auth/jwt.go`: sentinel `ErrTokenRejected` (`%w`) для 401/403 — отличие от сетевых ошибок.
|
||
3. `app/admin/admin.go`: `jwtAuth` — всегда свежая проверка + `WarmTokenCache` после успеха;
|
||
`jwtMiddleware` — `PingNubesAPICached`; `NewHandler` — старт очистки.
|
||
|
||
**Проверено локально (мок-шлюз)**: логин с живым моком → 200; мок убит → логин 403 (свежо, правильно),
|
||
CREDS → 200 по кэшу (шлюз не нужен), неизвестный токен → 403. Vet/тесты OK.
|
||
|
||
**Smoke на проде**: 11 PASS, 1 FAIL — только логин (шлюз режет IP платформы; по дизайну логин всегда свежо).
|
||
SQS-цикл create→send→receive→delete→delete-queue — PASS.
|
||
|
||
**Особенность платформы**: GET /ui через ingress не закрывает соединение (~150с, curl exit 28),
|
||
внутри пода мгновенно. В smoke /ui проверяется range-запросом (206, 0.2с).
|
||
Браузеру не мешает (HTML приходит сразу).
|
||
|
||
**Деплой**: v0.1.34 digest `sha256:17f9bbdb…`, под Ready, /health → v0.1.34.
|
||
|
||
---
|
||
|
||
## Программная проверка SQS API — CLI и SDK, локально и с ВМ (14.08.2026)
|
||
|
||
Добавлены тесты:
|
||
- `tests/api_test.sh` — aws cli, 22 проверки (полный цикл + FIFO + теги + purge).
|
||
- `tests/sdk_test.py` — boto3 (как реальное приложение), 15 проверок, таймауты на уровне SDK.
|
||
|
||
Результаты:
|
||
- Локально, CLI: 22/22 PASS; SDK (boto3 1.43.71): 15/15 PASS.
|
||
- С ВМ, CLI: 22/22 PASS; SDK (boto3 1.34.46): 15/15 PASS.
|
||
|
||
Единственный разовый FAIL (DeleteMessage после ChangeMessageVisibility) при первом
|
||
прогоне не воспроизвёлся — повторный прогон и ручная проверка: exit=0. Случайный сбой сети.
|
||
|
||
---
|
||
|
||
## Нагрузочный тест 30 мин (tests/load_test.py) — результаты и находки (14.08.2026)
|
||
|
||
**Прогон**: 1800с, 4 воркера, против прода. Итог: 776 операций.
|
||
- Корректных: 75; ожидаемых отказов: 0; НЕОЖИДАННЫХ отказов: 653 (коды: 400×549, 404×44, ReadTimeoutError×60);
|
||
- Некорректные условия приняты без ошибок: 47; сбоев /health: 1 (разовый таймаут); под НЕ рестартовал.
|
||
- Латентность: min 0.08s, avg 2.63s, max 54.4s. Вердикт: ЕСТЬ ПРОБЛЕМЫ.
|
||
|
||
**Критично — потери сообщений**: send возвращает успех, receive не отдаёт сообщение.
|
||
Воспроизведено БЕЗ нагрузки: 3 из 25 циклов (и 2 из 25 под нагрузкой). Зависания receive >10с.
|
||
|
||
**Недостатки обработки ошибок (пробы)**: на некорректные запросы сервис отдаёт сырые
|
||
HTTP 400/404 без AWS-XML (SDK получает Code=None) или молча принимает недопустимое:
|
||
- SetQueueAttributes: VisibilityTimeout=43201 принят; неверное имя атрибута принято;
|
||
нечисловое значение → сырой 400.
|
||
- SendMessage: пустое тело → сырой 400; 300КБ → сырой 400; несуществующая очередь → сырой 400.
|
||
- Batch: 11 записей / дубли Id / пустой список → сырой 400.
|
||
- DeleteMessage с битым ReceiptHandle → сырой 404.
|
||
- ReceiveMessage MaxNumberOfMessages 11/0/-1 — принято (клэмпится).
|
||
- Повторный PurgeQueue (<60с) — принят; DeleteQueue несуществующей — принят.
|
||
- FIFO SendMessage без MessageGroupId — принят.
|
||
|
||
**Мой предварительный анализ кода** (для Sonnet):
|
||
- models.go IsReadyForReceipt(): showAt = SentTime + randomLatency(конфиг, сейчас 0/0) + DelaySecs.
|
||
- gosqs.go PeriodicTasks (1с): сброс видимости, dedup, DLQ; LockGroup держится до UnlockGroup.
|
||
- send_message.go: добавление в SyncQueues.Queues[key].Messages + SaveMessage(Redis);
|
||
память — единственный источник для receive (Redis не читается) — при 2+ подах/редиплое
|
||
сообщения, принятые одним подом, невидимы другому.
|
||
- receive_message.go: skip сообщений с ReceiptHandle != "" и не готовых; long poll 100ms тик.
|
||
- ReadTimeout 60× и max latency 54с — подозрение на платформенный ingress (аналог /ui 152с).
|
||
|
||
---
|
||
|
||
## Q&A с Claude Sonnet — разбор дефектов по итогам нагрузочного теста (14.08.2026)
|
||
|
||
**Ответ Sonnet (по файлам из промпта)**:
|
||
Q1 Потери сообщений: TOCTOU в long-poll (receive_message.go:82-99) — два потока видят сообщение,
|
||
один забирает, второй возвращает пусто; включается при WaitTimeSeconds>0 (клиент или атрибут очереди).
|
||
Вторично для FIFO: LockGroup (models.go:82-89) пересоздаёт FIFOMessages map целиком — теряются локи групп.
|
||
Q2 Receive >10с: (а) атрибут очереди ReceiveMessageWaitTimeSeconds без гарда (receive_message.go:62-66);
|
||
(б) SaveMessage/SaveQueue внутри глобального Lock (send_message.go:97-103) — медленный Redis блокирует всех;
|
||
(в) PeriodicTasks держит Lock на весь обход всех очередей (gosqs.go:18-58).
|
||
Q3 Сырые 400/404: actionHandler пишет "Bad Request" текстом (router.go:199-201);
|
||
encodeResponse для JSON-протокола не пишет тело при GetResult()==nil (router.go:110-113).
|
||
Q4 Валидации: VisibilityTimeout clamp→bounds-check (queue_attributes/set_queue_attributes.go:29);
|
||
whitelist имён атрибутов (IgnoreUnknownKeys в utils.go:29 пропускает всё); MaxNumberOfMessages clamp→ошибка
|
||
(receive_message.go:41); PurgeQueue повторный<60с→PurgeQueueInProgress (LastPurgeTime в Queue);
|
||
FIFO без MessageGroupId→MissingParameter (send_message.go:46-49); actionHandler→AWS-XML GeneralError.
|
||
Q5 Сервис сам может держать запрос >25с: Redis-записи под Lock + PeriodicTasks с полным обходом.
|
||
Q6 Дополнительно: SetQueueAttributes обнуляет не переданные атрибуты (zero-value >= 0);
|
||
unprotected map read (receive_message.go:51) — data race; NumberOfReceives не инкрементируется;
|
||
DeleteQueue всегда успешен; NextSequenceNumber пересоздаёт map (models.go:62-70).
|
||
|
||
**ПЛАН v0.1.35 (принят)**:
|
||
1. TOCTOU: выборка сообщений — единая критическая секция под Lock.
|
||
2. Redis-сетевые вызовы — вне глобального Lock (снапшот под локом, запись после).
|
||
3. PeriodicTasks: сузить критическую секцию.
|
||
4. RLock для Queues[key] в receive_message.go:51.
|
||
5. SetQueueAttributes: применять только явно переданные атрибуты.
|
||
6. LockGroup/NextSequenceNumber: не пересоздавать map (вставлять ключ).
|
||
7. actionHandler: AWS-XML GeneralError вместо текста.
|
||
8. encodeResponse (JSON): тело ошибки при GetResult()==nil; проверить CreateErrorResponseV1.
|
||
9. Валидации: VisibilityTimeout bounds; whitelist атрибутов; MaxNumberOfMessages 1–10 ошибка;
|
||
PurgeQueueInProgress (LastPurgeTime); FIFO MissingParameter; SendMessage пустое тело/размер — AWS-коды.
|
||
10. NumberOfReceives инкремент при выдаче.
|
||
После: 25-цикловая проверка целостности, smoke, повторный 30-мин нагрузочный тест.
|
||
|
||
---
|
||
|
||
## v0.1.35 — реализация фиксов по итогам нагрузочного теста (14.08.2026)
|
||
|
||
**Изменённые файлы** (правки с подробными комментариями каждой функции и логики):
|
||
|
||
1. `app/router/router.go`:
|
||
- `resolveProtocol`: заголовок `x-amzn-query-mode: true` (AWS CLI v2/boto3) →
|
||
принудительно Query-протокол (XML-ответы). Раньше query-mode запросы получали
|
||
JSON-ошибки → botocore Code=None («сырые 400/404»).
|
||
- `extractAction`: Action берётся сначала из `X-Amz-Target` (JSON и query-mode
|
||
клиенты), затем из form `Action` — для query-mode раньше Action не находился.
|
||
- `encodeResponse` (JSON): ошибки — в формате AWS-JSON
|
||
`{"__type":"com.amazonaws.sqs#<Code>","message":"..."}` (раньше голый ErrorResult).
|
||
- `actionHandler`: неизвестный Action → GeneralError в формате протокола клиента
|
||
(раньше текст "Bad Request"). Убран импорт `io`.
|
||
|
||
2. `app/gosqs/receive_message.go`:
|
||
- TOCTOU устранён: проверка готовности И пометка in-flight — под одним Lock
|
||
(`receiveMessagesUnderLock`). Раньше два параллельных receive видели одно
|
||
сообщение, второй получал пусто → «потери».
|
||
- MaxNumberOfMessages: 1–10, выход → InvalidParameterValue (был clamp).
|
||
- Проверка существования очереди — под RLock (была гонка чтения map).
|
||
- `msg.NumberOfReceives++` при выдаче (счётчик доставок).
|
||
|
||
3. `app/gosqs/send_message.go`:
|
||
- FIFO без MessageGroupId → MissingParameter (было: принималось).
|
||
- Док-схема функции; Redis-записи асинхронны, под Lock только marshal.
|
||
|
||
4. `app/gosqs/gosqs.go`:
|
||
- `PeriodicTasks`: снапшот указателей под RLock + обработка каждой очереди под
|
||
отдельным Lock (раньше глобальный Lock на весь обход всех очередей).
|
||
|
||
5. `app/models/models.go`:
|
||
- `LockGroup`/`NextSequenceNumber`: map больше НЕ пересоздаётся (терялись
|
||
блокировки/счётчики остальных групп).
|
||
- Новое поле `Queue.LastPurgeTime`.
|
||
|
||
6. `app/gosqs/queue_attributes.go` + `set_queue_attributes.go` + `create_queue.go`:
|
||
- Применяются только явно переданные атрибуты (!= 0) — раньше zero-value
|
||
обнулял непереданные атрибуты.
|
||
- Bounds-check → InvalidParameterValue (был clamp).
|
||
- Whitelist имён → InvalidAttributeName (Query-протокол).
|
||
|
||
7. `app/gosqs/purge_queue.go`: повторный purge < 60 с → PurgeQueueInProgress.
|
||
|
||
8. `app/gosqs/delete_queue.go`: удаление несуществующей очереди → QueueNotFound.
|
||
|
||
9. `app/models/errors.go`: добавлены `InvalidAttributeName`, `PurgeQueueInProgress`.
|
||
|
||
**Проверки**: go build OK (v0.1.35), go vet чистый (кроме известной некомпиляции
|
||
тестов gosqs — пакет fixtures), go test app/models OK.
|
||
|
||
**Ход деплоя и эмпирические уточнения**:
|
||
- Первый вариант фикса resolveProtocol («query-mode → XML») оказался НЕВЕРНЫМ:
|
||
эмпирика показала, что AWS CLI v2 в query-mode парсит именно JSON-ответы
|
||
(list-queues на XML вернул None, на JSON — список). Откачено: протокол ответа
|
||
по Content-Type, как было; дефект «Code=None» закрыт правильным форматом
|
||
JSON-ошибок {"__type":"com.amazonaws.sqs#Code","message":"..."} в encodeResponse
|
||
и GeneralError в actionHandler.
|
||
- Whitelist имён атрибутов для JSON-протокола реализован через
|
||
QueueAttributes.UnmarshalJSON (единый список models.AttrNameWhitelist);
|
||
в whitelist включены FifoQueue/ContentBasedDeduplication (иначе CLI v2
|
||
не мог создать FIFO — регрессия была поймана api_test).
|
||
- Прод v0.1.35, digest `sha256:7d1c82e9…` (по digest).
|
||
|
||
**Проверки против прода (v0.1.35)**:
|
||
- Пробы ошибок: пустое тело → MissingParameter; max=11 → InvalidParameterValue;
|
||
VisibilityTimeout=43201 → InvalidParameterValue; FooBar=1 → InvalidParameterValue;
|
||
повторный purge → PurgeQueueInProgress; delete несуществующей → NonExistentQueue;
|
||
FIFO без MessageGroupId → MissingParameter. Все с AWS-кодами (Code=None устранён).
|
||
- api_test.sh (CLI v2): 22/22 PASS. sdk_test.py (boto3): 15/15 PASS.
|
||
- Целостность 25 циклов: с ВМ 25/25 PASS (lost=0, mismatch=0, errors=0).
|
||
С локальной машины периодически ReceiptHandleIsInvalid — анализ логов пода
|
||
показал: это retry-дубли (первый delete успешен, ответ теряется на пути
|
||
клиент→шлюз; сообщение фактически удалено и подбирается следующими циклами).
|
||
Инфраструктурная потеря ответов (DDoS-Guard/Nubes gateway), не дефект сервиса.
|
||
- smoke.sh: 11/11 PASS.
|
||
- 30-мин нагрузочный тест с ВМ — в процессе.
|
||
|
||
## Лимит очередей 50 + очистка (14.08.2026)
|
||
|
||
**Нагрузочный тест (1-й прогон v0.1.35, с ВМ)**: 217440 операций, но 214683
|
||
«неожиданных» = LimitExceeded. Причина: у JWT-тенанта лимит 10 очередей
|
||
(CreateFromJWT хардкод 10), а 9 мусорных очередей от прежних тестов заняли
|
||
почти весь лимит — 4 воркера теста упирались в лимит на create-queue.
|
||
Некорректное принято: 0. /health сбоев: 0. max latency 77.6с = ReadTimeoutError
|
||
(25с × 3 ретрая бото3) — потери ответов на пути, не сервис.
|
||
|
||
**Изменения (по команде пользователя)**:
|
||
- `app/tenant/tenant_store.go`: константа `DefaultTenantMaxQueues = 50`;
|
||
`CreateFromJWT` теперь обновляет `MaxQueues` существующего тенанта до актуального
|
||
значения при КАЖДОМ логине + персистит в Redis (иначе после рестарта пода из
|
||
Redis восстановился бы старый лимит).
|
||
- `app/admin/admin.go`: `CreateFromJWT(..., tenant.DefaultTenantMaxQueues)`.
|
||
- Деплой v0.1.35 digest `sha256:72f11c67…`.
|
||
- Удалены все мусорные очереди тенанта (11 штук: probe-*, err-test*, integr-*,
|
||
vt-check-*, load-*, int-*): осталось 0.
|
||
|
||
**Осталось**: логин в консоль для применения лимита 50 к существующему тенанту,
|
||
затем повторный 30-мин нагрузочный тест.
|
||
|
||
## Фиксы гонок map — под падал под нагрузкой (14.08.2026)
|
||
|
||
**Инцидент**: 30-мин прогон показал 4 сбоя /health; под перезапускался 2 раза
|
||
(Exit Code 2). Логи упавшего контейнера: `fatal error: concurrent map read and map write`,
|
||
горутина `ChangeMessageVisibilityV1`, `change_message_visibility.go:50` —
|
||
чтение `SyncQueues.Queues[key]` БЕЗ блокировки; писатель — DeleteQueueV1 (delete под Lock),
|
||
массово вызываемый тестом.
|
||
|
||
**Анализ Соннета** (подтверждён): 3 файла читают map до Lock; смежный риск nil-deref
|
||
(очередь удаляется между проверкой и Lock); receive_message.go имел два RLock с окном
|
||
nil-deref; рекомендован перенос проверки внутрь Lock + defer Unlock.
|
||
|
||
**Исправлено (v0.1.35, digest `sha256:7436efd8…`)**:
|
||
- `change_message_visibility.go`: проверка существования внутри Lock, defer Unlock.
|
||
- `change_message_visibility_batch.go`: то же.
|
||
- `delete_message_batch.go`: то же.
|
||
- `send_message.go`: nil-guard под Lock + повторная проверка OOM-лимита под Lock.
|
||
- `send_message_batch.go`: nil-guard под Lock.
|
||
- `receive_message.go`: один RLock вместо двух (nil-deref окно закрыто).
|
||
- `errors.go`: добавлен код ошибки `OverLimit` (раньше отсутствовал — zero SqsErrorType).
|
||
|
||
**Проверки**: go build/vet OK, models-тесты OK; короткий прогон 120с: неожиданных 2
|
||
(оба сетевые ReadTimeout), сервисных 0, health_fail 0; под restarts 0.
|
||
30-мин прогон запущен 18:52:18 +04 — в процессе.
|
||
|
||
## Сетевые сбои на ВМ и фикс DNS (14.08.2026)
|
||
|
||
**Факт**: 30-мин прогон после фиксов гонок: неожиданных 19 (17 ReadTimeout +
|
||
2 EndpointConnection), health_fail 8, НО под restarts 0 (гонки устранены).
|
||
Замер 30 × /health с ВМ: 4 сбоя `[Errno -3] Temporary failure in name resolution` —
|
||
DNS ВМ. Проверки ВМ: resolv.conf = один `8.8.8.8`; dig 20/20 ок, python 60/60 ок —
|
||
сбои периодические (потери UDP к резолверу).
|
||
|
||
**Фикс (по команде пользователя)**:
|
||
1. В `/etc/resolv.conf` ВМ добавлен резерв `nameserver 1.1.1.1` — сбои остались (2/60).
|
||
2. В `/etc/hosts` ВМ добавлено `185.247.187.151 sqs.containerk8s.dev.nubes.ru` —
|
||
резолв из файла, 100/100, 0–5 мс. Зависимость от DNS устранена.
|
||
|
||
**Финальный 30-мин прогон**: запущен 18:30:52 +03 (ВМ) — в процессе.
|
||
|
||
## Q&A Соннета — какие ещё тесты провести (14.08.2026)
|
||
|
||
**Вопрос (кратко)**: достаточен ли набор проверок; какие сценарии не покрыты; норма ли
|
||
ReadTimeout 0.04%; серверные таймауты; нужен ли CLI v2 в нагрузке; что ещё до релиза.
|
||
|
||
**Ответ Соннета (ключевое)**:
|
||
- Не покрыто, но критично: рестарт пода с in-flight сообщениями (asyncWrite до смерти пода
|
||
→ двойная доставка); unbounded goroutines при деградации Redis (OOM на 512Mi);
|
||
long-poll goroutine leak; FIFO-порядок под конкурентными receive; DLQ e2e; dedup-окно e2e.
|
||
- Восстановление очередей из Redis после рестарта — обязательный тест.
|
||
- ReadTimeout 0.042% — граница, снижать до <0.01%: проверить кластеризацию ошибок во времени
|
||
(lock contention vs gateway), keep-alive/RST на шлюзе, TCP retransmits пода.
|
||
- Серверные таймауты добавить: ReadHeaderTimeout 10s, WriteTimeout 35s (20с long-poll + буфер),
|
||
IdleTimeout 90s, MaxHeaderBytes 8192. ReadTimeout НЕ ставить (рвёт long-poll).
|
||
- AWS CLI v2 (query-mode JSON) в нагрузке — обязательно, другой путь сериализации.
|
||
- Память (RSS 2+ часа), billing-счётчики, JWT-истечение mid-request, лимит 50 очередей
|
||
(граница включительно), /metrics корректность после прогона.
|
||
- Приоритеты: P0 серверные таймауты + рестарт с in-flight; P1 Redis chaos + long-poll leak;
|
||
P2 CLI v2 нагрузка + FIFO dedup/DLQ e2e; P3 RSS.
|
||
|
||
## План диагностики САМОЙ платформы (HTTP контейнер), независимо от SQS (14.08.2026)
|
||
|
||
Пользователь: проверить, как HTTP-контейнер платформы сбоит сам по себе (аналогия:
|
||
managed flask/nodejs/lucee не пропускают большие файлы с локали). Отделяем транспорт от логики SQS.
|
||
|
||
A. Через /health (минимум логики): burst 500 параллельных; большие заголовки 8–16KB;
|
||
медленный запрос (1 байт/с); keep-alive 50 запросов на одном соединении; HTTP/1.0, HEAD, POST.
|
||
B. Через SQS API, смотрим транспорт: SendMessage 256KB (с ВМ И с локальной машины);
|
||
chunked-тело; Receive 10×256KB (~2.5MB ответ); long-poll 20с (локаль vs ВМ).
|
||
C. Известные аномалии: GET /ui через ingress висит ~150с (внутри пода мгновенно);
|
||
ReadTimeout-кластеры; max latency 52с.
|
||
D. TCP: retransmit-счётчики пода (/proc/net/snmp) до/после.
|
||
Итог: таблица «локаль vs ВМ vs внутри пода» — что режет платформа, а что сервис.
|
||
|
||
## РЕЗУЛЬТАТЫ диагностики платформы (tests/platform_probe.py, 14.08.2026)
|
||
|
||
**ГЛАВНАЯ НАХОДКА — большие POST-тела зависают на ~51с (защитный слой платформы):**
|
||
- send 2–48KB — 0.09–0.22с; 56–64KB — то быстро, то виснет (порог плавает);
|
||
80KB/100KB — первый запрос 50–52с, следующие 0.03с.
|
||
- Воспроизводится и с ВМ, и с локальной машины → общий узел DDoS-Guard/WAF.
|
||
- 100KB с БИТОЙ SigV4 — тоже 50.4с (висит путь, не наш auth/сервис).
|
||
- /health параллельно зависшему запросу — 0.02–0.05с (сервис жив).
|
||
- TCP connect+TLS мгновенны всегда (в т.ч. после idle 60/120с).
|
||
- Большие ОТВЕТЫ не режутся: receive 10×256KB (2.5MB) — 0.26с.
|
||
- Long-poll 20с держится честно (20.01с) — платформа не режет до 20с.
|
||
|
||
**Прочие ограничения платформы:**
|
||
- Заголовки >16KB → 400 Bad Request (8KB проходит).
|
||
- Chunked Transfer-Encoding → 400.
|
||
- POST /health → 400; HEAD /health → 405 (наш mux); HTTP/1.0 → ок (апгрейд).
|
||
|
||
**Нагрузка на под**: burst 500 × /health: 500/500 ok, p50=2.54с, p95=3.43с —
|
||
это CPU-лимит пода 500m, а не платформа (внутренние запросы мгновенны).
|
||
|
||
**Следствие**: клиенты с сообщениями >~64KB и read_timeout < 51с получают
|
||
ReadTimeout на первом запросе в окне (это объясняет кластеры ReadTimeout и
|
||
max latency 52с в нагрузочных тестах). Рекомендации: клиентам read_timeout ≥ 60с
|
||
и retries; вопрос про WAF-инспекцию больших POST — в поддержку платформы.
|
||
|
||
## УТОЧНЕНИЕ ПРИЧИНЫ 51с — это MTU, как в drhider (14.08.2026)
|
||
|
||
**Гипотеза WAF выше — НЕВЕРНА.** Найдено в `~/nubes/drhider`:
|
||
- `PROBLEM-AND-SOLUTION.md` «Проблема 1: 51-секундная задержка (MTU)»: POST из
|
||
внешней сети — пауза 51с; внутри кластера — мгновенно. Причина: underlay Nubes
|
||
MTU 1450, Cilium Geneve +50, MTU пода 1500 → cross-node пакет 1550 → фрагментация
|
||
→ дроп → retransmit ~51с. Решение: `cilium mtu: 1400` (1400+50=1450).
|
||
- `docs/about1500.md`: готовый текст обращения в поддержку Nubes.
|
||
- VIP drhider — тот же `185.247.187.151`, что у `sqs.containerk8s.dev.nubes.ru`.
|
||
|
||
**Проверено у нас (факты)**:
|
||
- Cilium кластера iot-naeel: `mtu: 1400` — внутренний участок УЖЕ исправлен;
|
||
под eth0 MTU 1400 ✓.
|
||
- ВМ: ens192 MTU **1500** → сегменты 1460 не влезают в underlay 1450 на участке
|
||
ВМ→шлюзы платформы (traceroute: 10.10.102.1 → 100.64.x.x → 10.200.x.x → 5.181.12.1).
|
||
- Локальная машина (интернет → DDoS-Guard): та же 51с.
|
||
- Большие ОТВЕТЫ (под→клиент, 1400-байтные сегменты) проходят быстро.
|
||
- Порог зависания плавает ~56–64KB: зависит от необходимости полных 1460-байтных
|
||
сегментов.
|
||
|
||
**РЕШЕНИЕ ПОЛЬЗОВАТЕЛЯ (14.08.2026)**:
|
||
- КОД НЕ МЕНЯТЬ (лимит размера остаётся 262144/AWS).
|
||
- kubectl — только для диагностики; прод — кластер облака БЕЗ нашего доступа
|
||
(править MTU можем только там через поддержку Nubes).
|
||
- ТЕСТЫ — без больших сообщений: valid-тела ≤48KB; сценарий «превышение лимита»
|
||
через очередь с MaximumMessageSize=1024 и телом 2KB (граничное условие
|
||
сохраняется, большое тело не шлётся).
|
||
- Ограничение платформы (51с на большие POST) остаётся для реальных клиентов —
|
||
адресовать в поддержку Nubes текстом из drhider/docs/about1500.md.
|
||
|
||
## Тесты без больших тел (14.08.2026)
|
||
|
||
**Правки тестов (код сервиса НЕ менялся)**:
|
||
- `load_test.py`: `rand_body` ≤48KB; сценарий «превышение лимита» переделан —
|
||
очередь с `MaximumMessageSize=1024` + тело 2KB (граничное условие MessageTooBig
|
||
сохраняется, большое тело по сети не шлётся, MTU-зависаний нет).
|
||
- `platform_probe.py`: B6/B8 — 32KB вместо 256KB.
|
||
- Коммит `96f55be`, запушен, синхронизирован на ВМ.
|
||
|
||
**Короткий прогон 120с**: expected 3691, unexpected 2 (оба сетевые ReadTimeout),
|
||
soft_warn=0, health_fail=0. Новый граничный сценарий работает (InvalidParameterValue 246).
|
||
|
||
**30-мин прогон**: запущен 19:56:34 +03 (ВМ) — в процессе.
|
||
|
||
**ИТОГИ 30-мин прогона (завершён 20:26:52 +03)**:
|
||
- Операций 105 810 (рекорд серии); корректных 12 433; ожидаемых отказов 93 362;
|
||
неожиданных **15 = все ReadTimeoutError (сеть, 0.014%)**; сервисных 0.
|
||
- Некорректное принято: 0. Сбоев /health: 0. Рестарты пода: 0 (157 мин работы).
|
||
- Латентность: min 0.004с, avg 0.045с, max 51.9с (один потерянный ответ + ретраи).
|
||
- MTU-зависаний нет (большие тела исключены из тестов).
|
||
- Все граничные сценарии работают: NonExistentQueue 24 896, InvalidParameterValue
|
||
(AWS) 18 672, MissingParameter 12 448, ReceiptHandleIsInvalid 6 226, Purge ×2,
|
||
batch 11/дубли/пустой — по 6 224, превышение лимита (очередь 1024) 6 224.
|
||
|
||
**ИТОГ СЕРИИ v0.1.35**: сервис под нагрузкой стабилен (0 падений, 0 сервисных
|
||
ошибок, 0 принятых недопустимых условий). Остаточные неожиданные — только
|
||
сетевые потери ответов (0.01–0.04%). Ограничение платформы (большие POST ~51с,
|
||
MTU) — в поддержку Nubes.
|
||
|
||
## СУРОВЫЙ ТЕСТ: 3 тенанта одновременно (14.08.2026)
|
||
|
||
**Подготовка**: токены 3 юзеров (`~/nubes/tokens_all.txt`); email из JWT;
|
||
детерминированные креды вычислены алгоритмом сервиса; логин `POST /ui/api/auth`
|
||
по всем трём токенам — 200, max_queues=50 (PingNubesAPI перебирает стенды
|
||
PROD/TEST/DEV — токен от любого стенда принимается). Мусор вычищен до старта.
|
||
|
||
**Прогон**: три `load_test.py` параллельно, 12 воркеров на один под (CPU 500m),
|
||
30 мин, запуск 21:18:21 +03, завершение ≈ 21:48 +03.
|
||
|
||
**Итоги по тенантам** (неожиданные — только ReadTimeout, сеть):
|
||
|
||
| Тенант | Операций | ok | Ожид. отказов | Неожиданных | soft_warn | /health | avg |
|
||
|---|---|---|---|---|---|---|---|
|
||
| tazetdinovn@gmail.com | 60 792 | 7 148 | 53 640 | 4 | 0 | 0 | 0.097с |
|
||
| tazet@narod.ru | 56 083 | 6 593 | 49 485 | 5 | 0 | 0 | 0.104с |
|
||
| ntazetdinov@nubes.ru | 51 716 | 6 075 | 45 632 | 9 | 0 | 0 | 0.107с |
|
||
|
||
Суммарно 168 591 операция (×1.6 одиночного прогона). Некорректное принято: 0 у всех.
|
||
Под: restarts 0 (4ч13м работы). После прогона очереди вычищены до 0/0/0.
|
||
В `load_test.py` добавлена авточистка очередей до и после прогона (коммит `77b8586`).
|
||
|
||
## Локализация 51с — финальные факты (15.08.2026)
|
||
|
||
**Топология входа** (kubectl, только чтение):
|
||
- Все домены (sqs/drhider/pythonk8s/…) — один `shturval-ingress-controller`
|
||
(nginx) в ns `ingress`, Service LoadBalancer externalIP **185.247.187.151**,
|
||
NodePort 443:32391; kube-vip (DaemonSet `shturval-vip-*`, hostNetwork) — по
|
||
поду на каждую из 4 нод.
|
||
- Наш сервис: ClusterIP 10.104.111.239:4100 → под 172.16.2.226 (нода bhbvs).
|
||
- Ingress-поды: control-plane + v8zq4, MTU 1400; наш под MTU 1400 — внутри кластера чисто.
|
||
|
||
**Решающие замеры с ВМ**:
|
||
- MSS от VIP = **1448** (платформа клампит, но недостаточно: сегменты 1448 →
|
||
IP-пакеты 1488 > underlay 1450 → дроп при DF).
|
||
- Большой POST (100KB) через VIP → ответ 403 через **51.08с** (воспроизведено сырым сокетом).
|
||
- Ноды кластера с ВМ НЕ доступны напрямую (`No route to host` на 10.10.102.4:32391) —
|
||
L2-обхода нет, ВМ→ноды идёт тем же платформенным маршрутом.
|
||
|
||
**ВЫВОД**: дроп — на платформенном участке «клиент → VIP» (underlay 1450,
|
||
MSS clamp 1448, PMTUD мёртв). Кластером (kubectl) НЕ чинится — чинится только:
|
||
1) поддержкой Nubes (clamp MSS ≤1400 на шлюзах / починить PMTUD); 2) обходным
|
||
прокси на ВМ с MTU 1400 на ens192 (вход на ВМ чист — факт drhider; выход ВМ→кластер
|
||
починится уменьшением MTU ВМ); 3) временным лимитом размера в сервисе.
|
||
|
||
## План Соннета — выполнение (15.08.2026)
|
||
|
||
**Backup**: ветка `backup-v0.1.35-2026-08-15` (запушена в Gitea), работа в `main`.
|
||
|
||
**P0.1 Серверные таймауты — СДЕЛАНО** (коммит `af0325e`, digest `sha256:4385597f…`):
|
||
`http.Server{ReadHeaderTimeout:10s, WriteTimeout:35s, IdleTimeout:90s, MaxHeaderBytes:8192}`,
|
||
`ReadTimeout` убран (не рвать большие/медленные тела и long-poll).
|
||
|
||
**P0.2 Рестарт с in-flight — СДЕЛАНО, PASS** (`tests/reboot_probe.py`):
|
||
10 очередей × 20 сообщений + 100 сообщений (50 in-flight) → `kubectl delete pod
|
||
--grace-period=0 --force` → после подъёма: /health ok; restore 10×20 OK;
|
||
main: получено 100, уникальных 100, пропущено 0, дублей 0.
|
||
|
||
**НАЙДЕН ДЕФЕКТ (критичный)**: после деплоя таймаутов под рестартовал — и у
|
||
тенанта «воскресли» 50 удалённых ранее очередей (лимит 50 → LimitExceeded).
|
||
Причина: `DeleteQueue` пишет удаление в Redis АСИНХРОННО (asyncWrite); при
|
||
SIGKILL/рестарте пода запись не успевает → очередь остаётся в Redis →
|
||
`LoadAllQueues` восстанавливает её. Удаления должны быть синхронными
|
||
(хотя бы DEL очереди) — НЕ ИСПРАВЛЕНО, задокументировано.
|
||
|
||
## P2.6 FIFO/DLQ e2e — результаты (15.08.2026)
|
||
|
||
`tests/fifo_dlq_probe.py`:
|
||
- **FIFO порядок**: PASS — 10 сообщений группы выданы строго m0..m9.
|
||
- **FIFO dedup** (дубль с тем же MessageDeduplicationId внутри окна): PASS —
|
||
в очереди одно сообщение («first»).
|
||
- **DLQ (RedrivePolicy)**: **FAIL — найден баг**: `setQueueAttributesV1` ищет DLQ
|
||
по ГОЛОМУ имени `models.SyncQueues.Queues[queueName]`, а ключи в map
|
||
tenant-scoped "{accessKey}:{queueName}" → DLQ не находится → CreateQueue
|
||
с RedrivePolicy всегда InvalidAttributeValue. RedrivePolicy не работает.
|
||
НЕ ИСПРАВЛЕНО, задокументировано.
|
||
|
||
**Истечение dedup-окна (5 мин) не проверено** — тест быстрый, только «внутри окна».
|
||
|
||
## ИСПРАВЛЕНИЯ дефектов (15.08.2026, коммит `7467ecc`, digest `sha256:b81f855a…`)
|
||
|
||
1. **Воскрешение удалённых очередей/сообщений** (critical): все УДАЛЕНИЯ из
|
||
Redis переведены в синхронный режим (DeleteQueue, DeleteMessagePersist,
|
||
DeleteMessagesPersist, PurgeMessagesPersist; таймауты 3–5с). Записи
|
||
(SaveMessage/SaveMessages/SaveQueue/SaveTenantRaw) остались асинхронными.
|
||
Причина дефекта: удаления шли через asyncWrite и терялись при SIGKILL/рестарте.
|
||
2. **RedrivePolicy (DLQ) не работал**: `setQueueAttributesV1` искал DLQ по
|
||
голому имени, а ключи очередей tenant-scoped → InvalidAttributeValue всегда.
|
||
Теперь DLQ ищется по `{tenantAccessKey}:{queueName}`; в вызовы передан t.AccessKey.
|
||
|
||
**Верификация после деплоя**:
|
||
- `fifo_dlq_probe.py`: FIFO порядок PASS, FIFO dedup PASS, **DLQ PASS**
|
||
(сообщение после 2 попыток ушло в DLQ, в основной пусто).
|
||
- Тест «воскрешение»: 3 очереди созданы и удалены → SIGKILL пода → после подъёма
|
||
удалённые НЕ вернулись (осталась только сирота от первого FAIL-прогона DLQ —
|
||
удалена вручную). Фикс работает.
|
||
|
||
|
||
## 2026-08-15 (продолжение) — план Соннета: P1.4, P2.5, P3.7, P3.8, фикс api_test
|
||
|
||
**Регрессия api_test после фиксов удалений (найдена и закрыта)**:
|
||
- После деплоя синхронных удалений api_test стал 21/22: FAIL на DeleteMessage
|
||
(ReceiptHandleIsInvalid), стабильно 2 прогона.
|
||
- Диагностика: тест делал receive → ChangeMessageVisibility(timeout=**1с**) → delete.
|
||
По AWS-семантике через 1с сообщение снова видимо и handle сбрасывается →
|
||
delete по старому handle → ReceiptHandleIsInvalid. Сетевые задержки >1с
|
||
(в т.ч. шум MTU-проблемы платформы) приводили к случайному FAIL.
|
||
- Замеры: CV=1с → run1 FAIL, runs 2-3 OK; CV=5с → **5/5 OK (fails=0/5)**.
|
||
Сервис корректен (AWS-семантика), тест использовал слишком агрессивный таймаут.
|
||
- Фикс: `tests/api_test.sh` — `--visibility-timeout 1` → `5` + комментарий.
|
||
- Прогон после фикса: **22/22**. Коммит bdc7336, запушен.
|
||
|
||
**P1.4 long-poll leak (200 клиентов)**:
|
||
- 200 одновременных receive с WaitTimeSeconds=20 (boto3, без ретраев), очередь
|
||
longpoll-leak-*, после — cleanup. Ошибок клиента: 0.
|
||
- Threads пода: 12 до → **13 после**. Утечки горутин/потоков НЕТ. PASS.
|
||
|
||
**P2.5 CLI v2 в нагрузке** (`tests/cli_load_test.py`, на ВМ):
|
||
- 50 циклов aws cli send/receive/delete = 150 операций, failed=0.
|
||
- Латентность: send p50=775мс p95=936мс; receive p50=761мс p95=826мс;
|
||
delete p50=761мс p95=839мс. PASS.
|
||
|
||
**P3.8 граница лимита очередей (DefaultTenantMaxQueues=50)**:
|
||
- Создание до отказа: ровно **50** создаются, **51-я → LimitExceeded**
|
||
(AWS.SimpleQueueService.LimitExceeded). После cleanup 0 очередей. PASS.
|
||
|
||
**P3.7 RSS 2+ часа (запущен фоновый мониторинг)**:
|
||
- nohup на ВМ: каждые 60с пишет VmRSS/Threads в `rss_monitor.log` (125 сэмплов).
|
||
- Стартовый сэмпл 08:16 +0300: VmRSS=16588 kB, Threads=13.
|
||
- Результат — после завершения мониторинга.
|
||
|
||
## 2026-08-15 (вечер) — финальная полная регрессия на ВМ + флап fifo_dlq_probe
|
||
|
||
**Запуск**: всё на ВМ (пользователь оффлайн), `tests/run_full_regression.sh`
|
||
в nohup → `full_regression.log`. Длительность 24с.
|
||
|
||
**Результаты**:
|
||
- api_test.sh: **22/22** (PASS=22 FAIL=0)
|
||
- sdk_test.py: **15/15** (PASS=15 FAIL=0)
|
||
- fifo_dlq_probe.py: **флап** — 3 прогона: PASS / FAIL / PASS (dlq: count=0).
|
||
|
||
**Разбор флапа dlq (сервис корректен, гонка в тесте)**:
|
||
- Механика сервиса: возврат видимости и перенос в DLQ делает тик PeriodicTasks
|
||
(период 1с) при истечении VisibilityTimeout. Перенос — при Retry >= MaxReceiveCount.
|
||
- Тест: VisibilityTimeout=1с, sleep 1.5с между receive, после 3-й попытки
|
||
СРАЗУ drain(dlq). При рассинхроне фазы тика (attempt 2 empty — тик не успел
|
||
вернуть сообщение за 1.5с) вторая доставка случается в attempt 3, и проверка
|
||
DLQ выполняется до тика переноса → dlq пуст → FAIL. Через ~2с сообщение
|
||
всё равно ушло бы в DLQ.
|
||
- Доказательство: `tests/dlq_deterministic_probe.py` — тот же сценарий, но с
|
||
ожиданием 2.5с после каждой доставки: **5/5 PASS**
|
||
(r1=1 r2=1 r3=0 dlq=1 main_left=0 во всех 5 прогонах).
|
||
- Вывод: перенос в DLQ работает стабильно; флап — исключительно тайминг теста.
|
||
|
||
**Предложенная правка** `tests/fifo_dlq_probe.py` (жду «делай»): после цикла
|
||
попыток — опрос DLQ до 10с (drain каждые 1с) вместо мгновенного drain.
|
||
|
||
**RSS-мониторинг P3.7**: продолжает писаться в `rss_monitor.log` (старт
|
||
08:16 +0300, VmRSS=16588 kB, Threads=13).
|
||
|
||
## 2026-08-15 — фикс fifo_dlq_probe применён, финальная регрессия зелёная
|
||
|
||
**Правка** (`tests/fifo_dlq_probe.py`, по команде «делай»): после цикла попыток
|
||
DLQ опрашивается до 10с (drain каждые 1с) вместо мгновенной проверки —
|
||
устранена гонка с тиком PeriodicTasks (период 1с).
|
||
|
||
**Проверка**: 5/5 PASS подряд, включая «тяжёлые» фазы (attempt 2 empty /
|
||
attempt 3 received), которые раньше флапали.
|
||
|
||
**Финальная полная регрессия на ВМ** (`full_regression2.log`, 26с):
|
||
- api_test.sh: **PASS=22 FAIL=0**
|
||
- sdk_test.py: **PASS=15 FAIL=0**
|
||
- fifo_dlq_probe.py: fifo_order PASS, fifo_dedup PASS, dlq PASS, exit=0
|
||
|
||
ВСЕ тесты зелёные. План Соннета выполнен: P0.1, P0.2, P1.4, P2.5, P2.6,
|
||
P3.8 — PASS; P3.7 (RSS 2ч) — мониторинг в процессе.
|
||
|
||
## 2026-08-15 — P3.7 (RSS 2ч) завершён: PASS
|
||
|
||
Мониторинг отработал полные 125 сэмплов (каждые 60с), 08:16 → 10:21 +0300:
|
||
- VmRSS: старт 16588 kB → финал **15648 kB** (не растёт, даже −940 kB после
|
||
стабилизации GC). Плато 15648 kB держится последние сэмплы.
|
||
- Threads: стабильно **13** на всём интервале.
|
||
- Утечки памяти/потоков НЕТ. План Соннета выполнен полностью.
|
||
|
||
## 2026-08-15 (день) — сравнение shared-SQS vs Yandex YMQ (очередь newsqs)
|
||
|
||
**Скрипт**: `tests/compare_ymq_vs_shared.py` (новый, локально + ВМ). N=50,
|
||
тело 512 байт, последовательно: send x50 → receive+delete x50. retries=0.
|
||
|
||
**Прогон 1** (ВМ):
|
||
- shared-SQS: send p50=6мс p95=17мс **max=12648мс**; receive p50=7мс; delete p50=8мс; 50/50, errs=0, total=13.9с.
|
||
- YMQ: send p50=62мс p95=76мс max=84мс; receive p50=60мс; delete p50=62мс; 50/50, errs=0, total=9.4с.
|
||
|
||
**Прогон 2** (ВМ, повтор):
|
||
- shared-SQS: send p50=8мс p95=16мс **max=25446мс**; receive p50=6мс; delete p50=7мс; 50/50, errs=0, total=26.6с.
|
||
- YMQ: send p50=62мс p95=69мс max=72мс; receive p50=60мс; delete p50=62мс; 50/50, errs=0, total=9.5с.
|
||
|
||
**Выводы (факты)**:
|
||
1. По p50 наш сервис в ~8–10 раз быстрее YMQ (6–8мс против 60–62мс).
|
||
2. У shared-SQS в КАЖДОМ прогоне ровно один send зависает на 12.6с / 25.4с
|
||
(тело 512 байт — размер ни при чём; errs=0, ответ приходит). YMQ — без
|
||
выбросов, стабилен.
|
||
3. Гипотеза выброса: платформенные «паузы» сети к поду Nubes (ранее фиксировали
|
||
зависания ~51с на больших POST из-за MTU/MSS 1448) — но при 512 байтах
|
||
причина требует отдельного расследования (tcpdump/тайминги платформы).
|
||
|
||
## 2026-08-15 — ДЛИННОЕ сравнение с ЛОКАЛИ (30 мин), shared-SQS vs YMQ
|
||
|
||
**Скрипт**: `tests/long_compare_local.py` (ping-pong: пул K=10, раунды
|
||
попеременно shared→ymq→shared..., receive→delete→send; retries=0,
|
||
read_timeout=30с; лог `tests/long_compare.log`). Локаль (WSL), 12:27–12:57.
|
||
|
||
**Итог (317 раундов):**
|
||
- shared-SQS: send n=302 p50=90мс p95=269мс max=2785мс; receive n=293 p50=89мс
|
||
p95=279мс max=2577мс; delete n=293 p50=90мс p95=103мс max=2462мс;
|
||
**errors=49 ReadTimeoutError (30с)**.
|
||
- YMQ: send n=327 p50=138мс p95=157мс max=355мс; receive n=316 p50=150мс
|
||
p95=421мс max=2473мс; delete n=316 p50=138мс p95=152мс max=2105мс;
|
||
errors=1 ProxyConnectionError.
|
||
|
||
**Выводы (факты):**
|
||
1. По p50 наш быстрее: 89–90мс против 138–150мс (~1.5–1.7 раза).
|
||
2. НО у нас **49 запросов из 888 (5.5%) вообще не получили ответ за 30с**
|
||
(каждый таймаут — 30с простоя); у Яндекса 1 ошибка из 959.
|
||
3. Полезной работы за 30 мин: наш 888 операций, Яндекс 959 — таймауты съели
|
||
преимущество по скорости.
|
||
4. Тренд: таймауты равномерны весь тест (~1 в 37с) — системная проблема
|
||
сетевого пути локаль→шлюз Nubes, не код сервиса (с ВМ их нет).
|
||
5. Выбросы max (2.5–2.8с) есть у ОБОИХ: наш send max=2785мс, YMQ receive
|
||
max=2473мс — сетевые, не сервисные.
|
||
|
||
## 2026-08-15 — план сетевых экспериментов от DeepSeek V4 Flash (НЕ Соннет)
|
||
|
||
Пользователь по ошибке задал вопрос DeepSeek V4 Flash вместо Соннета.
|
||
План сохранён: `doc/thinking/nubes-network-bug-plan.md` с пометкой о слабом
|
||
агенте и поправками основного агента.
|
||
|
||
Оценка плана (основной агент): в целом сильный — верно выделяет ДВА явления
|
||
(MTU для больших + независимые потери для малых 512 байт), верная декомпозиция
|
||
по request_time/upstream_response_time nginx. Но: ошибка в команде cilium
|
||
monitor (через cilium-operator — неверно, надо cilium-под), echo-сервис
|
||
переоценён (проблема между интернетом и внутренней сетью — echo внутри неё),
|
||
tcpdump на WSL требует sudo/интерфейс, шаг 7 (матрица размеров) слишком
|
||
дорогой. Рекомендованный старт: nginx-логи → tcpdump → cilium monitor.
|
||
|
||
## 2026-08-15 — план v2 от DeepSeek V4 Pro (новый чат, НЕ Соннет) + сравнение
|
||
|
||
Сохранён: `doc/thinking/nubes-network-bug-plan-v2-deepseek-pro.md`.
|
||
|
||
Оценка: план Pro сильнее Flash. Плюсы: сырые TCP с критерием доставки по TCP
|
||
ACK (не HTTP), PMTU-зонд ping -M do с порогами 1372/1422/1460/1472, второй
|
||
клиент с другого ISP, kubectl port-forward как изоляция приложения, таблица
|
||
«что считается доказательством», верное наблюдение что 51с — прикладной таймер
|
||
а не TCP-backoff. Минусы: kube-proxy может отсутствовать (Cilium replacement),
|
||
debug-контейнеры на managed скорее всего запрещены (план сам оговаривает),
|
||
эхо-NodePort требует открыть NodePort на платформе (проверяемо).
|
||
|
||
Сравнение Flash vs Pro: Flash — декомпозиция по nginx-логам, но ошибка
|
||
cilium-operator, echo переоценён, нет PMTU/port-forward/второго ISP.
|
||
Pro — методологически полнее, грубых ошибок нет.
|
||
Рекомендованный синтез-порядок: nginx-логи → PMTU-зонд → сырые TCP →
|
||
port-forward → tcpdump клиента → второй ISP → cilium monitor → echo → тикет.
|
||
|
||
## 2026-08-15 — план v3 от СОННЕТА + сравнение трёх планов
|
||
|
||
Сохранён: `doc/thinking/nubes-network-bug-plan-v3-sonnet.md`.
|
||
|
||
Оценка (основной агент): план Соннета — лучший из трёх. Ключевые достоинства:
|
||
явное разделение явлений A (MTU/PMTUD, 51с = экспонента RTO 1-2-4-8-16) и
|
||
B (регулярность 1/37с = таймер gateway/kube-vip/conntrack), port-forward первым
|
||
kubectl-тестом, точный список размеров ping-зонда, корреляция по TCP seq.
|
||
Поправки: fallback если ICMP закрыт (mtr --tcp/TCP-порог), добавить второй
|
||
ISP и контрольный канал YMQ, port-forward идёт через API-сервер (исключает
|
||
приложение, но не доказывает шлюз), точный интервал таймаутов мерить новым
|
||
тестом с timestamp на каждый таймаут.
|
||
Сравнение: Flash — ошибки и недооценка; Pro — силён, но без явного разделения
|
||
явлений; Соннет — принять за основу с поправками.
|
||
|
||
## 2026-08-15 — план v4 от ОПУС 4.8 + ответы на его уточнения
|
||
|
||
Сохранён: `doc/thinking/nubes-network-bug-plan-v4-opus.md` (план + ответы).
|
||
|
||
Опус изучил репозиторий и три предыдущих плана; его план согласуется с
|
||
Соннетом (разделение A/B, port-forward первым, tcpdump по seq, cilium monitor
|
||
на поде). Ответы основного агента на уточнения Опуса:
|
||
1) второго ISP нет, полагаемся на контрольный канал YMQ (1 ошибка на 959
|
||
против 49 таймаутов Nubes — локальный интернет исключён);
|
||
2) debug-под с NET_RAW скорее всего запрещён, серверный tcpdump не получится —
|
||
nginx-логи + cilium monitor + echo А/Б;
|
||
3) echo деплоить только если фазы 0–2 не хватят (риск: NodePort на managed).
|
||
Поправка: port-forward идёт через API-сервер — доказывает невиновность
|
||
приложения, но не вину шлюза.
|
||
Все 4 плана сохранены в doc/thinking/: Flash, Pro, Sonnet, Opus.
|
||
|
||
## 2026-08-15 — финал плана (Опус) + сравнение ЧЕТЫРЁХ агентов
|
||
|
||
Финал дописан в `doc/thinking/nubes-network-bug-plan-v4-opus.md` (решения
|
||
зафиксированы: YMQ-канал вместо второго ISP; NET_RAW — одна попытка; echo
|
||
отложить; port-forward = только невиновность приложения).
|
||
|
||
Мнение основного агента о четырёх ответах на один промпт: условия не равны
|
||
(Соннет читал Flash/Pro, Опус читал все + репозиторий — накопленный контекст).
|
||
Вклады: Flash — первым увидел явление B; Pro — инструменты (PMTU-зонд, второй
|
||
ISP, port-forward, таблица доказательств); Sonnet — структура (A/B, RTO,
|
||
таймер 37с); Opus — интеграция и фиксация решений. Конвергенция на одной
|
||
цепочке = план устойчив. Итог — продукт всех участников. План готов к
|
||
исполнению: Фаза 0 → 1 → 2 → 3 → 4.
|
||
|
||
## 2026-08-15 — Фаза 0 старт: kubectl НЕДОСТУПЕН (токен истёк)
|
||
|
||
Шаги:
|
||
1. `kubectl -n ingress get pods/svc` с ВМ → "You must be logged in to the
|
||
server (server has asked for credentials)".
|
||
2. Диагностика: kubeconfig ~/.kube/config на ВМ от **Aug 14 12:29**, контекст
|
||
`tazetdinovn@gmail.com@iot-naeel`, авторизация — token (exec-plugin нет).
|
||
Токен истёк к 14:13 15.08 (срок жизни ~26 ч).
|
||
3. История shell ВМ: kubeconfig ранее обновлялся вручную:
|
||
`xclip -selection clipboard -o > ~/.kube/config` — т.е. пользователь копирует
|
||
новый kubeconfig из буфера (консоль Nubes) на ВМ.
|
||
|
||
Результат: Фаза 0 заблокирована — нужен новый kubeconfig от пользователя
|
||
(консоль Nubes → скопировать → на ВМ: xclip -o > ~/.kube/config или заменить
|
||
файл). После обновления продолжить: nginx access-log → kube-vip логи →
|
||
логи пода.
|
||
|
||
## 2026-08-15 — Фаза 0 ВЫПОЛНЕНА: nginx чист, kube-vip с ошибками leaderelection
|
||
|
||
Окно теста: 08:26:47–09:00 UTC (30-мин тест 12:27–12:57 локального времени).
|
||
|
||
**nginx access-log (оба пода ingress, за окно):**
|
||
- 874 записи к нашему хосту: 862×status=200, 12×404 (боты).
|
||
- **max ingress_request_time = 0.068с** за весь тест.
|
||
- **status=499 = 0** (ни одного клиентского обрыва).
|
||
- Вывод: с точки зрения nginx ВСЕ запросы обработаны за миллисекунды и
|
||
успешно. 49 клиентских таймаутов по 30с nginx НЕ видит → ответы теряются
|
||
МЕЖДУ nginx и клиентом (обратный путь/шлюз), либо часть запросов не дошла
|
||
до nginx (записей 862 против 888 операций — но боты засоряют счёт, точный
|
||
сплит не выводим).
|
||
|
||
**kube-vip (новое подозрение):**
|
||
- Под shturval-vip-dp24p (нода control-plane) — **каждые ~1с ошибка**:
|
||
`leaderelection: error initially creating leader election record: namespaces
|
||
"0a42bef3-7ce1-42b7-aa80-4274c4d9568f" not found`.
|
||
- Под пытается создать запись лидера в НЕСУЩЕСТВУЮЩЕМ namespace — похоже на
|
||
настроечный баг платформы. Влияние на VIP неизвестно — нужны логи остальных
|
||
4 vip-подов (кто лидер, были ли флапы).
|
||
|
||
**Наш под:** 682 строки логов за окно, ошибок нет; 13 warnings «Receipt Handle
|
||
not found» только в 08:58–08:59 (фаза cleanup — норма).
|
||
|
||
**Следующие шаги Фазы 0:** логи остальных kube-vip подов (лидерство/флапы) →
|
||
externalTrafficPolicy сервиса ingress → Фаза 1 (PMTU-зонд + новый тест с
|
||
timestamp таймаутов).
|
||
|
||
## 2026-08-15 — Фаза 0 добита: kube-vip provider ломается, vipHost на control-plane
|
||
|
||
- Сервис ingress: `kube-vip.io/vipHost: iot-naeel-control-plane-xb699` —
|
||
VIP (185.247.187.151) статически закреплён за control-plane нодой.
|
||
- externalTrafficPolicy: Cluster, type: LoadBalancer, nodePorts: 32391 (https),
|
||
30739 (http).
|
||
- Provider kube-vip: «failed to ensure load balancer: no address pools could be
|
||
found» с экспоненциальным backoff — в окне теста события в 08:20:56, 08:25:57,
|
||
08:30:57 UTC (потолок 5 мин). Прямой корреляции с таймаутами ~37с НЕТ.
|
||
- ВСЕ 4 vip-пода (3 worker + 1 control-plane) каждые ~1с бьются в leaderelection:
|
||
namespaces "0a42bef3-..." not found (namespace удалён, поды остались).
|
||
- Итог Фазы 0: nginx чист (max 68мс, 499=0) → потери между nginx и клиентом;
|
||
kube-vip сконфигурирован с ошибками (сломанный leader election + LB без
|
||
пулов) — кандидат в тикет, но прямой связи с 37с пока не доказано.
|
||
|
||
## 2026-08-15 — Фаза 1: 403 InvalidClientTokenId С ЛОКАЛИ (с ВМ работает)
|
||
|
||
Факты:
|
||
1. TCP-зонд MTU-порога (443, бинарный): до 20KB — всё мгновенно (403 от пода,
|
||
mss=1448 подтверждён). В диапазоне 20–200KB — кластеры зависаний на 20с
|
||
(«порог» ~82KB), но зависания кластерные и, вероятно, не от размера.
|
||
2. 60 чистых connect к 443 — 0 сбоев; 5-мин серия POST /health (512б, 538 проб)
|
||
— **0 сбоев** → явление B не воспроизводится на /health, привязано к
|
||
SQS-трафику или к другим условиям.
|
||
3. Порт 32391 (NodePort) с локали НЕДОСТУПЕН (SYN дроп), реальный путь — 443.
|
||
4. **НОВОЕ**: с ~14:30 все AWS-запросы С ЛОКАЛИ дают 403 InvalidClientTokenId
|
||
(aws CLI и boto3, креды те же). С ВМ те же креды РАБОТАЮТ (list-queues OK).
|
||
/health с локали — 200. Значит: внешний путь к шлюзу теперь отвергает
|
||
токен, внутренний — принимает. Днём (30-мин тест 12:27–12:57) внешний путь
|
||
РАБОТАЛ → аутентификация внешнего пути сломалась/сбросилась между 13:00 и
|
||
14:30. Возможная связь: обновление kubeconfig пользователем ~14:10–14:20.
|
||
5. Фаза 1 (SQS-цикл с таймстемпами) ЗАБЛОКИРОВАНА этим 403.
|
||
|
||
## 2026-08-15 — ОПРОВЕРЖЕНИЕ: «403 InvalidClientTokenId с локали» = моя ошибка env
|
||
|
||
Разбор: после команд YMQ в терминале остались экспортированные
|
||
AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEY от Яндекса (YCAJE.../YCNbo...).
|
||
boto3-скрипт и aws CLI без явных кредов подхватывали ИХ → shared-SQS отвечал
|
||
InvalidClientTokenId. С ВМ я всегда экспортировал креды shared-SQS явно —
|
||
поэтому «с ВМ работало». Явления C НЕТ. Фейковая подпись проверена: с локали
|
||
запрос доходит до пода корректно (SignatureDoesNotMatch), auth работает.
|
||
Урок: в boto3 всегда передавать aws_access_key_id/aws_secret_access_key явно,
|
||
не полагаться на env терминала.
|
||
|
||
## 2026-08-15 — ПЕРИОД ТАЙМАУТОВ ИЗМЕРЕН: ровно ~32.9с + полная проверка
|
||
|
||
**SQS-цикл 10 мин (Фаза 1, локаль, явные креды shared):** 348 раундов
|
||
(receive→delete→send 512б), 19 ReadTimeout (30с). Интервалы между сбоями:
|
||
32.8, 32.8, 32.8, 32.9, 32.8, 32.9, 32.9, 32.9, 32.8, 32.9, 32.9, 32.7, 32.9,
|
||
32.9, 32.9, 32.8, 33.0, 32.8 — **все 18 интервалов в 32.7–33.0с**.
|
||
Вывод: это ТОЧНО таймер (равномерность до 0.1с), не случайные потери.
|
||
Кандидаты: 32.9с ≈ период какого-то шлюзового цикла (kube-vip announce,
|
||
conntrack GC, session timeout) — искать у платформы.
|
||
Примечание: ранее оценка «~37с» была по косвенным данным, точный период 32.9с.
|
||
|
||
**Полная проверка (по требованию):**
|
||
1. py_compile всех tests/*.py — OK. bash -n всех tests/*.sh — OK.
|
||
2. Креды YMQ (newsqs) — рабочие (list-queues OK). Креды shared — рабочие.
|
||
3. В secrets/ лежат ДВА файла с ОДИНАКОВЫМИ новыми кредами: yandex_ymq_newsqs.txt
|
||
и yandex_ymq_fork8s.txt (дубликат, 461 байт, 11:54/11:55). Старых кредов
|
||
YCAJEQDz нигде нет. Дубликат fork8s.txt подлежит удалению (по команде).
|
||
4. git status: не закоммичен только tests/tcp_mtu_probe.py (сейчас коммитится).
|
||
5. Эпизод «403 с локали» — подтверждённо моя ошибка env (креды Яндекса в
|
||
терминале), не платформа. Сейчас все команды идут с ЯВНЫМИ кредами.
|