Files
contracts/History/connection-reset-analysis.md

198 lines
6.5 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Запрос на анализ: случайные обрывы соединения при HTTP POST
**Дата:** 2026-06-17
**Проект:** Contracts App (Flask + PostgreSQL на managed-хостинге)
**Версия:** v1.21
---
## Симптом
Любой HTTP POST на managed-хостинг pythonk8s.services.ngcloud.ru **случайно** обрывается:
```
100KB #1: HTTP:000
100KB #2: HTTP:000
100KB #3: HTTP:200
100KB #4: HTTP:000
100KB #5: HTTP:000
```
- HTTP 000 — curl не может установить TCP-соединение (Connection reset? Connection refused?)
- Не зависит от: размера (17KB падает, 35KB работает), типа файла, кодировки
- FormData и JSON — одинаково
- Health check (GET /health, без БД) — всегда 200 (5/5)
- Прямой доступ к БД через /test (JSON) — всегда работает
## Инфраструктура
```
Клиент → ddos-guard → nginx/Ingress → gunicorn → Flask (1 worker?)
PostgreSQL 17.6
```
- Managed-хостинг: pythonk8s.services.ngcloud.ru
- Деплой: git push master → автосборка
- БД: внутренний кластер postgresqlk8s-master...svc.cluster.local
- Код: Flask (app.run, host=0.0.0.0:5000)
## Что пробовали
1. **FormData/multipart** — POST / с файлом → ✗
2. **Чанки 50KB** (FormData) → только первый чанк доходит
3. **Чанки в PostgreSQL** (таблица chunks) → только первый чанк
4. **Пул соединений psycopg2** (ThreadedConnectionPool min=1 max=3) → ✗
5. **JSON/base64** (POST /upload, Content-Type: application/json) → ✗
6. **Прямой INSERT в БД** через /test endpoint (JSON) → ✓ всегда
## Код
### Сервер: простое сохранение файла в БД
```python
# app.py
def _upload_json(self):
data = request.get_json(silent=True)
if not data:
return jsonify({"error": "Нет JSON"}), 400
filename = data.get("filename", "")
b64 = data.get("data", "")
if not filename or not b64:
return jsonify({"error": "Нет filename или data"}), 400
import base64 as b64mod
file_bytes = b64mod.b64decode(b64)
mime = mimeutil.guess_mime(filename) or "application/octet-stream"
cid = data.get("cid")
conn = None
if cid:
contract_id = cid
else:
conn, err = db.connect()
if err:
return jsonify({"error": f"БД: {err}"}), 500
cur = conn.cursor()
cur.execute(
"INSERT INTO contracts (number, client) VALUES (%s, %s) RETURNING id",
("б/н " + __import__("datetime").datetime.now().strftime("%Y%m%d-%H%M"), ""),
)
contract_id = cur.fetchone()[0]
conn.commit()
doc_id = _save_file_to_db(filename, file_bytes, mime, str(contract_id), conn=conn)
if conn:
cur.close()
db.put_conn(conn)
return jsonify({"contract_id": str(contract_id), "doc_id": str(doc_id) if doc_id else None})
```
### Пул соединений (db.py)
```python
import os
from psycopg2 import pool as _pgpool
_pool = None
def _get_pool():
global _pool
if _pool is None:
_pool = _pgpool.ThreadedConnectionPool(
minconn=1,
maxconn=3,
host=os.getenv("DB_HOST"),
port=os.getenv("DB_PORT", 5432),
dbname=os.getenv("DB_NAME"),
user=os.getenv("DB_USER"),
password=os.getenv("DB_PASS"),
sslmode=os.getenv("DB_SSLMODE", "disable"),
)
return _pool
def connect():
try:
return _get_pool().getconn(), None
except Exception as e:
return None, str(e)
def put_conn(conn):
try:
_get_pool().putconn(conn)
except Exception:
pass
def execute(sql_text, params=None):
conn, err = connect()
if err:
return None, f"connect: {err}"
try:
cur = conn.cursor()
cur.execute(sql_text, params)
conn.commit()
rowcount = cur.rowcount
cur.close()
return rowcount, None
except Exception as e:
try: conn.rollback()
except: pass
return None, str(e)
finally:
put_conn(conn)
```
### Клиент (JavaScript)
```javascript
function readFileAsBase64(file, onProgress) {
return new Promise(function(resolve, reject) {
var reader = new FileReader();
reader.onload = function() {
var b64 = reader.result.split(',')[1];
resolve(b64);
};
reader.onerror = function() { reject(new Error('Read error')); };
reader.readAsDataURL(file);
});
}
function uploadFileJSON(file, cid, onProgress) {
return new Promise(function(resolve, reject) {
readFileAsBase64(file, onProgress).then(function(b64) {
var xhr = new XMLHttpRequest();
xhr.open('POST', '/upload');
xhr.setRequestHeader('Content-Type', 'application/json');
xhr.timeout = 120000;
xhr.onload = function() {
if (xhr.status === 200) {
try {
var resp = JSON.parse(xhr.responseText);
if (resp.error) { reject(new Error(resp.error)); return; }
resolve(resp);
} catch(e) { reject(new Error('Bad JSON')); }
} else {
reject(new Error('HTTP ' + xhr.status));
}
};
xhr.onerror = function() { reject(new Error('Сеть')); };
xhr.ontimeout = function() { reject(new Error('Таймаут')); };
xhr.send(JSON.stringify({
filename: file.name,
data: b64,
cid: cid || null
}));
}).catch(reject);
});
}
```
## Вопросы
1. Почему health check (GET /health, просто return "OK") всегда работает, а POST с любым телом — случайно обрывается?
2. Может ли проблема быть в ddos-guard, который считает частые POST за флуд?
3. Может ли gunicorn с 1 worker'ом не справляться с последовательными запросами (keepalive, таймауты)?
4. Почему curl с локалхоста показывает HTTP 000 (нет TCP-соединения), а не HTTP 502/504?
5. Может ли managed PostgreSQL обрывать соединения пула, вызывая каскадный отказ?