Эксплуатация Redis и карта выбора: когда он, а когда нет

Финал серии — про эксплуатацию Redis в бою и про трезвый выбор. Драйверы Go и Java и настройка пула соединений, что и как мониторить, типовые инциденты (latency-спайки, OOM, split-brain, шторм соединений) и их разбор. И главное — карта решений: когда Redis это правильный инструмент, а когда честнее взять полноценную БД, брокер или другое хранилище.

Знать внутренности Redis полезно, но в проде выигрывает тот, кто умеет его эксплуатировать: правильно настроить пул соединений, поймать деградацию по метрикам до того, как её заметят пользователи, и — что важнее всего — вовремя понять, что задача вообще не для Redis. Эта финальная статья серии «Redis: глубокое погружение» собирает предыдущие шесть в практическую рамку.

Сквозная нить серии — «Redis как кэш vs Redis как источник истины» — здесь сводится в явную карту выбора. Но начнём с трёх вещей, которые стенд этой серии обнаружил живьём и которые объединяет одно свойство: они врут молча.

Две половины. Слева тёмная диспетчерская с тремя ловушками. «а) Ретрай» — оператор довольно смотрит на зелёный экран «30», а за его спиной маскот в кепке RETRY выбрасывает пачку проваленных тикетов в урну под счётчиком «1660 / скрыто». «б) Restore» — работник с довольной улыбкой вставляет диск dump.rdb, горит зелёное «RESTORE OK», сейф за ним абсолютно пуст, рядом «DBSIZE = 0» и табличка «AOF включён — файл проигнорирован молча». «в) Latency history / slowlog» — клерк с журналом «по секундам» выбрасывает в урну листок «KEYS incident 38.6мс», а сосед держит журнал «по командам», где строка KEYS 38.6мс на месте. Справа солнечная развилка «карта решений»: указатель «восстановимо из источника → Redis» и указатель «потеряем деньги → нужен консенсус в фундаменте, не настройка Redis» к зданию ACID. Маскоты Redis и Valkey жмут руки под табличкой «по нагрузке различий не нашлось — но не идентичны»

В статье

Пул соединений: размер решает, и это измерено

Сценарий: 50 горутин одновременно пишут через клиент с пулом на 5, 20 и 50 соединений. Три отдельных клиента, не один растущий.

Сначала о двух вещах, без которых замер был бы пустым. Первая: в go-redis PoolSize — это только базовое число соединений; если их не хватает, пул растёт сверх него, пока не задан MaxActiveConns. Без явного ограничения все три «размера» были бы тремя одинаково безлимитными пулами, и ни один не дал бы ни единого таймаута. Вторая: ретраи выключены (MaxRetries: -1) — иначе клиент сам бы повторял команду после таймаута пула, и в таблице ниже стояли бы числа на порядок меньше реального контеншна. Почему именно на порядок — в следующем разделе. Обе вещи проверены на стенде, а не вычитаны.

Результаты по доле отказов (обе ориентации порядка × 3 повтора на образ, PoolTimeout 2 мс):

Образ Ориентация size=5 size=20 size=50
redis:8.8 прямая (5→20→50) 53.3 / 52.3 / 53.7% 4.9 / 3.3 / 8.2% 0.0% ×3
redis:8.8 обратная (50→20→5) 53.3 / 54.0 / 53.1% 5.1 / 3.9 / 5.0% 0.0% ×3
valkey:8.1 прямая (5→20→50) 52.7 / 54.0 / 53.9% 2.8 / 6.8 / 6.0% 0.0% ×3
valkey:8.1 обратная (50→20→5) 54.0 / 54.0 / 53.8% 3.0 / 3.9 / 3.3% 0.0% ×3

Сводно: 52.3–54.0% отказов на пуле в 5, 2.8–8.2% на пуле в 20, 0.0% на пуле в 50 — во всех 12 прогонах. Пул размером с конкурентность очереди не создаёт по построению.

Но процент отказов — целиком функция выбранного порога, и цитировать его без порога рядом нечестно. Тот же сценарий с разными значениями PoolTimeout:

PoolTimeout size=5 size=20 size=50
1 мс 73.2% 41.3% 0.0%
2 мс (взято для таблицы выше) 53.3% 6.7% 0.0%
5 мс 0.4% 0.2% 0.0%

Направление везде одно, но абсолютные проценты меняются в разы при сдвиге порога на пару миллисекунд. 2 мс здесь — не «типичный прод-таймаут», а значение, дающее содержательный контраст на быстром локальном Redis. В проде PoolTimeout обычно на порядки выше, и отказов при той же латентности почти не будет.

Ещё одна важная оговорка: абсолютные числа этого раздела — Docker Desktop на Windows, а не нативный Redis. На голом Linux по loopback round-trip — единицы-десятки микросекунд, здесь — единицы миллисекунд, то есть на два порядка больше. Надёжны только относительные сравнения внутри одного прогона.

Java даёт то же направление, но другие абсолюты — и это не вывод «Lettuce хуже go-redis». Зеркало на GenericObjectPool поверх Lettuce: ≈74% отказов на size=5, ≈42% на size=20, 0% на size=50. Сравнение не выровнено сразу по трём осям: Go-бинарник исполняется нативно на Windows-хосте, а Java — внутри WSL2, то есть по разному сетевому пути; алгоритмы ожидания в пулах разные; и на Java-стороне есть эффект порядка, которого у Go нет. Ни один из трёх вкладов стенд не изолирует.

Про эффект порядка стоит сказать отдельно, потому что он реально нашёлся. Армы идут последовательно в одном процессе, поэтому позиция в очереди — потенциальный конфаундер, и обе ориентации прогонялись именно ради его проверки. У Go порядок не значит ничего: size=5 даёт 52.3–53.7% первой армой и 53.1–54.0% последней, интервалы совпадают. А у Java значит, и заметно: на Redis size=5 даёт 78.0–78.6% первой армой и 70.2–70.3% последней — интервалы не пересекаются, разрыв около 8 процентных пунктов. Одна ориентация врала бы примерно на эту величину. Правдоподобный механизм — прогрев JVM, но стенд его не диагностировал: зафиксирован факт наличия эффекта порядка, а не объяснённый механизм.

А вот чего стенд честно не даёт — так это сравнимой латентности между размерами пула. Причина тонкая и стоит объяснения. Перцентили считаются только по успешным операциям, а на пуле в 5 «успешные» — это выжившая половина: те, кому соединение досталось быстро. Медленные попытки в выборку не попали, они стали таймаутами. На пуле в 50 в выборке все 100%. Это цензурирование, и оно систематически занижает перцентили тесных пулов. Насколько существенно — видно из собственной Java-армы того же стенда, где цензурирование сильнее: там p50 идёт в обратную сторону, 0.842 мс на size=5 против 1.659 мс на size=50. Тот же сценарий, противоположный порядок. Поэтому напрашивавшееся «без очереди успешные операции быстрее по построению» пришлось снять: это теоретическое рассуждение, поданное как измерение, и опровергается оно соседней армой того же стенда.

Что можно утверждать честно: доля отказов монотонно падает с ростом пула — устойчиво, в обеих ориентациях, на обоих образах, в обоих клиентах.

Ловушка в самом инструменте: клиент, который скрывает таймауты

Это первая из трёх ловушек, и она объясняет, почему наивная версия замера выше показала бы неправду.

По умолчанию go-redis сам повторяет команду после таймаута пула. Проверка живьём на пуле в одно соединение: внутренний счётчик клиента насчитал 1660 таймаутов ожидания за прогон, а до вызывающего кода в виде ошибки дошло только 30. Остальные 1630 ретрай тихо поглотил — ценой добавленной латентности, а не видимой ошибки.

Практический вывод: «число pool timeout ошибок» с дефолтными ретраями занижено на порядок и остроту контеншна не отражает. Если вы мониторите исчерпание пула по числу ошибок — вы видите верхушку.

Backup, который молча ничего не восстановил

Вторая ловушка, и самая неприятная, потому что обнаруживается в худший момент.

Цикл проверялся полностью: посеяли 6001 ключ, сняли живой снапшот через redis-cli --rdb, уничтожили volume, доказали, что данные реально исчезли (свежий инстанс: DBSIZE=0, точечный ключ отсутствует — иначе скрипт падает), восстановили и сверили.

redis:8.8 valkey/valkey:8.1
DBSIZE до backup 6001 6001
Размер снапшота 717372 байт 717373 байт
DBSIZE на доказательно пустом инстансе 0 0
Строка загрузки при restore Done loading RDB, keys loaded: 6001, keys expired: 0 то же
DBSIZE после restore 6001 6001
Содержимое точечного ключа побайтово совпало побайтово совпало

А теперь находка. Стандартная конфигурация поднимает сервер с --appendonly yes. Если положить dump.rdb в пустой volume и поднять сервер с этим дефолтом — сервер полностью игнорирует ваш dump.rdb: создаёт пустой AOF base file и стартует с DBSIZE=0. Без единой ошибки или предупреждения. В логе только BGSAVE done, 0 keys saved — сервер честно сохраняет пустое текущее состояние, а не ваше.

То есть план восстановления «положить dump.rdb в data dir и поднять сервис» молча не работает, если AOF включён конфигом по умолчанию. И это не бросается в глаза без явной проверки DBSIZE — процедура отработала, ошибок нет, данных нет.

Обход: поднимать сервер для загрузки с временно выключенным appendonly, либо убедиться, что каталога appendonlydir нет до первого старта. И проверять DBSIZE после восстановления — всегда.

Отдельная оговорка про сам снапшот: redis-cli --rdb — это снимок на момент завершения передачи, а не строго на момент вызова команды. В изолированном прогоне это не повлияло ни на что, но в общем случае «снапшот = момент T» — не гарантия.

LATENCY HISTORY — не журнал команд

Третья ловушка — в инструменте диагностики.

Проверка: latency-monitor-threshold в 1 мс, slowlog-log-slower-than в 1 мс, затем гарантированно медленная реальная операция — KEYS над 300000 ключами. (DEBUG SLEEP не подошёл: он заблокирован как protected config и неизменяем через CONFIG SET даже с правами по умолчанию — проверено живьём.)

redis:8.8 valkey/valkey:8.1
Клиентское время KEYS 65.386 мс 53.632 мс
SLOWLOG: серверное время KEYS 38.6 мс 25.3 мс
KEYS попал в SLOWLOG? да да
KEYS попал в LATENCY HISTORY command? НЕТ НЕТ

SLOWLOG поймал медленную команду, а LATENCY HISTORY — нет. Это не сбой стенда, а устройство latency-monitor: он хранит по событию максимум за секунду, а не каждое событие. KEYS (38.6 мс) исполнился в ту же секунду, что и посев данных (411 мс) — максимум забрал посев, и KEYS в историю не попал вообще. На Valkey видно ещё нагляднее: посев, KEYS и очистка уложились в одну секунду, и LATENCY HISTORY содержит одну-единственную запись — три команды, один максимум.

Вывод, который стоит унести: LATENCY HISTORY — не журнал команд. Медленная команда, попавшая в одну секунду с более медленной соседкой, в нём не появится, и её отсутствие там ничего не доказывает. Для вопроса «какая команда тормозила» есть SLOWLOG — у него отдельная запись на команду. LATENCY HISTORY отвечает на другой вопрос: «насколько плохо было в секунду N».

Заодно про сам KEYS: 38.6 мс серверного времени — это время, в течение которого сервер занят только им, потому что цикл событий один. Оговорка: конкурентных клиентов во время KEYS стенд не запускал, так что «все остальные ждали» здесь — известное свойство однопоточной модели, а не отдельно измеренный в этом прогоне факт. Измерено именно то, сколько сервер был занят.

Типовые инциденты серии

Собираем находки шести статей в набор того, что реально ломается.

Инцидент Симптом Что смотреть Откуда
Простой, невидимый для error rate 45 секунд без записи, при этом 1 ошибка на 279597 попыток — 0.0004%, любой порог по доле ошибок зелёный латентность хвоста, а не процент ошибок Cluster/Sentinel
Sentinel молча не сделал failover мастер мёртв, кворум жив, конфиг верен, переключения нет redis-cli -p 26379 info sentinel | grep tilt Cluster/Sentinel
Split-brain два мастера, подтверждённые записи не видны клиентам расхождение узлов по role, поведение при партиции Cluster/Sentinel
Внезапный отказ записи OOM command not allowed, чтения работают used_memory против maxmemory, evicted_keys память
Политика настроена, вытеснение не работает volatile-* без TTL вырождается в noeviction evicted_keys под нагрузкой, а не конфиг память
Ложная тревога по фрагментации mem_fragmentation_ratio 1.41, а фрагментации нет allocator_frag_ratio — если ≈1.00, аллокатор ни при чём память
Шторм соединений до 54% отказов на тесном пуле при PoolTimeout 2 мс (порог решает: при 5 мс — 0.4%), а с дефолтными ретраями видно на порядок меньше PoolStats().Timeouts клиента, не только ошибки эта статья
Restore прошёл, данных нет DBSIZE=0 после восстановления, ошибок ноль DBSIZE после каждого восстановления эта статья
Дубликаты после XCLAIM сообщение обработано дважды идемпотентность обработчика streams/Lua
Latency-спайк от одной команды сервер занят одной O(N)-операцией SLOWLOG (не LATENCY HISTORY) эта статья + event loop
Непонятно, кто нагружает инстанс CLIENT LIST полон, но name= пустое у всех, а addr за k8s или NAT не всегда указывает на под и уж тем более на процесс name= и user= вместо IP: ClientName в драйвере и отдельный ACL-пользователь эта статья

Общая мораль набора: почти все эти инциденты не кричат. Простой, невидимый для доли ошибок; failover без failover’а; восстановление без данных; вытеснение без вытеснения. Мониторинг, настроенный на пороги по процентам и на «упало/не упало», их не увидит.

Оговорка, которую здесь важно не смазать: 45-секундный простой всё-таки дал одну ошибку — детектор, срабатывающий на сам факт ошибки, его бы заметил. Проспал бы его монитор, считающий долю. А вот случай, когда клиент вообще не отдаёт ошибку и возвращает OK после многосекундной блокировки, на стенде поймался в сценарии Sentinel, а не Cluster. Это разные вещи, и путать их не стоит.

Логические базы: 16 — это не лимит, но и не изоляция

Регулярный вопрос при заезде нескольких сервисов на один Redis: «баз всего 16, как разделить?» В нём две ошибки сразу — и про число, и про слово «база».

Шестнадцать — это не потолок, а значение по умолчанию для обычного standalone-инстанса. Оно меняется в redis.conf:

databases 64

Параметр читается при старте, CONFIG SET его не меняет — нужен перезапуск. После него доступны базы 0–63, переключение обычное:

SELECT 17

Текущее значение всегда можно посмотреть:

CONFIG GET databases

Так что расширить можно далеко за 16. Проблема в другом: то, что Redis называет базами, — это пространства имён внутри одного инстанса, а не изолированные базы в смысле PostgreSQL. Всё, что серия разбирала как свойства инстанса, у них общее:

Что Общее на весь инстанс
Процесс и событийный цикл да — одна дорогая команда блокирует всех, независимо от номера базы
Память и maxmemory да — отдельного лимита на базу нет
Политика вытеснения да — maxmemory и выбор жертв относятся к инстансу, а не к базе; механика вытеснения
Персистентность RDB/AOF да — снимок и журнал общие, базы восстанавливаются вместе
Репликация и failover да — реплицируется инстанс целиком
Административные операции во многом да — например, FLUSHALL сносит все базы разом

Практическое следствие, которое ломает саму идею «разделили сервисы по базам»: давление памяти в одной базе выселяет ключи из другой. Если сервис в DB 5 залил данных до maxmemory, ключи сервиса в DB 0 начнут вытесняться — в зависимости от maxmemory-policy. Никакой границы, за которую отвечает номер базы, здесь нет. Оговорка про источник: это документированное устройство Redis (maxmemory — лимит инстанса, кандидаты на вытеснение выбираются по базам инстанса), а не результат стенда этой серии — стенд из статьи про вытеснение работал в пределах одной базы и cross-DB-эффект не проверял.

В Redis Cluster логических баз нет вовсе: поддерживается только DB 0, а команда SELECT в кластерном режиме не используется — она запрещена. Приложение должно исходить из DB 0 и разделять данные префиксами ключей. Это отдельная причина не строить схему на номерах баз: приложение, которое однажды может переехать в Cluster, придётся переписывать. Разбор самого Cluster — в статье про репликацию, Cluster и Sentinel.

Что делать вместо этого — три варианта, по убыванию связанности:

  1. Один инстанс, разные префиксы ключей — если совместное потребление памяти и одного событийного цикла допустимо. Это же единственный вариант, совместимый с Cluster:

    auth:session:123
       billing:invoice:456
       rate-limit:user:789
  2. Разные инстансы (или контейнеры) — если сервисам нужны разные TTL-политики, лимиты памяти, политики вытеснения, режимы персистентности или независимое обслуживание. Это единственный способ получить настоящую изоляцию ресурсов и отказов.

  3. Разные логические базы — только как слабое логическое разделение внутри одного приложения (например, отделить кэш от временных структур), когда общие память, персистентность и вытеснение — осознанно приемлемы.

Права доступа при этом удобнее строить не по базам, а по шаблонам ключей — ACL это умеет:

ACL SETUSER service-a on >password ~service-a:* -@all +get +set +del +expire

Порядок правил здесь не косметика: они применяются слева направо, поэтому -@all идёт первым и обнуляет права, а нужные команды разрешаются после него. Соблазнительное +@all вместо этого списка выглядит эквивалентно, но им не является: шаблон ~service-a:* ограничивает только команды, у которых есть аргумент-ключ, и на FLUSHALL, CONFIG или SHUTDOWN не распространяется вообще — у них ключей нет. То есть пользователь с ~service-a:* +@all спокойно снесёт весь инстанс. Плюс +@all автоматически включает команды, которых на момент настройки ещё не существует, — из будущих версий и загруженных модулей.

Короткий вывод: число баз расширяется тривиально, но изоляции это не даёт. Если нужна именно изоляция — это разные инстансы; если достаточно порядка в ключах — это префиксы.

Кто подключён: CLIENT LIST и почему name= пустое у всех

Разделили сервисы префиксами и нарезали ACL — и почти сразу упираетесь в вопрос, с которого начинается разбор любого инцидента на общем инстансе: кто в нём сейчас сидит. Встроенный ответ ровно один — CLIENT LIST, по строке на соединение:

id=42679 addr=10.0.4.19:49366 laddr=10.0.1.7:6379 fd=20 name= age=26739 idle=26738
flags=N db=7 cmd=get user=default resp=3 lib-name=go-redis(,go1.25.12) lib-ver=9.18.0

На вопрос «кто это» работают пять полей, и у каждого своя граница:

Поле Что говорит Граница
addr адрес и порт того, с чего пришло соединение это не обязательно адрес процесса: за NAT, Service или частью CNI-настроек виден адрес шлюза или узла
laddr адрес сервера, на который пришло соединение про клиента не говорит ничего
name имя, выставленное самим клиентом по умолчанию пустое; никак не подтверждается
user аутентифицированный ACL-пользователь по умолчанию у всех default
lib-name / lib-ver библиотека и её версия язык и драйвер, но не приложение

Плюс age и idle — возраст соединения и простой в секундах: пара, по которой видно залипшие коннекты, живущие сутки без единой команды.

Первая ловушка здесь — не в Redis, а в разборе его вывода, и она тихая. Имя поля addr является подстрокой laddr. Любая нежадно не закреплённая регулярка ловит второе вместо первого:

redis-cli CLIENT LIST | sed -E 's/.*addr=([0-9.]+):[0-9]+.*/\1/'

Результат выглядит абсолютно правдоподобно — список IP, в котором просто нет ничего подозрительного. Только это адреса самого сервера: на каждой строке подставился laddr. Инстанс, к которому ходят полсети, выглядит как инстанс, к которому ходит он сам. Ошибки нет, вывод есть, вывод неверный. Закрепление на начало строки лечит полностью:

redis-cli CLIENT LIST | sed -E 's/^id=[0-9]+ addr=([0-9.]+):[0-9]+ .*/\1/'

Оговорка про природу этого пункта: это детерминированное свойство формата, видное прямо из списка полей, а не находка стенда. Сама документация предупреждает о соседнем риске — набор полей расширяется от версии к версии (watch появился в 7.4, io-thread в 8.0), и парсер обязан переживать незнакомые и отсутствующие поля.

Вторая граница принципиальнее и регуляркой не лечится. Что именно окажется в addr у клиента из Kubernetes, зависит от сетевого пути: это может быть pod IP при маршрутизируемой сети, адрес узла при SNAT на выходе из кластера, адрес Service или вовсе внешнего шлюза. Универсального ответа нет, и в этом вся проблема — по одному IP надёжно выйти на приложение нельзя ни в одном из этих случаев. Худший вариант вполне обычен: несколько узлов дают несколько адресов на десятки приложений, и CLIENT LIST честно покажет ровно эти адреса. Идентифицировать себя должен клиент.

Способов ровно три, и путать их не стоит — они дают разную степень доверия:

Механизм Поле Кто выставляет Можно ли выдать себя за другого
CLIENT SETNAME name приложение да — это просто ярлык
CLIENT SETINFO (7.2+) lib-name, lib-ver драйвер, автоматически да
ACL-пользователь user сервер, по факту аутентификации нет

Отсюда практический вывод: name отвечает на вопрос «что это за процесс» для диагностики, а user — на вопрос «кто это на самом деле» для разграничения прав. Первое удобно, второе доказуемо.

Ключевая тонкость SETNAME — имя живёт на соединении, а не на клиенте. Выставить его вручную после подключения бессмысленно: в пуле имя получит ровно одно соединение из десятков, а после переподключения исчезнет и оно. Нужна опция драйвера, которая шлёт команду на каждом новом коннекте, — и здесь три языка серии ведут себя по-разному:

  • Go, go-redis (v9.21.0) — поле ClientName в Options, с прямым описанием в исходнике: «will execute the CLIENT SETNAME ClientName command for each conn». Задаётся и через URL-параметр client_name. Рядом IdentitySuffix (суффикс к имени) и DisableIdentity, отключающий CLIENT SETINFO.
  • Java, Lettuce (7.6.0.RELEASE)clientName в RedisURI (параметр URI или withClientName()), плюс отдельные libraryName и libraryVersion.
  • Rust, redis-rs (1.4.1) — а вот здесь опции нет. В RedisConnectionInfo есть lib_name, lib_ver и skip_set_lib_name, то есть CLIENT SETINFO настраивается, а вот поля под имя соединения не существует: CLIENT SETNAME реализован как обычная команда, и вызывать её нужно самому — в пуле это хук после установки соединения, а не разовый вызов при старте.

Версии здесь названы не для порядка: это состояние на тех релизах, что зафиксированы в стендах серии, и по master оно меняется — состав полей драйверов стоит перепроверять под свою версию.

rdb := redis.NewClient(&redis.Options{
    Addr:       "redis.internal:6379",
    DB:         5,
    ClientName: "billing-api",
})

После этого в CLIENT LIST появится name=billing-api — и вопрос «чей это коннект» закрывается без опроса разработчиков. Но поле user при этом останется default: ClientName шлёт только CLIENT SETNAME и к аутентификации отношения не имеет. Чтобы заполнилось и оно, клиент должен логиниться отдельным ACL-пользователем, то есть к тем же опциям добавляются Username и Password. Это ровно та развилка, о которой шла речь выше: имя вы себе назначаете сами, пользователя вам подтверждает сервер.

Последнее, о чём стоит помнить перед тем, как звать CLIENT LIST на нагруженном проде: у команды сложность O(N) по числу соединений, и выполняется она в том же единственном событийном цикле, что и весь трафик, — механика подробно в статье про event loop. На инстансе с десятками тысяч клиентов это не бесплатная диагностика. Сузить выборку помогают фильтры CLIENT LIST TYPE normal|master|replica|pubsub и CLIENT LIST ID <id>, а разорвать конкретное соединение — CLIENT KILL. Оговорка: сложность здесь взята из документации команды, отдельным замером стенда серии она не проверялась.

Карта выбора: Redis или не Redis

Теперь можно собрать нить серии в решение.

Redis почти вне конкуренции:

  • Кэш. Данные восстановимы из источника, потеря приемлема по определению, приоритет — скорость. Здесь всё, что серия разбирала как «границы», перестаёт быть проблемой: вытеснение — благо, асинхронная репликация — нормально, rdb-only или вовсе без персистентности — осознанный выбор.
  • Счётчики, rate limiting, лидерборды. Атомарные операции над структурами, измеренные 9000 сохранённых инкрементов против гонки — ровно про это.
  • Лёгкая шина, очередь задач. Streams с consumer groups, если объём укладывается в память, а история не нужна дольше дней.
  • Session store — с осознанным пониманием окна потери.

Где честнее взять другое:

  • Источник истины для денег и учёта. AOF everysec теряет до секунды при падении ОС или питания; под kill -9 процесса, как показал стенд, не теряется ничего ни в одном AOF-режиме, но это свойство краха процесса, а не durability-гарантия. Добавление реплик границу не убирает, а сдвигает: репликация асинхронна, WAIT сужает окно, но не закрывает, а split-brain реален и измерен. Если каждая подтверждённая запись обязана пережить любой отказ — это требование к системе с консенсусом в фундаменте.
  • Строгие транзакции. MULTI/EXEC — не то же самое, что ACID-транзакции; подробности в отдельной статье про транзакции.
  • Крупный датасет как первичное хранилище. Память дороже диска, и упор в maxmemory приходит молча и внезапно.
  • Аналитика. Не та модель данных и не та нагрузка.
  • Поток сообщений как источник истины на недели. Партиционирование под масштаб, долгий ретеншен, exactly-once — задачи брокерной платформы; Streams их не решают по конструкции.

Практический критерий, к которому сводится вся серия: спросите, что произойдёт, если конкретная подтверждённая запись исчезнет. Если ответ «перечитаем из источника» — Redis. Если ответ «потеряем деньги или доверие» — вопрос не в настройке Redis, а в том, тот ли это инструмент.

Что дальше

Серия закончена. Если читать её целиком: модель данных и кодировкисобытийный циклперсистентностьрепликация, Cluster, Sentinelпамять и вытеснениеstreams и Lua → эта статья.

Смежное: клиенты на Go, Java и Rust — паттерны устойчивости, переподключение и работа с failover со стороны приложения; введение, паттерны и антипаттерны — если нужен вход попроще; Valkey после форкаСкоро — лицензии, governance и что из этого следует при выборе; гео-поискСкоро — где Redis GEO на карте вариантов.

Про Redis и Valkey по итогам всей серии — аккуратно, потому что соблазн подвести красивую черту большой. Ни одно расхождение по производительности и поведению под нагрузкой проверки не пережило: каждое такое кажущееся различие при перепрогоне оказывалось либо разной загрузкой хоста, либо малой выборкой, либо разными сетевыми путями. Именно это — практический вывод про пару: на разобранных здесь механизмах они работают одинаково, и клиентский код их не различает.

Но «одинаково» — не «идентично», и две мелочи устояли. У Valkey в INFO sentinel нет счётчика sentinel_total_tilt, который есть у Redis, — детерминированное различие API, к загрузке хоста отношения не имеющее; если мониторинг считает накопленные входы в TILT, на Valkey метрики не будет. И точка отказа под noeviction у образов разная — диапазоны не пересеклись, направление подтвердилось всеми прогонами, а причина сдвига осталась неустановленной. Обе мелочи не меняют выбора между движками, но честнее их назвать, чем подвести под общую черту.

Источники

Обсуждение в Telegram

Присоединиться →

Комментарии