Low RPS when perfomance testings django website

I have a code like this that caches a page for 60 minutes:

import os
import time
from django.conf import settings
from django.core.cache import cache
from django.core.mail import send_mail
from django.contrib import messages
from django.http import FileResponse, Http404, HttpResponse
from django.shortcuts import render
from django.utils.translation import get_language, gettext as _

from apps.newProduct.models import Product, Variants, Category
from apps.vendor.models import UserWishList, Vendor
from apps.ordering.models import ShopCart
from apps.blog.models import Post
from apps.cart.cart import Cart

# Cache timeout for common data
CACHE_TIMEOUT_COMMON = 900  # 15 minutes

def cache_anonymous_page(timeout=CACHE_TIMEOUT_COMMON):
    from functools import wraps
    from django.utils.cache import _generate_cache_header_key

    def decorator(view):
        @wraps(view)
        def wrapper(request, *args, **kw):
            if request.user.is_authenticated:
                return view(request, *args, **kw)

            lang = get_language()                          # i18n
            curr = request.session.get('currency', '')
            country = request.session.get('country', '')
            cache_key = f"{view.__module__}.{view.__name__}:{lang}:{curr}:{country}"

            resp = cache.get(cache_key)
            if resp is not None:
                return HttpResponse(resp)

            response = view(request, *args, **kw)
            if response.status_code == 200:
                cache.set(cache_key, response.content, timeout)
            return response
        return wrapper
    return decorator


def get_cached_products(cache_key, queryset, timeout=CACHE_TIMEOUT_COMMON):
    lang = get_language()
    full_key = f"{cache_key}:{lang}"
    data = cache.get(full_key)
    if data is None:
        data = list(queryset)
        cache.set(full_key, data, timeout)
    return data

def get_cached_product_variants(product_list, cache_key='product_variants', timeout=CACHE_TIMEOUT_COMMON):
    lang = get_language()
    full_key = f"{cache_key}:{lang}"
    data = cache.get(full_key)
    if data is None:
        data = []
        for product in product_list:
            if product.is_variant:
                data.extend(product.get_variant)
        cache.set(full_key, data, timeout)
    return data

def get_all_cached_data():
    featured_products = get_cached_products(
        'featured_products',
        Product.objects.filter(status=True, visible=True, is_featured=True)
               .exclude(image='')
               .only('id','title','slug','image')[:8]
    )
    popular_products = get_cached_products(
        'popular_products',
        Product.objects.filter(status=True, visible=True)
               .exclude(image='')
               .order_by('-num_visits')
               .only('id','title','slug','image')[:4]
    )
    recently_viewed_products = get_cached_products(
        'recently_viewed_products',
        Product.objects.filter(status=True, visible=True)
               .exclude(image='')
               .order_by('-last_visit')
               .only('id','title','slug','image')[:5]
    )
    variants = get_cached_products(
        'variants',
        Variants.objects.filter(status=True)
                .select_related('product')
                .only('id','product','price','status')
    )
    product_list = get_cached_products(
        'product_list',
        Product.objects.filter(status=True, visible=True)
               .prefetch_related('product_variant')
    )
    return featured_products, popular_products, recently_viewed_products, variants, product_list

def get_cart_info(user, request):
    if user.is_anonymous:
        return {}, 0, [], 0, []
    cart = Cart(request)
    wishlist = list(UserWishList.objects.filter(user=user).select_related('product'))
    shopcart_qs = ShopCart.objects.filter(user=user).select_related('product','variant')
    shopcart = list(shopcart_qs)
    products_in_cart = [item.product.id for item in shopcart if item.product]
    total = cart.get_cart_cost()
    comparing = len(request.session.get('comparing', []))
    compare_var = len(request.session.get('comparing_variants', []))
    total_compare = comparing + compare_var
    if len(cart) == 0:
        shopcart = []
    return {
        'cart': cart,
        'wishlist': wishlist,
        'shopcart': shopcart,
        'products_in_cart': products_in_cart,
    }, total, wishlist, total_compare, shopcart

@cache_anonymous_page(3600)
def frontpage(request):
    featured_products, popular_products, recently_viewed_products, variants, product_list = get_all_cached_data()
    var = get_cached_product_variants(product_list)
    cart_ctx, total, wishlist, total_compare, shopcart = get_cart_info(request.user, request)
    context = {
        'featured_products': featured_products,
        'popular_products': popular_products,
        'recently_viewed_products': recently_viewed_products,
        'variants': variants,
        'var': var,
        **cart_ctx,
        'subtotal': total,
        'total_compare': total_compare,
    }
    return render(request, 'core/frontpage.html', context)

I installed django debug toolbar and it shows time ~40 ms for a cached frontpage. My server has 2 CPUs. When i try perfomance testing using locust I get around 3 RPS. I thought i would get around 2CPU*(1000/40) ~ 50 RPS.

I run my server using this command inside docker container:

gunicorn main.wsgi:application
            -k gevent
            --workers 6
            --bind 0.0.0.0:8080
            --worker-connections 1000
            --timeout 120

Also i use psycopg2 with psycogreen wsgi.py starts with this:

from psycogreen.gevent import patch_psycopg
patch_psycopg()

What am i doing wrong? Why can't handle more RPS?

your cache decorator is basically useless for logged-in users. Look at this line

if request.user.is_authenticated:
    return view(request, *args, **kw)  # Completely bypasses cache!

Every single authenticated user is hitting your database queries in get_cart_info(). That's why you're only getting 3 RPS instead of the 50+ you expected.

Cache the user data separately:

def get_cart_info(user, request):
    if user.is_anonymous:
        return {}, 0, [], 0, []
    
    cache_key = f"user_cart:{user.id}"
    data = cache.get(cache_key)
    if not data:
        # Do your DB queries here
        # Cache for like 1-2 minutes max
        cache.set(cache_key, data, 120)
    return data

Test with anonymous users first - you should easily hit 100+ RPS for truly cached content.

Gunicorn config is probably fine but try sync workers instead of gevent for this use case:

gunicorn main.wsgi:application --workers 4 --worker-class sync --bind 0.0.0.0:8080

Also, make sure you're actually testing what you think you're testing. Are your Locust users logging in? Because if they are, they're not hitting your cache at all.

The 40ms you're seeing is probably for an authenticated request, not a cached one. A truly cached page should be like 5-10ms tops.

You're likely hitting these issues:

1. Using LocMemCache (Default):

Each Gunicorn worker has its own cache = no shared cache.

** To fix this use Redis or Memcached in settings.py:

CACHES = {
    "default": {
        "BACKEND": "django.core.cache.backends.redis.RedisCache",
        "LOCATION": "redis://127.0.0.1:6379/1",
    }
}

2. Your Locust Load Test Might Use Logged-in Users

Your cache only works for anonymous users:

if request.user.is_authenticated:
    return view(request, *args, **kw)

** Load test with anonymous (non-authenticated) users.

3. Gunicorn Not Using All CPUs in Docker

** Use all cores dynamically:

--workers $(nproc)

Full command:

gunicorn main.wsgi:application \
    -k gevent \
    --workers $(nproc) \
    --worker-connections 1000 \
    --timeout 120 \
    --bind 0.0.0.0:8080

Also, ensure Docker is allowed to use 2 CPUs:

docker run --cpus="2.0" ...

4. Gunicorn is slow for static files.

** Use Nginx or a CDN for static files.

Вернуться на верх