23 KiB
23 KiB
История правок 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-sqsv0.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), hostnamenaeel-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-alllocation /на contractor убран, добавленlocation /rf) →nginx -tпроходит, nginx перезагружаем. - Следствие: переход к рабочему пути упростился (сначала была построена схема со вторым nginx, см. 2.4).
2.4. Второй nginx на 10443 (сделан, затем убран)
- Владелец пробросил порт: firewall
[22, 80, 443, 4443, 10443]. - Проверено:
10443реально доходит до ВМ (/health200),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+ envSHARED_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(или envSHARED_SQS_LEADER_ELECTION=true),--leader-ttl,--leader-interval;ReadyFuncставится только при включении; лок отпускается при graceful shutdown. По умолчанию выключено — поведение не изменилось.
- Замеры (не гипотезы), ВМ 213, два временных инстанса 4200/4210:
- одна копия →
/ready200, вторая →503(split-brain нет);redis-cli get ssq:leader=<hostname>-<pid>лидера; 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 отсутствует.
- 🔴1 фатал
- Нужны решения владельца:
- JWT: проверяет ли шлюз подпись (trusted-perimeter из
jwt.go:7-11)? Независимо чинятся: три шлюза prod/test/dev → prod; потолок grace вcache.go:68-75; идентичность поsub, не email. - Лимит по байтам (OOM, 🔴14): нужны RAM ВМ + число тенантов → cap на тенанта/очередь + глобальный предохранитель по RSS.
- JWT: проверяет ли шлюз подпись (trusted-perimeter из
- Порядок исправления — §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. Грабли (для повторного использования)
proxy_set_header Host $hostтеряет порт → SigV4SignatureDoesNotMatch. Нужен$http_host. (nginx$host— имя без порта;$http_host— заголовок целиком.)~/.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.- Креды демо-тенанта рандомны при каждом старте — брать из
~/sqs-test/sqs.log(seed: создан тенант ... AccessKey=...). При «address already in use» в логе креды мёртвого инстанса. pkill -f "http.server 10443"по SSH самоматчится на строку ssh-команды и убивает сессию — использоватьpkill -x <имя>.- Порт
8443на 213 нельзя занимать — там DNAT транзита на Vultr. - Managed Redis недоступен с 213: его endpoint — внутренний k8s DNS
(
redisk8s.<uid>.svc.cluster.local:6379), резолвится только изнутри кластера. С ВМ — только локальный Redis или внешний endpoint от NUBES. - Локальный 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/<tenantID>/<queue>.
5.2. Как поднять заново
# на ВМ 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 </dev/null &
# креды демо-тенанта — в sqs.log
grep "seed: создан тенант" ~/sqs-test/sqs.log | tail -1
# проверка снаружи
curl -sS https://sqs.sinefo.ru/health
5.3. Откат
sudo rm /etc/nginx/conf.d/sqs-sinefo.conf && sudo nginx -t && sudo systemctl reload nginx
pkill -x shared-sqs
# бэкапы nginx на ВМ: /etc/nginx/conf.d.bak-*
6. Открытые вопросы
- Клиенты без явного
endpoint_url— есть ли такие (вопрос №5 мануала).QueueUrlуже корректный. - Egress ВМ:
curl https://api.ipify.orgс ВМ вернул пусто — влияет на billing (PG) и JWT-API. SaveQueue(метаданные очереди) всё ещё пишется асинхронно — при failover новый лидер может не увидеть очередь, созданную незадолго до падения. Первый шаг к варианту A (см. §8 дока по HA) — сделать синхронно, какSaveMessagesSync.- Для включения HA нужны: общий Redis (сейчас только локальный на 213 — SPOF),
вторая ВМ, LB с проверкой
/ready. Пока всё это не готово — флаг выключен. - Дальше по мануалу: обновить устаревший
MANUAL-NEXT-2026-10-08.md(ещё говорит «Шаг 1 — проверить гипотезу» и «вопрос по домену открыт»); решить вопрос push в origin (18 коммитов впереди). - JWT (решение владельца): проверяет ли шлюз подпись и отдаёт 200 только для валидных токенов?
От этого — 🔴 или «доверяем шлюзу». См.
opus-report-analysis-2026-10-10.md§3. - Лимит по байтам (решение владельца): нужны RAM ВМ 213 и целевое число тенантов для цифр cap.
См.
opus-report-analysis-2026-10-10.md§4. - 🔴1/🔴5/🔴8 + fail-fast + JWT + heap-guard — применены (коммиты 654e315/1230149/2187027), не задеплоено. Осталось: порядок локов + I/O под глобальным локом (рефакторинг).