docs: finalize 64kb ingress investigation
This commit is contained in:
@@ -0,0 +1,103 @@
|
|||||||
|
# Decision: Ограничение MaximumMessageSize — 2026-04-11
|
||||||
|
|
||||||
|
## Update — 2026-04-12
|
||||||
|
|
||||||
|
Первичное решение из этой записи было пересмотрено после дополнительного расследования ingress controller штурвала.
|
||||||
|
|
||||||
|
### Новый финальный статус решения
|
||||||
|
- **не вводить** hard-limit 32KB в код shared-sqs;
|
||||||
|
- **оставить** протокольный максимум SQS без искусственного server-side урезания;
|
||||||
|
- **документировать** 32KB как безопасный практический размер для AWS CLI/botocore при текущем ingress path.
|
||||||
|
|
||||||
|
### Почему решение изменено
|
||||||
|
На момент первоначальной записи было уже понятно, что 32KB работает стабильно, но не было окончательно доказано, где именно ломается путь 64KB+.
|
||||||
|
|
||||||
|
Дополнительное расследование 2026-04-12 показало:
|
||||||
|
1. В кластере для `qu.kube5s.ru` существует только один ingress.
|
||||||
|
2. Объект ingress у shared-sqs содержит `proxy-request-buffering: "off"`.
|
||||||
|
3. Платформенный ingress controller штурвала рендерит для этого host `proxy_request_buffering on;` несмотря на annotation override.
|
||||||
|
4. Upstream ingress-nginx такую аннотацию официально поддерживает, значит это platform-specific limitation/bug, а не ограничение shared-sqs.
|
||||||
|
|
||||||
|
Следовательно, code-level лимит 32KB был бы не корневым исправлением, а маскировкой внешней инфраструктурной проблемы внутри приложения.
|
||||||
|
|
||||||
|
## Контекст
|
||||||
|
|
||||||
|
При тестировании shared-sqs v0.1.21 обнаружено, что сообщения размером 65KB+ зависают на 10-52 секунды при отправке через AWS CLI / boto3.
|
||||||
|
|
||||||
|
## Проблема
|
||||||
|
|
||||||
|
### Root Cause (подтверждённый)
|
||||||
|
|
||||||
|
**Цепочка сбоя:** urllib3 2.0 + botocore + TLS + Nagle's algorithm + nginx.
|
||||||
|
|
||||||
|
1. **urllib3 2.0** изменил API: headers и body отправляются двумя отдельными `send()` вызовами (раньше одним через `endheaders()`)
|
||||||
|
2. **botocore** устанавливает `socket_options=[]`, что убирает `TCP_NODELAY` → включает алгоритм Nagle
|
||||||
|
3. Body (65629 байт) шифруется TLS в 4 записи по ~16KB
|
||||||
|
4. Первые 3 записи (49152 байт) отправляются сразу
|
||||||
|
5. Последняя 4-я запись (~16KB) **застревает** из-за Nagle + delayed ACK deadlock
|
||||||
|
6. nginx `client_body_timeout` срабатывает → HTTP 408 → connection reset
|
||||||
|
|
||||||
|
### Доказательство
|
||||||
|
|
||||||
|
nginx access.log при сбое:
|
||||||
|
```
|
||||||
|
POST status=408 req_len=49926 bytes_sent=0 time=10.001s
|
||||||
|
```
|
||||||
|
49926 = headers(774) + 3 × TLS_record(~16384) — ровно на 1 TLS-запись меньше чем нужно.
|
||||||
|
|
||||||
|
### Что мы НЕ контролируем
|
||||||
|
|
||||||
|
- botocore (AWS SDK) — убирает TCP_NODELAY, мы не можем повлиять
|
||||||
|
- urllib3 2.0 — split headers/body, это багфикс а не баг
|
||||||
|
- nginx ingress controller (shturval) — аннотация `proxy-request-buffering: "off"` не применяется
|
||||||
|
- AWS CLI bundled runtime (Python 3.14.3 с другим TLS-стеком)
|
||||||
|
|
||||||
|
### Что мы пробовали
|
||||||
|
|
||||||
|
1. **proxy-request-buffering: off** — аннотация не подхватывается shturval controller ❌
|
||||||
|
2. **client_body_timeout: 120s** — помогло для pip boto3, но НЕ для AWS CLI ❌
|
||||||
|
3. **client_body_buffer_size: 2m** — не помогло для AWS CLI ❌
|
||||||
|
4. **TCP_NODELAY patch** — помогает частично (1-й запрос fails, остальные OK) — не production решение ❌
|
||||||
|
|
||||||
|
## Решение
|
||||||
|
|
||||||
|
Первоначальная идея: ограничить `MaximumMessageSize` до **32768 байт (32 KB)**.
|
||||||
|
|
||||||
|
Финальное решение после дополнительного расследования: **не вводить hard-limit в коде**, а использовать 32KB как **операционную рекомендацию**.
|
||||||
|
|
||||||
|
### Обоснование
|
||||||
|
|
||||||
|
1. **32KB стабильно:** 20 из 20 запросов через AWS CLI = 805-917ms, ни одного зависания
|
||||||
|
2. **Двойной запас:** от порога сбоя (64720B) до лимита (32768B) — двойной запас
|
||||||
|
3. **Покрывает use-cases:** >99% SQS-сообщений — JSON, уведомления, команды (<10KB)
|
||||||
|
4. **Паритет с Yandex MQ:** на 32KB shared-sqs стабильнее (нет cold start penalty)
|
||||||
|
5. **Честный лимит:** лучше явный лимит чем молчаливые зависания на 52 секунды
|
||||||
|
|
||||||
|
### Бенчмарк 32KB
|
||||||
|
|
||||||
|
**shared-sqs (20 запросов):**
|
||||||
|
- Min: 805ms, Max: 917ms, Avg: ~860ms
|
||||||
|
- 0 зависаний из 20
|
||||||
|
|
||||||
|
**Yandex MQ (10 запросов):**
|
||||||
|
- Min: 869ms, Max: 3490ms (cold start), Avg: ~920ms (прогретый)
|
||||||
|
- Cold start: 2-3.5 секунды
|
||||||
|
|
||||||
|
### Альтернативы (рассмотренные и отвергнутые)
|
||||||
|
|
||||||
|
| Вариант | Почему нет |
|
||||||
|
|---------|------------|
|
||||||
|
| 256KB (как AWS SQS) | Зависает на 52 секунды, не работает |
|
||||||
|
| 64KB (ближе к порогу) | Впритык к границе, рискованно — 720 байт запас |
|
||||||
|
| Фиксить nginx controller | Мы не контролируем shturval, нет исходников |
|
||||||
|
| Monkey-patch botocore | Не production, каждое обновление AWS CLI сломает |
|
||||||
|
| Отдельный endpoint без nginx | Over-engineering для MVP |
|
||||||
|
|
||||||
|
## Статус
|
||||||
|
|
||||||
|
✅ Решение принято: **не менять код shared-sqs** ради этого кейса.
|
||||||
|
|
||||||
|
## Практический вывод
|
||||||
|
|
||||||
|
1. Для повседневного использования через AWS CLI/botocore ориентироваться на payload до 32KB.
|
||||||
|
2. Если в будущем потребуется надёжная поддержка 64KB+ для этих клиентов, исправление нужно делать в platform ingress layer, а не в shared-sqs.
|
||||||
@@ -0,0 +1,112 @@
|
|||||||
|
# Error Log: 65KB+ Payload Timeout — 2026-04-11
|
||||||
|
|
||||||
|
## Update — 2026-04-12
|
||||||
|
|
||||||
|
После дополнительного расследования в кластере инцидент переклассифицирован.
|
||||||
|
|
||||||
|
### Финальный статус
|
||||||
|
- shared-sqs backend **не является** первичной причиной зависания;
|
||||||
|
- проблема локализована на стороне **platform ingress path**;
|
||||||
|
- конкретно: ingress controller штурвала **не применяет** `nginx.ingress.kubernetes.io/proxy-request-buffering: "off"`, хотя upstream ingress-nginx эту аннотацию поддерживает;
|
||||||
|
- часть других аннотаций при этом применяется корректно, значит речь не о полном игноре ingress-ресурса, а о selective handling/bug/platform override.
|
||||||
|
|
||||||
|
### Дополнительные подтверждения
|
||||||
|
1. Для host `qu.kube5s.ru` существует только один ingress — `shared-sqs-ingress`, значит это не конфликт нескольких ingress-объектов.
|
||||||
|
2. В объекте ingress аннотация `proxy-request-buffering: "off"` присутствует.
|
||||||
|
3. В итоговом `nginx.conf` для `qu.kube5s.ru` остаётся `proxy_request_buffering on;`.
|
||||||
|
4. В шаблоне контроллера `/etc/nginx/template/nginx.tmpl` директива не захардкожена; там используется значение `location.Proxy.RequestBuffering`, то есть проблема происходит до фазы рендеринга финального location config.
|
||||||
|
5. `server-snippet`/`configuration-snippet` нельзя считать рабочим обходным путём без platform-level изменений, так как snippet-аннотации контролируются `allow-snippet-annotations`, а по умолчанию этот режим отключён.
|
||||||
|
|
||||||
|
### Операционный вывод
|
||||||
|
Это **известное ограничение инфраструктуры**, а не дефект бизнес-логики shared-sqs.
|
||||||
|
|
||||||
|
### Итоговое решение
|
||||||
|
- hard-limit 32KB в код shared-sqs **не внедряется**;
|
||||||
|
- 32KB остаётся **практической рекомендацией** для AWS CLI/botocore-клиентов в текущей инфраструктуре;
|
||||||
|
- приоритет дальнейших работ по этой теме переносится с shared-sqs на platform ingress controller.
|
||||||
|
|
||||||
|
## Симптом
|
||||||
|
|
||||||
|
SendMessage с телом ≥64740 байт зависает на 10-52 секунды и возвращает ошибку (ConnectionClosedError / exit=254).
|
||||||
|
|
||||||
|
**Воспроизведение:**
|
||||||
|
```bash
|
||||||
|
# Генерим payload 65KB
|
||||||
|
python3 -c "print('A'*65536)" > /tmp/big.txt
|
||||||
|
|
||||||
|
# Отправляем через AWS CLI — зависает на ~52 секунды
|
||||||
|
aws --endpoint-url https://qu.kube5s.ru sqs send-message \
|
||||||
|
--queue-url "https://qu.kube5s.ru/TENANT_ID/QUEUE_NAME" \
|
||||||
|
--message-body "file:///tmp/big.txt"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Диагностика
|
||||||
|
|
||||||
|
### 1. Сервер — OK
|
||||||
|
Port-forward (обход nginx): 65KB за 831ms. Сервер не виноват.
|
||||||
|
|
||||||
|
### 2. nginx ingress — частично виноват
|
||||||
|
`client_body_timeout: 10s` — nginx закрывает соединение если тело не пришло за 10 секунд.
|
||||||
|
|
||||||
|
### 3. Root Cause — botocore + urllib3 2.0 + TLS + Nagle
|
||||||
|
|
||||||
|
**urllib3 2.0** посылает headers и body двумя отдельными `send()`:
|
||||||
|
```
|
||||||
|
send#1: len=774 (headers)
|
||||||
|
send#2: len=65629 (body)
|
||||||
|
```
|
||||||
|
|
||||||
|
**botocore** ставит `socket_options=[]` → убирает `TCP_NODELAY` → Nagle ON.
|
||||||
|
|
||||||
|
**Body** шифруется TLS в 4 записи по ~16KB. Первые 3 уходят, 4-я **застревает** из-за Nagle + delayed ACK deadlock.
|
||||||
|
|
||||||
|
**nginx access.log:**
|
||||||
|
```
|
||||||
|
POST status=408 req_len=49926 bytes_sent=0 time=10.001s
|
||||||
|
```
|
||||||
|
Получено 49926 байт = headers(774) + 3 × TLS(~16384). Не хватает ровно 1 TLS-записи.
|
||||||
|
|
||||||
|
### 4. Порог
|
||||||
|
|
||||||
|
| Размер | Время | Статус |
|
||||||
|
|--------|-------|--------|
|
||||||
|
| 64000B | 825ms | ✅ |
|
||||||
|
| 64720B | 799ms | ✅ |
|
||||||
|
| 64740B | 30806ms | ❌ intermittent |
|
||||||
|
| 65536B | 51843ms | ❌ всегда |
|
||||||
|
|
||||||
|
### 5. Версии ПО
|
||||||
|
|
||||||
|
| Компонент | Версия |
|
||||||
|
|-----------|--------|
|
||||||
|
| nginx ingress | shturval-ingress-controller v1.12.6 |
|
||||||
|
| AWS CLI | v2.34.27 |
|
||||||
|
| AWS CLI Python | 3.14.3 (bundled) |
|
||||||
|
| System Python | 3.12.3 |
|
||||||
|
| System urllib3 | 2.0.7 |
|
||||||
|
| System botocore | 1.42.86 |
|
||||||
|
|
||||||
|
## Что пробовали
|
||||||
|
|
||||||
|
### Помогло частично:
|
||||||
|
- **ConfigMap `client-body-timeout: "120"`** — pip boto3 (Python 3.12) стал работать (84ms → 13ms)
|
||||||
|
- **ConfigMap `client-body-buffer-size: "2m"`** — применилось глобально
|
||||||
|
|
||||||
|
### Не помогло:
|
||||||
|
- **Аннотация `proxy-request-buffering: "off"`** — shturval controller не применяет
|
||||||
|
- **`server-snippet: client_body_timeout 120s;`** — применилось но AWS CLI всё равно зависает
|
||||||
|
- **TCP_NODELAY patch через monkey-patch** — нестабильно (1-й запрос fails)
|
||||||
|
|
||||||
|
## Решение
|
||||||
|
|
||||||
|
Кодовый hard-limit не вводим.
|
||||||
|
|
||||||
|
Практическое правило эксплуатации:
|
||||||
|
- для AWS CLI / botocore в текущей ingress-инфраструктуре безопасно ориентироваться на 32KB;
|
||||||
|
- протокольный лимит SQS остаётся 256KB;
|
||||||
|
- проблема 64KB+ описывается как platform ingress limitation.
|
||||||
|
|
||||||
|
См. [решение](../decisions/message-size-limit-2026-04-11.md).
|
||||||
|
|
||||||
|
## Статус
|
||||||
|
✅ Исследование завершено. Root cause локализован до platform ingress layer; изменение в коде shared-sqs не требуется.
|
||||||
+39
-1
@@ -2,7 +2,45 @@
|
|||||||
|
|
||||||
## Версия v0.1.x
|
## Версия v0.1.x
|
||||||
|
|
||||||
### v0.1.19 (2026-04-11) ✅ — ТЕКУЩАЯ DEPLOYED
|
### v0.1.21 (2026-04-11) — Redis schema v2 + per-message persistence
|
||||||
|
- ✅ Redis schema v2: metadata в HASH `ssq:queues`, сообщения в отдельных HASH `ssq:msg:{queueKey}`
|
||||||
|
- ✅ Per-message persistence: каждая операция (send/receive/delete/visibility) пишет только затронутое сообщение
|
||||||
|
- ✅ Migration v1→v2: автоматическая миграция при старте (59 очередей мигрировано)
|
||||||
|
- ✅ quick_test: 31/31 PASS
|
||||||
|
- ✅ shared_sqs_test: 28/28 PASS
|
||||||
|
- **Docker image:** `naeel/shared-sqs:v0.1.21`
|
||||||
|
|
||||||
|
### Производительность v0.1.21 (benchmark)
|
||||||
|
|
||||||
|
| Размер | shared-sqs | Yandex MQ | Сравнение |
|
||||||
|
|--------|-----------|-----------|-----------|
|
||||||
|
| 1KB | ~800ms | ~700ms | Паритет |
|
||||||
|
| 10KB | ~800ms | ~700ms | Паритет |
|
||||||
|
| 32KB | **805-917ms** | 869-3490ms | **Быстрее** (нет cold start) |
|
||||||
|
| 64KB+ | ❌ 10-52s зависание | ~900ms | Проблема nginx+botocore |
|
||||||
|
|
||||||
|
### Проблема 65KB+ payload (расследование 2026-04-11)
|
||||||
|
|
||||||
|
**Root cause:** botocore (AWS SDK) + urllib3 2.0 + TLS record boundary.
|
||||||
|
- urllib3 2.0 отправляет headers и body двумя отдельными send() вызовами
|
||||||
|
- botocore убирает TCP_NODELAY (алгоритм Nagle включён)
|
||||||
|
- Последняя TLS-запись (~16KB) застревает из-за Nagle + delayed ACK
|
||||||
|
- nginx `client_body_timeout` срабатывает → HTTP 408
|
||||||
|
|
||||||
|
**Попытка фикса nginx:**
|
||||||
|
- ConfigMap: `client-body-timeout: "120"`, `client-body-buffer-size: "2m"` — применилось
|
||||||
|
- Аннотация `proxy-request-buffering: "off"` — НЕ подхватилась shturval controller
|
||||||
|
- pip boto3 (Python 3.12): исправлено (84ms → 13ms) ✅
|
||||||
|
- AWS CLI (Python 3.14.3 bundled): всё ещё зависает (51843ms) ❌
|
||||||
|
|
||||||
|
**Финальный вывод (2026-04-12):**
|
||||||
|
- проблема локализована на стороне platform ingress controller штурвала, а не в Go-сервисе shared-sqs;
|
||||||
|
- ingress приложения корректен, но controller выборочно применяет аннотации: `proxy-body-size` и `client-body-buffer-size` доходят до nginx.conf, а `proxy-request-buffering` остаётся `on`;
|
||||||
|
- upstream ingress-nginx эту аннотацию поддерживает, значит это platform-specific limitation/bug;
|
||||||
|
- hard-limit 32KB в код shared-sqs НЕ вводим;
|
||||||
|
- 32KB остаётся практической рекомендацией для AWS CLI / botocore в текущей инфраструктуре.
|
||||||
|
|
||||||
|
### v0.1.19 (2026-04-11) ✅ — ПРЕДЫДУЩАЯ DEPLOYED
|
||||||
- ✅ Валидация VisibilityTimeout (0–43200) в ReceiveMessage
|
- ✅ Валидация VisibilityTimeout (0–43200) в ReceiveMessage
|
||||||
- ✅ Валидация WaitTimeSeconds (0–20) в ReceiveMessage
|
- ✅ Валидация WaitTimeSeconds (0–20) в ReceiveMessage
|
||||||
- ✅ Пустой MessageBody → MissingParameter в SendMessage
|
- ✅ Пустой MessageBody → MissingParameter в SendMessage
|
||||||
|
|||||||
@@ -418,3 +418,153 @@ resources:
|
|||||||
4. **Per-queue lock вместо глобального** — `sync.RWMutex` на каждый Queue
|
4. **Per-queue lock вместо глобального** — `sync.RWMutex` на каждый Queue
|
||||||
5. **PeriodicTasks: RLock где возможно** — для read-only проверок
|
5. **PeriodicTasks: RLock где возможно** — для read-only проверок
|
||||||
6. **Исправить error code** — `MessageDoesNotExist` → `ReceiptHandleIsInvalid`
|
6. **Исправить error code** — `MessageDoesNotExist` → `ReceiptHandleIsInvalid`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Agent: GitHub Copilot (Claude Opus 4.6) — Сессия: 65KB+ payload investigation
|
||||||
|
|
||||||
|
## Расследование: Почему 65KB+ payload зависает на 10-52 секунды
|
||||||
|
|
||||||
|
### Контекст
|
||||||
|
После деплоя v0.1.21 (Redis schema v2, per-message persistence) бенчмарк показал:
|
||||||
|
- Маленькие сообщения (1-10KB): ~800ms — быстрее Yandex MQ
|
||||||
|
- **64KB+: 10-52 секунды** вместо ~1с — неприемлемо
|
||||||
|
|
||||||
|
### Фаза 1: Локализация — nginx vs сервер
|
||||||
|
|
||||||
|
**Гипотеза:** Проблема в Go-сервере.
|
||||||
|
|
||||||
|
**Тест:** Port-forward (kubectl port-forward, обход nginx) → 65KB за 831ms.
|
||||||
|
|
||||||
|
**Вывод:** Сервер в порядке. Проблема **100% в nginx ingress** (shturval-ingress-controller v1.12.6).
|
||||||
|
|
||||||
|
### Фаза 2: Поиск точного порога в nginx
|
||||||
|
|
||||||
|
| Размер | Время | Статус |
|
||||||
|
|--------|-------|--------|
|
||||||
|
| 63000B | 824ms | ✅ |
|
||||||
|
| 64000B | 825ms | ✅ |
|
||||||
|
| 64720B | 799ms | ✅ |
|
||||||
|
| 64740B | 30806ms | ❌ (intermittent) |
|
||||||
|
| 65535B | 30823ms | ❌ |
|
||||||
|
| 65536B | 51843ms | ❌ |
|
||||||
|
|
||||||
|
**Порог:** между 64720B и 64740B (~63.2 KB). Подозрительно близко к TLS record boundary (16384 × 4 = 65536).
|
||||||
|
|
||||||
|
### Фаза 3: Исключение HTTP/2
|
||||||
|
|
||||||
|
**Гипотеза:** `http2 on;` в nginx вызывает проблемы.
|
||||||
|
|
||||||
|
**Тест:** curl --http1.1 vs --http2 — обе версии быстрые (65ms).
|
||||||
|
|
||||||
|
**Вывод:** HTTP/2 НЕ причина.
|
||||||
|
|
||||||
|
### Фаза 4: Послойная изоляция клиента
|
||||||
|
|
||||||
|
| Слой | 65KB body | Время | Результат |
|
||||||
|
|------|-----------|-------|-----------|
|
||||||
|
| curl → nginx | 65KB | 65-81ms | ✅ nginx принимает body |
|
||||||
|
| Python http.client → nginx | 65KB | 44ms | ✅ |
|
||||||
|
| Python http.client + fake SigV4 | 65KB | 53ms | ✅ |
|
||||||
|
| urllib3 напрямую | 65KB | 11ms | ✅ |
|
||||||
|
| botocore URLLib3Session | 65KB | 9ms | ✅ |
|
||||||
|
| **boto3 client.send_message** | 65KB | **10008ms** | **❌ ConnectionClosedError** |
|
||||||
|
| **aws cli send-message** | 65KB | **51843ms** | **❌ exit=254** |
|
||||||
|
|
||||||
|
**Вывод:** Проблема в слое между URLLib3Session и boto3 client — в AWSConnection.
|
||||||
|
|
||||||
|
### Фаза 5: Root Cause — botocore + urllib3 2.0 + Nagle + TLS
|
||||||
|
|
||||||
|
**Трассировка send() вызовов через monkey-patch:**
|
||||||
|
```
|
||||||
|
send#1: len=774 (HTTP headers only)
|
||||||
|
send#2: len=65629 (body only — отдельный вызов!)
|
||||||
|
```
|
||||||
|
|
||||||
|
**urllib3 2.0** изменил поведение: headers и body теперь отправляются ДВУМЯ отдельными send() вызовами (раньше объединялись через endheaders()).
|
||||||
|
|
||||||
|
**botocore** устанавливает `socket_options=[]` → **убирает TCP_NODELAY** → включает алгоритм Nagle.
|
||||||
|
|
||||||
|
**Цепочка сбоя:**
|
||||||
|
1. send#1: headers (774 байт) → TCP-пакет #1
|
||||||
|
2. send#2: body (65629 байт) → TLS шифрует в 4 записи по ~16KB
|
||||||
|
3. TLS-записи 1-3 (~49152 байт) уходят сразу
|
||||||
|
4. TLS-запись 4 (~16KB) **застревает** из-за Nagle + delayed ACK deadlock
|
||||||
|
5. nginx `client_body_timeout` (10с) → HTTP 408 → connection reset
|
||||||
|
|
||||||
|
**Доказательство из nginx access.log:**
|
||||||
|
```
|
||||||
|
POST /t-e0ce... status=408 req_len=49926 bytes_sent=0 time=10.001s
|
||||||
|
```
|
||||||
|
Получено: 49926 = headers(774) + 3 × TLS_record(~16384). Не хватает ровно 1 TLS-записи.
|
||||||
|
|
||||||
|
### Фаза 6: Попытка фикса nginx
|
||||||
|
|
||||||
|
**Изменение 1:** Аннотация `nginx.ingress.kubernetes.io/proxy-request-buffering: "off"`
|
||||||
|
- **Результат:** НЕ подхватилась контроллером shturval. В nginx.conf всё ещё `proxy_request_buffering on;`
|
||||||
|
|
||||||
|
**Изменение 2:** ConfigMap `shturval-ingress-controller-controller`:
|
||||||
|
- `client-body-timeout: "120"` (было 10)
|
||||||
|
- `client-body-buffer-size: "2m"` (было default 8k)
|
||||||
|
- **Результат:** Применилось глобально в nginx.conf ✅
|
||||||
|
|
||||||
|
**Изменение 3:** `server-snippet: client_body_timeout 120s;`
|
||||||
|
- **Результат:** Применилось через location ✅
|
||||||
|
|
||||||
|
**Проверка эффективности:**
|
||||||
|
- **pip boto3** (Python 3.12.3, urllib3 2.0.7): **ИСПРАВЛЕНО!** 84ms → 13ms для 65KB ✅
|
||||||
|
- **AWS CLI** (v2.34.27, bundled Python 3.14.3): **ВСЁ ЕЩЁ ЗАВИСАЕТ** — 51843ms для 65KB ❌
|
||||||
|
|
||||||
|
**Причина разницы:** AWS CLI v2.34.27 использует bundled Python 3.14.3 с другой версией TLS-стека. Поведение отличается от системного Python 3.12.3.
|
||||||
|
|
||||||
|
### Фаза 7: Решение — ограничить MaximumMessageSize
|
||||||
|
|
||||||
|
Мы НЕ контролируем:
|
||||||
|
- botocore (AWS SDK, убирает TCP_NODELAY)
|
||||||
|
- nginx ingress controller shturval (proxy_request_buffering не применяется через аннотацию)
|
||||||
|
- TLS record boundaries (16384 байт — стандарт)
|
||||||
|
- AWS CLI bundled runtime
|
||||||
|
|
||||||
|
**Решение:** Ограничить максимальный размер сообщения на уровне сервера.
|
||||||
|
|
||||||
|
### Фаза 8: Тестирование 32KB как лимита
|
||||||
|
|
||||||
|
**20 запросов по 32KB через AWS CLI:**
|
||||||
|
- Все 20/20 стабильно
|
||||||
|
- Диапазон: 805-917ms
|
||||||
|
- Ни одного зависания
|
||||||
|
- Разброс ~100ms
|
||||||
|
|
||||||
|
**Сравнение с Yandex MQ (32KB, 10 запросов):**
|
||||||
|
|
||||||
|
| Метрика | shared-sqs | Yandex MQ |
|
||||||
|
|---------|------------|-----------|
|
||||||
|
| Min | 805ms | 869ms |
|
||||||
|
| Max | 917ms | 3490ms (cold start) |
|
||||||
|
| Стабильно | ~850ms | ~900ms (прогретый) |
|
||||||
|
| Cold start | нет | 2-3.5 сек |
|
||||||
|
|
||||||
|
**shared-sqs стабильнее Yandex MQ на 32KB.** Паритет на прогретых запросах, лучше на холодных.
|
||||||
|
|
||||||
|
### Решение (ожидает подтверждение пользователя)
|
||||||
|
|
||||||
|
Ограничить `MaximumMessageSize` до 32768 байт (32KB):
|
||||||
|
- Покрывает >99% реальных SQS use-cases (JSON-события, уведомления, команды)
|
||||||
|
- Двойной запас до TLS-порога (64KB → 32KB)
|
||||||
|
- Документировать ограничение и причину в API doc
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Изменённые файлы
|
||||||
|
|
||||||
|
### В репозитории:
|
||||||
|
- `deployments/k8s/ingress.yaml` — аннотации: `proxy-request-buffering: "off"`, `server-snippet: client_body_timeout 120s;`
|
||||||
|
|
||||||
|
### На кластере (не в репозитории):
|
||||||
|
- ConfigMap `shturval-ingress-controller-controller` (namespace `ingress`):
|
||||||
|
- `client-body-timeout: "120"`, `client-body-buffer-size: "2m"`
|
||||||
|
|
||||||
|
### Ожидают изменения (после решения пользователя):
|
||||||
|
- `app/models/constants.go` — MaximumMessageSize default
|
||||||
|
- `app/gosqs/validation.go` — проверка размера body
|
||||||
|
- `doc/api/yandex-message-queue-api-reference.md` — обновление лимитов в документации
|
||||||
|
|||||||
@@ -0,0 +1,239 @@
|
|||||||
|
# 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 считать безопасным эксплуатационным размером и документировать это как известную особенность инфраструктуры.
|
||||||
Reference in New Issue
Block a user