Locust Connection Refused Troubleshooting
बनाया गया: 18 जनवरी 2025
बनाया गया: 18 जनवरी 2025
кодовая база:
#/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"
textdef 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
BASE_DIR = Path(file).resolve().parent.parent
SECRET_KEY = 'django-insecure-_skjesb)_v0u6j4%p(2qc))k&nk^@q%cn^j=efl*+a_u3_gxo8'
DEBUG = True
ALLOWED_HOSTS = []
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'
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'),
}
}
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',
},
]
LANGUAGE_CODE = 'en-us'
TIME_ZONE = 'UTC'
USE_I18N = True
USE_TZ = True
STATIC_URL = 'static/'
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):
"""
Модель для хранения информации о кошельке пользователя.
textАтрибуты: 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):
"""
Модель для учета операций с кошельком.
textАтрибуты: 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.
textЭтот сериализатор используется для преобразования данных о кошельке в формат JSON и обратно. Атрибуты: model (class): Модель, к которой применяется сериализатор. fields (str or list): Список полей, которые будут включены в сериализацию. """ class Meta: model = Wallet fields = '__all__'
class WalletOperationSerializer(serializers.Serializer):
"""
Сериализатор для создания операций на кошельке.
textЭтот сериализатор используется для валидации данных о типе операции и сумме. """ 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):
"""
Сериализатор для вывода баланса кошелька.
textЭтот сериализатор используется для отображения текущего баланса кошелька. """ 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']
textif 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
textdef 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"}%
пробразываются без проблем, но нагрузузочное тестирование не работает
1000 POST /api/v1/wallets/
ConnectionRefusedError(111, 'Connection refused')
Сообщение об ошибке «ConnectionRefusedError(111, 'Connection refused')» указывает на то, что при нагрузочном тестировании клиент (Locust) не может установить соединение с сервером. При этом ручные запросы проходят успешно. Возможные причины и решения:
Стандартный сервер разработки Django (runserver
) не предназначен для обработки большого числа одновременных подключений. Под нагрузкой он может не выдерживать множество параллельных запросов, что приводит к отказу в соединении.
Решения:
Если вы уже используете производительный сервер, возможно, есть ограничения на уровне операционной системы или конфигурации самого сервера.
Проверьте и настройте:
Убедитесь, что указан правильный хост и порт для тестирования, и сервер действительно слушает на localhost:8000
.
Шаги проверки:
curl
одновременно в нескольких терминалах, чтобы воспроизвести нагрузку.host
(в вашем случае это "http://localhost:8000"
).При большом количестве запросов серверное приложение может вызывать ошибки (например, исчерпание ресурсов БД, блокировки и т.д.) и прекращать работу.
Проверьте:
runserver
на Gunicorn/uWSGI для проведения нагрузочного тестирования.Эти шаги помогут устранить ошибку «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 | Ошибка при создании кошелька:
Ситуация, когда ручные запросы проходят успешно, а запросы из Locust завершаются ошибкой при создании кошелька, может быть связана с формированием некорректного URL в тестовых сценариях Locust.
В методе on_start
вашего класса WalletUser
в файле locustfile.py
указан URL без ведущего слэша:
pythonresponse = self.client.post("api/v1/wallets/", json={"balance": 1000.0})
При задании хоста как "http://localhost:8000"
отсутствие ведущего слэша приведёт к тому, что конечный URL будет сформирован как:
http://localhost:8000api/v1/wallets/
Такой URL является некорректным и приводит к ошибкам соединения или 404 Not Found.
Добавление ведущего слэша в URL запроса:
Измените строку в методе on_start
, добавив слэш в начале пути:
pythonresponse = self.client.post("/api/v1/wallets/", json={"balance": 1000.0})
Это приведёт к корректному формированию полного URL:
http://localhost:8000/api/v1/wallets/
Изменение параметра host:
Альтернативно, можно добавить завершающий слэш в конец параметра host
, чтобы пути без начального слэша формировались корректно:
pythonhost = "http://localhost:8000/"
Однако явное указание полного пути с ведущим слэшем в запросах зачастую предпочтительнее для ясности.
/
либо URL формируется корректно благодаря настройке host).После исправления URL ошибки «Ошибка при создании кошелька:» должны исчезнуть, и запросы будут успешно обрабатываться сервером.