4

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?

  1. I'm using redis like this:

    CACHES = { 'default': { 'BACKEND': 'django_redis.cache.RedisCache', 'LOCATION': 'redis://supapp_redis_1:6379/1', 'OPTIONS': { 'CLIENT_CLASS': 'django_redis.client.DefaultClient', } } }

  2. i'm trying to test with anonymous users because this is the firstpage every user will see. My locustfile.py look like this:

    from locust import HttpUser, task, between

    class WebsiteUser(HttpUser): def index(self): self.client.get("/")

  3. Gunicorn is using 2 cpus. When i use htop and test with locust i can see 100% usage on both cpus

  4. My testsite.com.conf has these lines:

    Serve static files

    location /static/ { alias /opt/supapp/data/Shop/static/; }

    Serve media files

    location /media/ { alias /opt/supapp/data/Shop/media/; }

I use nginx for static content

Unfortunately i still get 3 RPS with 2 CPUS

PS: I use Django templates for my frontend(SSR)

4
  • 1
    Make sure that when locust makes request it's using your cache (add some logging in your caching decorator) Commented May 27 at 6:12
  • @PTomasz, I added logging, it uses cache. It seems the problem is something else Commented May 27 at 15:36
  • Is DDT deactivated when you run gunicorn via your command? Is DEBUG set to False with your command? Commented May 30 at 5:34
  • @FrançoisConstant, yeah django debug toolbar is deactivated and DEBUG set to False Commented May 30 at 17:07

1 Answer 1

1

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.

AS you still get 4 RPS, Now try below changes might help:

  1. Update your cache_anonymous_page to Cache the entire HttpResponse object, not just .content
def cache_anonymous_page(timeout=CACHE_TIMEOUT_COMMON):
    from functools import wraps
    def decorator(view):
        @wraps(view)
        def wrapper(request, *args, **kw):
            if request.user.is_authenticated:
                return view(request, *args, **kw)

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

            cached_resp = cache.get(cache_key)
            if cached_resp:
                return cached_resp  # full HttpResponse object

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

2. Try Using threads or processes instead of gevent

gunicorn main.wsgi:application --workers 2 --threads 10 --timeout 120 --bind 0.0.0.0:8080

3. Check if template rendering is the problem:
Try returning a hard-coded HTML string:

from django.http import HttpResponse

def cached_healthcheck(request):
    return HttpResponse("<html><body><h1>ok</h1></body></html>")

Benchmark it with wrk or ab:

ab -n 1000 -c 100 http://localhost:8080/healthcheck/

f that gives you 1000+ RPS, but your frontpage gives 4 RPS then template rendering is the problem.

4. Try running container like:

docker run --cpus=2.0 --memory="2g" ...

5.Try below, If RPS doesn’t increase with higher concurrency then server is bottleneck. If it does, your Locust setup was too small.

locust -u 100 -r 20 --headless -t 1m -H http://yourserver

Also try using wrk:

wrk -t10 -c100 -d30s http://localhost:8080/
Sign up to request clarification or add additional context in comments.

6 Comments

1) I'm using redis like this: CACHES = { 'default': { 'BACKEND': 'django_redis.cache.RedisCache', 'LOCATION': 'redis://supapp_redis_1:6379/1', 'OPTIONS': { 'CLIENT_CLASS': 'django_redis.client.DefaultClient', } } }
2) Yeah i'm trying to test with anonymous users because this is the firstpage every user will see. My locustfile.py look like this: from locust import HttpUser, task, between class WebsiteUser(HttpUser): def index(self): self.client.get("/")
3) Gunicorn is using 2 cpus. When i use htop and test with locust i can see 100% usage on both cpus
4) My testsite.com.conf has these lines: # Serve static files location /static/ { alias /opt/supapp/data/Shop/static/; } # Serve media files location /media/ { alias /opt/supapp/data/Shop/media/; } I use nginx for static content
Unfortunately i still get 4 RPS with 2 CPUS PS: I use Django templates for my frontend(SSR)
|

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.