Персистентность в Redis
db redisВ Redis есть два вида персистентного хранения данных:
- RDB
- AOF
Разберём каждый из них.
RDB
Суть режима проста - через определённое количество времени или выполненных команд Redis делает снапшот всех своих данных в файл с расширением .rdb
. Всё :) Вот такой простой режим работы.
Тот, кто немного знаком с Redis, знает, что он однопоточный. Получается, пока делается снапшот, Redis недоступен? Например, снапшот 200 миллионов ключей (в зависимости от диска, конечно) будет происходить порядка 150 секунд. И что, получается, все эти 150 секунд мы будем ждать?
Конечно, если бы Redis был написан так, что блокируется на время создания снапшота, то ни о какой популярности речи бы не шло. Никто бы просто не пользовался таким продуктом. Так что же делает Redis? Он пользуется средствами операционной системы: в момент, когда процесс Redis решает, что нужно сделать снапшот (или когда мы отправляем команду BGSAVE
), он fork-ается, и уже самим снапшотом занимается форк, а не главный процесс. Главный процесс продолжает принимать и исполнять запросы.
Использование подхода fork даёт нам концепцию CoW (copy-on-write), реализуемую операционной системой: когда главный процесс пытается что-то в своей памяти изменить, операционная система копирует страницу памяти, для которой происходит изменение, и только после этого мы можем мутировать память, тем самым сохраняется консистентность. Этот простой в реализации механизм несёт в себе ряд недостатков:
- если у нас достаточно большая нагрузка на мутацию данных, то в худшем случае для создания снапшота нам нужно в два раза больше памяти (скопировать каждую из страниц)
- чем больше страница памяти (а я напомню, что в Linux есть такое понятие, как HugePages, когда мы можем настройками ядра управлять размером страницы, увеличивая её значение с 4 KB до аж 1 GB), тем чаще будет происходить копирование и тем дольше оно будет происходить. Тут всё просто: чем меньше страница памяти, тем меньше данных в неё попадает, тем ниже шанс того, что при мутации случайного ключа мы попадём на конкретную страницу
- и главный недостаток - мы имеем ограниченные настройки запуска создания снапшота: либо по времени, либо по количеству выполненных команд, либо в ручном режиме. Вы можете спросить: а что если выставить создание снапшота каждую секунду? Это плохой подход, так как вы всё ещё можете потерять данные за это секундное окно. Второе - снапшоты не инкрементальные, то есть при создании нового снапшота информация о старом никак не учитывается. В итоге мы получаем ещё и высокий уровень потребления CPU.
Полезные метрики
rdb_bgsave_in_progress
- флаг активного фонового сохранения (0 или 1)rdb_last_bgsave_status
- статус последнего BGSAVE (ok/err)rdb_last_bgsave_time_sec
- длительность последнего BGSAVE в секундахrdb_changes_since_last_save
- количество изменений с последнего RDB сохраненияlatest_fork_usec
- время последней fork-операции в микросекундахrdb_last_cow_size
- размер Copy-on-Write памяти при последнем RDB сохранении
AOF
AOF - расшифровывается как Append Only File. Призван решить проблемы RDB-снапшота. Суть его очень похожа на WAL
в какой-нибудь реляционной базе данных: каждая мутационная команда не только меняет состояние в памяти, но и записывается в AOF файл. Таким образом, мы лишены проблемы двукратного потребления памяти, не зависим от размера страниц памяти и, что главное, теперь каждая команда записывается на диск, что даёт нам durability.
Но всё ли так хорошо с AOF? Нет! С настройками по умолчанию AOF не так хорош, потому что запускается с опцией appendfsync everysec, что говорит процессу Redis делать fsync
на AOF-файл только раз в секунду, то есть мы всё так же можем потерять данные за окно в 1 секунду (в худшем случае). Поэтому для полного durability нужно выставлять настройку appendfsync always, с ней Redis будет вызывать fsync
после каждого write.
Всё, теперь AOF - это best of the best, лучшая технология? Всё ещё нет! Представим ситуацию: у вас есть ключ, который вы только и делаете, что инкрементируете на единицу (команда INCR
в Redis). После 1000000 вызовов у вас всего один ключ со значением 1000000, но вот в AOF файле у вас 1000000 записей INCR
. Как-то не очень эффективно хранить столько записей для одного ключа. Для решения этой проблемы в Redis существует подход AOF rewriting: его суть в том, что при достижении какого-то порога размера AOF файла Redis создаёт новый AOF-файл, все новые записи происходят в него, а для старого AOF файла происходит компакшн. В нашем случае 1000000 записей INCR
сожмутся до всего одной записи SET <key> 1000000
.
Полезности
Настройки:
auto-aof-rewrite-percentage
- запускает перезапись AOF, когда размер файла увеличился на указанный процентappendfsync
auto-aof-rewrite-min-size
- минимальный размер AOF-файла для автоматической перезаписиaof-use-rdb-preamble
Метрики:
aof_last_rewrite_time_sec
- длительность последней перезаписи в секундахaof_last_bgrewrite_status
- статус последней перезаписи (ok/err)aof_last_write_status
- статус последней записи в AOF (ok/err)aof_last_cow_size
- размер CoW памяти при последней перезаписи AOFaof_delayed_fsync
- высокие значения указывают на проблемы I/O
Итог
Так какой же подход использовать? У RDB есть неоспоримое преимущество перед AOF - скорость восстановления: у нас уже готовый стейт, никакой лог не нужно проигрывать. Поэтому Redis пошёл гибридным путём: при переписывании AOF-файла Redis может создать файл, который начинается с RDB-снимка, а дальше продолжается AOF-журналом. В итоге у нас два в одном: скорость восстановления и гарантии AOF. То есть при старте Redis сначала загружает состояние из RDB preamble, а потом применяет только последние команды из AOF. Это сильно ускоряет загрузку и уменьшает размер файла.
Автор: Никита Галушко - Golang инженер, специализирующийся на производительности и распределённых системах. Больше технических разборов в Telegram: https://t.me/b1tw1se