Verify: migrate from Redis to KiviDB
Verify. Migrate to KiviDB: Move from Redis, Valkey, ElastiCache, Redis Cloud, Aiven, Aerospike, Hazelcast and more: getting the data out, loading it, cutting
Compare key counts first:
redis-cli -h redis.internal INFO keyspace
redis-cli -h kividb.internal -p 6380 INFO keyspace
keys= should match for every database; expires= should match too, give or
take keys that expired during the copy. Then spot-check values. This script
compares N random keys (or every key, with 0) between the two servers: the
type, the whole value, the TTL, the expiry of each hash field, and each stream's
entries and consumer groups.
#!/usr/bin/env python3
"""Spot-check a migration: compare keys between a source and KiviDB.
pip install "redis>=5"
python3 verify.py [--src-cluster] [--dst-cluster] SOURCE_URL TARGET_URL [N] [DB ...]
N random keys per database (default 1000); N=0 checks every key, by SCAN.
--src-cluster / --dst-cluster: that side is a cluster (database 0 only).
URLs look like redis://:password@host:6379 or rediss://user:password@host:6380.
Exits 1 if any key differs.
"""
import sys
import redis
flags = {a for a in sys.argv[1:] if a.startswith("--")}
args = [a for a in sys.argv[1:] if not a.startswith("--")]
src_url, dst_url = args[0], args[1]
n = int(args[2]) if len(args) > 2 else 1000
dbs = [int(d) for d in args[3:]] or [0]
TTL_SLACK_MS = 5000 # TTLs keep counting down while the check runs
def value(r, key, kind):
"""The whole value of a key, in a form two servers can be compared by."""
if kind == "string":
v = r.get(key)
if v and v.startswith(b"HYLL"): # a HyperLogLog: compare what it counts
return "hyperloglog", r.pfcount(key)
return v
if kind == "list":
return r.lrange(key, 0, -1)
if kind == "set":
return sorted(r.smembers(key))
if kind == "zset":
return r.zrange(key, 0, -1, withscores=True)
if kind == "hash":
h = r.hgetall(key)
fields = sorted(h)
try: # per-field expiry as a unix time in ms (Redis 7.4+); -1 = none
when = r.execute_command("HPEXPIRETIME", key, "FIELDS", len(fields), *fields)
except redis.ResponseError:
when = None # the source has no field expiry
return [(f, h[f]) for f in fields], when
if kind == "stream":
entries = r.xrange(key)
groups = []
for g in r.xinfo_groups(key):
pending = r.xpending(key, g["name"])
groups.append((g["name"], g["last-delivered-id"], g["consumers"], pending["pending"]))
return entries, sorted(groups)
return ("unchecked type", kind)
def ttl_close(a, b):
if a < 0 or b < 0:
return a == b # -1 no expiry, -2 gone
return abs(a - b) <= TTL_SLACK_MS
def same(a, b, kind):
if kind != "hash":
return a == b
(fa, wa), (fb, wb) = a, b
if fa != fb:
return False
if wa is None or wb is None: # one side has no field expiry: the other must have none set
return all(x == -1 for x in (wa or []) + (wb or []))
return all(ttl_close(x, y) for x, y in zip(wa, wb))
def dbsize(r):
if isinstance(r, redis.RedisCluster):
return r.dbsize(target_nodes=redis.RedisCluster.PRIMARIES) # summed over the shards
return r.dbsize()
bad = checked = 0
for db in dbs:
src = (redis.RedisCluster.from_url(src_url) if "--src-cluster" in flags
else redis.Redis.from_url(src_url, db=db))
dst = (redis.RedisCluster.from_url(dst_url) if "--dst-cluster" in flags
else redis.Redis.from_url(dst_url, db=db))
print(f"db{db}: DBSIZE source={dbsize(src)} target={dbsize(dst)}")
keys = src.scan_iter(count=1000) if n == 0 else (src.randomkey() for _ in range(n))
for key in keys:
if key is None:
break
kind = src.type(key).decode()
problems = []
if dst.type(key).decode() != kind:
problems.append(f"type {kind} vs {dst.type(key).decode()}")
else:
if not same(value(src, key, kind), value(dst, key, kind), kind):
problems.append("value differs")
if not ttl_close(src.pttl(key), dst.pttl(key)):
problems.append(f"pttl {src.pttl(key)} vs {dst.pttl(key)}")
checked += 1
if problems:
bad += 1
print(f" db{db} {key!r} ({kind}): {'; '.join(problems)}")
print(f"checked {checked} keys, {bad} differ")
sys.exit(1 if bad else 0)
Run it for each database you use:
python3 verify.py redis://:$REDIS_PASSWORD@redis.internal:6379 \
rediss://kividbapp:$KIVIDB_PASSWORD@your-name.cloud.kividb.io:6380 1000 0 1
# db0: DBSIZE source=4123 target=4123
# db1: DBSIZE source=101 target=101
# checked 2000 keys, 0 differ
For a rediss:// URL with KiviDB Cloud's private CA, add
?ssl_ca_certs=kividb-ca.pem to the URL. The script only reads. While writes
are still flowing, a key that changed between the two reads can show as
different; run it again after writes stop. Keys of a module type are reported
as unchecked type.