janigo — docker compose up --build
BUILD
$

Django · Python · Docker · Deploy

قصد همکاری داری؟ تماس بگیر

قبل از خرید سرور قوی‌تر این کارها رو انجام بده | چرا پروژه‌های Django کند می‌شه؟

امیرحسین علیجانی 27 مرداد، 1405 0 دیدگاه
قبل از خرید سرور قوی‌تر این کارها رو انجام بده | چرا پروژه‌های Django کند می‌شه؟

اولین واکنش یک مدیر یا استارتاپی که با کندی سیستم مواجه می‌شه، معمولاً خرید منابع بیشتره. «سرور رو آپگرید کنیم»، «RAM رو ببریم ۳۲ گیگ»، «از SSD استفاده کنیم». طبیعیه؛ فکر می‌کنی اگه ماشین قوی‌تری داشته باشی، همون مسیری که با پراید میرفتی رو با بوگاتی میری :)

اما توی دنیای نرم‌افزار و دیتابیس، قانون اول اینه: کندی معمولاً نشونه‌ی ناکارآمدی کد هست نه ضعف سخت‌افزار

من خودم چندین بار دیدم تیم‌ها روزها روی خرید سرورهای قدرتمند وقت و بودجه می‌ذارن، در حالی که یه باگ ساده در ORM باعث می‌شه به جای ۲ تا Query، ۲۰۰۰ تا Query به دیتابیس بفرستید! توی این مقاله، دقیقاً مثل یه دیباگینگ حرفه‌ای، لایه به لایه پیش می‌ریم تا بفهمیم چرا پروژه‌های Django کند می‌شن و قبل از اینکه حتی یک ریال برای سرور خرج کنید، چه کارهایی باید انجام بدید ;)

A conceptual illustration showing a powerful server being ignored while a tangled web of inefficient code is being untangled. The server is in the background, slightly out of focus, emphasizing the code logic in the foreground.

 


داستان: وقتی سرور قوی‌تر فقط گرون‌تر شد

چند سال پیش روی یه پروژه SaaS کار می‌کردیم. داشبوردهای مدیریتی به شدت کند شده بودن و بارگذاری صفحه‌ای که باید ۲۰۰ میلی‌ثانیه باز می‌شد الان ۴ ثانیه طول می‌کشید. تیم عملیات (DevOps) بلافاصله پیشنهاد داد: سرور رو به نسخه Enterprise ارتقا بدیم

من با ابزار django-debug-toolbar صفحه رو باز کردم و لایه SQL رو بررسی کردم و ... 😐  دیتابیس توی ۴ ثانیه بیش از ۱۵۰۰ کوئری اجرا کرده بود! اکثر این کوئری‌ها برای دریافت اطلاعات مربوط به کاربران و سوابق سفارشات بود که بدون استفاده از select_related یا prefetch_related نوشته شده بودن

اگر اون روز سرور رو ارتقا می‌دادیم، فقط هزینه‌ها رو بالا می‌بردیم بدون اینکه تاثیری توی ۴ ثانیه‌ی زمان بارگذاری ایجاد کنیم. سرور قوی‌تر می‌تونست ۱۵۰۰ کوئری رو در ۳ ثانیه پردازش کنه نه کمتر و مشکل از قدرت سرور نبود مشکل از «شکلی» بود که به دیتابیس نزدیک می‌شدیم و کالش میکردیم


قدم اول: دیباگ کردن واقعی، نه حدس و گمان

قبل از هر کاری، باید بفهمید گلوگاه کجاست. آیا CPU پایدار است؟ آیا I/O دیسک محدودکننده است؟ یا اینکه دیتابیس زیر فشار کوئری‌های سنگین له می‌شه؟

ابزار django-debug-toolbar رفیق 6 شماست :) این ابزار رو نصب کنید و اون رو در محیط توسعه (Development) به خوبی پیکربندی کنید. نگاهی به بخش SQL و Timer بندازید

# settings.py

INSTALLED_APPS = [
    # ... سایر اپلیکیشن‌ها
    'debug_toolbar',
]

MIDDLEWARE = [
    'debug_toolbar.middleware.DebugToolbarMiddleware',
    # ... سایر میانی‌افزارها
]

INTERNAL_IPS = [
    "127.0.0.1",
]

هر صفحه‌ای که باز می‌کنید، یک پنل کوچیک در سمت راست ظاهر می‌شه روش کلیک کنید و ببینید:
1. چند کوئری در هر درخواست (Request) اجرا می‌شه؟ (یک درخواست سالم باید کمتر از ۱۰-۲۰ کوئری داشته باشه)
2. هر کوئری چقدر طول می‌کشه؟
3. آیا کوئری‌های تکراری می‌بینید؟

اگر می‌بینید که یک صفحه ساده ۱۰۰ کوئری یا بیشتر اجرا می‌کنه مشکل قطعا N+1 است و نه سخت‌افزار


قدم دوم: شکارچی N+1 و بهینه‌سازی ORM

مشکل N+1 بدترین دشمن توسعه‌دهندگان Django هستش! وقتی شما لیستی از اشیاء رو می‌گیرید و برای هر شیء یک کوئری جداگانه برای دریافت داده‌های وابسته (مثل اطلاعات کاربر یا دسته‌بندی) می‌زنید، این پدیده رخ می‌ده

مثال زیر رو تصور کنید: ۱۰۰ تا سفارش (Order) دارید و برای هر سفارش می‌خواید نام کاربر رو نمایش بدید

# View نابهینه
orders = Order.objects.all()
# در اینجا ۱ کوئری برای گرفتن سفارشات اجرا می‌شود.

# در قالب (Template) یا حلقه‌ی Python:
for order in orders:
    print(order.user.name) 
    # برای هر بار دسترسی به order.user.name، یک کوئری SQL جدید اجرا می‌شود!
    # نتیجه: ۱ + ۱۰۰ = ۱۰۱ کوئری!

راه حل ساده و قدرتمند Django، استفاده از select_related برای فیلدهای ForeignKey و prefetch_related برای فیلدهای ManyToMany یا Reverse ForeignKey است

# View بهینه
orders = Order.objects.select_related('user', 'product').all()
# این یک کوئری SQL با JOIN می‌سازد و همه‌ی اطلاعات را در همان لحظه بارگذاری می‌کند.
# نتیجه: ۱ کوئری برای ۱۰۰ تا سفارش.

تفاوت سرعت می‌تونه از چند ثانیه به چند میلی‌ثانیه برسه . قبل از فکر کردن به RAM سرور، این کار رو برای Viewهای اصلی (Dashboard، لیست‌ها، گزارش‌ها) انجام بدید

 

A diagram illustrating the N+1 query problem, showing a single request triggering multiple database queries, compared to a single optimized query using JOINs.

بیشتر بخون : اگر دلت میخواد بیشتر در مورد مشکل N+1 Queries بدونی این مقاله رو از دست نده ! فقط 11 دقیقه وقتتو میگیره 😉


قدم سوم: ایندکس‌گذاری دیتابیس (Indexing)؛ معجزه‌ی جاافتاده

اگر کوئری‌هاتون بهینه  هستن اما هنوز کندند احتمالاً دیتابیس برای جستجوی داده‌ها «صفحه‌ی فهرست» (Index) نداره 

فرض کنید می‌خواید توی یک کتابخونه‌ دنبال کتاب خاصی بگردید اگر فهرستی وجود نداشته باشه باید تک‌تک کتاب‌ها رو باز کنید ایندکس، همون فهرسته است

در PostgreSQL و MySQL، ستون‌هایی که در WHERE، JOIN و ORDER BY استفاده می‌شن باید ایندکس داشته باشن

-- اضافه کردن ایندکس به ستون 'status' در جدول 'orders'
CREATE INDEX idx_orders_status ON orders(status);

نکته مهم: روی هر ستوری ایندکس نگذارید! ایندکس حجم دیتابیس رو زیاد می‌کنه و عملیات INSERT و UPDATE رو کند می‌کنه فقط روی ستون‌های پرتکرار در کوئری‌های SELECT ایندکس بسازید ابزار django-debug-toolbar یا لاگ‌های دیتابیس به شما نشون می‌ده که کدوم کوئری‌ها از Seq Scan (اسکن کامل جدول) استفاده می‌کنند که نشونه‌ی ضعف شدید هست


قدم چهارم: کشینگ (Caching) را جدی بگیرید

گاهی اوقات کوئری بهینه است، اما دیتابیس زیر فشار تعداد بالای درخواست‌ها (High Concurrency) دهنش سرویس میشه اینجا جاییه که کش (Cache) وارد عمل می‌شه Django به صورت دیفالت از Redis و Memcached پشتیبانی می‌کنه

از کش استفاده کنید وقتی:
1. داده‌ها به ندرت تغییر می‌کنن (مثل تنظیمات سایت، اطلاعات پروفایل کاربر)
2. محاسبات سنگینی روی داده‌ها انجام می‌دید

from django.core.cache import cache

def get_dashboard_data(user_id):
    cache_key = f'dashboard_data_{user_id}'
    data = cache.get(cache_key)

    if data is None:
        # اگر در کش نبود، از دیتابیس بگیر و ذخیره کن
        data = calculate_complex_dashboard_data(user_id)
        cache.set(cache_key, data, timeout=60 * 15) # ۱۵ دقیقه

    return data

استراتژی صحیح کشینگ، فشار رو از روی دیتابیس برمی‌داره و اجازه می‌ده سرور فعلی شما هزاران درخواست رو بدون کند شدن پردازش کنه  این قبل از خرید سرور جدید، تاثیر مستقیمی روی مقیاس‌پذیری داره


قدم پنجم: چک‌لیست نهایی قبل از خرید سرور

حالا که کد رو بهینه کردیم، این چک‌لیست رو مرور کنید. اگر همه‌ی موارد بالا رو رعایت کردید و سیستم هنوز کند بود، حالا وقت خرید سرور جدیده

  1. تست بارگذاری (Load Testing): آیا با ابزارهایی مثل locust یا k6 سیستم رو تست کردید؟ کندی فقط تحت فشار بالا دیده می‌شه
  2. لاگ‌های Gunicorn/Nginx: آیا تعداد 502 Bad Gateway زیاده؟ این نشانه‌ی کمبود Memory یا Worker در Gunicorn هست
  3. سایز دیتابیس: آیا جداول بیش از ۵ میلیون رکورد هستند؟ اگر بله، ممکنه به دیتابیس جداگانه و قدرتمندتر نیاز داشته باشید

اگر بعد از انجام تمام این کارها، مصرف CPU و Memory سرور شما روی ۳۰-۴۰٪ ثابت بود، یعنی شما کد رو بهینه کردید و حالا می‌تونید با خیال راحت سرور رو آپگرید کنید، چون می‌دونید پولتون صرف کندی کد نخواهد شد


جمع‌بندی

چرا پروژه‌های Django کند می‌شوند؟ اغلب به خاطر انتخاب سخت‌افزار ضعیف نیست، بلکه به خاطر نادیده گرفتن اصول ORM، عدم استفاده از ایندکس و کشینگ هست قبل از اینکه برای هر مشکلی پول خرج کنید، با ابزارهای دیباگینگ و اصول بهینه‌سازی، کد خود زو شفاف سازی کنید

«سرور قوی‌تر صورت‌حساب رو زیاد می‌کنه اما کوئری بهینه، آینده‌ی پروژه رو تضمین می‌کنه اول کد رو تمیز کن، بعد پول بده.»

امیرحسین علیجانی

امیرحسین علیجانی

برنامه‌نویس Django و Python

درباره نویسنده · نمونه کارها

ارسال دیدگاه

نام
ایمیل
متن دیدگاه