이번 시간에는 Redis와 Valkey 이미지를 나란히 띄우고, Python 클라이언트 하나로 두 서버가 얼마나 같은지 확인해 보며 배워가는 시간입니다.
확인할 건 세 가지예요. 같은 명령에 같은 응답이 오는지, 한쪽에만 있는 명령은 무엇인지, RDB 파일을 넘기면 읽히는지입니다. 두 프로젝트 모두 릴리스가 잦으니 다른 버전에서는 결과가 달라질 수 있어요.
예를 들어서 우리 서비스에서 Redis를 Valkey로 옮기자는 이야기가 나왔다고 해볼게요. 그러면 "Redis에서 갈라져 나온 프로젝트니까 이미지 이름만 바꾸면 된다"는 말이 함께 나오곤 합니다.
같은 클라이언트 라이브러리로 접속해 같은 명령을 보낼 수 있으니 틀린 말은 아니에요. 그런데 두 프로젝트가 갈라진 지 2년 반이 지나는 동안 각자 추가한 명령이 생겼고, 디스크에 남기는 RDB 파일의 형식도 달라졌습니다.
실습 준비
이 글의 명령은 모두 실습 코드의 redis-vs-valkey/ 디렉터리에서 실행합니다.
compose.yaml: Redis, Valkey, Python 클라이언트 컨테이너 정의compat.py: 같은 명령 79개를 두 서버에 보내 응답 비교cmddiff.py: 두 서버의 명령 이름 목록 비교seed.py: 자료구조별 시험 데이터 넣기check.py: 키 수와 내용 해시 출력
Redis, Valkey, Python 클라이언트 컨테이너는 같은 Docker 네트워크에 띄워요. 두 서버는 영속화를 끄고(--save "" --appendonly no) CPU 2개와 메모리 1GB로 똑같이 제한했습니다.
VALKEY_ARGS는 뒤에서 Valkey에 옵션을 하나 더 줄 때 쓸 거예요. compose.yaml의 서비스는 redis, valkey, 클라이언트 app 셋입니다.
compose 프로젝트 이름은 valk로 두고 셸 함수를 만들어 놓을게요. rcli와 vcli는 둘 다 Redis 컨테이너 안의 같은 redis-cli를 실행하고, vcli만 -h valkey로 Valkey에 접속한다고 보시면 됩니다.
아래 블록은 파일로 만들지 말고 실습 디렉터리에서 연 터미널에 그대로 붙여 넣어 주세요. 이후 명령은 모두 이 터미널에서 실행하고, 터미널을 새로 열면 다시 붙여 넣으시면 됩니다.
export COMPOSE_PROJECT_NAME=valk
docker compose up -d
rcli() { docker compose exec -T redis redis-cli --no-raw "$@"; }
vcli() { docker compose exec -T redis redis-cli --no-raw -h valkey "$@"; }
py() { docker compose exec -T app python "$@"; }
먼저 두 서버의 버전을 확인해 볼게요.
rcli INFO server | grep -E "^(redis_version|valkey_version|server_name)"
vcli INFO server | grep -E "^(redis_version|valkey_version|server_name)"
redis_version:8.10.2
redis_version:7.2.4
server_name:valkey
valkey_version:9.1.2
여기서 눈여겨볼 건 Valkey 9.1.2가 redis_version 필드에 7.2.4를 보고한다는 점이에요. 갈라져 나온 시점의 Redis 버전을 호환용으로 남겨 둔 값이고, 실제 버전은 valkey_version에 있습니다.
그래서 모니터링 도구나 클라이언트가 redis_version을 보고 기능을 켜고 끈다면 Valkey 9.1을 Redis 7.2.4로 판단하게 돼요. 쓰는 도구가 이 값을 읽는지 미리 확인해 두세요.
실습 1: 같은 클라이언트, 같은 명령
compat.py는 같은 명령 목록을 두 서버에 차례로 보내고 응답을 비교해서 다른 것만 출력합니다. 명령은 서비스에서 자주 쓰는 것을 자료구조별로 골랐어요. 자료구조를 무엇으로 고르는지 궁금하면 Redis 자료구조를 다룬 글을 참고하세요.
문자열, Hash(필드 TTL을 거는 HEXPIRE 포함), List, Set, Sorted Set, Stream과 컨슈머 그룹, Bitmap, HyperLogLog, Geo에 더해 Lua 스크립트, 함수(FUNCTION LOAD, FCALL), 오류 응답, 트랜잭션, Pub/Sub까지 79개입니다.
py compat.py
[다름] OBJECT ENCODING h:1
redis : listpackex
valkey: hashtable
[다름] NOSUCHCMD
redis : (error) unknown command 'NOSUCHCMD'
valkey: (error) unknown command 'NOSUCHCMD', with args beginning with:
명령 79개 중 77개 응답 일치
출력을 보면 79개 중 77개가 같았어요. 다른 두 개는 이렇습니다.
OBJECT ENCODING h:1: 필드에 TTL을 한 번 건 Hash의 내부 인코딩입니다. Redis는listpackex, Valkey는hashtable을 씁니다. 같은 스크립트에서 TTL을 걸기 전에는 둘 다listpack으로 같았습니다. 저장된 값과 다른 명령의 응답은 같았습니다.NOSUCHCMD: 인자 없이 없는 명령을 보냈을 때 Redis 8.10은with args beginning with:부분을 붙이지 않습니다. 인자를 붙여 보내면 두 서버의 오류 문자열이 똑같았습니다.
트랜잭션, Lua, 함수, Pub/Sub, 컨슈머 그룹, 오류 응답(WRONGTYPE, 인자 수 오류)은 모두 같았어요. 그러니 이 79개 명령만 쓰고 오류 문자열을 그대로 비교하는 코드가 없다면, 클라이언트 코드는 고치지 않아도 됩니다.
실습 2: 한쪽에만 있는 명령
이번에는 두 서버가 아는 명령 이름을 모두 받아 비교해 볼게요. COMMAND LIST는 하위 명령(CLIENT|CAPA 같은 것)도 따로 셉니다.
Redis 쪽에서는 모듈이 제공하는 명령을 COMMAND LIST FILTERBY MODULE로 따로 빼냅니다. 이 비교는 cmddiff.py가 해요.
py cmddiff.py
rcli MODULE LIST | grep -A1 name
redis 669개, valkey 399개, 공통 385개
redis 모듈(bf, timeseries, ReJSON, search, vectorset) 명령 233개
redis에만 있는 명령(모듈 제외) 51개
arcount ardel ardelrange arget argetrange argrep arinfo arinsert arlastitems arlen armget armset arnext arop arring arscan arseek arset backup backup|abort backup|cleanup backup|help backup|list backup|seal backup|start backup|status blmovem cluster|migration delex digest himport himport|discard himport|discardall himport|prepare himport|set hotkeys hotkeys|get hotkeys|help hotkeys|reset hotkeys|start hotkeys|stop increx lmovem sdiffcard sunioncard trimslots xackdel xcfgset xdelex xidmprecord xnack
valkey에만 있는 명령 14개
client|capa client|import-source clusterscan cluster|cancelslotmigrations cluster|flushslot cluster|getslotmigrations cluster|migrateslots commandlog commandlog|get commandlog|help commandlog|len commandlog|reset delifeq script|show
1) 1) "name"
2) "bf"
2) 1) "name"
2) "timeseries"
3) 1) "name"
2) "ReJSON"
4) 1) "name"
2) "search"
5) 1) "name"
2) "vectorset"
Redis 8.10.2 이미지는 모듈 다섯 개를 불러온 상태로 떠요. Redis 8.0 릴리스 노트에 따르면 Redis 8부터 검색, JSON, 시계열, 확률 자료구조(Bloom 필터 등)가 기본 배포에 들어갔고, 이 명령이 233개입니다.
valkey/valkey:9.1.2 이미지에는 이런 모듈이 없어요. 그래서 JSON.SET이나 FT.SEARCH를 쓰는 서비스는 이 이미지로 바로 옮길 수 없습니다.
모듈을 빼고도 Redis에만 있는 명령이 51개, Valkey에만 있는 명령이 14개였어요. 릴리스 노트에서 추가 버전을 확인한 것만 적으면 다음과 같습니다.
- Redis 8.2:
XDELEX,XACKDEL - Redis 8.4:
DELEX,DIGEST - Redis 8.6:
HOTKEYS - Redis 8.8:
INCREX,XNACK, Array 자료구조(AR로 시작하는 명령) - Redis 8.10:
HIMPORT,LMOVEM,BLMOVEM,SDIFFCARD,BACKUP - Valkey 8.1:
COMMANDLOG - Valkey 9.0:
DELIFEQ - Valkey 9.1:
CLUSTERSCAN
같은 일을 하는 명령이 이름만 다른 경우도 있어요. 값이 같을 때만 키를 지우는 동작은 Redis에서는 DELEX key IFEQ value, Valkey에서는 DELIFEQ key value입니다.
분산 락을 풀 때 쓰는 동작인데, 상대 서버에 보내면 없는 명령이라는 오류가 납니다. 락을 풀 때 값을 비교하는 이유가 궁금하면 Redis 분산 락을 다룬 글을 참고하세요.
실습 3: RDB 파일을 넘기면 읽힐까
여기서는 파일 호환만 볼게요. 영속화 자체(RDB와 AOF가 언제 무엇을 남기는지)가 궁금하면 Redis 영속화를 다룬 글을 참고하세요.
시험 데이터는 seed.py로 만듭니다. 자료구조마다 키 10개씩 90개를 넣고, --bulk를 주면 문자열 키를 더 넣어요.
check.py는 키 수와 함께, 대량으로 넣은 key:*를 뺀 자료구조 키를 모두 읽어 내용 해시를 만들어요. 두 서버에서 해시가 같으면 내용이 같다고 보시면 됩니다.
파일을 옮기는 함수도 만들어 둘게요. Redis의 dump.rdb를 호스트로 꺼낸 뒤, 멈춘 Valkey 컨테이너의 데이터 디렉터리에 넣고 다시 띄웁니다.
fresh는 두 서버를 데이터째 지우고 새로 띄우고, q는 명령의 출력을 버립니다.
to_valkey() {
docker compose cp redis:/data/dump.rdb ./dump.rdb
docker compose stop valkey
docker compose cp ./dump.rdb valkey:/data/dump.rdb
docker compose up -d valkey
}
q() { "$@" > /dev/null 2>&1; }
fresh() { q docker compose down -v; unset VALKEY_ARGS; q docker compose up -d; sleep 12; }
Redis에 데이터를 넣고 SAVE로 파일을 만든 뒤 파일 앞 9바이트를 볼게요.
fresh
py seed.py redis --bulk 100000
rcli SAVE
docker compose exec redis sh -c 'head -c 9 /data/dump.rdb; echo'
py check.py redis
redis: DBSIZE 100090
OK
REDIS0015
DBSIZE 100090, 자료구조 키 90개, 내용 해시 427025fb948b
hash:3 -> [('age', '23'), ('name', 'user3')]
zset:3 -> [('m0', 0.0), ('m1', 1.5), ('m2', 3.0), ('m3', 4.5), ('m4', 6.0)]
hll:3 -> 99
RDB 파일은 REDIS 다섯 글자 뒤에 네 자리 RDB 버전이 붙어요. Redis 8.10.2는 RDB 버전 15로 저장했습니다. 이 파일을 Valkey로 옮겨 볼게요.
to_valkey
docker compose logs --no-log-prefix valkey | tail -2
7:M 02 Oct 2026 00:35:48.447 # Can't handle RDB format version 15
7:M 02 Oct 2026 00:35:48.447 # Fatal error loading the DB, check server logs. Exiting.
Valkey는 기동하자마자 RDB 버전 15를 처리할 수 없다며 종료했어요. 왜 그럴까요?
Valkey 9.1.2 소스(src/rdb.h)를 보면 Valkey 자신의 RDB 버전은 80이고, 12부터 79까지는 다른 구현이 쓰는 "foreign" 버전으로 비워 두었습니다. 이 실습의 Redis 8.10.2가 만든 버전 15도 이 범위에 들어가요. 기본 설정(rdb-version-check strict)에서는 이 범위의 파일을 읽지 않습니다.
valkey.conf에는 이 검사를 느슨하게 하는 설정이 있어요.
rdb-version-check strict: 기본값입니다. 자기가 모르는 RDB 버전이면 읽지 않습니다.rdb-version-check relaxed: 일단 읽어 보고, 모르는 정보가 나오면 그 자리에서 중단합니다.RESTORE와 복제에도 똑같이 적용됩니다.
relaxed로 다시 띄워 볼게요.
export VALKEY_ARGS="--rdb-version-check relaxed"
docker compose up -d valkey
docker compose logs --no-log-prefix valkey | tail -6
7:M 02 Oct 2026 00:35:59.512 * Cleaning slot migration log in anticipation of a load operation.
7:M 02 Oct 2026 00:35:59.512 * Loading RDB produced by Redis version 8.10.2
7:M 02 Oct 2026 00:35:59.512 * RDB age 33 seconds
7:M 02 Oct 2026 00:35:59.512 * RDB memory usage when created 7.74 Mb
7:M 02 Oct 2026 00:35:59.515 # Can't handle foreign type or opcode 27 in RDB with version 15
7:M 02 Oct 2026 00:35:59.515 # Fatal error loading the DB, check server logs. Exiting.
이번에는 Redis 8.10.2가 만든 파일이라는 것까지 읽었지만, 타입 27에서 멈췄어요.
Redis 8.10.2 소스의 src/rdb.h에서 27은 RDB_TYPE_STREAM_LISTPACKS_5, 즉 XNACK을 지원하는 새 Stream 형식입니다. 이 실습의 Stream은 XNACK을 쓴 적이 없는데도 이 형식으로 저장됐어요.
정리
범위: redis:8.10.2와 valkey/valkey:9.1.2, Python redis 6.4.0, 2026년 10월 2일 측정, 다른 작업과 함께 쓰는 노트북 한 대.
- 같은 클라이언트로 보낸 명령 79개 중 77개의 응답이 같았고, 다른 두 개는 필드 TTL을 건 Hash의
OBJECT ENCODING과 인자 없이 보낸 없는 명령의 오류 문자열이었어요. 옮기기 전에 오류 문자열을 그대로 비교하는 코드가 있는지 찾아 보세요. - Valkey 9.1.2는
redis_version에 7.2.4를 보고했어요.redis_version을 보고 기능을 켜고 끄는 도구나 클라이언트가 있는지 확인하세요. - 모듈을 빼고 Redis 8.10.2에만 있는 명령이 51개, Valkey 9.1.2에만 있는 명령이 14개였고, Redis 이미지의 모듈 명령 233개는 Valkey 이미지에 없었어요. 모듈 명령이나
DELEX,DELIFEQ같은 한쪽 전용 명령을 쓰는 코드는 서버를 바꿀 때 함께 고쳐 주세요. - Redis 8.10.2의 RDB 파일(버전 15)은 Valkey 9.1.2 기본 설정에서 거부됐고,
relaxed로 바꿔도 Stream(타입 27)에서 멈췄어요. 이 글에 싣지 않은 추가 측정에서는 Stream 키를 지워도 필드 TTL Hash(타입 25)에서 멈췄고, Valkey의 RDB 파일(버전 80)은 Redis가 읽지 못했습니다. 옮기기 전에 실제 RDB 파일을 대상 서버에 올려 보세요.