Author SHA1 Message Date
“Naeel” 5fa34c2ca4 doc: HANDOFF-NEW-CHAT.md — резюме сессии для перехода в новый чат 2026-08-16 07:55:43 +04:00
“Naeel” 30bc5a135d HISTORY: тест после починки — период 31-33с не ушёл, таймер на внешнем шлюзе; тикет остаётся главным инструментом 2026-08-15 21:55:34 +04:00
“Naeel” 96ddb4adcc HISTORY: баги платформы починены — kube-vip рестарт (leader election ок), пул в kube-system/kubevip (provider Ensured) 2026-08-15 21:44:54 +04:00
“Naeel” 73e38bd426 HISTORY: план починки багов платформы (ConfigMap kubevip + restart DS) 2026-08-15 21:38:43 +04:00
“Naeel” 97c2c67b3b doc: тикет — дополнение: таймера 31с внутри кластера нет, он на внешнем шлюзе 2026-08-15 21:28:55 +04:00
“Naeel” 63a8319364 HISTORY: полный обзор кластера — 2 дефекта платформы (kube-vip), таймера 31с внутри кластера нет (он на внешнем шлюзе) 2026-08-15 21:28:37 +04:00
“Naeel” 22cad485c5 doc: черновик тикета в Nubes (явления A/B, 6 доказательств); все фазы плана выполнены 2026-08-15 21:17:17 +04:00
“Naeel” 4fdc5dce9c HISTORY: серверный tcpdump — 8 дыр по 29.5с, запросы до пода не доходят; потеря на участке клиент→ingress 2026-08-15 21:16:44 +04:00
“Naeel” ccab093621 HISTORY: Фаза 2 — port-forward 24977 раундов 0 сбоев, приложение невиновно 2026-08-15 20:50:28 +04:00
“Naeel” f33134d0c1 HISTORY: период таймаутов 32.9с (18 интервалов 32.7-33.0с) — таймер доказан; полная проверка скриптов и кредов OK; +tcp_mtu_probe.py 2026-08-15 20:38:01 +04:00
“Naeel” 65f151d093 HISTORY: опровержение явления C — 403 был из-за env-кредов Яндекса в терминале, не платформа 2026-08-15 16:06:28 +04:00
“Naeel” b31c5eb7e9 HISTORY: Фаза 1 — с локали 403 InvalidClientTokenId (с ВМ ок); /health 5мин чист; 32391 закрыт снаружи 2026-08-15 16:01:22 +04:00
“Naeel” ecf66e4b27 HISTORY: Фаза 0 — vipHost на control-plane; kube-vip: leaderelection сломан у всех, LB без пулов (backoff 5мин) 2026-08-15 15:34:47 +04:00
“Naeel” 095bfce8c8 HISTORY: Фаза 0 — nginx max 68мс/499=0 (ответы теряются nginx→клиент); kube-vip ошибки leaderelection каждые 1с 2026-08-15 15:33:52 +04:00
“Naeel” e448b20916 HISTORY: Фаза 0 — kubectl недоступен, токен kubeconfig истёк (~26ч), нужен новый от пользователя 2026-08-15 15:14:47 +04:00
“Naeel” 951626c457 HISTORY: финал плана и сравнение четырёх агентов 2026-08-15 15:05:23 +04:00
“Naeel” a23e397907 doc: финал плана (решения зафиксированы) + сравнение четырёх агентов 2026-08-15 15:05:12 +04:00
“Naeel” 69bc33eab8 doc: план v4 от Опуса 4.8 + ответы на уточнения (нет второго ISP, NET_RAW вряд ли, echo отложить) 2026-08-15 15:03:33 +04:00
“Naeel” db90a6392e doc: план v3 от Соннета — разделение явлений A/B, port-forward первым; принят за основу 2026-08-15 14:56:13 +04:00
“Naeel” 71e774da77 doc: план v2 от DeepSeek V4 Pro (новый чат) — стратегия изоляции слоёв; сравнение с Flash 2026-08-15 14:50:02 +04:00
“Naeel” adfc9a3610 doc: план сетевых экспериментов от DeepSeek V4 Flash (не Соннет) с поправками — nubes-network-bug-plan 2026-08-15 14:41:53 +04:00
“Naeel” 28669a79a7 tests: 30-мин сравнение с локали — p50 90мс vs 140мс, но 49 таймаутов у shared (5.5%), YMQ 1 ошибка 2026-08-15 13:01:11 +04:00
“Naeel” 0bf20565ff tests: сравнение shared-SQS vs YMQ — p50 6-8мс vs 60-62мс; у shared выброс send 12-25с на прогон (платформа?) 2026-08-15 12:10:11 +04:00
“Naeel” 5bc7eb7fbb HISTORY: P3.7 RSS 2ч PASS — 16588→15648 kB, Threads 13, утечки нет 2026-08-15 11:29:42 +04:00
“Naeel” 6a292769a3 tests: fifo_dlq_probe — опрос DLQ до 10с вместо мгновенной проверки (гонка с тиком); финальная регрессия зелёная 2026-08-15 09:31:11 +04:00
“Naeel” 22d3ea3d40 HISTORY: полная регрессия на ВМ — api_test 22/22, sdk_test 15/15; флап fifo_dlq_probe разобран (гонка теста, сервис 5/5) 2026-08-15 09:25:32 +04:00
“Naeel” a502d7890a HISTORY: P1.4/P2.5/P3.8 PASS, фикс api_test; +cli_load_test.py 2026-08-15 09:17:04 +04:00
“Naeel” bdc7336a80 tests: change-visibility 5с вместо 1с — гонка с таймером AWS-семантики 2026-08-15 09:07:24 +04:00
“Naeel” 01b08fb6ca HISTORY: исправлены дефекты — синхронные удаления Redis + RedrivePolicy; верификация PASS 2026-08-15 09:00:34 +04:00
“Naeel” 7467eccd66 v0.1.35: фиксы — синхронные удаления в Redis (воскрешение очередей) + RedrivePolicy tenant-scoped DLQ 2026-08-15 08:57:25 +04:00
“Naeel” 26c77ff9e1 HISTORY: FIFO/DLQ e2e — FIFO PASS, найден баг RedrivePolicy (tenant-scoped ключ) 2026-08-15 08:43:10 +04:00
“Naeel” 63261d1238 HISTORY: P0.1 таймауты + P0.2 рестарт PASS + найден дефект воскрешения удалённых очередей 2026-08-15 08:41:58 +04:00
“Naeel” af0325ebbe v0.1.35: серверные таймауты по Соннету — ReadHeaderTimeout вместо ReadTimeout, MaxHeaderBytes 2026-08-15 08:34:02 +04:00
21 changed files with 2049 additions and 59 deletions
+91
View File
@@ -0,0 +1,91 @@
# HANDOFF для нового чата — shared-sqs (состояние на 2026-08-16)
> Этот файл — резюме длинной сессии 13–16.08.2026. Новый чат: прочитать
> этот файл + HISTORY/2026-08-14-session-log.md + doc/thinking/.
## 1. Что за проект
`shared-sqs` — SQS-совместимая очередь (AWS API), Go 1.23, multi-tenant,
форк GoAWS. Живёт на платформе Nubes (managed Kubernetes, realm iot-naeel).
Локально: `/home/naeel/nubes/SQS-service`, зеркало на ВМ:
`naeel@5.172.178.213:~/terra/SQS-service` (ssh-ключ `~/.ssh/naeel_vm_id_ed25519`).
**⛔ ГРАНИЦЫ ПРОЕКТА (записано в память):** работать ТОЛЬКО с локальной папкой
и её зеркалом на ВМ. Никуда больше без прямого указания пути.
## 2. Статус сервиса
- Прод-версия **v0.1.35** задеплоена по digest (Docker Hub `naeel/shared-sqs`).
- Все тесты зелёные: api_test 22/22, sdk_test 15/15, fifo_dlq_probe PASS.
- План Соннета выполнен: P0.1, P0.2, P1.4, P2.5, P2.6, P3.7, P3.8 — все PASS.
- Нагрузка 168 591 операция — 0 сервисных ошибок.
- Память стабильна 2ч (VmRSS 16588→15648 kB, Threads 13).
## 3. Главная открытая проблема — ПЛАТФОРМА, не код
1. **Таймауты каждые 31–33с** на внешнем пути (интернет→шлюз Nubes): ~5.5%
запросов не получают ответ 30с. Доказано серверным tcpdump (8 «дыр» по
29.5с: запросы не доходят до пода), nginx чист (max 68мс, 499=0), cilium
0 дропов, port-forward 24977 раундов 0 сбоев. Таймер — на ВНЕШНЕМ шлюзе,
вне кластера.
2. **MSS=1448** анонсируется шлюзом при underlay MTU 1450 → большие тела
(>~1400 байт) виснут ~51с (PMTUD сломан).
3. Готов тикет: `doc/thinking/nubes-ticket.md` — ОТПРАВИТЬ в поддержку Nubes.
## 4. Что мы починили в кластере (16.08, по команде пользователя)
- kube-vip: `rollout restart ds shturval-vip` — ушли ошибки leaderelection в
удалённый namespace `0a42bef3-...` (хвост июльского сервиса пользователя).
- provider LB: добавлен `cidr-global: 185.247.187.151/32` в ConfigMap
`kubevip` в **kube-system** (provider читает именно оттуда,
env KUBEVIP_NAMESPACE=kube-system). «Ensured load balancer» — OK.
- После починки таймауты НЕ ушли (тест: 19 сбоев, период 31.3–33.5с) —
подтверждение, что таймер на внешнем шлюзе.
## 5. Сравнение с Yandex YMQ (очередь `newsqs`)
- Наш p50: 68мс (с ВМ) / 89–90мс (с локали). YMQ: 6062мс / 138150мс.
- YMQ стабилен (1 ошибка на 959), у нас — таймауты платформы (см. п.3).
- YMQ-доступ восстановлен: SA `newsqs` (роль ymq.admin), креды в
`secrets/yandex_ymq_newsqs.txt` (имена переменных AWS_ACCESS_KEY_ID/
AWS_SECRET_ACCESS_KEY). Очередь: URL в HISTORY.
- Повторный бенчмарк — ПОСЛЕ фикса шлюза Nubes.
## 6. Ключевые доступы
- Креды shared-SQS (тенант t-fec713a719ad0b33):
AK `SSAK-fec713a719ad0b33c91fa54a`,
SK `e898ed410a20ff166f51a52ba39a2954fb87e2da200b1648c3975772bfe9e2b4`.
- Внешний endpoint: `https://sqs.containerk8s.dev.nubes.ru` (регион us-east-1).
- Внутренний (из подов кластера): сервис `containerk8s` в ns
`f1ffb134-7d16-45bd-8bef-69f6ec8ab33c`, порт 4100.
- Gitea: `gitea.services.ngcloud.ru/Nail/SQS-service` (токен — в secrets/gitea.txt).
- kubeconfig на ВМ протухает ~раз в сутки (~26ч) — обновлять через консоль
Nubes (пользователь копирует в буфер, на ВМ: `xclip -selection clipboard -o
> ~/.kube/config`).
- ⚠️ boto3/aws CLI: ВСЕГДА передавать креды явно (env терминала копит чужие
AWS_ACCESS_KEY_ID — был ложный 403 с кредами Яндекса).
## 7. Хвосты / TODO
1. Отправить тикет Nubes (файл готов).
2. Удалить дубликат `secrets/yandex_ymq_fork8s.txt` (одинаков с newsqs.txt) —
ждёт команды пользователя.
3. После фикса шлюза: повторный 30-мин бенчмарк vs YMQ, обновить
BENCHMARK-COMPARISON.
4. Эхо-сервис (Фаза 3 планов) не понадобился — доказательств хватило.
## 8. Тестовая инфраструктура (tests/)
- `run_full_regression.sh` — полная регрессия (api_test + sdk_test + fifo_dlq).
- `long_compare_local.py` — 30-мин сравнение с YMQ с таймстемпами.
- `compare_ymq_vs_shared.py` — короткое сравнение.
- `tcp_mtu_probe.py` — бинарный поиск MTU-порога (порт 443! 32391 снаружи закрыт).
- Планы 4 агентов: doc/thinking/nubes-network-bug-plan*.md.
## 9. Правила пользователя (обязательные)
- Без «делай» — ничего не менять, только отвечать/смотреть.
- Каждый шаг и результат — в HISTORY сразу; коммит после правок.
- Команды только с таймаутами. Никаких оценок — считать/мерить.
- Часовой пояс GMT+03. sed запрещён (только редакторы файлов).
+477
View File
@@ -771,3 +771,480 @@ MSS clamp 1448, PMTUD мёртв). Кластером (kubectl) НЕ чинит
прокси на ВМ с 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мс против 6062мс).
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.733.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 готов).
+14 -6
View File
@@ -140,13 +140,21 @@ func main() {
// Metrics: gauge updater — пересчёт очередей/сообщений per tenant каждые 15 секунд
metrics.StartGaugeUpdater(15*time.Second, quit)
// HTTP сервер с таймаутами
// HTTP сервер с таймаутами (рекомендации ревью Соннета, план v0.1.35):
// - ReadHeaderTimeout 10с — лимит ТОЛЬКО на чтение заголовков (slowloris).
// ReadTimeout НЕ ставим: он обрывал бы запросы с медленной/большой
// загрузкой тела (в т.ч. большие SendMessage) и ломал бы long-poll.
// - WriteTimeout 35с — ответ; чуть больше max WaitTimeSeconds (20с long-poll).
// - IdleTimeout 90с — keep-alive; больше интервала ретраев клиентских SDK.
// - MaxHeaderBytes 8192 — защита от гигантских заголовков
// (платформа режет >16KB, наш лимит строже).
srv := &http.Server{
Addr: "0.0.0.0:" + port,
Handler: r,
ReadTimeout: 30 * time.Second,
WriteTimeout: 35 * time.Second, // чуть больше чем max WaitTimeSeconds (20s)
IdleTimeout: 60 * time.Second,
Addr: "0.0.0.0:" + port,
Handler: r,
ReadHeaderTimeout: 10 * time.Second,
WriteTimeout: 35 * time.Second,
IdleTimeout: 90 * time.Second,
MaxHeaderBytes: 8192,
}
// Запуск в горутине для graceful shutdown
+1 -1
View File
@@ -71,7 +71,7 @@ func CreateQueueV1(req *http.Request) (int, interfaces.AbstractResponseBody) {
if req.Header.Get("Content-Type") != "application/x-amz-json-1.0" {
provided = utils.ExtractQueueAttributes(req.PostForm)
}
if err := setQueueAttributesV1(queue, requestBody.Attributes, provided); err != nil {
if err := setQueueAttributesV1(queue, requestBody.Attributes, provided, t.AccessKey); err != nil {
models.SyncQueues.Unlock()
return utils.CreateErrorResponseV1(err.Error(), true)
}
+8 -4
View File
@@ -22,9 +22,11 @@ import (
// обнулял все остальные атрибуты очереди.
// 3. Значения вне диапазонов AWS → ошибка InvalidParameterValue
// (раньше значения молча клэмпились в допустимый диапазон).
// 4. RedrivePolicy: ARN DLQ разбирается, DLQ должна существовать, иначе
// InvalidAttributeValue.
func setQueueAttributesV1(q *models.Queue, attr models.QueueAttributes, provided map[string]string) error {
// 4. RedrivePolicy: ARN DLQ разбирается, DLQ ищется по TENANT-SCOPED ключу
// "{accessKey}:{queueName}" (раньше — по голому имени, а ключи в map
// tenant-scoped → DLQ никогда не находилась, RedrivePolicy не работал).
// DLQ должна принадлежать тому же тенанту.
func setQueueAttributesV1(q *models.Queue, attr models.QueueAttributes, provided map[string]string, tenantAccessKey string) error {
// Шаг 1: whitelist имён атрибутов (единый список — models.AttrNameWhitelist).
for name := range provided {
if !models.AttrNameWhitelist[name] {
@@ -71,7 +73,9 @@ func setQueueAttributesV1(q *models.Queue, attr models.QueueAttributes, provided
if attr.RedrivePolicy != (models.RedrivePolicy{}) {
arnArray := strings.Split(attr.RedrivePolicy.DeadLetterTargetArn, ":")
queueName := arnArray[len(arnArray)-1]
deadLetterQueue, ok := models.SyncQueues.Queues[queueName]
// DLQ ищем по tenant-scoped ключу — как хранятся все очереди.
dlqKey := tenantAccessKey + ":" + queueName
deadLetterQueue, ok := models.SyncQueues.Queues[dlqKey]
if !ok {
log.Error("Invalid RedrivePolicy Attribute")
return fmt.Errorf("InvalidAttributeValue")
+1 -1
View File
@@ -61,7 +61,7 @@ func SetQueueAttributesV1(req *http.Request) (int, interfaces.AbstractResponseBo
provided = utils.ExtractQueueAttributes(req.PostForm)
}
if err := setQueueAttributesV1(queue, requestBody.Attributes, provided); err != nil {
if err := setQueueAttributesV1(queue, requestBody.Attributes, provided, t.AccessKey); err != nil {
return utils.CreateErrorResponseV1(err.Error(), true)
}
// Сохраняем атрибуты в Redis пока держим Lock (через defer)
+40 -45
View File
@@ -184,55 +184,49 @@ func SaveMessages(queueKey string, msgs []models.SqsMessage) {
})
}
// DeleteMessagePersist — удаляет одно сообщение из Redis асинхронно.
// Вызывать при DeleteMessage (после удаления из in-memory).
// DeleteMessagePersist — удаляет одно сообщение из Redis СИНХРОННО.
// Синхронность обязательна: если удаление потеряется при рестарте пода,
// сообщение «воскреснет» из Redis (факт: удалённые очереди возвращались
// после рестарта, когда удаления были асинхронными).
func DeleteMessagePersist(queueKey string, msgUuid string) {
if Client == nil || msgUuid == "" {
return
}
hashKey := redisMsgHashPrefix + queueKey
asyncWrite(func() {
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
if err := Client.HDel(ctx, hashKey, msgUuid).Err(); err != nil {
log.Errorf("persistence: HDel message %s/%s: %v", queueKey, msgUuid, err)
}
})
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
if err := Client.HDel(ctx, hashKey, msgUuid).Err(); err != nil {
log.Errorf("persistence: HDel message %s/%s: %v", queueKey, msgUuid, err)
}
}
// DeleteMessagesPersist — удаляет несколько сообщений из Redis.
// Вызывать при DeleteMessageBatch.
// DeleteMessagesPersist — удаляет несколько сообщений из Redis СИНХРОННО
// (иначе удалённые сообщения воскреснут после рестарта пода).
func DeleteMessagesPersist(queueKey string, uuids []string) {
if Client == nil || len(uuids) == 0 {
return
}
hashKey := redisMsgHashPrefix + queueKey
// Копируем uuids — вызывающий может переиспользовать slice
ids := make([]string, len(uuids))
copy(ids, uuids)
asyncWrite(func() {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := Client.HDel(ctx, hashKey, ids...).Err(); err != nil {
log.Errorf("persistence: HDel messages %s (%d): %v", queueKey, len(ids), err)
}
})
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := Client.HDel(ctx, hashKey, uuids...).Err(); err != nil {
log.Errorf("persistence: HDel messages %s (%d): %v", queueKey, len(uuids), err)
}
}
// PurgeMessagesPersist — удаляет ВСЕ сообщения очереди из Redis (для PurgeQueue).
// Удаляет весь HASH ssq:msg:{queueKey}.
// PurgeMessagesPersist — удаляет ВСЕ сообщения очереди из Redis СИНХРОННО.
// Синхронность обязательна: при рестарте пода удалённые сообщения не должны
// воскреснуть (факт: асинхронное удаление очередей приводило к их возврату).
func PurgeMessagesPersist(queueKey string) {
if Client == nil {
return
}
hashKey := redisMsgHashPrefix + queueKey
asyncWrite(func() {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := Client.Del(ctx, hashKey).Err(); err != nil {
log.Errorf("persistence: DEL messages hash %s: %v", queueKey, err)
}
})
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := Client.Del(ctx, hashKey).Err(); err != nil {
log.Errorf("persistence: DEL messages hash %s: %v", queueKey, err)
}
}
// DeleteQueue — удаляет метаданные очереди И все её сообщения из Redis асинхронно.
@@ -242,21 +236,22 @@ func DeleteQueue(key string) {
}
writeSeq := nextQueueWriteSeq(key)
msgHashKey := redisMsgHashPrefix + key
asyncWrite(func() {
if !isLatestQueueWriteSeq(key, writeSeq) {
return
}
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
// Удаляем метаданные из общего HASH
if err := Client.HDel(ctx, redisHashQueues, key).Err(); err != nil {
log.Errorf("persistence: HDel queue meta %q: %v", key, err)
}
// Удаляем весь HASH с сообщениями
if err := Client.Del(ctx, msgHashKey).Err(); err != nil {
log.Errorf("persistence: DEL messages hash %q: %v", key, err)
}
})
// СИНХРОННО: удаление очереди обязано попасть в Redis до ответа клиенту.
// Асинхронное удаление приводило к «воскрешению» удалённых очередей после
// рестарта пода (факт 2026-08-15: 50 мусорных очередей вернулись из Redis).
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if !isLatestQueueWriteSeq(key, writeSeq) {
return
}
// Удаляем метаданные из общего HASH
if err := Client.HDel(ctx, redisHashQueues, key).Err(); err != nil {
log.Errorf("persistence: HDel queue meta %q: %v", key, err)
}
// Удаляем весь HASH с сообщениями
if err := Client.Del(ctx, msgHashKey).Err(); err != nil {
log.Errorf("persistence: DEL messages hash %q: %v", key, err)
}
}
// LoadAllQueues — загружает все очереди из Redis в память при старте сервиса.
@@ -0,0 +1,137 @@
# Стратегия доказательства сетевого бага Nubes — v2
> ⚠️ **ПОМЕТКА: анализ от DeepSeek V4 Pro (основная модель) в НОВОМ чате — НЕ Соннет.**
> Пользователь задал тот же вопрос в новом чате DeepSeek V4 Pro.
> Сохранено 2026-08-15. Сравнение с планом DeepSeek V4 Flash — в HISTORY
> и в конце этого файла.
## Стратегия доказательства
Доказывается изоляцией слоёв. Каждый тест исключает один слой пути:
```
клиент (локальная машина) → интернет → шлюз платформы (VIP 185.247.187.151)
→ kube-vip → NodePort 32391 → kube-proxy → nginx-ingress → Cilium → под shared-sqs
```
«Виновата платформа» доказано, если ВСЕ три условия:
1. Таймауты воспроизводятся на пути БЕЗ нашего кода (сырой TCP, эхо) с тем же паттерном.
2. tcpdump фиксирует потерю сегмента клиент↔кластер при том, что под/nginx
ответил за миллисекунды (по логам).
3. Потери коррелируют с MTU-порогом (~1448) или платформенным таймером
(conntrack/RST), а не с логикой приложения.
## 1. Эхо или сырые TCP
Делать ОБА, в порядке: сырые TCP, потом эхо.
**Сырые TCP на 185.247.187.151:32391** — бесплатный первый тест. Покрывает
путь до nginx-пода без HTTP и без нашего кода. Отправлять сегменты 512, 1380,
1448, 1488+ байт. Критерий доставки — TCP ACK, не HTTP-ответ (nginx на мусор
ответит 400/RST — нормально).
**Эхо-сервис** — второй шаг, ~20 строк Go, Deployment + Service с отдельным
NodePort (без ingress). Параметризуемый размер тела ответа (`?bytes=512|1400`) —
большие POST тестируют прямой путь, большие ОТВЕТЫ — обратный. Гонять тем же
клиентом. Таймауты воспроизвелись → nginx-ingress и наше приложение исключены.
## 2. Как поймать таймаут
tcpdump на клиенте: `tcpdump -i any -s 0 -w /tmp/cap.pcap host 185.247.187.151 and tcp port 32391`
| Симптом | Что смотреть |
|---|---|
| 51-с зависание большого POST | ACK на сегмент с полным MSS? ретрасмиты и интервалы? чем сессия завершилась (RST/FIN, кто первый) |
| 30-с ReadTimeout на 512 байт | запрос отправлен полностью? ACK получен? ответ не пришёл? RST после idle (conntrack)? keep-alive пул? |
| MTU-гипотеза | ICMP type 3 code 4 (Fragmentation Needed)? Не приходят → PMTUD сломан → механизм зависаний |
Замечание: 51 с не похоже на TCP-backoff Linux (tcp_retries2=15 → сотни секунд).
Скорее прикладной таймер (~50–60 с клиента/nginx/шлюза). Точный интервал в pcap
покажет, чей таймер.
Серверный захват — только через kubectl: `kubectl debug -it <pod> --image=<tcpdump-образ>`
(нужны NET_RAW/NET_ADMIN — на managed может быть запрещено). Если запрещено —
роль серверного захвата выполняют nginx access log и эхо-А/Б.
Ключевой приём: двусторонний захват, корреляция по TCP seq/ack (не по времени):
- запрос ушёл с клиента, не появился у пода → потеря на прямом пути;
- ответ ушёл из пода, клиент не получил → потеря на обратном;
- в обоих случаях приложение исключено.
## 3. kube-vip / NodePort / nginx через kubectl
nginx access log + `$request_time $upstream_response_time $status $bytes_sent`:
| В логе | Вывод |
|---|---|
| 499 + большой request_time | клиент ушёл до ответа |
| 200 + малый upstream, клиент без ответа | потеря между nginx и клиентом |
| записи нет | потеря до nginx (kube-vip/NodePort/шлюз) |
kube-vip: `kubectl get pods -A | grep -i vip`, логи (ARP/BGP-анонсы, флап VIP),
сопоставить ноду-держателя VIP с реально принимающей нодой.
kube-proxy/NodePort: `kubectl get ds -n kube-system kube-proxy`, логи;
`externalTrafficPolicy: Cluster` → лишний хоп + SNAT; iptables/conntrack ноды
не видны (нет доступа к нодам) — debug-под с hostNetwork+privileged+nodeSelector
или тикет.
Cilium: `kubectl exec -n kube-system <cilium-pod> -- cilium monitor --type drop`
(в cilium-под, НЕ в operator). Дроп с причиной — прямое доказательство.
Ограничение: внешний шлюз (VIP, MSS=1448) из kubectl не виден. Если кластерные
точки чистые, а снаружи потери есть — виноват шлюз/underlay, оформляется тикет.
## 4. Пошаговый план: дешевле → дороже
1. Инвентаризация логов: nginx access/error за тестовый период (499, request_time
30+/51), логи приложения (фактическое время обработки). Параллельно открыть
тикет Nubes.
2. PMTU-зонд с клиента: `ping -M do -s 1372/1422/1460/1472` до VIP (пороги MTU
1400/1450/1488/1500); при запрете ICMP — `mtr --tcp -P 443`, tcptraceroute.
MSS=1448 → сегменты 1488 при underlay 1450 — шлюз анонсирует неверный MSS.
3. Второй клиент из другого сегмента (VPS/другой ISP): тот же скрипт.
Воспроизвелось → исключены ISP и локальная сеть пользователя.
4. Сырые TCP на 32391 с обеих точек: потери на больших сегментах без HTTP →
исключены HTTP и приложение.
5. kubectl port-forward с ВМ на shared-sqs (localhost:4100): чистый тест →
приложение и под исправны (в обход шлюза через API-туннель).
6. tcpdump на клиенте + сверка с nginx access log: классификация таймаутов.
7. Эхо-сервис NodePort без ingress: А/Б 512/1400 тем же клиентом. Остались
таймауты → nginx и приложение исключены; исчезли → завести эхо через ingress
и повторить (изоляция nginx).
8. Захват в кластере: debug-контейнеры tcpdump, cilium monitor drop, логи
kube-vip/kube-proxy; двусторонняя корреляция по seq.
9. Матрица размеров (опционально): 100–1500 байт на эхо и на 32391. Резкий
порог 1380–1448 → MTU; плавный рост → шлюз/перегрузка.
## Итог: что считается доказательством
| Наблюдение | Вывод |
|---|---|
| Таймауты на эхо-NodePort (без ingress) с тем же паттерном | исключены nginx и наш код — виноват kube-vip/NodePort/шлюз/underlay |
| nginx: 200 + upstream <100 мс, клиент без ответа | потеря между nginx и клиентом |
| нет записи в access log | потеря до nginx |
| pcap: сегмент не дошёл до пода / ответ не дошёл до клиента | потеря в сетевом пути |
| ping -M do: резкий порог размера | MTU-дефект пути |
| cilium monitor показывает drop | внутрикластерная потеря |
| port-forward чистый, внешний IP таймаутит | виноват шлюз/NodePort/kube-vip |
Итоговая цепочка для тикета: pcap с клиента + access log nginx (200/upstream
малый) + воспроизведение на эхо без нашего кода + PMTU-порог ≈1448.
---
## Сравнение с планом DeepSeek V4 Flash (основной агент)
Flash: силён декомпозицией по nginx-логам, но с ошибкой (cilium monitor через
cilium-operator), переоценён echo, нет PMTU-зонда, нет порога ping -M do, нет
port-forward, нет второго ISP.
Pro (этот план): сильнее методологически — сырые TCP с критерием ACK,
PMTU-зонд ping -M do, второй клиент, port-forward, таблица «что считается
доказательством». Грубых ошибок нет.
Синтез (рекомендация): дешёвые тесты обоих планов + финал echo.
Порядок: nginx-логи → PMTU-зонд → сырые TCP → port-forward → tcpdump клиента →
второй ISP (если есть) → cilium monitor → echo → тикет.
@@ -0,0 +1,150 @@
# Plan: Доказательство сетевого бага Nubes — v3 (СОННЕТ)
> ✅ **ПОМЕТКА: это план от Соннета** (пользователь передал ответ Соннета).
> Сохранено 2026-08-15. Сравнение с v1 (DeepSeek Flash) и v2 (DeepSeek Pro) —
> в конце файла и в HISTORY.
## TL;DR
Два РАЗНЫХ явления с разными причинами — нельзя объяснять одной гипотезой и
нельзя ловить одним методом. Цепочка: nginx-логи → port-forward (бесплатное
доказательство невиновности приложения) → PMTUD-зонд → tcpdump → cilium
monitor → echo-сервис (только если предыдущих данных не хватает).
## Разделение двух явлений (критически важно)
**Явление A — большие POST (~51с).** Причина: MSS=1448, underlay MTU=1450,
сегменты 1488 → дроп. PMTUD сломан (ICMP Type 3 Code 4 заблокирован шлюзом) →
TCP ретрасмитит по экспоненте: 1с,2с,4с,8с,16с ≈ 31с, ещё попытка → ~51с.
Задача: подтвердить ping-зондом и зафиксировать в тикете.
**Явление B — малые POST 512 байт (~1/37с).** MTU-моделью НЕ объясняется.
Ключевая улика: РЕГУЛЯРНОСТЬ 1/37с — это таймер, не случайные потери.
Кандидаты: сессионный таймер gateway на VIP 185.247.187.151 (вероятнее всего),
kube-vip ARP keepalive цикл (флап при смене лидера), conntrack GC с агрессивным
idle-таймером.
## 1. Echo vs сырые TCP
Сырые TCP на 32391 — недостаточны (проверяют только фазу установления; проблемы
в фазе ДАННЫХ; nginx ответит RST/400 на мусор — неинформативно).
Правильный порядок:
1. kubectl port-forward (бесплатно, 5 минут) — чище и быстрее echo.
2. Echo NodePort без ingress — если port-forward и логи не дали ответа.
Echo: принимает POST, возвращает тело; `?bytes=N` для размера ответа
(обратный путь отдельно от прямого). Деплой БЕЗ ingress, прямой NodePort.
## 2. Как поймать таймаут: tcpdump
Клиент (запустить ДО теста, параллельно):
`tcpdump -i <iface> -s 0 -w /tmp/sqs.pcap 'host 185.247.187.151 and port 32391'`
Явление A (51с, большой POST): ICMP Type 3 Code 4 от 185.x → PMTUD работает;
нет ICMP, ретрасмиты 1/2/4/8/16с → PMTUD сломан (основная гипотеза); кто шлёт
RST в конце и через сколько — чей таймаут (клиент vs nginx/шлюз).
Явление B (30с, 512 байт): ACK на запрос получен → запрос дошёл, ответ потерян
на обратном пути; ACK нет → потеря до nginx (kube-vip/NodePort/gateway);
точный интервал между таймаутами (~37с ± 1с → таймер подтверждён); RST от
сервера — чей (nginx idle keepalive или gateway).
Ключевой приём: корреляция по TCP seq/ack, не по времени.
## 3. kube-vip / NodePort / nginx — только kubectl
nginx access log: `kubectl logs -n ingress <nginx-pod> --since=35m | grep -E " (499|5[0-9][0-9]) "`.
Нужны поля `$request_time $upstream_response_time`. Если нет — проверить
ConfigMap nginx.
| Запись в nginx log | Вывод |
|---|---|
| 499 + большой request_time | клиент ушёл до ответа (наш ReadTimeout) |
| 200 + upstream <100мс, клиент получил таймаут | ответ потерян между nginx и клиентом |
| записи нет | потеря ДО nginx: kube-vip/NodePort/gateway |
kube-vip: `kubectl get pods -A | grep -i vip`, логи (leader/ARP/flap/error).
ARP-флап во время теста → кратковременная потеря VIP → дропы.
Cilium (на ПОДЕ, не на operator): `kubectl get pods -n kube-system -l k8s-app=cilium -o wide`;
`kubectl exec -n kube-system <cilium-pod> -- cilium monitor --type drop`
параллельно с тестом.
NodePort/SNAT: `externalTrafficPolicy` — Cluster → любая нода + SNAT (лишний хоп,
conntrack); Local → только нода-держатель VIP (миграция VIP → brief outage).
port-forward: `kubectl port-forward -n <ns> svc/<sqs-svc> 14100:4100` — тот же
скрипт на localhost:14100. 0 таймаутов → приложение невиновно.
## 4. Пошаговый план: дешевле → дороже
**Фаза 0 — бесплатно, уже есть данные:**
1. nginx access log за период прошлого теста (499, request_time/upstream_response_time).
2. kube-vip логи: флапы в тот же промежуток.
3. Тикет Nubes: два явления отдельно — (A) MTU/PMTUD порог 1448, (B) таймер ~37с.
**Фаза 1 — клиентские тесты, 30 минут:**
4. PMTUD-зонд: `ping -M do -s N 185.247.187.151` для N=1300,1400,1420,1422,
1448,1450,1460,1472 — точный порог. Нет ответа ≥1448 → ICMP заблокирован →
тикет с числами.
5. tcpdump + тот же 30-мин тест. Wireshark: `tcp.analysis.retransmission || icmp`.
Точный интервал между таймаутами.
**Фаза 2 — kubectl, без деплоя:**
6. port-forward тест ПЕРВЫМ: 15-мин прогон через туннель. 0 таймаутов →
приложение доказанно невиновно.
7. cilium monitor параллельно со свежим тестом против внешнего IP.
8. Если нет upstream_response_time в формате — обновить ConfigMap, повторить.
**Фаза 3 — echo (только если 0–2 не дали ответа):**
9. Minimal Go echo (NodePort, без ingress), тот же тест. Воспроизвелось →
nginx и приложение исключены → kube-vip/NodePort/gateway. Нет → echo через
ingress → изоляция nginx.
10. Матрица размеров 512/1000/1400/1448/1460/1500 × 10 мин, порог бинарным поиском.
**Фаза 4 — финальная корреляция для тикета:**
11. Одновременно tcpdump + nginx access log + cilium monitor. Корреляция по
seq: «запрос ушёл, ACK получен, ответа нет» = потеря между nginx и клиентом.
Артефакты: pcap + логи + ping-зонд → тикет.
**Решения о приоритетах:**
- port-forward — самый дешёвый и чистый способ изолировать приложение. ДО echo.
- Сырые TCP на 32391 — пропустить.
- Echo — только если port-forward + nginx-логи не дали ответа.
- Явления A и B документировать в тикете РАЗДЕЛЬНО.
---
## Поправки основного агента (GitHub Copilot)
1. **ICMP может быть закрыт на шлюзе** — тогда ping-зонд не даст порога.
Нужен fallback: mtr --tcp / tcptraceroute или TCP-сегменты переменного
размера (то, что Соннет отбросил как «сырые TCP» — но для определения
ПОРОГА размера они валидны).
2. **Второй клиент с другого ISP** (был у Pro) — отсутствует. Дешёво исключает
локальную сеть пользователя. Добавить в фазу 1.
3. **Параллельный контрольный канал YMQ** — стоит сохранить во всех тестах:
отделяет «плохой интернет вообще» от «плох путь до Nubes».
4. **port-forward идёт через API-сервер** — не тот сетевой путь; чистый
результат исключает приложение, но НЕ доказывает шлюз. Формулировка Соннета
«доказательство невиновности приложения» — корректна, использовать её.
5. **Точный интервал таймаутов** надо измерять новым тестом с timestamp на
каждый таймаут (в прошлом тесте тайминги не логировались поштучно).
6. Смена nginx log format (ConfigMap) на managed может требовать релода/помощи
платформы — держать как запасной шаг.
## Сравнение трёх планов
| | Flash (v1) | Pro (v2) | Соннет (v3) |
|---|---|---|---|
| Разделение явлений A/B | нет | частично | **явно, с причиной B=таймер** |
| Ошибки команд | cilium через operator | нет | нет |
| PMTU-зонд | нет | есть | есть, точный список размеров |
| port-forward | нет | есть | **первым kubectl-тестом** |
| Сырые TCP | недооценены | да | отброшены (спорно для порога) |
| Второй ISP | нет | есть | нет |
| Критерии доказательства | слабые | таблица | таблицы + фазы |
| Объяснение 51с | нет | «прикладной таймер» | **экспонента RTO 1-2-4-8-16** |
Вердикт: план Соннета — лучший из трёх. Принять за основу с поправками 1–6.
@@ -0,0 +1,132 @@
# План: доказать, что таймауты — в пути/шлюзе Nubes — v4 (ОПУС 4.8)
> ✅ **ПОМЕТКА: план от Claude Opus 4.8** (пользователь передал ответ Опуса,
> который изучил репозиторий и три предыдущих плана).
> Сохранено 2026-08-15. Ниже — план Опуса + ответы основного агента на его
> уточняющие вопросы.
## TL;DR
Доказательство — изоляцией слоёв. «Виновата платформа» = таймауты
воспроизводятся БЕЗ нашего кода (port-forward чист, echo/сырой TCP таймаутит)
+ tcpdump с клиента ловит потерю сегмента, когда nginx/под ответили за
миллисекунды.
**Два РАЗНЫХ явления — раздельно:**
- **A. Большие POST ~51с** — локализовано: MSS=1448 → пакет 1488 > underlay
1450 → дроп при DF, PMTUD мёртв. Осталось зафиксировать порог ping-зондом.
- **B. Малые тела 512б, ~1 таймаут/37с** — MTU не объясняет. Регулярность =
таймер (шлюз VIP / ARP-флап kube-vip / conntrack GC). Главная нераскрытая часть.
## Ответы Опуса на вопросы
1. **Echo vs сырой TCP.** Сырой TCP недостаточен (только SYN/ACK; потери в
фазе данных). Самое дешёвое доказательство невиновности приложения —
`kubectl port-forward` мимо шлюза, а не echo. Echo — позже, только если
port-forward+логи не закрыли вопрос (он внутри кластера, а подозрение на шлюзе).
2. **Поймать таймаут.** tcpdump на клиенте, корреляция по TCP seq/ack: ретрансмиты
SYN → потеря на установлении; тишина после ACK → потеря после nginx; ICMP
frag-needed → PMTUD; кто и через сколько шлёт RST → чей таймер; интервал
между таймаутами → подтвердить ~37с.
3. **Через kubectl.** nginx access-log request_time/upstream_response_time:
499+большой request_time = клиент ушёл; 200+малый upstream без ответа клиенту
= потеря nginx↔клиент; записи нет = потеря до nginx. cilium monitor --type
drop — на cilium-ПОДЕ нужной ноды (не operator). externalTrafficPolicy
Cluster/Local.
## Шаги (дешевле → дороже)
- **Фаза 0 (бесплатно):** nginx access-log за прошлый тест → логи kube-vip
(флапы) → тикет Nubes (A и B раздельно).
- **Фаза 1 (клиент, ~30 мин):** PMTUD-зонд ping -M do (fallback mtr --tcp) →
tcpdump + повтор теста через long_compare_local.py с логом timestamp каждого
таймаута → контрольный канал YMQ во всех прогонах.
- **Фаза 2 (kubectl, без деплоя):** port-forward ПЕРВЫМ (невиновность приложения)
→ cilium monitor drop параллельно → проверка externalTrafficPolicy/nodePort.
- **Фаза 3 (echo, если 0–2 не хватило):** Go echo без ingress на NodePort →
таймаутит → nginx и код исключены → потом echo через ingress (изоляция nginx).
- **Фаза 4 (тикет):** одновременный tcpdump + nginx-log + cilium monitor,
корреляция по seq → pcap+логи+ping в тикет.
**Что считается доказательством:** echo-NodePort таймаутит тем же паттерном →
nginx+код исключены; nginx 200/upstream<100мс без ответа клиенту → потеря
nginx↔клиент; pcap показывает недоставленный сегмент; ping-порог → MTU;
cilium drop → внутри кластера; port-forward чист + внешний IP таймаутит →
приложение невиновно; стабильный интервал ~37с → таймер шлюза.
---
## Уточнения Опуса и ответы основного агента
1. **Второй клиент с другого ISP?** НЕТ. В распоряжении только ВМ
(5.172.178.213), но она рядом с платформой — с неё таймаутов нет, как
«второй клиент из другого сегмента» не подходит. Частичная замена уже есть:
контрольный канал YMQ с той же локали — 1 ошибка на 959 при 49 таймаутах на
Nubes (виноват локальный интернет → YMQ страдал бы так же). Полагаемся на
YMQ-канал; второй ISP — если появится доступ к VPS.
2. **Debug-под с NET_RAW?** Не проверяли; платформа managed, ноды недоступны,
привилегии урезаны — с высокой вероятностью запрещено. Под — alpine без
NET_RAW, серверный tcpdump не получится. Ограничиваемся nginx access-log +
cilium monitor + echo А/Б. Возможность kubectl debug проверяется одной
попыткой.
3. **Echo сразу?** НЕТ — только если фазы 0–2 не дадут однозначного ответа.
Доп. риск: новый NodePort для echo на managed может быть недоступен.
## Поправка основного агента
port-forward идёт через API-сервер (не тот сетевой путь) — он доказывает
только невиновность приложения, а не вину шлюза. Использовать формулировку
Опуса из таблицы доказательств (она корректна).
---
## ФИНАЛ (обновление Опуса с зафиксированными решениями, 2026-08-15)
**TL;DR.** Изоляция слоёв. «Виновата платформа» = два несмешиваемых вывода:
(1) приложение невиновно — port-forward через API-сервер чист (иной путь,
шлюз этим НЕ обвиняется); (2) виноват путь/шлюз — echo/сырой TCP таймаутит тем
же паттерном И tcpdump с клиента ловит потерю сегмента, когда nginx/под
ответили за миллисекунды.
**Зафиксированные решения:**
1. Второго ISP нет; ВМ рядом с Nubes → не «другой сегмент». Опора — контрольный
канал YMQ (49 таймаутов Nubes vs 1/959 YMQ уже исключают локальный интернет).
2. Серверный tcpdump практически недоступен (managed, exec только в alpine-под
без NET_RAW). kubectl debug с NET_RAW — одна проверочная попытка. Замена:
nginx access-log + cilium monitor + echo А/Б.
3. Echo — не сразу, только если фазы 0–2 неоднозначны; доп. риск — новый
NodePort для echo может быть закрыт на managed.
4. port-forward доказывает ТОЛЬКО невиновность приложения, НЕ вину шлюза.
**Шаги (дешевле → дороже):**
- Фаза 0 (бесплатно): nginx access-log за прошлый тест → логи kube-vip →
тикет Nubes (A и B раздельно).
- Фаза 1 (клиент, ~30 мин): PMTUD-зонд ping -M do (fallback mtr --tcp) →
tcpdump + long_compare_local.py с timestamp каждого таймаута → YMQ-канал
во всех прогонах.
- Фаза 2 (kubectl): port-forward первым → cilium monitor drop параллельно →
externalTrafficPolicy/nodePort. Одна попытка kubectl debug NET_RAW.
- Фаза 3 (echo, если 0–2 не хватило): Go echo без ingress на NodePort →
затем через ingress (изоляция nginx).
- Фаза 4 (тикет): tcpdump клиента + nginx-log + cilium monitor, корреляция
по seq → pcap+логи+ping.
---
## Сравнение ответов ЧЕТЫРЁХ агентов на один промпт (основной агент)
Условия НЕ равны: Соннет читал планы Flash и Pro; Опус читал все три плана и
весь репозиторий. Это «накопленный контекст», а не чистый тест моделей.
| | Flash | Pro | Sonnet | Opus |
|---|---|---|---|---|
| Вклад | первым заметил: 512б-таймауты не объясняются MTU | инструменты изоляции: PMTU-зонд, второй ISP, port-forward, таблица доказательств | концептуальная структура: A/B, 51с=экспонента RTO, 1/37с=таймер, port-forward первым | интеграция: синтез всего + факты репозитория + фиксация решений |
| Ошибки | cilium через operator; echo переоценён | «51с = прикладной таймер» (хуже экспоненты RTO) | нет | нет |
| Итог | черновик | сильный методолог | лучший аналитик | лучший интегратор |
Вывод основного агента: ключевые идеи распределились по всем четырём —
даже слабый Flash увидел главное (второе явление), Pro дал инструменты,
Sonnet построил структуру, Opus всё собрал. Конвергенция четырёх моделей на
одной цепочке (nginx-логи → PMTU → tcpdump → port-forward → cilium → echo) —
признак устойчивости плана. Плюс поправки основного агента приняты Опуса
в финал (port-forward). Итоговый план — продукт всех участников, исполнять.
+106
View File
@@ -0,0 +1,106 @@
# План сетевых экспериментов: shared-sqs на Nubes
> ⚠️ **ПОМЕТКА: анализ от СЛАБОГО агента — DeepSeek V4 Flash (не Соннет).**
> Пользователь по ошибке задал вопрос DeepSeek V4 Flash вместо Соннета.
> План сохранён как рабочий черновик; критику основного агента см. в HISTORY
> (2026-08-15) и в конце беседы. Технические неточности Flash см. ниже в разделах
> «Поправки основного агента».
## Контекст
Платформа Nubes (managed Kubernetes). Сервис shared-sqs (SQS-очередь, Go,
HTTP 4100). С ВМ внутри сети p50 = 6–8 мс, таймаутов нет. С интернета —
проблемы.
**Два независимых явления:**
1. Большие POST (>~1400 байт) виснут ~51 с — объясняется MTU/MSS 1448
(пакеты 1488 > underlay 1450, дроп).
2. Малые тела (512 байт): 49 из 888 запросов ReadTimeout 30 с, равномерно
~1 в 37 с — MTU-моделью НЕ объясняется. Второй независимый источник потерь.
Топология: клиент → 185.247.187.151:32391 (kube-vip NodePort) →
shturval-ingress-controller (nginx, ns ingress) → Cilium (pod mtu=1400) → под.
---
## 1. Echo-сервис или сырые TCP?
Сырых TCP недостаточно (проверяют только установление). Echo нужен, но:
1. Тот же путь: тот же кластер, тот же ingress (отдельный host/path), тот же Cilium.
2. Echo должен возвращать **тело запроса** (не «pong»), чтобы гонять тела
1400/1500/2000 байт в обе стороны.
3. Тот же network policy/namespace.
## 2. Как поймать момент таймаута
tcpdump с локали (нет доступа к нодам):
- захват: `tcpdump -i any -nn -s 0 'host 185.247.187.151 and port 32391' -w /tmp/cap.pcap`
- смотреть: SYN/ретрасмиты/RST/DUP ACK/сегменты >1450 с DF.
- `ss -ti` на установленном соединении → реальный MSS.
Признаки:
| Признак | Значение |
|---|---|
| SYN→SYN-ACK, потом тишина на данных | потери после установления (nginx/Cilium/под) |
| ретрасмиты SYN | потери на установлении (kube-vip/NodePort/шлюз) |
| ретрасмиты с растущим RTO (1,2,4,8,16,30+) | потеря пакета |
| сегменты >1450 с DF без ответа | MTU-гипотеза |
| RST | смотреть кто шлёт (платформа vs приложение) |
Вместо tcpdump на нодах — kubectl:
- **cilium monitor --type drop** — дропы с причинами (MTU/policy/conntrack).
- Логи nginx-ingress.
## 3. kube-vip / NodePort / nginx — проверка через kubectl
Декомпозиция по access-log nginx:
- запись есть → дошло до nginx → проблема после nginx.
- записи нет → потеря до nginx (kube-vip/NodePort/шлюз).
Метрики: `request_time` vs `upstream_response_time`:
- request_time≈30с, upstream мал → задержка между nginx и клиентом (обратный путь/шлюз).
- upstream≈30с → задержка nginx→под (Cilium/наш под).
Проверки:
- `kubectl -n kube-system get pods -o wide | grep kube-vip` + логи.
- `kubectl -n ingress get svc -o yaml` — nodePort 32391, externalTrafficPolicy
(Local → обслуживает только нода с VIP; Cluster → SNAT через любую ноду).
- access-log nginx с таймингами.
Дешёвый тест без приложения: GET на несуществующий путь через тот же NodePort
(ответит nginx default backend) — если GET без тела стабилен, а POST теряется —
потери в передаче данных.
## 4. План «дешевле → дороже»
1. Анализ access-log nginx за 30-мин тест (бесплатно).
2. Connect-тест 32391: 1000+ соединений, мерить connect (бесплатно).
3. GET без тела, 1000+ запросов (бесплатно).
4. tcpdump с локали + стрельба POST 512 и 1500 байт, поймать 2–3 таймаута в pcap.
5. Echo-сервис на ту же платформу (средняя стоимость).
6. cilium monitor параллельно со стрельбой.
7. Матрица размеров тел 512/1000/1400/1450/1500/2000 × 15+ мин через echo
+ контрольный канал к YMQ.
8. Тикет платформе с пачкой доказательств.
Критический вопрос: малые тела теряются ~1 раз в 37 с. Кандидаты: потери на
шлюзе, ARP/leader kube-vip, conntrack GC Cilium. Шаги 4–6 должны их разделить.
---
## Поправки основного агента (GitHub Copilot)
1. **Ошибка Flash**: `kubectl exec -n kube-system deploy/cilium-operator -- cilium monitor`
— неверно. cilium monitor запускается на **cilium-поде (DaemonSet)**:
`kubectl exec -n kube-system <cilium-pod> -- cilium monitor --type drop`.
И только на ноде, где живёт интересующий под.
2. **Приоритеты**: echo-сервис — НИЖЕ по ценности, чем кажется. Уже известно,
что с ВМ таймаутов нет → проблема между интернетом и внутренней сетью
(kube-vip/NodePort/шлюз), а echo сидит внутри. Сначала: nginx-логи (ш.1),
tcpdump (ш.4), cilium monitor (ш.6) — они дешёвые и локализуют лучше.
3. **WSL-нюанс**: tcpdump на локальной WSL-машине может требовать sudo и
конкретного интерфейса (`-i any` работает не всегда).
4. **Шаг 7 дорогой**: 7 размеров × 15 мин ≈ 2 часа. Достаточно 512/1400/1450/
1500/2000 × 10 мин, порог искать бинарно.
5. **Шаг 3 неточен**: GET на несуществующий путь того же host может уйти в наш
под (его router отдаст 400/404) — «приложение не участвует» не гарантировано.
+66
View File
@@ -0,0 +1,66 @@
# Тикет в Nubes: периодические сетевые таймауты внешнего пути (shared-sqs)
**Дата:** 2026-08-15
**Сервис:** shared-sqs (SQS-очередь, Go), инстанс iot-naeel,
ns f1ffb134-7d16-45bd-8bef-69f6ec8ab33c, deployment containerk8s.
**Внешний адрес:** 185.247.187.151:443 (домен sqs.containerk8s.dev.nubes.ru,
NodePort 32391), kube-vip, ingress shturval-ingress-controller.
## Явление A: большие POST (~51с зависания)
- MSS, анонсируемый шлюзом = 1448 (проверено на живом соединении), MTU пода
1400, underlay MTU 1450. Сегменты 1488 байт превышают underlay → дроп при DF.
- PMTUD не работает: ICMP Type 3 Code 4 до клиента не доходит.
- Воспроизводится POST с телом >~1.4KB: зависание ~51с (экспонента ретрасмитов
1-2-4-8-16с).
- Просьба: исправить MSS анонса на шлюзе (или включить MSS clamping /
PMTUD на внешнем шлюзе).
## Явление B: периодические таймауты на МАЛЫХ телах (512 байт) — ГЛАВНОЕ
**Симптом:** с внешней машины SQS-операции с телом 512 байт получают
ReadTimeout 30с с периодом **31.232.9с** (внутри прогона стабилен до 0.1с;
18+9+8 интервалов измерены в трёх прогонах). Параллельный канал Yandex YMQ с
той же машины — 1 ошибка на 959 запросов.
**Доказательства (все — в HISTORY/2026-08-14-session-log.md):**
1. **nginx access-log** за 30-мин тест: 862×200, **max request_time 68мс,
499=0**. nginx все запросы обрабатывает за миллисекунды.
2. **Серверный tcpdump** (ephemeral-контейнер в поде shared-sqs, eth0:4100):
в момент каждого таймаута — «дыра» 29.5с БЕЗ единого пакета: ни SYN, ни
данных, ни ретрансмитов. Зависший запрос **не доходит до пода** (иначе был
бы виден). 8 дыр на 8 сбоев, период 31.2с.
3. **cilium monitor --type drop** на ноде пода во время теста — **0 дропов**.
4. **kubectl port-forward** (в обход шлюза) — тот же клиент, та же нагрузка:
**24977 раундов, 0 сбоев**. Приложение невиновно.
5. С ВМ (внутренняя сеть): таймаутов нет вообще (p50 6–8мс).
6. **kube-vip в ошибках**: все 4 vip-пода каждую ~1с не могут создать leader
election record (namespaces "0a42bef3-7ce1-42b7-aa80-4274c4d9568f" not found),
provider — «failed to ensure load balancer: no address pools could be found»
(ретраи с backoff до 5 мин). vipHost закреплён за control-plane нодой.
**Вывод:** запросы периодически теряются на участке внешний клиент → ingress
(шлюз/VIP/kube-vip/NodePort платформы). Внутри кластера потерь нет. Период
31–33с указывает на платформенный таймер/цикл (сессионный таймер шлюза,
kube-vip announce, conntrack GC).
**Просьба:**
1. Проверить конфигурацию kube-vip (leader election в несуществующем
namespace, address pools) и внешний шлюз 185.247.187.151.
2. Объяснить/устранить периодический сбой каждые ~31с.
3. Исправить MSS анонс (1448 → ≤1410) для явления A.
**Дополнение (полный read-only обзор кластера 2026-08-15):**
- Внутри кластера цикла с периодом 31–33с НЕТ нигде: kube-vip пишет ошибки
каждые 1с, provider/services-controller — каждые 5 мин. Таймер 31с — на
внешнем шлюзе (вне кластера), что совпадает с потерей на участке
клиент→ingress.
- kube-vip поды 34 дня без рестарта: leader election бьётся в namespace
удалённого инстанса (0a42bef3-...) — вероятно, кэш удалённого LB-сервиса;
перезапуск DaemonSet убрал бы этот шум.
- ConfigMap kubevip в ns kube-vip отсутствует полностью — пулы LB никогда не
были настроены; VIP держится только аннотацией vipHost.
**Как воспроизвести:** приложить tests/ (long_compare_local.py, tcp_mtu_probe.py)
или: цикл send/receive/delete 512б с retries=0 и read_timeout=30 с внешней
машины → сбой каждые ~31с.
+6 -2
View File
@@ -68,10 +68,14 @@ done
N=$(echo "$RM" | json 'len(d.get("Messages", []))')
[[ "$N" == "3" ]] && ok "ReceiveMessage (3 сообщения)" || bad "ReceiveMessage: получили $N"
# ChangeMessageVisibility по первому handle
# ChangeMessageVisibility по первому handle.
# visibility-timeout=5с (НЕ 1с): после истечения таймаута сообщение снова
# видимо, а handle сбрасывается — delete по старому handle даёт
# ReceiptHandleIsInvalid (корректная AWS-семантика). При 1с сетевые задержки
# приводили к случайным FAIL теста.
RH1=$(echo "$RM" | json 'd["Messages"][0]["ReceiptHandle"]')
if [[ -n "$RH1" ]]; then
"${AWS[@]}" sqs change-message-visibility --queue-url "$QUEUE_URL" --receipt-handle "$RH1" --visibility-timeout 1 >/dev/null 2>&1 \
"${AWS[@]}" sqs change-message-visibility --queue-url "$QUEUE_URL" --receipt-handle "$RH1" --visibility-timeout 5 >/dev/null 2>&1 \
&& ok "ChangeMessageVisibility" || bad "ChangeMessageVisibility"
fi
+59
View File
@@ -0,0 +1,59 @@
#!/usr/bin/env python3
"""CLI v2 (aws cli) в нагрузке: 50 циклов send/receive/delete с замером латентности.
Запуск на ВМ: python3 tests/cli_load_test.py"""
import boto3
import subprocess
import time
import os
import re
ENV = dict(os.environ)
EP = 'https://sqs.containerk8s.dev.nubes.ru'
sqs = boto3.client('sqs', endpoint_url=EP, region_name='us-east-1')
def cli(*a, tmo=20):
t = time.time()
p = subprocess.run(['aws', '--endpoint-url', EP, '--region', 'us-east-1', 'sqs', *a],
env=ENV, capture_output=True, text=True, timeout=tmo)
return p, time.time() - t
def main():
q = 'cliload-%d' % int(time.time())
sqs.create_queue(QueueName=q)
u = EP + '/' + q
lat = {'send': [], 'receive': [], 'delete': []}
failed = 0
for i in range(50):
p, t = cli('send-message', '--queue-url', u, '--message-body', 'msg-%d' % i)
lat['send'].append(t)
if p.returncode != 0:
failed += 1
print('send FAIL', i, p.stderr[:120])
continue
p, t = cli('receive-message', '--queue-url', u, '--max-number-of-messages', '1', '--visibility-timeout', '3')
lat['receive'].append(t)
m = re.search(r'"ReceiptHandle":\s*"([^"]+)"', p.stdout)
if not m:
failed += 1
print('receive FAIL', i, p.stdout[:100], p.stderr[:100])
continue
p, t = cli('delete-message', '--queue-url', u, '--receipt-handle', m.group(1))
lat['delete'].append(t)
if p.returncode != 0:
failed += 1
print('delete FAIL', i, p.stderr[:120])
for k in lat:
v = sorted(lat[k])
n = len(v)
if n:
print('%s: n=%d p50=%.0fms p95=%.0fms max=%.0fms' % (k, n, v[n // 2] * 1000, v[int(n * .95)] * 1000, v[-1] * 1000))
else:
print('%s: n=0' % k)
sqs.delete_queue(QueueUrl=u)
print('CLI load done, failed=%d' % failed)
if __name__ == '__main__':
main()
+153
View File
@@ -0,0 +1,153 @@
#!/usr/bin/env python3
"""Сравнение shared-SQS vs Yandex YMQ (очередь newsqs).
Короткий замер: последовательные операции, N send → N receive+delete.
Метрики: p50/p95/max латентности send/receive/delete, число ошибок.
Требуемые переменные окружения:
SHARED_AK, SHARED_SK — креды shared-SQS
YMQ_AK, YMQ_SK — креды Yandex YMQ (SA newsqs)
YMQ_QUEUE_URL — URL очереди Яндекса (newsqs)
Запуск: python3 tests/compare_ymq_vs_shared.py [N]
"""
import os
import sys
import time
import uuid
import boto3
from botocore.config import Config
N = int(sys.argv[1]) if len(sys.argv) > 1 else 50
BODY = "x" * 512
SHARED_EP = "https://sqs.containerk8s.dev.nubes.ru"
YMQ_EP = "https://message-queue.api.cloud.yandex.net"
SHARED_AK = os.environ["SHARED_AK"]
SHARED_SK = os.environ["SHARED_SK"]
YMQ_AK = os.environ["YMQ_AK"]
YMQ_SK = os.environ["YMQ_SK"]
YMQ_QUEUE_URL = os.environ["YMQ_QUEUE_URL"]
CFG = Config(connect_timeout=15, read_timeout=30, retries={"max_attempts": 0})
def mk_client(endpoint, region, ak, sk):
return boto3.client(
"sqs",
endpoint_url=endpoint,
region_name=region,
aws_access_key_id=ak,
aws_secret_access_key=sk,
config=CFG,
)
def stats(name, values):
v = sorted(values)
if not v:
print("%s: n=0" % name)
return
n = len(v)
print("%s: n=%d p50=%.0fms p95=%.0fms max=%.0fms" %
(name, n, v[n // 2] * 1000, v[int(n * .95)] * 1000, v[-1] * 1000))
def bench(client, queue_url, label):
errs = 0
lat_send, lat_recv, lat_del = [], [], []
t_start = time.time()
for i in range(N):
t0 = time.time()
try:
client.send_message(QueueUrl=queue_url, MessageBody=BODY)
lat_send.append(time.time() - t0)
except Exception as e:
errs += 1
if errs <= 3:
print(" send err: %s %s" % (type(e).__name__, str(e)[:80]))
got = 0
tries = 0
while got < N and tries < N * 6:
tries += 1
t0 = time.time()
try:
r = client.receive_message(
QueueUrl=queue_url, MaxNumberOfMessages=1, VisibilityTimeout=30)
lat_recv.append(time.time() - t0)
msgs = r.get("Messages", [])
except Exception as e:
errs += 1
if errs <= 3:
print(" recv err: %s %s" % (type(e).__name__, str(e)[:80]))
continue
for m in msgs:
got += 1
t0 = time.time()
try:
client.delete_message(QueueUrl=queue_url, ReceiptHandle=m["ReceiptHandle"])
lat_del.append(time.time() - t0)
except Exception as e:
errs += 1
if errs <= 3:
print(" del err: %s %s" % (type(e).__name__, str(e)[:80]))
total = time.time() - t_start
print("== %s (N=%d) ==" % (label, N))
stats(" send ", lat_send)
stats(" receive", lat_recv)
stats(" delete ", lat_del)
print(" received=%d/%d errors=%d total=%.1fs" % (got, N, errs, total))
print(" throughput: send=%.1f op/s, recv+del=%.1f op/s" %
(N / total if total else 0, got / total if total else 0))
return errs
def drain(client, queue_url):
while True:
r = client.receive_message(QueueUrl=queue_url, MaxNumberOfMessages=10)
msgs = r.get("Messages", [])
if not msgs:
break
for m in msgs:
try:
client.delete_message(QueueUrl=queue_url, ReceiptHandle=m["ReceiptHandle"])
except Exception:
pass
def main():
shared = mk_client(SHARED_EP, "us-east-1", SHARED_AK, SHARED_SK)
ymq = mk_client(YMQ_EP, "ru-central1", YMQ_AK, YMQ_SK)
# Временная очередь shared-SQS
qname = "cmp-%s" % uuid.uuid4().hex[:8]
shared_url = shared.create_queue(QueueName=qname)["QueueUrl"]
print("shared queue: %s" % shared_url)
# YMQ newsqs: перед тестом чистим
drain(ymq, YMQ_QUEUE_URL)
print("ymq queue: %s (drained)" % YMQ_QUEUE_URL)
print()
e1 = bench(shared, shared_url, "shared-SQS (наш сервис)")
print()
e2 = bench(ymq, YMQ_QUEUE_URL, "Yandex YMQ (newsqs)")
# Очистка
drain(ymq, YMQ_QUEUE_URL)
try:
shared.delete_queue(QueueUrl=shared_url)
print("\ncleanup: shared temp queue deleted, ymq drained")
except Exception as ex:
print("\ncleanup err: %s" % str(ex)[:80])
print("\nRESULT: errors shared=%d ymq=%d" % (e1, e2))
if __name__ == "__main__":
main()
+72
View File
@@ -0,0 +1,72 @@
#!/usr/bin/env python3
"""Детерминированная проверка DLQ-переноса (без гонок тайминга):
после каждой доставки ждём истечения visibility + тик PeriodicTasks (2.5с).
Если 5/5 PASS — сервис корректен, флап fifo_dlq_probe — гонка теста."""
import json
import time
import uuid
import boto3
from botocore.config import Config
ENDPOINT = "https://sqs.containerk8s.dev.nubes.ru"
REGION = "us-east-1"
sqs = boto3.client("sqs", endpoint_url=ENDPOINT, region_name=REGION,
config=Config(connect_timeout=10, read_timeout=30, retries={"max_attempts": 0}))
def drain(url, n=10):
msgs = []
while True:
b = sqs.receive_message(QueueUrl=url, MaxNumberOfMessages=n).get("Messages", [])
if not b:
break
msgs.extend(b)
return msgs
def run(i):
uid = str(uuid.uuid4())[:8]
dlq_name = "dlqdet-%s-%d" % (uid, i)
main_name = "maindet-%s-%d" % (uid, i)
dlq_url = sqs.create_queue(QueueName=dlq_name)["QueueUrl"]
tenant = dlq_url.split("/")[-2]
arn = "arn:aws:sqs:%s:%s:%s" % (REGION, tenant, dlq_name)
policy = json.dumps({"deadLetterTargetArn": arn, "maxReceiveCount": "2"})
main_url = sqs.create_queue(
QueueName=main_name,
Attributes={"VisibilityTimeout": "1", "RedrivePolicy": policy},
)["QueueUrl"]
sqs.send_message(QueueUrl=main_url, MessageBody="to-dlq")
r1 = sqs.receive_message(QueueUrl=main_url, MaxNumberOfMessages=1).get("Messages", [])
time.sleep(2.5) # visibility истёк + тик вернул (Retry=1)
r2 = sqs.receive_message(QueueUrl=main_url, MaxNumberOfMessages=1).get("Messages", [])
time.sleep(2.5) # visibility истёк + тик: Retry=2 >= maxReceiveCount=2 -> DLQ
r3 = sqs.receive_message(QueueUrl=main_url, MaxNumberOfMessages=1).get("Messages", [])
time.sleep(2.0)
dlq_msgs = drain(dlq_url)
main_left = drain(main_url)
ok = (len(r1) == 1 and len(r2) == 1 and len(r3) == 0
and len(dlq_msgs) == 1 and dlq_msgs[0]["Body"] == "to-dlq"
and len(main_left) == 0)
print("run %d: r1=%d r2=%d r3=%d dlq=%d main_left=%d -> %s"
% (i, len(r1), len(r2), len(r3), len(dlq_msgs), len(main_left),
"PASS" if ok else "FAIL"), flush=True)
try:
sqs.delete_queue(QueueUrl=main_url)
sqs.delete_queue(QueueUrl=dlq_url)
except Exception:
pass
return ok
ok = 0
for i in range(1, 6):
try:
if run(i):
ok += 1
except Exception as e:
print("run %d: EXC %s %s" % (i, type(e).__name__, str(e)[:120]), flush=True)
print("TOTAL: %d/5 PASS" % ok, flush=True)
+130
View File
@@ -0,0 +1,130 @@
#!/usr/bin/env python3
"""tests/fifo_dlq_probe.py — e2e: FIFO порядок + dedup внутри окна + DLQ (план Соннета P2).
Проверки:
1. FIFO: 10 сообщений в одну группу выдаются строго по порядку.
2. FIFO dedup: два send с одинаковым MessageDeduplicationId внутри окна
(5 мин) → в очереди оказывается ОДНО сообщение.
3. DLQ: сообщение после MaxReceiveCount=2 попыток (VisibilityTimeout=1с)
переносится в dead-letter очередь.
"""
import json
import os
import sys
import time
import uuid
import boto3
from botocore.config import Config
ENDPOINT = os.environ.get("ENDPOINT_URL", "https://sqs.containerk8s.dev.nubes.ru")
REGION = os.environ.get("REGION", "us-east-1")
sqs = boto3.client("sqs", endpoint_url=ENDPOINT, region_name=REGION,
config=Config(connect_timeout=10, read_timeout=30, retries={"max_attempts": 3}))
uid = str(uuid.uuid4())[:8]
def drain(url, n=10):
msgs = []
while True:
b = sqs.receive_message(QueueUrl=url, MaxNumberOfMessages=n).get("Messages", [])
if not b:
break
msgs.extend(b)
return msgs
results = {}
def check_fifo_order():
q = "fifo-order-%s.fifo" % uid
u = sqs.create_queue(QueueName=q, Attributes={"FifoQueue": "true"})["QueueUrl"]
for i in range(10):
sqs.send_message(QueueUrl=u, MessageBody="m%d" % i, MessageGroupId="g", MessageDeduplicationId="d%d" % (i,))
got = []
while True:
b = sqs.receive_message(QueueUrl=u, MaxNumberOfMessages=10).get("Messages", [])
if not b:
break
got.extend(b)
for m in b:
sqs.delete_message(QueueUrl=u, ReceiptHandle=m["ReceiptHandle"])
bodies = [m["Body"] for m in got]
want = ["m%d" % i for i in range(10)]
results["fifo_order"] = bodies == want
print("fifo_order: got=%s want=%s %s" % (bodies, want, "PASS" if bodies == want else "FAIL"), flush=True)
sqs.delete_queue(QueueUrl=u)
def check_fifo_dedup():
q = "fifo-dedup-%s.fifo" % uid
u = sqs.create_queue(QueueName=q, Attributes={"FifoQueue": "true"})["QueueUrl"]
sqs.send_message(QueueUrl=u, MessageBody="first", MessageGroupId="g", MessageDeduplicationId="SAME")
sqs.send_message(QueueUrl=u, MessageBody="dup", MessageGroupId="g", MessageDeduplicationId="SAME")
msgs = drain(u)
ok = len(msgs) == 1 and msgs[0]["Body"] == "first"
results["fifo_dedup"] = ok
print("fifo_dedup: count=%d body=%s %s" % (len(msgs), msgs[0]["Body"] if msgs else None,
"PASS" if ok else "FAIL"), flush=True)
sqs.delete_queue(QueueUrl=u)
def check_dlq():
dlq_name = "fifo-dlq-%s" % uid
main_name = "fifo-main-%s" % uid
dlq_url = sqs.create_queue(QueueName=dlq_name)["QueueUrl"]
# tenantID из URL DLQ: http://us-east-1.goaws.com:4100/t-<id>/<name>
tenant = dlq_url.split("/")[-2]
arn = "arn:aws:sqs:%s:%s:%s" % (REGION, tenant, dlq_name)
policy = json.dumps({"deadLetterTargetArn": arn, "maxReceiveCount": "2"})
main_url = sqs.create_queue(
QueueName=main_name,
Attributes={"VisibilityTimeout": "1", "RedrivePolicy": policy},
)["QueueUrl"]
sqs.send_message(QueueUrl=main_url, MessageBody="to-dlq")
for attempt in range(3):
b = sqs.receive_message(QueueUrl=main_url, MaxNumberOfMessages=1).get("Messages", [])
print("attempt %d: %s" % (attempt + 1, "received" if b else "empty"), flush=True)
time.sleep(1.5)
# Перенос в DLQ выполняет фоновый тик PeriodicTasks (период 1с) при
# истечении VisibilityTimeout. Мгновенная проверка гоняет с фазой тика —
# опрашиваем DLQ до 10с.
dlq_msgs = []
for _ in range(10):
dlq_msgs = drain(dlq_url)
if dlq_msgs:
break
time.sleep(1)
ok = len(dlq_msgs) == 1 and dlq_msgs[0]["Body"] == "to-dlq"
results["dlq"] = ok
print("dlq: count=%d body=%s %s" % (len(dlq_msgs), dlq_msgs[0]["Body"] if dlq_msgs else None,
"PASS" if ok else "FAIL"), flush=True)
sqs.delete_queue(QueueUrl=main_url)
sqs.delete_queue(QueueUrl=dlq_url)
def main():
try:
check_fifo_order()
except Exception as e:
results["fifo_order"] = False
print("fifo_order: EXC %s %s" % (type(e).__name__, str(e)[:120]), flush=True)
try:
check_fifo_dedup()
except Exception as e:
results["fifo_dedup"] = False
print("fifo_dedup: EXC %s %s" % (type(e).__name__, str(e)[:120]), flush=True)
try:
check_dlq()
except Exception as e:
results["dlq"] = False
print("dlq: EXC %s %s" % (type(e).__name__, str(e)[:120]), flush=True)
print("SUMMARY: %s" % json.dumps(results), flush=True)
return 0 if all(results.values()) else 1
if __name__ == "__main__":
sys.exit(main())
+165
View File
@@ -0,0 +1,165 @@
#!/usr/bin/env python3
"""ДЛИННОЕ сравнение shared-SQS vs Yandex YMQ — с ЛОКАЛИ, по таймеру.
Схема (честная — обе очереди в одних сетевых условиях, раунды попеременно):
1. Старт: seed K сообщений в каждую очередь.
2. Раунд для каждой очереди: receive(1) -> delete -> send(1) (восполнение пула).
3. Раунды чередуются (наш, яндекс, наш, яндекс...) до истечения DURATION.
Метрики: p50/p95/max для send/receive/delete, ошибки по типам (с таймаутами),
op/s. Прогресс каждые 60с пишется в LOG и stdout.
Env: SHARED_AK, SHARED_SK, YMQ_AK, YMQ_SK, YMQ_QUEUE_URL, DURATION (сек, default 1800)
"""
import os
import sys
import time
import uuid
import boto3
from botocore.config import Config
DURATION = int(os.environ.get("DURATION", "1800"))
K = 10
BODY = "x" * 512
LOG = "tests/long_compare.log"
SHARED_EP = "https://sqs.containerk8s.dev.nubes.ru"
YMQ_EP = "https://message-queue.api.cloud.yandex.net"
CFG = Config(connect_timeout=15, read_timeout=30, retries={"max_attempts": 0})
def mk(endpoint, region, ak, sk):
return boto3.client("sqs", endpoint_url=endpoint, region_name=region,
aws_access_key_id=ak, aws_secret_access_key=sk, config=CFG)
class Stat:
def __init__(self):
self.vals = {"send": [], "receive": [], "delete": []}
self.errs = {} # type -> count
self.ops = {"send": 0, "receive": 0, "delete": 0}
def op(self, name, fn):
self.ops[name] += 1
t0 = time.time()
try:
fn()
self.vals[name].append(time.time() - t0)
except Exception as e:
k = type(e).__name__
self.errs[k] = self.errs.get(k, 0) + 1
def stat_line(st, label):
out = [label]
for name in ("send", "receive", "delete"):
v = sorted(st.vals[name])
n = len(v)
if n:
out.append("%s: n=%d p50=%.0fms p95=%.0fms max=%.0fms" %
(name, n, v[n // 2] * 1000, v[int(n * .95)] * 1000, v[-1] * 1000))
else:
out.append("%s: n=0" % name)
out.append("errors=%s" % (st.errs if st.errs else "0"))
return " | ".join(out)
def logp(msg):
line = "%s %s" % (time.strftime("%H:%M:%S"), msg)
print(line, flush=True)
try:
with open(LOG, "a") as f:
f.write(line + "\n")
except Exception:
pass
def main():
shared = mk(SHARED_EP, "us-east-1", os.environ["SHARED_AK"], os.environ["SHARED_SK"])
ymq = mk(YMQ_EP, "ru-central1", os.environ["YMQ_AK"], os.environ["YMQ_SK"])
ymq_url = os.environ["YMQ_QUEUE_URL"]
# временная очередь shared
qname = "longcmp-%s" % uuid.uuid4().hex[:8]
shared_url = shared.create_queue(QueueName=qname)["QueueUrl"]
def drain(client, url):
while True:
try:
r = client.receive_message(QueueUrl=url, MaxNumberOfMessages=10)
msgs = r.get("Messages", [])
except Exception:
break
if not msgs:
break
for m in msgs:
try:
client.delete_message(QueueUrl=url, ReceiptHandle=m["ReceiptHandle"])
except Exception:
pass
drain(shared, shared_url)
drain(ymq, ymq_url)
ss = Stat()
ys = Stat()
sts = {"shared": (shared, shared_url, ss), "ymq": (ymq, ymq_url, ys)}
# seed K в обе очереди
for key in ("shared", "ymq"):
c, u, st = sts[key]
for _ in range(K):
st.op("send", lambda c=c, u=u: c.send_message(QueueUrl=u, MessageBody=BODY))
logp("LONG COMPARE START: duration=%ds K=%d shared=%s" % (DURATION, K, shared_url))
logp("ymq=%s" % ymq_url)
deadline = time.time() + DURATION
last_prog = time.time()
rounds = 0
def round_for(key):
nonlocal rounds
c, u, st = sts[key]
# receive (замер) + получить сообщения для delete
got = []
t0 = time.time()
st.ops["receive"] += 1
try:
r = c.receive_message(QueueUrl=u, MaxNumberOfMessages=1, VisibilityTimeout=30)
st.vals["receive"].append(time.time() - t0)
got = r.get("Messages", [])
except Exception as e:
st.errs[type(e).__name__] = st.errs.get(type(e).__name__, 0) + 1
for m in got:
st.op("delete", lambda c=c, u=u, m=m: c.delete_message(QueueUrl=u, ReceiptHandle=m["ReceiptHandle"]))
st.op("send", lambda c=c, u=u: c.send_message(QueueUrl=u, MessageBody=BODY))
while time.time() < deadline:
round_for("shared")
round_for("ymq")
rounds += 1
if time.time() - last_prog >= 60:
last_prog = time.time()
logp("progress %d/%ds" % (int(time.time() - (deadline - DURATION)), DURATION))
logp(" " + stat_line(ss, "SHARED"))
logp(" " + stat_line(ys, "YMQ"))
logp("LONG COMPARE DONE: rounds=%d" % rounds)
logp(stat_line(ss, "FINAL SHARED"))
logp(stat_line(ys, "FINAL YMQ"))
# cleanup
drain(shared, shared_url)
drain(ymq, ymq_url)
try:
shared.delete_queue(QueueUrl=shared_url)
logp("cleanup: shared temp queue deleted, both drained")
except Exception as e:
logp("cleanup err: %s" % str(e)[:100])
if __name__ == "__main__":
main()
+126
View File
@@ -0,0 +1,126 @@
#!/usr/bin/env python3
"""tests/reboot_probe.py — тест рестарта пода с in-flight сообщениями (план Соннета P0).
Фаза 1 (phase1): 10 очередей x 20 сообщений (проверка восстановления из Redis)
+ основная очередь со 100 сообщениями, из которых 50 выдаются (in-flight).
Фаза 2 (phase2, ПОСЛЕ kubectl delete pod --grace-period=0):
- /health поднимается;
- во всех 10 очередях должны быть ровно по 20 сообщений;
- из основной должны выдаться все 100 уникальных тел (50 in-flight + 50 видимых).
"""
import json
import os
import sys
import time
import uuid
import boto3
import urllib.request
from botocore.config import Config
ENDPOINT = os.environ.get("ENDPOINT_URL", "https://sqs.containerk8s.dev.nubes.ru")
REGION = os.environ.get("REGION", "us-east-1")
STATE = "/tmp/reboot_state.json"
sqs = boto3.client("sqs", endpoint_url=ENDPOINT, region_name=REGION,
config=Config(connect_timeout=10, read_timeout=30, retries={"max_attempts": 3}))
def wait_health(timeout=180):
t0 = time.time()
while time.time() - t0 < timeout:
try:
with urllib.request.urlopen(ENDPOINT + "/health", timeout=10) as r:
if r.status == 200:
return True
except Exception:
pass
time.sleep(3)
return False
def drain(url):
got = []
while True:
msgs = sqs.receive_message(QueueUrl=url, MaxNumberOfMessages=10).get("Messages", [])
if not msgs:
break
got.extend(msgs)
return got
def phase1():
uid = str(uuid.uuid4())[:8]
state = {"uid": uid, "restore_queues": [], "main_queue": None, "bodies": []}
# 10 очередей по 20 сообщений — проверка восстановления из Redis
for i in range(10):
u = sqs.create_queue(QueueName="reboot-restore-%d-%s" % (i, uid))["QueueUrl"]
for j in range(20):
sqs.send_message(QueueUrl=u, MessageBody="r-%d-%d-%s" % (i, j, uid))
state["restore_queues"].append(u)
print("restore queue %d ready" % i, flush=True)
# основная: 100 сообщений, 50 выдаём (in-flight)
u = sqs.create_queue(QueueName="reboot-main-%s" % uid)["QueueUrl"]
bodies = ["m-%03d-%s" % (i, uid) for i in range(100)]
for b in bodies:
sqs.send_message(QueueUrl=u, MessageBody=b)
first = sqs.receive_message(QueueUrl=u, MaxNumberOfMessages=10, WaitTimeSeconds=1).get("Messages", [])
# добираем до 50 in-flight (Max=10 за вызов)
while len(first) < 50:
batch = sqs.receive_message(QueueUrl=u, MaxNumberOfMessages=10, WaitTimeSeconds=1).get("Messages", [])
if not batch:
break
first.extend(batch)
print("in-flight на момент рестарта: %d" % len(first), flush=True)
state["main_queue"] = u
state["bodies"] = bodies
json.dump(state, open(STATE, "w"))
print("PHASE1_DONE", flush=True)
def phase2():
st = json.load(open(STATE))
print("ждём /health после рестарта...", flush=True)
if not wait_health():
print("HEALTH_FAIL: /health не поднялся", flush=True)
sys.exit(1)
print("health ok", flush=True)
# восстановление 10 очередей
restore_ok = True
for u in st["restore_queues"]:
msgs = drain(u)
if len(msgs) != 20:
restore_ok = False
print("RESTORE FAIL: %s -> %d сообщений" % (u.split("/")[-1], len(msgs)), flush=True)
print("restore: %s" % ("OK (10x20)" if restore_ok else "FAIL"), flush=True)
# основная: все 100 уникальных
got = drain(st["main_queue"])
got_bodies = {m["Body"] for m in got}
want = set(st["bodies"])
missing = want - got_bodies
dups = len(got) - len(got_bodies)
print("main: получено %d, уникальных %d, пропущено %d, дублей %d"
% (len(got), len(got_bodies), len(missing), dups), flush=True)
print("MAIN_RESULT: %s" % ("PASS" if not missing else "FAIL (потеря!)"), flush=True)
# cleanup
for u in st["restore_queues"] + [st["main_queue"]]:
try:
sqs.delete_queue(QueueUrl=u)
except Exception:
pass
print("PHASE2_DONE", flush=True)
if __name__ == "__main__":
if len(sys.argv) != 2 or sys.argv[1] not in ("phase1", "phase2"):
print("usage: reboot_probe.py phase1|phase2")
sys.exit(2)
if sys.argv[1] == "phase1":
phase1()
else:
phase2()
+25
View File
@@ -0,0 +1,25 @@
#!/bin/bash
# Полная финальная регрессия после фиксов v0.1.35.
# Запуск на ВМ: nohup bash tests/run_full_regression.sh > full_regression.log 2>&1 &
export AWS_ACCESS_KEY_ID=SSAK-fec713a719ad0b33c91fa54a
export AWS_SECRET_ACCESS_KEY=e898ed410a20ff166f51a52ba39a2954fb87e2da200b1648c3975772bfe9e2b4
export ENDPOINT_URL=https://sqs.containerk8s.dev.nubes.ru
export REGION=us-east-1
cd "$HOME/terra/SQS-service" || exit 1
echo "=== FULL REGRESSION START $(date +%FT%T%z) ==="
echo "--- [1/3] api_test.sh ---"
timeout 900 ./tests/api_test.sh
echo "api_test.sh exit=$?"
echo "--- [2/3] sdk_test.py ---"
timeout 1200 python3 tests/sdk_test.py
echo "sdk_test.py exit=$?"
echo "--- [3/3] fifo_dlq_probe.py ---"
timeout 1200 python3 tests/fifo_dlq_probe.py
echo "fifo_dlq_probe.py exit=$?"
echo "=== FULL REGRESSION END $(date +%FT%T%z) ==="
+90
View File
@@ -0,0 +1,90 @@
#!/usr/bin/env python3
"""tcp_mtu_probe.py — бинарный поиск MTU-порога по TCP к шлюзу Nubes.
Критерий: время до ответа nginx. Тело дошло → мгновенный ответ (<2с).
Сегмент дропнут (MTU/PMTUD) → ответ не приходит (nginx ждёт тело) → считаем
"не прошло" по таймауту FAST_TIMEOUT. Порог ищем делением пополам между
LO и HI, потом уточняем повторными пробами вокруг порога.
"""
import socket
import ssl
import time
HOST = "185.247.187.151"
PORT = 443 # реальный путь клиентов; 32391 (NodePort) снаружи закрыт
LO = 512 # заведомо проходит
HI = 20000 # заведомо не проходит (14+ сегментов по MSS)
FAST_TIMEOUT = 20 # быстрее полного зависания ~51с
FAST_OK = 2.0 # ответ быстрее этого = прошло
ctx = ssl.create_default_context()
ctx.check_hostname = False
ctx.verify_mode = ssl.CERT_NONE
def probe(n):
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.settimeout(FAST_TIMEOUT)
t0 = time.time()
try:
s.connect((HOST, PORT))
mss = s.getsockopt(socket.IPPROTO_TCP, socket.TCP_MAXSEG)
ss = ctx.wrap_socket(s, server_hostname="sqs.containerk8s.dev.nubes.ru")
body = b"x" * n
req = (b"POST / HTTP/1.1\r\n"
b"Host: sqs.containerk8s.dev.nubes.ru\r\n"
b"Content-Type: application/x-www-form-urlencoded\r\n"
b"Content-Length: " + str(n).encode() + b"\r\n"
b"Connection: close\r\n\r\n" + body)
ss.sendall(req)
data = ss.recv(4096)
dt = time.time() - t0
first = data.split(b"\r\n")[0][:40].decode("latin1", "replace")
return dt, mss, first
except Exception as e:
return time.time() - t0, None, "%s" % type(e).__name__
finally:
try:
s.close()
except Exception:
pass
def passed(n):
dt, mss, resp = probe(n)
ok = dt < FAST_OK
print(" size=%6d: %.2fs mss=%s %s -> %s" %
(n, dt, mss if mss else "-", resp, "OK" if ok else "DROP"), flush=True)
return ok
def main():
# санпроверка границ
print("check LO=%d:" % LO)
if not passed(LO):
print("LO не проходит — границы неверны, выход")
return
print("check HI=%d:" % HI)
if passed(HI):
print("HI тоже проходит мгновенно — порога нет в диапазоне, выход")
return
lo, hi = LO, HI
while hi - lo > 50:
mid = (lo + hi) // 2
if passed(mid):
lo = mid
else:
hi = mid
print("\nпорог найден: lo=%d hi=%d (проходит <=lo, дроп >=hi)" % (lo, hi))
# уточнение вокруг порога: 3 пробы на каждую из 4 точек рядом
print("\nуточнение (по 3 пробы):")
for n in (lo - 200, lo, (lo + hi) // 2, hi + 200):
res = sum(1 for _ in range(3) if passed(max(n, 1)))
print(" size=%d: %d/3 прошли" % (n, res))
if __name__ == "__main__":
main()