Redis в контейнере
Redis — это in-memory key-value хранилище, написанное на C, single-threaded для команд, безумно быстрое (миллион ops/sec на одном ядре). Внешне выглядит как HashMap, доступный по сети, с поддержкой строк, списков, sets, hashes, sorted sets, streams. Внутри — событийный цикл на epoll/kqueue, всё в RAM, периодическая сериализация на диск.
Для Junior DE Redis встречается в трёх ролях: брокер очереди Celery (Airflow, Prefect используют Celery executor), кэш для метаданных pipeline (Schema registry, feature store), rate limiter (когда пишем DAG, которые дёргают внешний API с лимитами).
Базовый запуск
Официальный образ — redis. Стабильные теги: redis:7, redis:7.4, redis:7-alpine. Между debian и alpine разница в размере: redis:7 около 130 МБ, redis:7-alpine около 50 МБ. Для Redis alpine — отличный выбор, никаких extensions с native-кодом тут нет, проблем не будет.
Минимальный запуск:
docker run -d \
--name redis \
-p 6379:6379 \
redis:7-alpine
И всё. Redis уже работает, слушает порт 6379, никакого пароля. Проверим через redis-cli внутри контейнера:
docker exec -it redis redis-cli
127.0.0.1:6379> PING
PONG
127.0.0.1:6379> SET hello "world"
OK
127.0.0.1:6379> GET hello
"world"
127.0.0.1:6379> exit
С хоста — тоже работает, если установлен redis-cli (brew install redis на macOS):
redis-cli -h localhost -p 6379 PING
# PONG
В дефолтной конфигурации Redis запускается без пароля и слушает все интерфейсы внутри контейнера. Если ты сделаешь -p 6379:6379 на сервере с публичным IP — это открытый Redis в интернет. До 2017 года в интернете были тысячи открытых Redis-серверов с данными ботнетов и криптомайнерами в redis-cli config set dir /var/spool/cron && config set dbfilename root && save. Сейчас Redis 7+ добавил protected mode по умолчанию (отказывается работать с внешних адресов без пароля), но не полагайся на это — всегда ставь пароль.
Пароль через requirepass
Самый простой способ — передать команду:
docker run -d \
--name redis \
-p 6379:6379 \
redis:7-alpine \
redis-server --requirepass "verystrongpassword"
Команда после имени образа заменяет CMD из Dockerfile. По умолчанию там redis-server, мы передаём redis-server --requirepass "verystrongpassword". Теперь:
docker exec -it redis redis-cli
127.0.0.1:6379> PING
(error) NOAUTH Authentication required.
127.0.0.1:6379> AUTH verystrongpassword
OK
127.0.0.1:6379> PING
PONG
Или сразу с паролем:
docker exec -it redis redis-cli -a verystrongpassword
127.0.0.1:6379> PING
PONG
В URI пароль кодируется так: redis://:verystrongpassword@localhost:6379/0 (нет username, только пароль, поэтому двоеточие перед паролем).
AOF vs RDB persistence
По умолчанию Redis 7 пишет на диск в режиме RDB — snapshot всей базы в файл /data/dump.rdb раз в N секунд (если изменилось M ключей). Это компромисс: быстро, маленький файл, но при креше теряешь всё, что было между snapshot’ами.
AOF (Append Only File) — каждая write-команда дописывается в файл /data/appendonly.aof. При рестарте Redis проигрывает AOF и восстанавливает состояние. Минусы: больше места на диске (10-20x относительно RDB), медленнее запись (хотя fsync можно настроить). Плюсы: при креше теряешь максимум 1 секунду данных (или 0, если fsync=always, но это медленно).
Для большинства DE-задач (кэш, очередь Celery) данные не критичны — можно дефолтный RDB. Если Redis — единственное хранилище (например, rate limiter в проде, и потеря данных = race condition), включай AOF.
Включение AOF:
docker run -d \
--name redis \
-p 6379:6379 \
-v redis-data:/data \
redis:7-alpine \
redis-server --requirepass "pass" --appendonly yes
Volume redis-data:/data нужен независимо от RDB/AOF — без него persistence бессмысленна, файлы умрут с контейнером.
Что внутри /data
Заглянем в data dir:
docker exec -it redis ls -la /data
Без AOF:
total 12
drwxr-xr-x 2 redis redis 4096 May 15 09:42 .
drwxr-xr-x 1 root root 4096 May 15 09:40 ..
-rw-r--r-- 1 redis redis 93 May 15 09:42 dump.rdb
С AOF (после --appendonly yes):
total 16
drwxr-xr-x 3 redis redis 4096 May 15 09:45 .
drwxr-xr-x 1 root root 4096 May 15 09:40 ..
-rw-r--r-- 1 redis redis 93 May 15 09:42 dump.rdb
drwxr-xr-x 2 redis redis 4096 May 15 09:45 appendonlydir
В Redis 7 AOF переехал в директорию appendonlydir/ (multi-part AOF): там лежат appendonly.aof.1.base.rdb, appendonly.aof.1.incr.aof, appendonly.aof.manifest. Раньше был один файл — теперь base + incr, чтобы упростить rewrite.
Типичные use cases для DE
1. Rate limiter
DAG’и часто дёргают внешние API с rate limits (1000 запросов в минуту на ключ). Redis-based rate limiter — стандарт.
import redis
import time
r = redis.Redis(host="localhost", port=6379, password="pass", decode_responses=True)
def allow_request(api_key: str, limit: int = 1000, window: int = 60) -> bool:
key = f"ratelimit:{api_key}:{int(time.time()) // window}"
count = r.incr(key)
if count == 1:
r.expire(key, window)
return count <= limit
Каждый вызов делает атомарный INCR, который возвращает новое значение. Если первый раз — ставим TTL. Если превысили — отказываем. Простота на 5 строк, скорость 200k проверок/сек на одном ядре.
2. Кэш метаданных
Airflow DAG, который для каждого row дёргает Schema Registry — медленно. С Redis-кэшем:
def get_schema(table: str) -> dict:
cached = r.get(f"schema:{table}")
if cached:
return json.loads(cached)
schema = fetch_from_registry(table)
r.setex(f"schema:{table}", 300, json.dumps(schema)) # 5 минут TTL
return schema
3. Брокер Celery
Airflow с CeleryExecutor использует Redis или RabbitMQ как брокер очереди. Конфиг:
broker_url = "redis://:pass@redis:6379/0"
result_backend = "redis://:pass@redis:6379/1"
DB 0 для очереди, DB 1 для результатов. Redis даёт 16 виртуальных DB по умолчанию — это просто префиксы ключей, изоляции на уровне памяти нет.
Compose-пример с Redis
services:
redis:
image: redis:7-alpine
command: redis-server --requirepass "${REDIS_PASSWORD}" --appendonly yes
ports:
- "6379:6379"
volumes:
- redis-data:/data
healthcheck:
test: ["CMD", "redis-cli", "-a", "$$REDIS_PASSWORD", "PING"]
interval: 5s
timeout: 3s
retries: 5
app:
image: my-etl
depends_on:
redis:
condition: service_healthy
environment:
REDIS_URL: "redis://:${REDIS_PASSWORD}@redis:6379/0"
volumes:
redis-data:
Обрати внимание на $$REDIS_PASSWORD в healthcheck — двойной знак доллара экранирует переменную, чтобы compose не подставлял её на этапе парсинга, а оставил в команде для shell внутри контейнера.
Память и eviction policy
Что происходит, когда процесс исчерпывает RAMRedis — in-memory, и это значит, что когда RAM кончится — он либо упадёт, либо начнёт удалять ключи. По умолчанию maxmemory не задан — Redis ест столько, сколько есть. Для кэша всегда задавай лимит и eviction policy:
docker run -d \
--name redis \
-p 6379:6379 \
redis:7-alpine \
redis-server \
--maxmemory 512mb \
--maxmemory-policy allkeys-lru
allkeys-lru = когда дойдёт до 512 МБ, удаляй наименее недавно использованный ключ. Альтернативы:
noeviction— отказывать в write-командах (дефолт). Для кэша не подходит.allkeys-lru— LRU на всех ключах. Стандарт для кэша.volatile-lru— LRU только на ключах с TTL. Для смешанных use cases (кэш + persistent).allkeys-lfu— LFU (Least Frequently Used). Для очень неравномерных нагрузок.
Попробуй сам
- Запусти Redis с паролем, AOF и лимитом памяти:
docker run -d --name redis \ -p 6379:6379 \ -v lab-redis-data:/data \ redis:7-alpine \ redis-server --requirepass labpass --appendonly yes --maxmemory 64mb --maxmemory-policy allkeys-lru - Подключись через
docker exec -it redis redis-cli -a labpass. Положи 100 ключей:MSET k1 v1 k2 v2 ...(или цикл из bash). - Перезапусти контейнер (
docker restart redis), проверь, что ключи на месте — это работает потому что AOF восстанавливает состояние из/data/appendonlydir/. - Сделай
INFO memory, посмотриused_memory_humanиmaxmemory_human. - Из Python (
pip install redis) подключись и сделай 10000 SET-операций в цикле. Замерь время. Должно быть около 0.5 секунды (50k ops/sec в неоптимизированном клиенте).