# Thinking Log — 2026-04-12 # Agent: GitHub Copilot (GPT-5.4) --- ## Задача: Довести расследование проблемы 64KB+ payload до окончательного технического вывода ### Контекст На момент начала этой сессии уже было подтверждено следующее: - сервер shared-sqs после перехода на Redis schema v2 и per-message persistence работает быстро на малых и средних сообщениях; - проблема проявляется именно на payload около 64KB и выше; - через port-forward тот же запрос проходит быстро, значит Go-сервис и Redis не являются первичным узким местом; - через ingress проблема воспроизводится у boto3 и AWS CLI, но не воспроизводится у curl, http.client и низкоуровневого urllib3. Главный незакрытый вопрос был таким: это баг нашего сервиса или поведение платформенного ingress controller штурвала? ### Рабочая гипотеза в начале сессии Если объект ingress у shared-sqs настроен корректно, а итоговый nginx.conf внутри ingress controller не отражает часть аннотаций, то причина находится в платформенном ingress controller, а не в приложении. ### Почему выбрал именно эту гипотезу Потому что она была самой дешёвой для проверки и лучше всего объясняла противоречие: - в YAML ingress аннотация `nginx.ingress.kubernetes.io/proxy-request-buffering: "off"` есть; - в фактическом nginx.conf для host `qu.kube5s.ru` всё равно остаётся `proxy_request_buffering on;`. Если это подтверждается, дальнейший поиск в коде shared-sqs теряет смысл. --- ## Ход расследования ### 1. Проверка, чей это ingress вообще Сначала была цель не гадать, а проверить ownership в кластере. Что было подтверждено: - ingress для `qu.kube5s.ru` обслуживается IngressClass `nginx`; - этот класс ведёт на deployment `shturval-ingress-controller-controller`; - контроллер живёт в namespace `ingress`; - используется образ `r.shturval.tech/ingress-nginx/controller:v1.12.6`; - это не ingress, встроенный в shared-sqs, а платформенный ingress controller кластера. Вывод: проблема находится в общей ingress-инфраструктуре штурвала. ### 2. Проверка, нет ли конфликта нескольких ingress-ресурсов Следующая гипотеза была локальная и простая: возможно, для одного host существует несколько Ingress-объектов, и location-блок в nginx собирается из другого ресурса, не из того YAML, который мы смотрим. Проверка показала: - в кластере для host `qu.kube5s.ru` существует только один ingress: `shared-sqs/shared-sqs-ingress`. Вывод: это не конфликт нескольких ingress-объектов на один host. ### 3. Сверка объекта ingress с фактическим nginx.conf Дальше был ключевой шаг: сравнить декларацию и факт. В самом ingress-объекте у shared-sqs присутствуют: - `nginx.ingress.kubernetes.io/proxy-body-size: "10m"` - `nginx.ingress.kubernetes.io/client-body-buffer-size: "512k"` - `nginx.ingress.kubernetes.io/proxy-request-buffering: "off"` - `nginx.ingress.kubernetes.io/server-snippet: client_body_timeout 120s;` - proxy timeouts. В сгенерированном nginx.conf для `qu.kube5s.ru` было найдено: - `client_max_body_size 10m;` - `client_body_buffer_size 512k;` - `proxy_send_timeout 30s;` - `proxy_read_timeout 30s;` - `proxy_buffering off;` - `proxy_request_buffering on;` Это важнейшая развилка расследования. Что это означает: - ingress controller видит ingress-ресурс; - часть аннотаций применяет корректно; - но конкретно `proxy-request-buffering` не доходит до итоговой конфигурации; - следовательно проблема не в том, что ingress целиком игнорируется; - проблема в selective handling конкретных директив/аннотаций. ### 4. Проверка версии и документации ingress-nginx Дальше нужно было отсечь ещё одну ложную ветку: а вдруг upstream ingress-nginx вообще не поддерживает `proxy-request-buffering` в нашей версии? Проверка документации и исходников upstream ingress-nginx показала: - аннотация `nginx.ingress.kubernetes.io/proxy-request-buffering` официально поддерживается; - в коде есть parser для этого поля; - в upstream есть e2e-тест на сценарий `should turn off proxy-request-buffering`; - в шаблоне nginx используется переменная `location.Proxy.RequestBuffering`. Вывод: upstream ingress-nginx такую аннотацию умеет. Значит поведение штурвала отличается не потому, что аннотация “не существует”, а потому что либо: - в платформенной сборке/конфигурации происходит баг; - либо значение не прокидывается на этапе построения location model; - либо контроллер живёт в состоянии, где дефолт `on` побеждает annotation override. ### 5. Проверка самого шаблона в pod ingress controller Чтобы не строить догадки про кастомный шаблон, была проверка прямо внутри pod. Что найдено: - в `/etc/nginx/template/nginx.tmpl` директива не захардкожена; - там стоит шаблонная подстановка `{{ $location.Proxy.RequestBuffering }}`; - в итоговом `/etc/nginx/nginx.conf` для конкретного host всё равно стоит `on`. Это сузило диагноз ещё сильнее: - шаблон не виноват; - parser и upstream поддержка есть; - значит проблема в данных, которыми шаблон кормят, либо в платформенном runtime поведении контроллера. Именно здесь стало окончательно понятно, что дальше копать shared-sqs бессмысленно. ### 6. Почему `server-snippet` тоже не дал ожидаемого эффекта Параллельно был вопрос: если `proxy-request-buffering` не работает, можно ли продавить workaround через snippet. Проверка документации ingress-nginx показала: - snippet-аннотации по умолчанию контролируются флагом `allow-snippet-annotations`; - его дефолтное значение — `false`. В ConfigMap ingress controller у штурвала этот флаг не был включён. Вывод: - рассчитывать на `server-snippet` и `configuration-snippet` без изменения platform ConfigMap нельзя; - даже если ingress-объект принимает такую аннотацию, итоговая конфигурация может её не внедрить по политике безопасности. ### 7. Наблюдение про ConfigMap drift Ещё один важный операционный вывод дал повторный просмотр ConfigMap ingress controller. Ранее вручную поднимался `client-body-timeout`, но позже в ConfigMap снова был виден `client-body-timeout: "10"`. Это сильный индикатор того, что: - ручные правки штурвального ingress controller могут откатываться; - platform layer, вероятно, управляется Helm/GitOps/reconcile-процессом; - даже если бы ручной patch помог, он мог бы быть временным. Вывод: править такие настройки нужно не как разовую операцию в живом кластере, а в источнике правды платформы. --- ## Итоговые технические выводы ### Что подтверждено надёжно 1. **Go-сервис shared-sqs не является первичной причиной зависания 64KB+ сообщений.** Это доказано быстрым прохождением запросов через port-forward. 2. **Проблемный слой находится на ingress path.** Конкретно — в поведении платформенного ingress controller штурвала. 3. **Ingress YAML приложения сам по себе не является ошибочным.** Нужные аннотации на объекте есть. 4. **Контроллер применяет аннотации выборочно.** `proxy-body-size` и `client-body-buffer-size` доходят до nginx.conf, а `proxy-request-buffering` — нет. 5. **Upstream ingress-nginx поддерживает `proxy-request-buffering`.** Следовательно, это не “неподдерживаемая фича”, а platform-specific проблема/баг/ограничение. 6. **Snippet-аннотации в текущем штурвальном контроллере по факту недоступны как безопасный пользовательский workaround** без отдельного platform-level разрешения. 7. **Ручные правки ConfigMap контроллера выглядят нестабильными и могут откатываться.** ### Что больше НЕ считаю разумным делать 1. Продолжать искать root cause в Go-коде shared-sqs. 2. Тратить время на новые попытки “починить” только ingress приложения без изменения platform controller. 3. Внедрять большой рефакторинг сервиса ради проблемы, лежащей за пределами сервиса. 4. Форсить hard-limit в коде без острой продуктовой необходимости. ### Финальное продуктово-техническое решение После всех проверок наиболее прагматичный вывод такой: - протокольный лимит SQS остаётся 256KB; - **в коде shared-sqs hard-limit 32KB не вводим**; - **операционно считаем 32KB безопасным практическим размером** для клиентов AWS CLI/botocore в текущей инфраструктуре; - проблему 64KB+ классифицируем как ограничение платформенного ingress path, а не баг shared-sqs business logic. --- ## Мысли и оценка инженерного качества решения ### Почему не стоит вводить hard-limit 32KB в коде Изначально идея казалась хорошей: жёстко ограничить размер сообщения и снять проблему. Но по мере расследования стало ясно, что это слишком грубое лечение чужой инфраструктурной болезни. Если мы режем размер в коде, мы: - маскируем platform issue под якобы ограничение сервиса; - вводим продуктовое ограничение, которого нет в протоколе SQS; - создаём технический долг: потом придётся объяснять, почему сервис “совместим с SQS”, но режет на 32KB. То есть hard-limit удобен как короткий workaround, но архитектурно это неправильное место для фикса. ### Почему 32KB всё-таки остаётся хорошей практической рекомендацией Потому что 32KB: - заметно ниже порога деградации; - стабильно проходит через AWS CLI/botocore в текущем ingress path; - покрывает подавляющее большинство типовых сообщений SQS; - даёт пользователю рабочее эксплуатационное правило без вранья про реальные причины. ### Почему идея “переписать сервис с нуля” не выглядит рациональной Этот вопрос возник естественно на фоне раздражения из-за 64KB+ проблемы. Но расследование показало обратное: - core shared-sqs работает хорошо; - сервер не является бутылочным горлышком в текущем кейсе; - переписывание сервиса не уберёт поведение ingress controller штурвала; - значит ROI у полного переписывания низкий. Гораздо разумнее развивать текущий код и отдельно эскалировать platform ingress issue. --- ## Что считать окончательным статусом инцидента ### Статус **Исследование завершено на уровне, достаточном для инженерного решения.** ### Причина остановки дальнейшего копания Не потому, что “не нашли”, а потому что нашли достаточно: - место проблемы локализовано; - границы ответственности определены; - прикладное решение выбрано; - дальнейшее время будет тратиться уже с плохим ROI. ### Если когда-нибудь возвращаться к теме Возвращаться стоит только в двух случаях: - если появится доступ к source-of-truth штурвального ingress controller; - если 64KB+ payload станет реально важным use-case для пользователей. Иначе правильнее оставить это как известное platform limitation. --- ## Короткий финальный вывод одним абзацем Проблема 64KB+ payload у shared-sqs оказалась не багом Go-сервиса и не проблемой Redis/persistence, а ограничением ingress path в кластере штурвала: объект ingress у приложения настроен корректно, часть аннотаций применяется, но именно `proxy-request-buffering` platform controller в итоговый nginx.conf не прокидывает, при этом upstream ingress-nginx такую аннотацию поддерживает. Поэтому вводить hard-limit 32KB в коде я считаю неправильным; правильное практическое решение на текущий момент — оставить сервис без искусственного code-level ограничения, а 32KB считать безопасным эксплуатационным размером и документировать это как известную особенность инфраструктуры. --- # Agent: GitHub Copilot (GPT-5.4) ## Задача: дать обычному demo-пользователю понятный вход в UI без личного Nubes JWT ### Контекст После переработки публичного showcase-репозитория осталась продуктовая дыра: AWS CLI уже имел публичные demo credentials, а UI по-прежнему требовал личный nubes JWT. Для обычного пользователя это ломало демо-сценарий: CLI можно попробовать сразу, а UI нет. ### Локальная гипотеза Если в коде уже существует сидированный demo tenant с фиксированными AWS credentials, то самый дешёвый и чистый путь — не придумывать новую сущность, а добавить отдельный UI demo token, который логинит ровно в этот tenant. Тогда CLI и UI будут опираться на один и тот же демонстрационный контур. ### Что проверил перед правкой 1. В `app/admin/admin.go` единственный публичный UI login endpoint `POST /ui/api/auth` принимал только JWT, парсил claims и всегда вызывал `PingNubesAPI`. 2. В `app/ui/index.html` логин-форма явно требовала `API Token (nubes JWT)`. 3. В `app/cmd/seed.go` уже существует seeded demo tenant: - tenant ID: `t-demo-shared-sqs-ngcloud` - access key: `SSAK-demo-shared-sqs` - secret key: `demo-secret-key-shared-sqs-ngcloud-2026` 4. Дополнительно нашёл соседний дефект: UI routes переиспользовали admin handlers и `GET /ui/api/tenants` возвращал весь список tenant-ов, а `/ui/api/health` показывал глобальные счётчики сервиса. Для demo-login это недопустимо. ### Решение Сделал один узкий срез: 1. Добавил публичный UI demo token `demo-ui-shared-sqs-ngcloud-2026` с возможностью переопределения через env. 2. Привязал его к уже существующему seeded demo tenant. 3. Оставил существующий JWT flow без изменения для реальных пользователей. 4. Начал класть авторизованный UI tenant в request context. 5. Ограничил UI API текущим tenant-ом: - `GET /ui/api/tenants` возвращает только своего tenant-а; - `GET /ui/api/health` считает только свои очереди и сообщения; - создание и удаление tenant-а через UI запрещены. 6. Обновил встроенный UI: форма логина теперь прямо подсказывает demo token и больше не выглядит как админская панель для управления всеми tenant-ами. ### Почему именно так - Это минимальное изменение с хорошим ROI: один новый demo token закрывает UX-проблему без нового storage, без нового auth-service и без изменения AWS credentials. - Demo-пользователь теперь видит только demo tenant и не получает случайный обзор всей системы. - CLI и UI сходятся на одной и той же demo-учётке, то есть продуктовая история становится понятной. ### Что сознательно НЕ делал - Не убирал JWT flow. - Не строил отдельную demo role model. - Не менял SQS auth path для AWS CLI. - Не пытался превращать UI в полноценную admin console и пользовательскую console одновременно: для UI выбрал явный user/demo режим с одним tenant-ом. --- # Agent: GitHub Copilot (GPT-5.4) ## Задача: перепроверить live-стенд и довести deploy до рабочего состояния ### Что проверял После жалобы на `invalid JWT: expected 3 parts, got 1` я не стал объяснять по памяти, а перепроверил три вещи по факту: 1. В `main` уже есть demo UI код. 2. В кластере реально был запущен образ `naeel/shared-sqs:v0.1.21`. 3. Live `POST /ui/api/auth` на demo token действительно отвечал старой JWT-ошибкой. ### Вывод Проблема была не в коде ветки и не в браузере, а в том, что стенд ещё не был выкатан на новый образ. ### Что сделал 1. Обновил version surfaces до `v0.1.22` в deploy/helm файлах. 2. Собрал и запушил `naeel/shared-sqs:v0.1.22`. 3. Попытался выкатить через Helm. 4. Helm upgrade упёрся в field-manager conflict на поле image (`kubectl-set` vs Helm). 5. Чтобы не тратить время на долгую reconcile-разборку прямо посреди проверки стенда, переключил live deployment через `kubectl set image`. 6. Дождался успешного rollout. 7. Перепроверил live demo auth и потом прогнал `bash tests/quick_test.sh` против `https://qu.kube5s.ru`. ### Итог - live deployment теперь на `naeel/shared-sqs:v0.1.22`; - demo token работает; - реальный JWT flow не сломан; - `quick_test.sh` дал `31/31 PASS`. ### Почему этот путь был правильным Пользователь явно потребовал не рассказывать, а сначала всё перепроверять и доводить до рабочего состояния. Поэтому после обнаружения расхождения между `main` и live-стендом я не остановился на объяснении, а довёл цепочку до фактического результата на боевом endpoint. --- ## Внешние ссылки для возврата к теме 64KB+ Если к проблеме придётся вернуться через недели или месяцы, начинать смотреть отсюда. ### Платформа Штурвал - Архитектура платформы Штурвал Community Edition: https://docs.k8s.ngcloud.ru/2.12/docs/common/structure/ Зачем это важно: - страница подтверждает, что Nginx Ingress Controller входит в состав платформы; - это усиливает вывод, что ingress controller для `qu.kube5s.ru` является platform-managed компонентом, а не частью shared-sqs. ### Официальная документация ingress-nginx - Аннотации ingress-nginx: https://kubernetes.github.io/ingress-nginx/user-guide/nginx-configuration/annotations/ - ConfigMap ingress-nginx: https://kubernetes.github.io/ingress-nginx/user-guide/nginx-configuration/configmap/ Ключевые места в этих документах: - `nginx.ingress.kubernetes.io/proxy-request-buffering` официально поддерживается; - `allow-snippet-annotations` по умолчанию имеет значение `false`; - `server-snippet` и `configuration-snippet` нельзя считать доступным workaround без platform-level разрешения. ### Upstream исходники ingress-nginx - parser аннотации `proxy-request-buffering`: https://github.com/kubernetes/ingress-nginx/blob/main/internal/ingress/annotations/proxy/main.go - e2e-тест на `should turn off proxy-request-buffering`: https://github.com/kubernetes/ingress-nginx/blob/main/test/e2e/annotations/proxy.go Зачем это важно: - это прямое подтверждение, что в upstream поведение поддерживается и ожидается; - если проблема повторится, не надо заново спорить, существует ли такая аннотация вообще. ### Что проверить первым делом при новом раунде расследования 1. Существует ли по-прежнему только один ingress для `qu.kube5s.ru`. 2. Осталась ли аннотация `proxy-request-buffering: "off"` на объекте ingress. 3. Что реально сгенерировано в `/etc/nginx/nginx.conf` у ingress controller. 4. Не изменились ли версия штурвала и версия ingress-nginx controller. 5. Не включили ли на platform уровне `allow-snippet-annotations`. 6. Не появился ли доступ к source-of-truth конфигурации платформенного ingress controller. --- ## Задача: собрать отдельный сравнительный benchmark-отчёт только до 32KB ### Контекст После завершения расследования по 64KB+ пользователь явно зафиксировал новую рамку: в сравнительном отчёте не трогать `64KB` и выше, а ограничиться practically useful диапазоном до `32KB`. ### Что сделал 1. Выделил отдельный benchmark-сценарий `tests/benchmark_compare_32k.sh`, чтобы не смешивать его с прежними широкими сценариями. 2. Запустил прогон по общим операциям API и получил полноценную таблицу latency для control-plane и data-plane вызовов. 3. Нашёл, что секция `PurgeQueue` искусственно раздувает время всего прогона, потому что повторный purge требует cooldown `60s` по самому контракту API. Убрал многократные sleep и оставил одиночный контрольный замер. 4. Нашёл ещё один дефект уже в самом benchmark harness: в общем длинном прогоне для `SendMessage 10KB` и `SendMessage 32KB` у shared-sqs появились артефактные нули, хотя отдельная точечная проверка `5/5` показала, что обе операции реально проходят стабильно. 5. Чтобы не оставлять сомнительные данные, вынес для этих размеров отдельный узкий probe `tests/payload_latency_probe.sh` и снял повторные latency-цифры отдельно. 6. На основе общего прогона и узкого probe собрал отдельный документ `doc/api/benchmark-comparison-2026-04-12.md`. ### Что подтвердилось - До `32KB` shared-sqs не проиграл Yandex MQ ни по одной из общих измеренных операций. - На `SendMessage 10KB` и `SendMessage 32KB` shared-sqs в повторном узком прогоне получился немного быстрее Yandex MQ. - На control-plane вызовах `GetQueueUrl`, `ListQueues`, `GetQueueAttributes`, `SetQueueAttributes` shared-sqs выглядит стабильно сильнее в текущей конфигурации. - Throughput в тесте через AWS CLI фактически ограничивается самим клиентом, поэтому там паритет по грубому `msg/s` и небольшой выигрыш shared-sqs по общему времени. ### Почему это важно Этот отчёт теперь отделяет две разные темы, которые раньше легко спутать: - вопрос прикладной конкурентоспособности shared-sqs в practically useful диапазоне до `32KB`; - отдельную transport/platform проблему `64KB+`, уже локализованную на ingress path. --- # Agent: GitHub Copilot (Claude Opus 4.6) ## Billing: учёт использования SQS-операций в PostgreSQL ### Контекст Пользователь решил добавить billing-учёт в shared-sqs. Задача сервиса — только собирать данные (tenant, операция, количество, объём). Подсчёт денег — отдельный биллинг-сервис. ### Решения (согласованы с пользователем) 1. **Одна таблица** `sqs_usage_records` — не по тенанту. PostgreSQL держит сотни миллионов строк с индексом. 2. **Строка на каждый API-вызов** — без агрегации. Место дешёвое. 3. **PostgreSQL** — внешний инстанс IoT-PG (iot-naeel realm), база `sqsdb`, юзер `super`. 4. **Опциональность** — если `BILLING_PG_HOST` не задан, billing отключён, SQS работает как раньше. 5. **При старте** — auto-migrate: CREATE TABLE IF NOT EXISTS + индекс. 6. **Helm chart** — секция `billing:` с enabled/postgres параметрами. ### Реализация - Новый пакет `app/billing/billing.go`: - `Init()` — подключение к PG, auto-migrate, пул 5 коннектов - `RecordUsage(tenantID, operation, msgCount, msgBytes)` — async INSERT через горутину - `Close()` — graceful shutdown - Если PG недоступен — лог ошибки, SQS продолжает работать - Интеграция в `router.go` → `actionHandler()` — единая точка для ВСЕХ SQS-операций - Записывается только при statusCode < 400 (успешные операции) - tenantID из request context, operation из action string, msg_bytes из Content-Length - `goaws.go` (main) — `billing.Init()` при старте, `billing.Close()` при shutdown - Helm: `values.yaml` (billing section), `secret-billing.yaml`, `deployment.yaml` (env vars) ### Схема таблицы ```sql sqs_usage_records ( id BIGSERIAL PK, tenant_id TEXT NOT NULL, operation TEXT NOT NULL, msg_count INTEGER DEFAULT 1, msg_bytes BIGINT DEFAULT 0, recorded_at TIMESTAMPTZ DEFAULT NOW() ) INDEX: idx_sqs_usage_tenant_time (tenant_id, recorded_at) ``` Именно такое разделение и нужно, чтобы дальше не смешивать хорошие рабочие метрики сервиса с чужим инфраструктурным ограничением. --- ## Prometheus Metrics + Victoria Metrics integration ### Контекст После добавления billing (PostgreSQL) решено добавить Prometheus-метрики для мониторинга в реальном времени. Проверил кластер — уже установлены: - Victoria Metrics с оператором (namespace `victoria-metrics`) - VMAgent с pod collectors — скрейпят через VMServiceScrape / VMPodScrape CRD - Grafana на `grafana.ngcloud.ru` — подключена к VM ### Решение Не писать свой мониторинг — подключиться к существующей инфраструктуре: 1. shared-sqs отдаёт `/metrics` в Prometheus-формате 2. VMServiceScrape говорит VMAgent скрейпить наш Service 3. Grafana видит данные через VM — дашборд можно создать вручную ### Метрики | Метрика | Тип | Labels | Назначение | |---------|-----|--------|-----------| | `sqs_requests_total` | Counter | tenant, operation | Количество запросов | | `sqs_request_bytes_total` | Counter | tenant, operation | Объём трафика | | `sqs_request_duration_seconds` | Histogram | operation | Latency (бакеты 1ms — 30s) | | `sqs_errors_total` | Counter | operation | Ошибки (HTTP >= 400) | | `sqs_queues_count` | Gauge | tenant | Текущее кол-во очередей | | `sqs_messages_count` | Gauge | tenant | Текущее кол-во сообщений | ### Реализация - `app/metrics/metrics.go` — определение метрик через promauto - `app/metrics/gauge_updater.go` — горутина, пересчёт gauges каждые 15s через RLock - `router.go` — `/metrics` endpoint + инструментация actionHandler (duration, counters, errors) - `goaws.go` — запуск gauge updater из main - `deployments/k8s/vmservicescrape.yaml` — VMServiceScrape (каждые 30s, port http) ### Проблема: Go 1.22 → 1.23 Prometheus client v1.23.2 требует Go >= 1.23. go.mod обновился автоматически. Пришлось обновить Dockerfile с `golang:1.22-alpine` на `golang:1.23-alpine`. ### Результат - `/metrics` отдаёт все sqs_* метрики - VMServiceScrape applied, status pending (VMAgent подхватывает) - quick_test.sh: 31/31 PASS - Docker image: `naeel/shared-sqs:v0.1.24`