Postgres here allows 60 connections, but each of the 96 gunicorn threads held its own persistent one: at 100 concurrent users (local Postgres with the same limit) 21% of requests were 500 "remaining connection slots are reserved". Now each worker has a psycopg pool (8), so the same load gives 0 errors and 714 req/s (was 460). - A full pool answers 503 + Retry-After (DRF handler and middleware), not 500. - The AI tutor and the OTP SMS call return their connection before the network wait. - statement_timeout 30 s, idle-in-transaction 60 s; bad pool settings and the old DB_CONN_MAX_AGE stop startup instead of failing every request. - /api/health dbPool: per-worker pool stats and workers x pool + 6 vs max_connections. - Indexes on the columns global counters filter by time (weekly leaderboard, global SMS cap, global AI cap); the leaderboard recomputes once per expiry. - Worker count moves into gunicorn.conf.py (4 on a shared 8-core box). - security doc: Redis item closed (server uses our own Redis since 2026-10-08). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
77 lines
5.5 KiB
Python
77 lines
5.5 KiB
Python
"""
|
||
تنظیماتِ gunicorn — خودِ gunicorn این فایل را از پوشهی کاری
|
||
(WorkingDirectory=~/mathapp-v2 در unitِ systemd) خودکار میخواند، پس بدونِ
|
||
دست زدن به /etc با همان deploy.sh به سرور میرسد. آرگومانهای خطِ فرمانِ
|
||
unit (مثلِ `--workers 3` و `--bind`) بر این فایل مقدماند.
|
||
|
||
چرا لازم شد (۲۰۲۶-۰۹-۲۴): unit با workerهای sync و timeoutِ پیشفرضِ ۳۰
|
||
ثانیه اجرا میشد. هر استریمِ «از پی بپرس» یک worker را تا پایانِ جواب (~۱۰
|
||
ثانیه، چون GapGPT خودش تا آماده شدنِ کلِ جواب نگه میدارد) اشغال میکرد؛ سه
|
||
دانشآموزِ همزمان با دستیار یعنی بقیهی اپ — نقشه، آزمون، همهچیز — در صف.
|
||
جوابِ بالای ۳۰ ثانیه هم وسطِ کار کشته میشد.
|
||
|
||
gthread هر worker را چند نخی میکند: ۳ worker × ۸ نخ = ۲۴ درخواستِ همزمان.
|
||
کدِ جنگو برای نخ امن است و SQLite با `OPTIONS.timeout=20` نوشتنِ همزمان را
|
||
تحمل میکند. timeout در gthread به ضربانِ خودِ worker است، نه طولِ هر
|
||
درخواست، پس استریمِ طولانی دیگر کشته نمیشود.
|
||
|
||
۳۲ نخ: هر جوابِ دستیار یک نخ را تا آخرِ جواب نگه میدارد (روی سرور میانهی ۳ تا ۶ ثانیه،
|
||
گاهی ۴۰). gthread اتصال را حتی وقتی همهی نخهایش مشغولاند میپذیرد و workerها یکنواخت پر
|
||
نمیشوند، پس با نخِ کم، صفحههای عادیِ اپ پشتِ استریمهای دستیار صف میکشند. در لودتستِ
|
||
۲۰۲۶-۱۰-۰۴ (لپتاپ، مدل و Qdrantِ شبیهسازیشده با زمانبندیِ سرور) ۳×۳۲ تا ۷۵ پرسشِ همزمان
|
||
صفحهها را زیرِ ۰٫۱ ثانیه نگه داشت و از ۱۰۰ به بعد صف پیدا شد. CPUِ مشترکِ سرور در این بار
|
||
سنجیده نشده. نخِ منتظرِ شبکه تقریباً هزینه ندارد؛ اتصالِ دیتابیس را فقط حینِ کار از poolِ همان
|
||
worker قرض میگیرد (config/settings.py)، پس تعدادِ نخ به سقفِ اتصالِ Postgres ربطی ندارد.
|
||
"""
|
||
|
||
# ۴ worker: هر worker با GIL حداکثر یک هسته است، پس گذرش با تعدادِ worker بالا میرود (لپتاپ، ۱۰۰
|
||
# کاربرِ همزمان، ۲۰۲۶-۱۰-۱۱: ۳ → ۵۵۰، ۴ → ۶۲۰، ۶ → ۸۱۰ درخواست در ثانیه). ماشین مشترک است: همان روز
|
||
# بقیهی تیمها ~۵۵٪ِ ۸ هسته را مصرف میکردند (load ≈ ۵)، پس ۴ و نه ۶، تا در اوجِ ما سرویسِ دیگران
|
||
# گرسنه نماند. هر worker ~۱۰۰ مگابایت. بودجهی اتصالِ دیتابیس: ۴ × DB_POOL_MAX_SIZE(۸) + ۶ = ۳۸ از ۵۷.
|
||
# این عدد فقط وقتی اثر دارد که یونیتِ systemd خودش `--workers` نداشته باشد (خطِ فرمان مقدم است).
|
||
workers = 4
|
||
worker_class = 'gthread'
|
||
threads = 32
|
||
timeout = 120
|
||
graceful_timeout = 30
|
||
keepalive = 5
|
||
|
||
# worker بعد از چند هزار درخواست تازه میشود تا نشتِ حافظهی احتمالی جمع
|
||
# نشود؛ jitter نمیگذارد هر سه با هم ریاستارت شوند.
|
||
max_requests = 2000
|
||
max_requests_jitter = 200
|
||
|
||
|
||
def post_fork(server, worker):
|
||
"""
|
||
تعدادِ واقعیِ workerها (با `--workers`ِ خطِ فرمانِ unit، که بر این فایل مقدم است) را به جنگو
|
||
میدهد تا /api/health بودجهی اتصالِ دیتابیس را درست بسنجد: workers × DB_POOL_MAX_SIZE باید زیرِ
|
||
max_connectionsِ Postgres بماند (config/health.py، _db_pool). پیش از بار شدنِ اپ اجرا میشود.
|
||
"""
|
||
import os
|
||
|
||
os.environ['GUNICORN_WORKERS'] = str(server.cfg.workers)
|
||
|
||
|
||
def post_worker_init(worker):
|
||
"""
|
||
کتابخانههای Qdrant و OpenAI را همان لحظهی بالا آمدنِ worker بار میکند، نه سرِ اولین
|
||
سوالِ دستیار. بدونِ این، اولین پرسشها بعد از هر شروعِ worker (ریاستارت، یا تعویض با
|
||
max_requests) چند ثانیه پیش از رسیدن به مدل معطل میمانند: روی سرور ~۶ ثانیه در برابرِ ~۱،
|
||
روی لپتاپ ۱٫۷ ثانیه (import ِ qdrant_client بهتنهایی ~۰٫۷).
|
||
|
||
ساختنِ کلاینت تماسِ شبکهای ندارد. شکستِ این گرمکردن نباید worker را بخواباند (کلِ اپ
|
||
بهخاطرِ یک بهینهسازی پایین نمیآید)؛ ثبت میشود و اولین سوال همان مسیرِ عادی را میرود.
|
||
"""
|
||
from rag.services.config import rag_is_configured
|
||
|
||
if not rag_is_configured():
|
||
return
|
||
try:
|
||
from rag.services import clients
|
||
|
||
clients.get_openai_client()
|
||
clients.get_qdrant_client()
|
||
except Exception:
|
||
worker.log.exception('گرم کردنِ کلاینتهای دستیار شکست خورد؛ اولین سوال آنها را میسازد.')
|