Выполнение приложения в виде stateless процессов без общего состояния
Выполняйте приложение в виде stateless процессов без общего состояния
Processes — шестой фактор 12-Factor App. Принцип гласит:
Приложение должно выполняться в виде одного или более stateless процессов. Процессы не хранят состояние между запросами — данные сохраняются во внешних сервисах (БД, кэш).
# ❌ Неправильно: состояние в памяти процесса
logged_in_users = set() # ❌ Потеряется при перезапуске
@app.route('/login')
def login():
logged_in_users.add(user_id)
return 'Logged in'
# ✅ Правильно: состояние во внешнем сервисе (Redis)
import redis
redis_client = redis.from_url(os.environ['REDIS_URL'])
@app.route('/login')
def login():
redis_client.set(f'session:{user_id}', 'active')
return 'Logged in'Stateless процесс — это процесс, который:
┌─────────────────────────────────────────────────────────┐
│ STATEFUL ПРОЦЕСС │
│ │
│ Запрос 1 → [Обработка] → Сохранить в памяти → │
│ Запрос 2 → [Чтение из памяти] → Ответ │
│ │
│ ❌ При перезапуске всё теряется │
│ ❌ Нельзя масштабировать горизонтально │
└─────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────┐
│ STATELESS ПРОЦЕСС │
│ │
│ Запрос 1 → [Обработка] → Сохранить в Redis/БД → │
│ Запрос 2 → [Чтение из Redis/БД] → Ответ │
│ │
│ ✅ Перезапуск не влияет на данные │
│ ✅ Можно запустить 100 экземпляров │
└─────────────────────────────────────────────────────────┘
# ❌ Неправильно: сессии в памяти процесса
sessions = {} # Словарь в памяти
@app.route('/login')
def login():
session_id = generate_session()
sessions[session_id] = {'user_id': user_id, 'expires': ...}
return session_id
# Проблема: при перезапуске все сессии теряются
# Проблема: при масштабировании каждый процесс имеет свои сессии# ✅ Правильно: сессии в Redis
import redis
redis_client = redis.from_url(os.environ['REDIS_URL'])
@app.route('/login')
def login():
session_id = generate_session()
redis_client.setex(
f'session:{session_id}',
3600, # TTL 1 час
json.dumps({'user_id': user_id})
)
return session_id
# Преимущество: сессии переживают перезапуск
# Преимущество: все процессы видят одни сессии# ❌ Неправильно: файлы на локальном диске
@app.route('/upload', methods=['POST'])
def upload():
file = request.files['file']
file.save(f'/tmp/uploads/{file.filename}') # ❌
return 'Uploaded'
# Проблема: файлы теряются при перезапуске контейнера
# Проблема: файлы не доступны другим процессам# ✅ Правильно: объектное хранилище (S3)
import boto3
s3 = boto3.client('s3')
@app.route('/upload', methods=['POST'])
def upload():
file = request.files['file']
s3.upload_fileobj(
file,
os.environ['S3_BUCKET'],
file.filename
)
return 'Uploaded'
# Преимущество: файлы сохраняются после перезапуска
# Преимущество: доступны всем процессам# ❌ Неправильно: кэш в памяти процесса
cache = {}
def get_expensive_data(key):
if key in cache:
return cache[key]
data = compute_expensive()
cache[key] = data
return data
# Проблема: кэш не переживает перезапуск
# Проблема: каждый процесс имеет свой кэш (неэффективно)# ✅ Правильно: кэш в Redis
import redis
redis_client = redis.from_url(os.environ['REDIS_URL'])
def get_expensive_data(key):
cached = redis_client.get(f'cache:{key}')
if cached:
return cached
data = compute_expensive()
redis_client.setex(f'cache:{key}', 300, data) # TTL 5 минут
return data
# Преимущество: общий кэш для всех процессов
# Преимущество: кэш переживает перезапуск процесса# Один процесс обрабатывает 100 запросов в секунду
kubectl scale deployment myapp --replicas=1
# Десять процессов обрабатывают 1000 запросов в секунду
kubectl scale deployment myapp --replicas=10
# Сто процессов обрабатывают 10000 запросов в секунду
kubectl scale deployment myapp --replicas=100Важно: это работает только если процессы stateless. Если сессии в памяти — каждый процесс будет иметь свои сессии, и пользователи будут терять сессию при переключении между процессами.
┌─────────────┐
│ Load │
│ Balancer │
└──────┬──────┘
│
┌─────────────────┼─────────────────┐
│ │ │
▼ ▼ ▼
┌───────────┐ ┌───────────┐ ┌───────────┐
│ Process 1 │ │ Process 2 │ │ Process 3 │
│ Stateless │ │ Stateless │ │ Stateless │
└─────┬─────┘ └─────┬─────┘ └─────┬─────┘
│ │ │
└─────────────────┼─────────────────┘
│
▼
┌─────────────┐
│ Redis │
│ (State) │
└─────────────┘
Все процессы обращаются к общему хранилищу состояния (Redis, БД).
Обработка HTTP-запросов:
# Gunicorn для Python
gunicorn app:app -w 4 -b 0.0.0.0:5000
# Node.js
node server.js
# Go
./myappОбработка фоновых задач:
# worker.py
from celery import Celery
app = Celery('worker', broker=os.environ['REDIS_URL'])
@app.task
def send_email(to, subject, body):
# Отправка email
pass
# Запуск воркера
celery -A worker worker --loglevel=info# scheduler.py
from apscheduler.schedulers.blocking import BlockingScheduler
scheduler = BlockingScheduler()
@scheduler.scheduled_job('cron', hour=0)
def daily_cleanup():
# Ежедневная очистка
pass
scheduler.start()# docker-compose.yml
version: '3.8'
services:
web:
build: .
command: gunicorn app:app -b 0.0.0.0:5000
replicas: 4 # 4 веб-процесса
worker:
build: .
command: celery -A tasks worker
replicas: 2 # 2 воркера
scheduler:
build: .
command: python scheduler.py
replicas: 1 # 1 планировщик# ❌ Неправильно
class ShoppingCart:
_carts = {} # Класс-уровень, память процесса
def add_item(self, user_id, item):
if user_id not in self._carts:
self._carts[user_id] = []
self._carts[user_id].append(item)Проблема: при перезапуске все корзины теряются.
Правильно:
# ✅ Правильно: состояние в Redis
import redis
class ShoppingCart:
def __init__(self):
self.redis = redis.from_url(os.environ['REDIS_URL'])
def add_item(self, user_id, item):
self.redis.rpush(f'cart:{user_id}', json.dumps(item))# ❌ Неправильно
@app.route('/export')
def export():
filename = f'/tmp/export_{user_id}.csv'
# Генерация файла
return send_file(filename)Проблема: файл не доступен другим процессам, теряется при перезапуске.
Правильно:
# ✅ Правильно: временные файлы в S3
@app.route('/export')
def export():
filename = f'exports/export_{user_id}.csv'
# Генерация и загрузка в S3
s3.upload_fileobj(buffer, os.environ['S3_BUCKET'], filename)
return redirect(f'https://s3.amazonaws.com/{filename}')# ❌ Неправильно: привязка сессии к процессу
upstream backend {
ip_hash; # Sticky sessions
server backend1:5000;
server backend2:5000;
}Проблема: sticky sessions маскируют проблему stateful процессов, но не решают её. При падении процесса сессия всё равно теряется.
Правильно: вынести сессии во внешний Redis.
# ❌ Неправильно: нет обработки SIGTERM
while True:
process_request()При получении SIGTERM процесс завершается немедленно, текущие запросы обрываются.
# ✅ Правильно: graceful shutdown
import signal
import sys
import time
shutdown = False
def handle_sigterm(signum, frame):
global shutdown
print('Received SIGTERM, shutting down gracefully...')
shutdown = True
signal.signal(signal.SIGTERM, handle_sigterm)
# Обработка текущих запросов
while not shutdown:
process_request()
# Завершение текущих операций
cleanup()
sys.exit(0)# gunicorn.conf.py
def child_exit(server, worker):
# Очистка ресурсов при завершении воркера
pass
def worker_abort(worker):
# Обработка прерывания
pass# deployment.yaml
spec:
containers:
- name: web
image: myapp:latest
lifecycle:
preStop:
exec:
command: ["/bin/sh", "-c", "sleep 10"]
# Даёт время завершить текущие запросыЗадайте себе вопросы:
# ❌ Неправильно
class Application:
def __init__(self):
self.users = {} # Пользователи в памяти
self.cache = {} # Кэш в памяти
self.uploads = '/tmp/uploads' # Локальные файлы
def login(self, user_id):
self.users[user_id] = {'logged_in': True}
def get_cached(self, key):
return self.cache.get(key)Проблемы:
# ✅ Правильно
import redis
import boto3
class Application:
def __init__(self):
self.redis = redis.from_url(os.environ['REDIS_URL'])
self.s3 = boto3.client('s3')
def login(self, user_id):
self.redis.set(f'user:{user_id}:logged_in', 'true', ex=3600)
def get_cached(self, key):
return self.redis.get(f'cache:{key}')
def save_upload(self, user_id, file):
self.s3.upload_fileobj(
file,
os.environ['S3_BUCKET'],
f'uploads/{user_id}/{file.filename}'
)Результат:
Ключевой вывод: Процессы должны быть stateless — не хранить данные в памяти. Состояние (сессии, файлы, кэш) должно храниться во внешних сервисах (БД, Redis, S3). Это позволяет масштабировать приложение горизонтально и перезапускать процессы без потери данных.
Вопросы ещё не добавлены
Вопросы для этой подтемы ещё не добавлены.