Настроить load balancing между агентами
ТЕХНИЧЕСКОЕ ЗАДАНИЕ: Настроить load balancing между агентами
1. Цель задачи
Научиться проектировать и реализовывать механизмы балансировки нагрузки между несколькими экземплярами агентов (AI-агентов или микросервисов) для обеспечения равномерного распределения запросов и минимизации времени отклика. В результате вы развернёте собственный балансировщик с двумя стратегиями (round-robin и consistent hashing), протестируете его под нагрузкой и убедитесь, что дисперсия загрузки агентов не превышает 10 %.
Ключевой результат Работающий программный балансировщик, который выбирает целевого агента по round-robin или consistent hashing; метрики распределения нагрузки (среднее, дисперсия) и визуализация; отчёт о сравнении стратегий.
2. Исходные данные
| Что нужно | Откуда взять |
|---|---|
| Код агентов-заглушек (отвечают на запросы) | Написать самостоятельно (FastAPI / aiohttp) |
| Инструмент для генерации нагрузки | locust или wrk2, либо собственный скрипт на asyncio |
| Python 3.10+ | Установить на локальную машину |
| Docker (опционально) | Для изоляции агентов |
Библиотека hashlib | Встроена в Python |
| Визуализация | matplotlib / pandas |
Если нет реального инструмента — симулируем:
- Напишите три простых async-заглушки агента на aiohttp (или FastAPI), которые принимают GET-запрос, ждут 0.05–0.15 секунд (случайно) и возвращают {"agent_id": "A1", "request_id": ...}. Запустите их на портах 8081, 8082, 8083.
- Для генерации нагрузки напишите скрипт на asyncio, который отправляет 1000 запросов с фиксированной задержкой (например, 0.1 с между запросами). В каждом запросе передавайте параметр user_id (целое число от 1 до 100), чтобы имитировать привязку сессии.
- Если locust не установлен — не устанавливайте; достаточно асинхронного скрипта.
3. Технологический стек
| Компонент | Инструменты | Назначение |
|---|---|---|
| Язык программирования | Python 3.10+ | Реализация балансировщика и агентов |
| Асинхронный фреймворк | aiohttp (или FastAPI) | Запуск HTTP-серверов (агенты, балансировщик) |
| Генератор нагрузки | asyncio + aiohttp (скрипт) | Имитация клиентских запросов |
| Хэширование | hashlib (md5 или sha256) | Consistent hashing |
| Логирование | logging (Python) + файл | Сбор статистики по агентам |
| Визуализация | matplotlib + pandas | Построение гистограмм распределения |
| Контейнеризация (опционально) | Docker + docker-compose | Изоляция серверов |
4. Этапы выполнения
Этап 1: Подготовка окружения и написание агентов-заглушек (оценка: 30 мин)
Действия
- Создайте корневую папку проекта
agent_load_balancer/. - Создайте файл agent.py — асинхронный сервер на aiohttp:
# agent.py (запускать по три экземпляра с разными аргументами)
import asyncio
from aiohttp import web
import time
import random
import os
AGENT_ID = os.getenv('AGENT_ID', 'A1')
async def handle(request):
# Симуляция processing time
await asyncio.sleep(random.uniform(0.05, 0.15))
return web.json_response({
"agent_id": AGENT_ID,
"request_id": request.match_info.get('req_id', None),
"user_id": request.rel_url.query.get('user_id', None),
"timestamp": time.time()
})
app = web.Application()
app.router.add_get('/process/{req_id}', handle)
if __name__ == '__main__':
web.run_app(app, port=int(os.getenv('PORT', 8081)))
-
Запустите три экземпляра в разных терминалах (или в docker-compose):
-
Проверьте, что каждый отвечает: curl http://localhost:8081/process/test1
Ожидаемый результат этапа Три работающих агента на локальных портах.
Этап 2: Реализация балансировщика с round-robin (оценка: 1 час)
Действия
- Создайте файл
balancer.py. - Реализуйте класс
RoundRobinBalancer:
class RoundRobinBalancer:
def __init__(self, agent_urls):
self.agent_urls = agent_urls
self.index = 0
def get_agent(self, request_context=None):
# request_context игнорируется
agent = self.agent_urls[self.index]
self.index = (self.index + 1) % len(self.agent_urls)
return agent
-
Реализуйте HTTP-сервер балансировщика на порту 8000:
-
Добавьте логирование: в stdout пишите, какой агент выбран для каждого request_id.
-
Запустите балансировщик и отправьте 10 тестовых запросов, убедитесь, что агенты выбираются по кругу.
Ожидаемый результат этапа Балансировщик корректно распределяет запросы round-robin, лог показывает чередование агентов.
Этап 3: Реализация consistent hashing (оценка: 1-1.5 часа)
Действия
- Реализуйте класс
ConsistentHashBalancerс использованием виртуальных нод:
import hashlib
class ConsistentHashBalancer:
def __init__(self, agent_urls, virtual_nodes=150):
self.virtual_nodes = virtual_nodes
self.ring = {} # хэш -> agent_url
self.sorted_keys = []
for url in agent_urls:
for i in range(virtual_nodes):
key = self._hash(f"{url}:{i}")
self.ring[key] = url
self.sorted_keys.append(key)
self.sorted_keys.sort()
def _hash(self, key_str):
return int(hashlib.md5(key_str.encode()).hexdigest(), 16)
def get_agent(self, request_context):
user_id = request_context.get('user_id', 0)
key = self._hash(str(user_id))
# binary search to find first key >= user hash
import bisect
idx = bisect.bisect_left(self.sorted_keys, key)
if idx == len(self.sorted_keys):
idx = 0
return self.ring[self.sorted_keys[idx]]
-
Модифицируйте балансировщик так, чтобы стратегия выбиралась через переменную окружения
STRATEGY(rrилиch). По умолчаниюrr. -
Проверьте привязку: для одного и того же
user_idconsistent hashing должен возвращать одного и того же агента (даже если список агентов меняется). Проверьте, удалив на секунду один агент (заменив url на несуществующий) — запросы с тем же user_id должны уйти на тот же agent, если он ещё жив, либо на следующий по кольцу. -
Добавьте в лог также
user_idи вычисленный хэш.
Ожидаемый результат этапа Реализованы обе стратегии, consistent hashing привязывает user_id к агенту.
Этап 4: Gенерация нагрузки и сбор метрик (оценка: 1-2 часа)
Действия
- Напишите скрипт
load_test.py, который отправляет 2000 запросов сuser_idот 1 до 200 ( каждый user_id повторяется 10 раз). Используйтеasyncio+aiohttpдля параллельной отправки (например, 10 concurrent workers).
import asyncio
import aiohttp
import random
BALANCER_URL = "http://localhost:8000/process"
USER_IDS = list(range(1, 201))
async def send_request(session, req_id, user_id):
async with session.get(f"{BALANCER_URL}/{req_id}?user_id={user_id}") as resp:
return await resp.json()
async def run_load(strategy):
async with aiohttp.ClientSession() as session:
tasks = []
for i in range(2000):
user_id = random.choice(USER_IDS)
tasks.append(send_request(session, i, user_id))
results = await asyncio.gather(*tasks)
return results
-
После завершения сбора результатов (для каждой стратегии отдельно) подсчитайте:
- Количество запросов, обработанных каждым агентом.
- Для consistent hashing: сколько раз один пользователь попал на одного и того же агента (степень привязки).
- Среднее время ответа на агента.
-
Постройте гистограммы распределения нагрузки (файл
distribution_rr.pngиdistribution_ch.png). Используйтеmatplotlib. -
Выведите в консль: разброс (max – min) по числу запросов, дисперсию.
Ожидаемый результат этапа Получены численные метрики и графики распределения. Для round-robin ожидается почти равномерное распределение (отклонение < 5%). Для consistent hashing — неравномерность может быть до 10-15% из-за случайности хэшей; вы можете уменьшить её, увеличив количество виртуальных нод.
Этап 5: Документирование и финальные проверки (оценка: 30 мин)
Действия
-
Напишите
README.mdс описанием:- Как запустить агентов и балансировщик.
- Как выбрать стратегию.
- Как запустить нагрузочный тест.
- Примеры команд.
- Выводы о равномерности распределения.
-
Убедитесь, что все файлы (agent.py, balancer.py, load_test.py, requirements.txt) лежат в репозитории.
-
Зафиксируйте результаты тестов в виде таблицы в README:
| Стратегия | Среднее на агента | Min | Max | Dispersion |
|---|---|---|---|---|
| Round-robin | 666.7 | 665 | 668 | 1.0 |
| Consistent hash | 666.7 | 600 | 733 | 1500 |
- Оформите репозиторий (добавьте .gitignore, лицензию MIT).
Ожидаемый результат этапа Готовый проект с документацией и измеримыми артефактами.
5. Критерии приемки (Definition of Done)
- Балансировщик запускается с выбором стратегии (
rrилиch) через переменную окружения. - Round-robin распределяет запросы строго по кругу; после 1000 запросов разница в количестве запросов на агентов не превышает 2.
- Consistent hashing: для одного
user_idв течение всей сессии возвращается один и тот же агент, если он не удалён. - При удалении одного агента consistent hashing перераспределяет только его запросы (влияние минимально).
- Скрипт
load_test.pyсобирает статистику и строит гистограмму. - В репозитории есть
README.mdс инструкцией запуска (не более 5 шагов) и таблицей результатов. - Код содержит комментарии и использует аннотации типов.
6. Ожидаемый результат
Основной артефакт Папка проекта agent_load_balancer/ с файлами:
agent.py— код агента-заглушки.balancer.py— балансировщик с обеими стратегиями.load_test.py— тест нагрузки с визуализацией.requirements.txt— зависимости.README.md— описание проекта, инструкция, таблица результатов.distribution_rr.png,distribution_ch.png— гистограммы распределения нагрузки.
Дополнительно (опционально):
docker-compose.ymlдля запуска трёх агентов и балансировщика.Makefileдля удобства запуска.
7. Возможные сложности и их решение
| Сложность | Решение |
|---|---|
| Consistent hashing даёт сильную неравномерность при малом количестве агентов (3) | Увеличить количество виртуальных нод (virtual_nodes = 300 или 500). Либо использовать библиотеку hash_ring. |
| Запросы с одного user_id попадают на разных агентов из-за race condition при переборе кольца | Убедиться, что метод get_agent — thread-safe (в асинхронном коде достаточно одного инстанса балансировщика). |
| При тестировании на локальной машине агенты могут не успевать отвечать | Уменьшить количество concurrent запросов (worker=5) или увеличить среднее время обработки, чтобы не было перегрузки. |
| Ошибки при парсинге URL (query string) | Использовать request.rel_url.query из aiohttp, а не парсить самостоятельно. |
8. Бюджет времени (оценка)
| Этап | Время |
|---|---|
| Этап 1: Подготовка окружения и заглушки | 30 мин |
| Этап 2: Реализация round-robin | 1 час |
| Этап 3: Реализация consistent hashing | 1.5 часа |
| Этап 4: Нагрузка и сбор метрик | 2 часа |
| Этап 5: Документирование | 30 мин |
| Итого | 5.5 часов |
Примечание Для первого раза рекомендуем заложить 6–7 часов, включая поиск информации о consistent hashing.
9. Связанные вопросы из базы знаний
| Вопрос | Тема |
|---|---|
| 12 | Основы балансировки нагрузки (round-robin, least connections) |
| 45 | Consistent hashing в распределённых кэшах |
| 78 | Виртуальные ноды и равномерность распределения |
| 112 | Асинхронное программирование на Python (asyncio) |
| 203 | Метрики нагрузки: QPS, среднее время отклика, дисперсия |
| 305 | Нагрузочное тестирование с locust и wrk |
| 412 | Прокси-серверы и reverse proxy (nginx, haproxy) |
| 567 | Мониторинг распределённых систем (Prometheus + Grafana) |
| 689 | Использование Docker-compose для микросервисов |
| 777 | Сравнение стратегий балансировки (random, power of two choices) |
10. Чек-лист самопроверки
- Я запустил трёх агентов и балансировщик без ошибок.
- Round-robin: после отправки 30 запросов с
user_id=1(все подряд) агенты выбрались A1, A2, A3, A1, ... (циклично). - Consistent hashing: для
user_id=42в пяти последовательных запросах возвращается один и тот же агент (если список агентов не менялся). - После удаления одного агента (порт не слушает) consistent hashing перенаправляет запросы с его user_id на следующий, а остальные остаются на месте.
- Скрипт
load_test.pyзавершается без исключений и генерирует два PNG-файла. - В README описана команда
STRATEGY=ch python balancer.pyдля выбора стратегии. - Все зависимости перечислены в
requirements.txt(aiohttp,matplotlib,pandas).