问题

кодовая база:
#/home/babyblinkfeeldark/PycharmProjects/prj_root/locust/locustfile.py
from locust import HttpUser, task, between

class WalletUser(HttpUser):
wait_time = between(1, 2)
host = "http://localhost:8000"

def on_start(self):
response = self.client.post("api/v1/wallets/", json={"balance": 1000.0})
if response.status_code == 201:
self.wallet_id = response.json()['id']
else:
print("Ошибка при создании кошелька:", response.text)

@task(2)
def deposit(self):
if hasattr(self, 'wallet_id'):
self.client.post(f"/api/v1/wallets/{self.wallet_id}/operation/", json={
"operation_type": "DEPOSIT",
"amount": 100.0
})

@task(1)
def withdraw(self):
if hasattr(self, 'wallet_id'):
self.client.post(f"/api/v1/wallets/{self.wallet_id}/operation/", json={
"operation_type": "WITHDRAW",
"amount": 50.0
})

@task(1)
def get_balance(self):
if hasattr(self, 'wallet_id'):
self.client.get(f"/api/v1/wallets/{self.wallet_id}/balance/")

#/home/babyblinkfeeldark/PycharmProjects/prj_root/project_root/settings.py
"""
Django settings for project_root project.

Generated by 'django-admin startproject' using Django 5.1.5.

For more information on this file, see
https://docs.djangoproject.com/en/5.1/topics/settings/

For the full list of settings and their values, see
https://docs.djangoproject.com/en/5.1/ref/settings/
"""
import os
from pathlib import Path

from dotenv import load_dotenv

# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent


# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/5.1/howto/deployment/checklist/

# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = 'django-insecure-_skjesb)_v0u6j4%p(2qc))k&nk^@q%cn^j=efl*+a_u3_gxo8'

# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True

ALLOWED_HOSTS = []


# Application definition

INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'rest_framework',
'wallets',
]

MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]

ROOT_URLCONF = 'project_root.urls'

TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
},
},
]

WSGI_APPLICATION = 'project_root.wsgi.application'


# Database
# https://docs.djangoproject.com/en/5.1/ref/settings/#databases
load_dotenv()

DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql',
'NAME': os.getenv('DB_NAME'),
'USER': os.getenv('DB_USER'),
'PASSWORD': os.getenv('DB_PASSWORD'),
'HOST': os.getenv('DB_HOST'),
'PORT': os.getenv('DB_PORT'),
}
}


# Password validation
# https://docs.djangoproject.com/en/5.1/ref/settings/#auth-password-validators

AUTH_PASSWORD_VALIDATORS = [
{
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
},
]


# Internationalization
# https://docs.djangoproject.com/en/5.1/topics/i18n/

LANGUAGE_CODE = 'en-us'

TIME_ZONE = 'UTC'

USE_I18N = True

USE_TZ = True


# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/5.1/howto/static-files/

STATIC_URL = 'static/'

# Default primary key field type
# https://docs.djangoproject.com/en/5.1/ref/settings/#default-auto-field

DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'

#/home/babyblinkfeeldark/PycharmProjects/prj_root/project_root/urls.py
"""
URL configuration for project_root project.

The `urlpatterns` list routes URLs to views. For more information please see:
https://docs.djangoproject.com/en/5.1/topics/http/urls/
Examples:
Function views
1. Add an import: from my_app import views
2. Add a URL to urlpatterns: path('', views.home, name='home')
Class-based views
1. Add an import: from other_app.views import Home
2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
Including another URLconf
1. Import the include() function: from django.urls import include, path
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
"""
from django.contrib import admin
from django.http import HttpResponse
from django.urls import path, include

def home(request):
return HttpResponse("Test page")

urlpatterns = [
path('', home),
path('', include('wallets.urls')), # Убедитесь, что здесь пустой префикс, так как 'api/v1/' уже включён в wallets.urls
path('admin/', admin.site.urls),
]

#/home/babyblinkfeeldark/PycharmProjects/prj_root/wallets/models.py
from email.policy import default
from django.db import models
import uuid

class Wallet(models.Model):
"""
Модель для хранения информации о кошельке пользователя.

Атрибуты:
id (UUIDField): Уникальный идентификатор кошелька (primary key).
balance (DecimalField): Баланс кошелька, с двумя знаками после запятой.
"""
id = models.UUIDField(primary_key=True, default= uuid.uuid4, editable=False)
balance = models.DecimalField(max_digits=20, decimal_places=2,default=0)

def __str__(self):
"""
Возвращает строковое представление объекта Wallet.

Возвращает:
str: Идентификатор кошелька.
"""
return str(self.id)

class WalletOperation(models.Model):
"""
Модель для учета операций с кошельком.

Атрибуты:
DEPOSIT (str): Тип операции для пополнения.
WITHDRAW (str): Тип операции для вывода средств.
OPERATION_TYPE_CHOICES (list): Список возможных типов операций.
id (AutoField): Уникальный идентификатор операции.
wallet (ForeignKey): Ссылка на кошелек, к которому привязана операция.
operation_type (CharField): Тип операции (DEPOSIT или WITHDRAW).
amount (DecimalField): Сумма операции.
timestamp (DateTimeField): Время проведения операции.
"""
DEPOSIT = 'DEPOSIT'
WITHDRAW = 'WITHDRAW'
OPERATION_TYPE_CHOICES = [(DEPOSIT,'DEPOSIT'), (WITHDRAW, "WITHDRAW")]

id = models.AutoField(primary_key = True)
wallet = models.ForeignKey(Wallet, related_name="operations", on_delete=models.CASCADE)
operation_type = models.CharField(max_length=10, choices=OPERATION_TYPE_CHOICES)
amount = models.DecimalField(max_digits=20, decimal_places=2)
timestamp = models.DateTimeField(auto_now_add=True)

def __str__(self):
"""
Возвращает строковое представление операции.

Возвращает:
str: Описание операции с указанием типа, суммы и идентификатора кошелька.
"""
return f"{self.operation_type} of {self.amount} to wallet {self.wallet.id}"

#/home/babyblinkfeeldark/PycharmProjects/prj_root/wallets/serializers.py
from rest_framework import serializers
from .models import Wallet, WalletOperation

class WalletSerializer(serializers.ModelSerializer):
"""
Сериализатор для модели Wallet.

Этот сериализатор используется для преобразования данных о кошельке в формат JSON
и обратно.

Атрибуты:
model (class): Модель, к которой применяется сериализатор.
fields (str or list): Список полей, которые будут включены в сериализацию.
"""
class Meta:
model = Wallet
fields = '__all__'


class WalletOperationSerializer(serializers.Serializer):
"""
Сериализатор для создания операций на кошельке.

Этот сериализатор используется для валидации данных о типе операции и сумме.
"""
operation_type = serializers.ChoiceField(choices=[('DEPOSIT', 'DEPOSIT'), ('WITHDRAW', 'WITHDRAW')])
amount = serializers.DecimalField(max_digits=20, decimal_places=2)

def validate_amount(self, value):
"""
Валидация суммы операции.
"""
if value <= 0:
raise serializers.ValidationError("Amount must be positive.")
return value

def validate_operation_type(self, value):
"""
Валидация типа операции.
"""
if value not in ['DEPOSIT', 'WITHDRAW']:
raise serializers.ValidationError("Invalid operation type.")
return value


class WalletBalanceSerializer(serializers.ModelSerializer):
"""
Сериализатор для вывода баланса кошелька.

Этот сериализатор используется для отображения текущего баланса кошелька.
"""
class Meta:
model = Wallet
fields = ['id', 'balance']


#/home/babyblinkfeeldark/PycharmProjects/prj_root/wallets/urls.py
from django.urls import path
from .views import WalletList, WalletDetail, WalletBalanceDetail, create_wallet_operation

urlpatterns = [
path('api/v1/wallets/', WalletList.as_view(), name='wallet-list'),
path('api/v1/wallets/<uuid:pk>/', WalletDetail.as_view(), name='wallet-detail'),
path('api/v1/wallets/<uuid:wallet_uuid>/balance/', WalletBalanceDetail.as_view(), name='wallet-balance'),
path('api/v1/wallets/<uuid:wallet_uuid>/operation/', create_wallet_operation, name='wallet-operation'),
]

#/home/babyblinkfeeldark/PycharmProjects/prj_root/wallets/views.py
from django.http import Http404
from rest_framework import generics
from .serializers import WalletSerializer, WalletOperationSerializer, WalletBalanceSerializer
from rest_framework.response import Response
from rest_framework import status
from rest_framework.decorators import api_view
from .models import Wallet, WalletOperation
from django.db import transaction
import uuid


class WalletList(generics.ListCreateAPIView):
queryset = Wallet.objects.all()
serializer_class = WalletSerializer


class WalletDetail(generics.RetrieveUpdateDestroyAPIView):
queryset = Wallet.objects.all()
serializer_class = WalletSerializer


@api_view(['POST'])
def create_wallet_operation(request, wallet_uuid):
try:
with transaction.atomic():
wallet = Wallet.objects.select_for_update().get(id=wallet_uuid)
serializer = WalletOperationSerializer(data=request.data)
if serializer.is_valid():
operation_type = serializer.validated_data['operation_type']
amount = serializer.validated_data['amount']

if operation_type == 'WITHDRAW' and wallet.balance < amount:
return Response({'error': 'Insufficient funds'}, status=status.HTTP_400_BAD_REQUEST)

if operation_type == 'DEPOSIT':
wallet.balance += amount
elif operation_type == 'WITHDRAW':
wallet.balance -= amount

wallet.save()

WalletOperation.objects.create(
wallet=wallet,
operation_type=operation_type,
amount=amount
)

return Response({'balance': wallet.balance}, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
except Wallet.DoesNotExist:
return Response({'error': 'Wallet not found'}, status=status.HTTP_404_NOT_FOUND)
except Exception as e:
return Response({'error': str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)


class WalletBalanceDetail(generics.RetrieveAPIView):
queryset = Wallet.objects.all()
serializer_class = WalletBalanceSerializer

def get_object(self):
wallet_uuid = self.kwargs['wallet_uuid']
try:
return Wallet.objects.get(id=wallet_uuid)
except Wallet.DoesNotExist:
raise Http404

В ручную запросы вида
❯ curl -X POST http://localhost:8000/api/v1/wallets/
{"id":"e4b88c44-8066-49f7-b737-b841d1c0a834","balance":"0.00"}%

пробразываются без проблем, но нагрузузочное тестирование не работает
# Failures Method Name Message
1000 POST /api/v1/wallets/

ConnectionRefusedError(111, 'Connection refused')

思考

Investigating issues

Locust load testing on a Django-based wallet application fails after 1000 POST requests due to a connection refusal. Ensuring the server runs correctly during testing is crucial.

Pinpointing issues

The 'Connection refused' error may stem from the server being unresponsive or overloaded with concurrent requests, especially if using Django's single-threaded runserver by default.

Assessing server capacity

I’m evaluating if Django's runserver can handle multiple concurrent connections during load testing or if a more robust server like Gunicorn is needed to prevent connection refusals.

Identifying potential causes

I’m assessing firewall or network restrictions, server configuration limits, and the impact of a single-threaded runserver on handling multiple concurrent connections during load testing. These factors need thorough evaluation.

Assessing server capacity

Hmm, I’m thinking about whether Django's runserver can handle 1000 connections and whether Locust's host setting is correct. Switching to gunicorn might mitigate server overload issues.

Evaluating server options

OK, let's think through the idea that a production server, like Gunicorn, might handle more connections and see if the server needs peak connections and a stable setup.

Thought about load testing issue для 7 секунд

回答

Сообщение об ошибке «ConnectionRefusedError(111, 'Connection refused')» указывает на то, что при нагрузочном тестировании клиент (Locust) не может установить соединение с сервером. При этом ручные запросы проходят успешно. Возможные причины и решения:

1. Сервер не справляется с высокой нагрузкой

Стандартный сервер разработки Django (runserver) не предназначен для обработки большого числа одновременных подключений. Под нагрузкой он может не выдерживать множество параллельных запросов, что приводит к отказу в соединении.

Решения:

  • Запустите приложение под более производительным WSGI-сервером (например, Gunicorn или uWSGI). Они лучше справляются с большим количеством одновременных соединений.
  • Настройте количество воркеров и потоков у выбранного сервера для обработки ожидаемой нагрузки.

2. Серверное окружение и настройки

Если вы уже используете производительный сервер, возможно, есть ограничения на уровне операционной системы или конфигурации самого сервера.

Проверьте и настройте:

  • Ограничения на количество открытых файлов/соединений (ulimit) в ОС.
  • Параметры конфигурации Gunicorn/uWSGI: количество воркеров, таймауты, размер очереди запросов и т.д.

3. Сетевая доступность

Убедитесь, что указан правильный хост и порт для тестирования, и сервер действительно слушает на localhost:8000.

Шаги проверки:

  • Попробуйте выполнить запросы с помощью утилиты вроде curl одновременно в нескольких терминалах, чтобы воспроизвести нагрузку.
  • Убедитесь, что в настройках Locust указано правильное значение host (в вашем случае это "http://localhost:8000").

4. Приложение падает под нагрузкой

При большом количестве запросов серверное приложение может вызывать ошибки (например, исчерпание ресурсов БД, блокировки и т.д.) и прекращать работу.

Проверьте:

  • Логи Django и используемого WSGI-сервера на предмет ошибок или сбоев.
  • Настройки базы данных и ее способность обрабатывать параллельные транзакции.

Рекомендации по дальнейшим действиям:

  1. Переключитесь с runserver на Gunicorn/uWSGI для проведения нагрузочного тестирования.
  2. Настройте нужное количество воркеров и потоков в конфигурации сервера.
  3. Убедитесь, что сервер и БД настроены для обработки высокого числа одновременных запросов.
  4. Проверьте системные лимиты и при необходимости увеличьте их.

Эти шаги помогут устранить ошибку «Connection refused» и позволят корректно провести нагрузочное тестирование вашего приложения.

问题

Проблема сохраняется, даже если я настраиваю locust на одно подключение в секунду, задержка у сервера проиемлимая
Type Name # Requests # Fails Median (ms) 95%ile (ms) 99%ile (ms) Average (ms) Min (ms) Max (ms) Average size (bytes) Current RPS Current Failures/s
POST /api/v1/wallets/ 21 21 2 7 9 2.64 1 9 0 1 1

при этом в логах контейнера ошибка:
locust_1 | Ошибка при создании кошелька:
locust_1 | Ошибка при создании кошелька:
locust_1 | Ошибка при создании кошелька:
locust_1 | Ошибка при создании кошелька:
locust_1 | Ошибка при создании кошелька:
locust_1 | Ошибка при создании кошелька:
locust_1 | Ошибка при создании кошелька:
locust_1 | Ошибка при создании кошелька:
locust_1 | Ошибка при создании кошелька:
locust_1 | Ошибка при создании кошелька:
locust_1 | Ошибка при создании кошелька:
locust_1 | Ошибка при создании кошелька:
locust_1 | Ошибка при создании кошелька:
locust_1 | Ошибка при создании кошелька:
locust_1 | Ошибка при создании кошелька:
locust_1 | Ошибка при создании кошелька:

思考

Investigating error patterns

The user is still dealing with persistent errors during wallet creation with Locust. Logs consistently show a failed attempt to POST /api/v1/wallets/, yet manual requests succeed. The root cause might involve connection issues or transient service unavailability.

Understanding flavor differences

I’m mapping out the discrepancy in wallet creation: the manual POST bypassed JSON prompts, while the Locust test demanded attributes, possibly confusing the serializer.

Scrutinizing content-type and URL

Taking a closer look at Locust's lack of Content-Type header and URL discrepancies, which might trigger CSRF validation despite API view settings.

Ensuring URL correctness

I’m thinking through the need for an initial slash in URLs in Locust post requests. This adjustment helps prevent connection issues, particularly aligned with host configurations.

Clarifying URL handling

I’m confirming that URLs in Locust requests should start with a slash. The lack of clarity on trailing slashes suggests adding one before a request path.

Thought about locust testing issues для 10 секунд

回答

Ситуация, когда ручные запросы проходят успешно, а запросы из Locust завершаются ошибкой при создании кошелька, может быть связана с формированием некорректного URL в тестовых сценариях Locust.

Проблема с путём запроса

В методе on_start вашего класса WalletUser в файле locustfile.py указан URL без ведущего слэша:

response = self.client.post("api/v1/wallets/", json={"balance": 1000.0})

При задании хоста как "http://localhost:8000" отсутствие ведущего слэша приведёт к тому, что конечный URL будет сформирован как:

http://localhost:8000api/v1/wallets/

Такой URL является некорректным и приводит к ошибкам соединения или 404 Not Found.

Решения

  1. Добавление ведущего слэша в URL запроса: Измените строку в методе on_start, добавив слэш в начале пути:

    response = self.client.post("/api/v1/wallets/", json={"balance": 1000.0})

    Это приведёт к корректному формированию полного URL:

    http://localhost:8000/api/v1/wallets/
    
  2. Изменение параметра host: Альтернативно, можно добавить завершающий слэш в конец параметра host, чтобы пути без начального слэша формировались корректно:

    host = "http://localhost:8000/"

    Однако явное указание полного пути с ведущим слэшем в запросах зачастую предпочтительнее для ясности.

Дальнейшие шаги

  • Проверьте, что все запросы в вашем тестовом сценарии Locust используют правильные пути (начинаются с / либо URL формируется корректно благодаря настройке host).
  • Перезапустите нагрузочное тестирование после внесения изменений.
  • Убедитесь, что сервер Django и база данных настроены должным образом для обработки даже небольшого количества одновременных запросов.

После исправления URL ошибки «Ошибка при создании кошелька:» должны исчезнуть, и запросы будут успешно обрабатываться сервером.

分享这个问答