Files
SQS-service/HISTORY/2026-08-14-session-log.md
T

96 KiB
Raw Blame History

Журнал сессии — 2026-08-14 (продолжение миграции shared-sqs)

Рабочая папка: /home/naeel/nubes/SQS-service


Текущее состояние

  • Сервис ЗАДЕПЛОЕН и работает: https://sqs.containerk8s.dev.nubes.ru
  • /healthOK (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):


Выводы (факты)

  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); bySubbyEmail в 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: индекс bySubbyEmail; 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 после успеха; jwtMiddlewarePingNubesAPICached; 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 110 ошибка; 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: 110, выход → 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 248KB — 0.090.22с; 5664KB — то быстро, то виснет (порог плавает); 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; таймауты 35с). Записи (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 15 + комментарий.
  • Прогон после фикса: 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:2712: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мс против 138150мс (~1.51.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:4709: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:5808: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 (креды Яндекса в терминале), не платформа. Сейчас все команды идут с ЯВНЫМИ кредами.

2026-08-15 — Фаза 2: port-forward тест PASS (приложение НЕВИНОВНО)

kubectl port-forward svc/containerk8s 14100:4100 на ВМ (через API-сервер, в обход шлюза/VIP/NodePort). SQS-цикл 10 мин (те же креды, та же нагрузка receive→delete→send 512б): 24977 раундов, 0 сбоев (~42 оп/с). Для сравнения: тот же цикл через внешний шлюз даёт сбой каждые 32.9с. Вывод: приложение и под полностью исправны; таймауты рождаются на внешнем пути (шлюз/VIP/NodePort/underlay). Следующий шаг — cilium monitor во время внешнего теста + попытка kubectl debug.

2026-08-15 — СЕРВЕРНЫЙ TCPDUMP: потеря на участке клиент→ingress, под чист

Метод: kubectl debug (ephemeral-контейнер netshoot) в поде shared-sqs + tcpdump -tttt -i eth0 port 4100 (вывод — в логи контейнера). Внешний SQS-тест 4 мин: 8 ReadTimeout, интервалы 31.131.4с.

Результат захвата (1564 пакета):

  • 8 «дыр» тишины по 29.5–29.7с, период между дырами 31.2с — ровно 8 сбоев.
  • Внутри каждой дыры НЕТ ни одного пакета: ни SYN, ни данных, ни ретрансмитов. Зависший запрос до пода НЕ ДОШЁЛ (иначе его SYN/данные были бы видны). Ретра-ответов пода нет → под ничего не отправлял.
  • Вне дыр — полные транзакции ingress(172.16.0.73/172.16.3.149)→под за 23мс.
  • Внутренний MSS ingress↔под = 1310 (MTU пода 1400), внешний MSS = 1448.

ВЫВОД (доказано захватом): в момент каждого таймаута запрос теряется на участке клиент→ingress (интернет-шлюз/VIP/kube-vip/NodePort платформы). Внутри кластера потерь нет: cilium drop = 0, ingress↔под 23мс, port-forward 24977 раундов 0 сбоев. Явление B = периодический (31–33с) сбой внешнего шлюза платформы на ПРЯМОМ пути.

2026-08-15 — Фаза 4: черновик тикета в Nubes готов

Собран doc/thinking/nubes-ticket.md: явление A (MSS 1448, PMTUD сломан) и явление B (периодический сбой каждые 31–33с на участке клиент→ingress) с 6 доказательствами и просьбой к платформе. Все фазы плана выполнены: Фаза 0 (nginx/kube-vip логи), Фаза 1 (PMTU/TCP-зонды, период 31-33с), Фаза 2 (port-forward 24977/0, cilium 0 дропов, серверный tcpdump — 8 дыр), Фаза 3 (echo НЕ ПОТРЕБОВАЛСЯ — доказательств достаточно), Фаза 4 (тикет).

2026-08-15 — ПОЛНЫЙ ОБЗОР КЛАСТЕРА (read-only): 2 дефекта платформы, таймера 31с внутри НЕТ

Здоровое: 4/4 ноды Ready (130д; bhbvs 47д), CPU max 36%, RAM max 54%; все поды Running, свежих рестартов нет; все DaemonSet 4/4; Cilium 1.17.7 Ok (KubeProxyReplacement=True, Direct Routing), 0 дропов; Kyverno — 0 нарушений; cert-expiration — чисто; единственный LB-сервис — ingress (185.247.187.151).

Найденные дефекты (всё — платформа):

  1. kube-vip leader election: все 4 пода (DaemonSet shturval-vip) каждую секунду бьются в несуществующий namespace 0a42bef3-7ce1-42b7-aa80-4274c4d9568f (удалён). Причина: svc_election=true в DS, поды живут 34 дня без рестарта, в их памяти кэш удалённого LB-сервиса. Перезапуск DS вылечил бы шум. vip_leaderelection=false, leasename sht-uservip-cp-lock.
  2. kube-vip provider: LB-пулы не настроены вообще (ConfigMap kubevip в ns kube-vip ОТСУТСТВУЕТ: нет cidr-global, нет range-*). SyncLoadBalancerFailed для сервиса ingress каждые ~5 мин (backoff-потолок). VIP живёт только благодаря статической аннотации kube-vip.io/vipHost=control-plane-xb699.
  3. services-controller платформы: цикл каждые 5 мин (deprecation warning Endpoints) — совпадает с ретраями provider.

Ключевое: в логах ВСЕХ компонентов кластера (kube-vip, cilium, контроллеры, ingress) НЕТ ни одного цикла с периодом 31–33с. Периоды внутри кластера: 1с (leader errors) и 5 мин (services/provider). Значит таймер 31–33с живёт НА ВНЕШНЕМ ШЛЮЗЕ (вне кластера) — согласуется с потерей на участке клиент→ingress. Это сильное дополнение к тикету.

2026-08-15 — ПОЧИНКА БАГОВ ПЛАТФОРМЫ (по команде) — план

План чинилки:

  1. Создать ConfigMap kubevip (ns kube-vip) с cidr-global: 185.247.187.151/32 — provider получит пул и перестанет ронять SyncLoadBalancerFailed.
  2. kubectl rollout restart ds -n kube-vip shturval-vip — сброс кэша удалённого LB-сервиса (namespace 0a42bef3-...) → уйдут ошибки leaderelection.
  3. Перепроверить: события кластера, логи vip-подов, /health с локали.
  4. Тест: внешний SQS-цикл с таймстемпами — проверить, ушёл ли период 31с. Риск: при рестарте vip-подов возможен краткий (секунды) флап ARP-анонса VIP.

2026-08-15 — ПОЧИНКА БАГОВ ПЛАТФОРМЫ ВЫПОЛНЕНА

Баг 1 (leaderelection в удалённый ns 0a42bef3): kubectl rollout restart ds -n kube-vip shturval-vip — после рестарта все 4 пода делают нормальные выборы лидера (lease ingress/kubevip-shturval-ingress-controller-controller), ошибок 0a42bef3 больше нет. Победил под на v8zq4, kube-vip обновил аннотацию vipHost: control-plane-xb699 → workers-vqphm-v8zq4 (VIP переехал штатно).

Баг 2 (provider без пулов): нашёл, что provider читает ConfigMap kubevip из namespace kube-system (env KUBEVIP_NAMESPACE=kube-system), а там лежал ПУСТОЙ ConfigMap (130д, 0 данных) — изначальная недонастройка платформы. Добавил cidr-global: 185.247.187.151/32 в kube-system/kubevip, удалил свой ошибочный CM в kube-vip, рестартнул provider. Результат: «Taking address from [cidr-global]», «Ensured load balancer», SyncLoadBalancerFailed прекратился.

Проверки после починки: /health с локали — 200 (36–41мс). LB IP сервиса ингресса в статусе: 185.247.187.151 (ipMode VIP). Далее — нагрузочный тест.

2026-08-15 — ТЕСТ ПОСЛЕ ПОЧИНКИ: период 31–33с НЕ ушёл

10-мин внешний SQS-цикл после фиксов kube-vip: 347 раундов, 19 ReadTimeout, интервалы 31.3–33.5с — тот же таймер. Вывод: починенные баги (leader election, LB-пулы) НЕ были причиной таймаутов — как и предполагалось. Таймер живёт на ВНЕШНЕМ шлюзе (вне кластера). Фиксы — гигиена платформы (шумы убраны), но решение таймаутов — только через поддержку Nubes (тикет doc/thinking/nubes-ticket.md готов).