# История правок shared-sqs — 2026-10-10 (публичная точка на ВМ, TLS, замеры) > Запись по требованию «документировать всё, включая собственные ошибки» (приказ 2026-09-24). > Предыдущие: `HISTORY/2026-10-07-session-log.md`, `doc/thinking/*-2026-10-08.md`. > Сессия велась двумя моделями: сначала DeepSeek Flash, затем DeepSeek Pro (архитектор) — > Pro перепроверял выводы Flash, часть из них оказалась неверной (см. раздел 4). ## 1. Задача Вынести публичную точку SQS на ВМ `5.172.178.213` (обход больного managed-шлюза) и **проверить гипотезу** из `MANUAL-NEXT-2026-10-08.md` (Шаг 1): нет ли на пути «интернет → ВМ» той же сетевой болезни (зависания ~51 с, таймауты 31–33 с). Требования владельца по пути: домен — только свой (`sinefo.ru`); `kube5s` из проекта убрать. ## 2. Хронология (что делалось и что получалось) ### 2.1. Сборка и запуск на ВМ - `make build` → `shared-sqs` v0.1.35 (статический, x86_64, 17 МБ). - `scp` → `naeel@5.172.178.213:~/sqs-test/shared-sqs`. - Запуск memory-only: `SHARED_SQS_SEED_DEMO=true ./shared-sqs --admin-token=dev-token-123 --port=4100`. - Локально на ВМ: `/health` → `200 {"status":"ok","version":"v0.1.35"}`; `ListQueues` без подписи → `403 MissingAuthenticationToken` (корректно). - ⚠️ Предупреждение `Failure to find default config file` — `goaws.yaml` на ВМ нет, приложение работает на встроенных дефолтах (см. 5.1). ### 2.2. Разведка ВМ - Публичный адрес — `5.172.178.213` (NAT); внутренний `10.10.102.21` (`ens192`), hostname `naeel-vm`. - Снаружи прилетают только 80/443. Проверка портов: - `8443` — занят DNAT: `iptables -t nat`: `--dport 8443 -j DNAT --to-destination 95.179.252.111:443` (сервис `vpn-transit-dnat`, `enabled/active`) → весь трафик на 8443 уходит на Vultr **до** ВМ. - `8888`/`9443` — `/dev/tcp` показывает «OPEN», но локально **никто не слушает** и HTTP не отвечает (curl timeout) → это SYN-proxy провайдерского firewall, порты непригодны. - `/etc/hosts` на ВМ содержит внутренние имена: `gitea.mgmt.nubes.ru`, `sqs.containerk8s.dev.nubes.ru` → `185.247.187.151`, `tf-registry.containerk8s.services.ngcloud.ru`, `s3.msk-1.ngcloud.ru`. Резолвер ВМ — публичный (`8.8.8.8`, `1.1.1.1`), поэтому имена `*.services.ngcloud.ru` не резолвятся. ### 2.3. Блокер nginx (возник 10-08, снят 10-10) - 10-08: `sudo nginx -t` **падал**: `host not found in upstream "contractor.pythonk8s.services.ngcloud.ru" in sites-enabled/check.kube5s.ru:44` — внутреннее имя не резолвится. - 10-10 (после ребута ВМ): конфиг `check.kube5s.ru` оказался **изменён** (catch-all `location /` на contractor убран, добавлен `location /rf`) → `nginx -t` **проходит**, nginx перезагружаем. - Следствие: переход к рабочему пути упростился (сначала была построена схема со вторым nginx, см. 2.4). ### 2.4. Второй nginx на 10443 (сделан, затем убран) - Владелец пробросил порт: firewall `[22, 80, 443, 4443, 10443]`. - Проверено: `10443` реально доходит до ВМ (`/health` 200), `4443` — нет. - Поднят **отдельный** nginx: `/etc/nginx-sqs/nginx.conf`, pid `/run/nginx-sqs.pid`, `listen 10443` → `proxy_pass http://127.0.0.1:4100`. Запуск `sudo nginx -c /etc/nginx-sqs/nginx.conf`. ### 2.5. Замеры (гипотеза ПОДТВЕРЖДЕНА) | Тест | Инструмент | Результат | |---|---|---| | HTTP, 30 запросов 1КБ/10КБ/100КБ/256КБ | `curl --aws-sigv4` | 30/30 **200**, max **0.99 с** | | HTTPS, 10 запросов 26КБ…260КБ | `curl --aws-sigv4` | 10/10 **200**, 0.7–3.2 с | | Long-poll `ReceiveMessage` `WaitTimeSeconds=20` | `curl` | **20.3 с, 200** (корректно) | Ни зависаний ~51 с, ни таймаутов 31–33 с. **На пути «интернет → ВМ 213» болезни нет.** ### 2.6. DNS - `sinefo.ru` делегирован в реестре .ru на `ns3.ngcloud.ru` / `ns4.ngcloud.ru` (проверено через `a.dns.ripn.net`). - `sqs.sinefo.ru A 5.172.178.213` — публично резолвится. Ранее запись была `185.247.187.83`, что = **IP самого DNS-сервера** `ns3.ngcloud.ru`, а не ВМ (исправлено владельцем). ### 2.7. TLS (Let's Encrypt) - `sudo certbot certonly --webroot -w /var/www/acme -d sqs.sinefo.ru --non-interactive --agree-tos` (аккаунт LE на ВМ уже существовал → без нового e-mail) → `/etc/letsencrypt/live/sqs.sinefo.ru/`. - Срок: до 2027-01-08, автопродление настроено. - ACME-челлендж обслуживается главным nginx (vhost 80 с `location /.well-known/acme-challenge/`). ### 2.8. Публичная точка (итог) - Главный nginx, `/etc/nginx/conf.d/sqs-sinefo.conf`: - **80**: ACME-челлендж + редирект на https; - **443 ssl**: сертификат `sqs.sinefo.ru` + `proxy_pass http://127.0.0.1:4100`, `Host $http_host`. - Проверено снаружи: `https://sqs.sinefo.ru/health` → 200 (cert valid), SigV4, 256 КБ за 1.05 с. ### 2.9. Уборка - Второй nginx (10443) остановлен, `/etc/nginx-sqs` удалён; порт 10443 свободен. - `conf.d/sqs-sinefo.conf` — заменён на нормальный vhost (было: HTTP-only левак от 10-08). - Из **актуального** дока `doc/2026-08-13-migration-to-managed.md` убран `kube5s.ru` (квинтет: `iot.kube5s.ru`→`iot.nubes.ru`, `qu.kube5s.ru`×3→`sqs.containerk8s.dev.nubes.ru`), коммит `a47743f`. В `doc/legacy/` (история) не трогал. ### 2.10. Фикс QueueUrl (кодинг) - Проблема: `QueueUrl` = `http://region.host:port/...` (плейсхолдеры) — клиенты без `endpoint_url` сломаны. - Реализовано: `Environment.ExternalURLBase` + env `SHARED_SQS_EXTERNAL_URL_BASE`. - **Ошибка по пути №1:** env-override сначала поместил после `models.CurrentEnvironment = envs[env]` — не сработал: `LoadYamlConfig` делает ранний `return` без `goaws.yaml`. Перенёс в `applyEnvironmentDefaults` (вызывается через defer → всегда). - **Ошибка по пути №2:** после переноса всё равно не работало — демо-seed (`app/cmd/seed.go`) собирал URL **своей копией** логики, а не через `tenantQueueURL`. Вынес `gosqs.TenantQueueURL`, seed переведён на него. - Коммиты `469132b`, `ca0372b`; тест `app/gosqs/tenant_helpers_test.go`. Проверено снаружи: `list-queues` → `https://sqs.sinefo.ru/t-.../orders`. ### 2.11. Устойчивость: systemd + локальный Redis - Установлен `redis-server` (7.0.15) на 213, включён AOF (`appendonly yes`). - systemd-юнит `shared-sqs.service` (`Restart=always`, `enable`) + `/etc/shared-sqs.env`. - Проверено: сообщение пережило рестарт; `systemctl is-enabled/active` → enabled/active. - **Дефекты seed (обнаружены и исправлены, коммит `c0aa618`):** демо-очереди не персистились; каждый старт плодил нового демо-тенанта (утечка при `Restart=always`). Фикс: `SaveQueue`+ `SaveMessages` + идемпотентность через `store.List()`. - Управление: `sudo systemctl {status,restart,stop} shared-sqs`. ### 2.12. Прод-хардненинг - `SHARED_SQS_SEED_DEMO=false`; admin-token сменён на случайный 64-hex (в `/etc/shared-sqs.env`); logrotate для `sqs.log`. - Billing/JWT не настроены. Проверено: сервис работает на существующем тенанте из Redis. ### 2.13. Шаг 2 — разбиение data-plane / control-plane - `router.NewDataPlane` (публичный SQS API) + `router.NewControlPlane` (админка/метрики/UI). - `main`: два листенера — `0.0.0.0:4100` (публично) и `127.0.0.1:4101` (control-plane, только локально). - Проверено: публично `/admin`/`/metrics` → 403 (наружу не доступны), SQS API работает; локально `4101/admin` → 200. Коммит `c1520c0`. - Примечание: пока один процесс с двумя листенерами (общее in-memory состояние); два процесса — после Шага 3. ### 2.14. Шаг 3 — начало: дизайн + тенанты read-through Redis - Дизайн: `doc/thinking/step3-state-extraction-design-2026-10-10.md` (варианты A/B/C, рекомендация C→A, что замерить). - Код (коммит `779a71d`): Redis-индекс `ssq:tenant_ak` (accessKey → JSON тенанта) + бэкфилл при старте; auth-middleware читает тенанта из Redis при промахе локальной памяти. - Нужно для ЛЮБОГО multi-instance варианта; в одном процессе — страховка. - Проверено: индекс заполнен (`hlen ssq:tenant_ak` = 1), регрессий нет. ### 2.15. Durability batch + billing - `SendMessageBatch` переведён на `SaveMessagesSync` (было async) + не персистит дубли (коммит `3559414`). - Проверено: batch в пустую очередь пережил рестарт. - Billing — предусмотрен (модуль есть), но выключен (`BILLING_PG_HOST` не задан). Решение: включить позже. ### 2.16. Шаг 3 — HA active/passive (leader-election) - Выбран **вариант C** (active/passive через Redis-лок) вместо полного выноса состояния (A) — обоснование: причина простоя — недоступность ВМ, а не CPU; C стоит ~200 строк и не трогает ядро очередей. Подробно: `doc/thinking/state-extraction-and-ha-2026-10-10.md`. - Код (коммит `bc00643`): - `app/persistence/redis.go`: `TryAcquireLock` (SETNX+PX), `RefreshLock`/`ReleaseLock` (Lua: только если значение наше). При `Client == nil` — всегда `true` (мы одни). - `app/leader/leader.go`: `Elector` — цикл «продлить/взять лок» каждые 3 с, TTL 10 с, `IsLeader()`. - `app/router/router.go`: `/ready` (в обоих плоскостях) — 200 если готов, 503 если пассивный; поведение задаётся пакетной `ReadyFunc`. - `app/cmd/goaws.go`: `--leader-election` (или env `SHARED_SQS_LEADER_ELECTION=true`), `--leader-ttl`, `--leader-interval`; `ReadyFunc` ставится только при включении; лок отпускается при graceful shutdown. **По умолчанию выключено — поведение не изменилось.** - **Замеры (не гипотезы), ВМ 213, два временных инстанса 4200/4210:** - одна копия → `/ready` `200`, вторая → `503` (split-brain нет); `redis-cli get ssq:leader` = `-` лидера; - `kill -9` лидера → пассивный стал лидером, `/ready` → `200`, перехват занял **~12 с** (максимум по формуле `ttl+interval` = 13 с); - прод-API после деплоя: `list-queues` / `send-message` / `receive-message` через `https://sqs.sinefo.ru` — OK; временные инстансы остановлены, ключ `ssq:leader` удалён. - **В проде выключено**: для включения нужны общий Redis + вторая ВМ + LB с проверкой `/ready` (иначе пассивный получит трафик и вернёт «пустые» очереди — главный риск варианта C, описан в доке). - Док (коммит `865db49`): почему C, механика, замеры, ограничения, пошаговое включение, откат, путь к варианту A (5 шагов) и триггеры перехода. ### 2.17. Код-ревью Opus + критический разбор - Дал промпт Opus: полное код-ревью + советы по архитектуре. Дословный ответ: `doc/thinking/opus-report-2026-10-10.md`; критический разбор + что подтверждено: `doc/thinking/opus-report-analysis-2026-10-10.md`. - Opus указал ~50 пунктов (🔴/🟡/🟢). Моя оценка: силён по конкуренции/durability, но смешивает баги / осознанный дизайн / отклонения от семантики AWS; часть 🔴 надо понизить. - **Подтверждено по коду (чинится без владельца):** - 🔴1 фатал `concurrent map iteration and map write`: `create_queue.go:84` зовёт `SaveQueue` под `SyncQueues.Lock` без `queue.mu`; `SaveQueue`→`json.Marshal` итерирует `Duplicates`/`FIFO*`/`Tags`, а мутаторы держат только `queue.mu`. Условие: идемпотентный CreateQueue существующей FIFO-очереди + SendMessage. - 🔴5 неполный откат dedup при сбое `SaveMessageSync` (`send_message.go:151-160`) → ретрай 200 без сохранения. - 🔴8 пустой `ReceiptHandle` в DeleteMessageBatch удаляет все невыданные сообщения — **только через JSON-протокол** (`x-amz-json-1.0`); Query-форма защищена (`requests.go:568` не на этом пути). - Порядок локов + I/O под глобальным локом; fail-fast на старте без Redis отсутствует. - **Нужны решения владельца:** - JWT: проверяет ли шлюз подпись (trusted-perimeter из `jwt.go:7-11`)? Независимо чинятся: три шлюза prod/test/dev → prod; потолок grace в `cache.go:68-75`; идентичность по `sub`, не email. - Лимит по байтам (OOM, 🔴14): нужны RAM ВМ + число тенантов → cap на тенанта/очередь + глобальный предохранитель по RSS. - Порядок исправления — §5 дока анализа. ### 2.18. Правки по ревью (применено, 3 коммита) - `654e315` — 🔴1 (SaveQueue под queue.mu), 🔴8 (пустой ReceiptHandle), 🔴5 (dedup только после успешной записи, single+batch), глобальный предохранитель HeapAlloc (`memory_guard.go`, `SHARED_SQS_MAX_HEAP_MB`, 0=выключен). - `1230149` — JWT: только prod-шлюз (убран test/dev); grace в `cache.go` ограничен `graceDeadline` от последней успешной проверки (не продлевается бесконечно). - `2187027` — fail-fast: при заданном `REDIS_ADDR` и недоступном Redis → `log.Fatalf`. - build + vet + test — зелёные. Не задеплоено на ВМ (жду команды). - НЕ сделано (нужны решения владельца): цифры лимита по байтам (RAM/тенанты); идентичность JWT по `sub` вместо email (зависит от того, проверяет ли шлюз подпись). Порядок локов и I/O под глобальным локом — осталось (более крупный рефакторинг). ## 3. Грабли (для повторного использования) 1. **`proxy_set_header Host $host` теряет порт** → SigV4 `SignatureDoesNotMatch`. Нужен `$http_host`. (nginx `$host` — имя без порта; `$http_host` — заголовок целиком.) 2. **`~/.aws/config [default]` содержит `endpoint_url = https://s3.msk-1.ngcloud.ru`** → `aws sqs ...` без явного `--endpoint-url` уходит в S3 и получает `405 MethodNotAllowed` (Ceph RGW, `HostId=...-dc1-msk-1`). **Всегда задавать `--endpoint-url`.** 3. **Креды демо-тенанта рандомны при каждом старте** — брать из `~/sqs-test/sqs.log` (`seed: создан тенант ... AccessKey=...`). При «address already in use» в логе креды **мёртвого** инстанса. 4. `pkill -f "http.server 10443"` по SSH **самоматчится** на строку ssh-команды и убивает сессию — использовать `pkill -x <имя>`. 5. Порт `8443` на 213 **нельзя занимать** — там DNAT транзита на Vultr. 6. **Managed Redis недоступен с 213**: его endpoint — внутренний k8s DNS (`redisk8s..svc.cluster.local:6379`), резолвится только изнутри кластера. С ВМ — только локальный Redis или внешний endpoint от NUBES. 7. Локальный Redis на 213 **не спасает от падения ВМ** (SQS и Redis — на одной машине, SPOF). ## 4. ОШИБКИ (сводка, включая ошибки Flash) | # | Что утверждалось/сделано | Что на самом деле | Исправление | |---|---|---|---| | 1 | «При ребуте ВМ nginx не поднимется» (из-за битого upstream) | После ребута nginx `active`, 80/443 слушают | Неверный вывод; снят при перепроверке | | 2 | «`.83` — мёртвый IP» | `185.247.187.83` = живой DNS `ns3.ngcloud.ru` (порт 53); проверял только 80/443 | Признано при перепроверке | | 3 | Запуск второго инстанса при живом первом | `fatal: bind: address already in use`; лог перезаписан кредами мёртвого → `InvalidClientTokenId` | `pkill -x shared-sqs`, один инстанс, креды из лога | | 4 | Второй nginx: `Host $host` | SigV4 `SignatureDoesNotMatch` | `Host $http_host` | | 5 | Прямые DNS-выводы по регексу `location / ` | Первый `grep` не нашёл (паттерн с пробелом) — чуть не сделал ложный вывод | Перечитал файл целиком (`cat`) | | 6 | «Только 80/443 открыты» | 8888/9443 «принимают» TCP (SYN-proxy), но пусты | Уточнено: рабочие — только 80/443 | ## 5. Состояние и воспроизведение ### 5.1. Итоговое состояние - **Работает:** `https://sqs.sinefo.ru` (443, TLS) → shared-sqs на ВМ 213 (memory-only, `127.0.0.1:4100`). - SQS: `~/sqs-test/shared-sqs`, лог `~/sqs-test/sqs.log`, pid меняется. Демо-тенант + 5 очередей. - **QueueUrl — ИСПРАВЛЕНО** (см. 2.10): `https://sqs.sinefo.ru//`. ### 5.2. Как поднять заново ```bash # на ВМ 213 cd ~/sqs-test && setsid nohup env SHARED_SQS_SEED_DEMO=true \ ./shared-sqs --admin-token=dev-token-123 --port=4100 --loglevel=info > sqs.log 2>&1