Это небольшой стенд распределенной системы. Пользователь работает только с gateway: создает задачу через HTTP API, получает TCP-порт и подключается к нему. Сама задача запускается не на gateway, а в отдельном контейнере на одном из worker-узлов.
Проще говоря: gateway принимает запрос, выбирает свободный worker, worker поднимает контейнер с вычислителем, а gateway прокидывает TCP-трафик от пользователя до этого контейнера.
В системе есть несколько частей:
gateway- единая точка входа. Хранит список workers и jobs, выбирает worker, выделяет порт и запускает TCP proxy.worker- внутренний сервис, который запускает и удаляет контейнеры с задачами через Docker.job-runner- контейнер с простой TCP-программой. Он отвечает на командыping,stats,load,stop,help.frontend- простой веб-интерфейс для просмотра состояния системы.PrometheusиGrafana- мониторинг gateway, workers и активных jobs.
Пользователь не ходит напрямую в worker и job-runner. Снаружи видны только gateway API, frontend, мониторинг и диапазон TCP-портов для jobs.
- Пользователь отправляет
POST /api/jobsна gateway. - Gateway смотрит зарегистрированные workers.
- Выбирается worker с наименьшей загрузкой.
- Gateway резервирует свободный порт из диапазона
5000-5099. - Worker запускает отдельный контейнер
job-runner. - Gateway запускает TCP proxy с выделенного порта на контейнер задачи.
- Пользователь подключается к этому порту через
ncили другой TCP-клиент.
Пример пути для одной задачи:
client -> gateway:5000 -> TCP proxy -> job-runner:7001
На уровне TCP это два отдельных соединения: клиент соединяется с gateway, а gateway отдельно соединяется с job-runner.
Проверка сокетов показывает ту же картину:
Workers сами регистрируются на gateway после запуска:
POST /internal/workers/register
Потом они регулярно отправляют heartbeat:
POST /internal/workers/heartbeat
В heartbeat worker передает свой worker_id, capacity и текущее количество активных jobs. Если heartbeat долго не приходит, gateway считает worker недоступным и больше не отправляет на него новые задачи.
Основные пользовательские endpoints:
POST /api/jobs
GET /api/jobs
GET /api/jobs/{job_id}
DELETE /api/jobs/{job_id}
GET /api/workers
GET /api/system/status
GET /health
GET /ready
GET /metrics
После создания job gateway возвращает примерно такой ответ:
{
"job_id": "job-8f34f05916",
"status": "running",
"worker_id": "worker-2",
"port": 5001,
"protocol": "tcp",
"connect": "tcp://localhost:5001"
}Команды внутри TCP-сессии:
ping
stats
load 1
help
stop
.
├── docker-compose.yml
├── Makefile
├── gateway/
├── worker/
├── job-runner/
├── frontend/
├── monitoring/
├── load-tests/
├── media/
└── report/
Нужны Docker, Docker Compose и Python 3.
Собрать образы:
docker compose buildЗапустить проект:
docker compose up -dПроверить контейнеры:
docker compose psЛокальные адреса:
Gateway API: http://localhost:18080
Frontend: http://localhost:8081
Prometheus: http://localhost:9090
Grafana: http://localhost:3000
Job ports: localhost:5000-5099
Проверить gateway:
curl http://localhost:18080/health
curl http://localhost:18080/ready
curl http://localhost:18080/api/workersСоздать задачу:
curl -X POST http://localhost:18080/api/jobsПодключиться к задаче по TCP. Порт нужно взять из ответа gateway:
nc localhost 5000Внутри соединения можно отправить:
ping
stats
load 1
help
Удалить задачу:
curl -X DELETE http://localhost:18080/api/jobs/JOB_IDПоставить зависимости для тестовых скриптов:
python3 -m venv .venv
source .venv/bin/activate
pip install -r load-tests/requirements.txtСоздать много jobs и посмотреть распределение по workers:
python3 load-tests/create_jobs.py --gateway http://localhost:18080 --count 30 --concurrency 5 --deleteСоздать одну job и проверить TCP-команды:
python3 load-tests/tcp_test.py --gateway http://localhost:18080 --host localhostТо же самое можно запустить через Makefile:
make test-load
make tcp-testОстановить сервисы:
docker compose down --remove-orphansОстановить сервисы и удалить сохраненное состояние gateway:
docker compose down -v --remove-orphansЕсли после ручных проверок остались контейнеры jobs:
docker rm -f $(docker ps -aq --filter label=distributed-computing-platform.job=true)

