запрос на анализ: чанковая загрузка падает с Сеть
This commit is contained in:
@@ -0,0 +1,128 @@
|
|||||||
|
# Запрос на анализ: чанковая загрузка падает с «Сеть»
|
||||||
|
|
||||||
|
**Дата:** 2026-06-17
|
||||||
|
**Версия:** v1.17
|
||||||
|
**СТРОГО: ТОЛЬКО АНАЛИЗ. КОД НЕ ПРАВИТЬ. Ответ — в этот же файл.**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Симптом
|
||||||
|
|
||||||
|
Загрузка PDF 153 KB (3 чанка по 50 KB) через `POST /chunk`:
|
||||||
|
- Прогресс быстро доходит до 100%
|
||||||
|
- Затем ~10 секунд висит «Загрузка...»
|
||||||
|
- Итог: `✗ Сеть` (XHR onerror — обрыв соединения, нет HTTP-ответа)
|
||||||
|
|
||||||
|
Пробовали:
|
||||||
|
- v1.15: чанки в памяти Python `_chunk_store = {}` → «Сеть»
|
||||||
|
- v1.17: чанки в PostgreSQL (таблица `chunks`) → ТО ЖЕ САМОЕ
|
||||||
|
|
||||||
|
Значит дело НЕ в worker'ах/shared memory — с БД чанки общие для всех процессов.
|
||||||
|
|
||||||
|
## Архитектура
|
||||||
|
|
||||||
|
- **Платформа:** pythonk8s.services.ngcloud.ru (managed Flask-хостинг, деплой = git push)
|
||||||
|
- **Ingress:** nginx перед приложением, таймаут ~30 сек (известно по предыдущему опыту)
|
||||||
|
- **Приложение:** Flask (app.run), Python 3.12
|
||||||
|
- **БД:** PostgreSQL 17.6, внутренний кластер
|
||||||
|
|
||||||
|
## Клиентский код (upload.html)
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
async function uploadFileChunked(file, cid, onProgress) {
|
||||||
|
var CHUNK_SIZE = 50 * 1024; // 50 KB
|
||||||
|
var totalChunks = Math.ceil(file.size / CHUNK_SIZE);
|
||||||
|
var uploadId = file.name + '_' + Date.now() + '_' + Math.random();
|
||||||
|
|
||||||
|
for (var i = 0; i < totalChunks; i++) {
|
||||||
|
var chunk = file.slice(i * CHUNK_SIZE, Math.min((i+1) * CHUNK_SIZE, file.size));
|
||||||
|
|
||||||
|
var resp = await new Promise((resolve, reject) => {
|
||||||
|
var xhr = new XMLHttpRequest();
|
||||||
|
xhr.open('POST', '/chunk');
|
||||||
|
xhr.timeout = 60000;
|
||||||
|
|
||||||
|
xhr.onload = function() {
|
||||||
|
if (xhr.status === 200) {
|
||||||
|
var r = JSON.parse(xhr.responseText);
|
||||||
|
if (r.error) reject(new Error(r.error));
|
||||||
|
else resolve(r);
|
||||||
|
} else reject(new Error('HTTP ' + xhr.status));
|
||||||
|
};
|
||||||
|
xhr.onerror = () => reject(new Error('Сеть'));
|
||||||
|
xhr.ontimeout = () => reject(new Error('Таймаут'));
|
||||||
|
|
||||||
|
var fd = new FormData();
|
||||||
|
fd.append('chunk', chunk);
|
||||||
|
fd.append('upload_id', uploadId);
|
||||||
|
fd.append('filename', file.name);
|
||||||
|
fd.append('chunk_index', i);
|
||||||
|
fd.append('total_chunks', totalChunks);
|
||||||
|
if (cid) fd.append('cid', cid);
|
||||||
|
xhr.send(fd);
|
||||||
|
});
|
||||||
|
// resp = {contract_id: "..."} только на последнем чанке
|
||||||
|
// или {chunk: N, received: N, total: N} на промежуточных
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Файл 153 KB → 3 чанка. Прогресс 33% → 67% → 100% (значит все 3 XHR завершились). Затем 10 сек паузы → «Сеть».
|
||||||
|
|
||||||
|
## Серверный код (app.py)
|
||||||
|
|
||||||
|
```python
|
||||||
|
def _chunk_upload(self):
|
||||||
|
upload_id = request.form.get("upload_id")
|
||||||
|
chunk_index = int(request.form.get("chunk_index", 0))
|
||||||
|
total_chunks = int(request.form.get("total_chunks", 1))
|
||||||
|
chunk_data = request.files.get("chunk").read()
|
||||||
|
|
||||||
|
# Сохранить в БД
|
||||||
|
db.execute(
|
||||||
|
"INSERT INTO chunks (...) VALUES (...) ON CONFLICT DO NOTHING",
|
||||||
|
(upload_id, chunk_index, chunk_data, ...)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Проверить: все чанки получены?
|
||||||
|
received = db.query("SELECT COUNT(*) FROM chunks WHERE upload_id=%s", ...)
|
||||||
|
|
||||||
|
if received == total_chunks:
|
||||||
|
# Собрать из БД
|
||||||
|
rows = db.query("SELECT chunk_data FROM chunks WHERE upload_id=%s ORDER BY chunk_index", ...)
|
||||||
|
file_bytes = b"".join(...)
|
||||||
|
|
||||||
|
# Сохранить как документ
|
||||||
|
_save_file_to_db(filename, file_bytes, mime, contract_id)
|
||||||
|
|
||||||
|
# Очистить чанки
|
||||||
|
db.execute("DELETE FROM chunks WHERE upload_id=%s", ...)
|
||||||
|
|
||||||
|
return jsonify({"contract_id": str(contract_id)})
|
||||||
|
|
||||||
|
return jsonify({"chunk": chunk_index, "received": received, "total": total_chunks})
|
||||||
|
```
|
||||||
|
|
||||||
|
## Вопросы для анализа
|
||||||
|
|
||||||
|
1. **Почему «Сеть» (XHR onerror), а не HTTP-ошибка?** Сервер не возвращает ответ — соединение обрывается на уровне TCP. Это не таймаут (был бы `ontimeout`) и не HTTP-ошибка (был бы `onload`).
|
||||||
|
|
||||||
|
2. **Почему прогресс 100% но потом 10 сек паузы?** Все 3 чанка отправлены и получили ответ? Или третий чанк висит 10 сек и потом обрыв?
|
||||||
|
|
||||||
|
3. **Может ли `ON CONFLICT DO NOTHING` + `SELECT COUNT(*)` дать гонку?** Два worker'а одновременно пишут чанки — COUNT может не увидеть только что вставленный?
|
||||||
|
|
||||||
|
4. **Может ли сборка (`b"".join` + `_save_file_to_db`) быть медленной и вызывать таймаут Ingress?** 153 KB — вроде быстро, но если БД тормозит...
|
||||||
|
|
||||||
|
5. **Может ли `client_max_body_size` на nginx резать чанки?** 50 KB — вроде мало.
|
||||||
|
|
||||||
|
6. **Есть ли ограничение на количество запросов от одного клиента?** Rate limiting?
|
||||||
|
|
||||||
|
7. **Что видят логи сервера?** Есть ли способ посмотреть?
|
||||||
|
|
||||||
|
## Что нужно
|
||||||
|
|
||||||
|
**ТОЛЬКО АНАЛИЗ. КОД НЕ ТРОГАТЬ.**
|
||||||
|
|
||||||
|
1. Определить наиболее вероятную причину обрыва
|
||||||
|
2. Предложить способ диагностики (как узнать точно)
|
||||||
|
3. Предложить решение (без правки кода)
|
||||||
Reference in New Issue
Block a user