作者 | 吳守陽
審校 | 重樓
簡介
RedisShake 是一個用于處理和遷移 Redis 數(shù)據(jù)的工具,它提供以下特性:
- Redis 兼容性:RedisShake 兼容從 2.8 到 7.2 的 Redis 版本,并支持各種部署方式,包括單機、主從、哨兵和集群。
- 云服務兼容性:RedisShake 與主流云服務提供商提供的流行 Redis-like 數(shù)據(jù)庫無縫工作,包括但不限于:
阿里云-云數(shù)據(jù)庫 Redis 版
阿里云-云原生內(nèi)存數(shù)據(jù)庫Tair
AWS - ElastiCache
AWS - MemoryDB
- Module 兼容:RedisShake 與 TairString,TairZSet 和 TairHash 模塊兼容。
- 多種導出模式:RedisShake 支持 PSync,RDB 和 Scan 導出模式。
- 數(shù)據(jù)處理:RedisShake 通過自定義腳本實現(xiàn)數(shù)據(jù)過濾和轉(zhuǎn)換。
遷移模式介紹
目前, RedisShake 有三種遷移模式:PSync、RDB 和 SCAN,分別對應 sync_reader、rdb_reader 和 scan_reader。
- 對于從備份中恢復數(shù)據(jù)的場景,可以使用 rdb_reader。
- 對于數(shù)據(jù)遷移場景,優(yōu)先選擇 sync_reader。一些云廠商沒有提供 PSync 協(xié)議支持,可以選擇scan_reader。
- 對于長期的數(shù)據(jù)同步場景,RedisShake 目前沒有能力承接,因為 PSync 協(xié)議并不可靠,當復制連接斷開時,RedisShake 將無法重新連接至源端數(shù)據(jù)庫。如果對于可用性要求不高,可以使用 scan_reader。如果寫入量不大,且不存在大 key,也可以考慮 scan_reader。不同模式各有優(yōu)缺點,需要查看各 Reader 章節(jié)了解更多信息。
Redis Cluster 架構
當源端 Redis 以 cluster 架構部署時,可以使用 sync_reader 或者 scan_reader。兩者配置項中均有開關支持開啟 cluster 模式,會通過 cluster nodes 命令自動獲取集群中的所有節(jié)點,并建立連接。
Redis Sentinel 架構
當源端 Redis 以 sentinel 架構部署且 RedisShake 使用 sync_reader 連接主庫時,會被主庫當做 slave,從而有可能被 sentinel 選舉為新的 master。
為了避免這種情況,應選擇備庫作為源端。
云 Redis 服務
主流云廠商都提供了 Redis 服務,不過有幾個原因?qū)е略谶@些服務上使用 RedisShake 較為復雜:
- 引擎限制。存在一些自研的 Redis-like 數(shù)據(jù)庫沒有兼容 PSync 協(xié)議。
- 架構限制。較多云廠商支持代理模式,即在用戶與 Redis 服務之間增加 Proxy 組件。因為 Proxy 組件的存在,所以 PSync 協(xié)議無法支持。
- 安全限制。在原生 Redis 中 PSync 協(xié)議基本會觸發(fā) fork(2),會導致內(nèi)存膨脹與用戶請求延遲增加,較壞情況下甚至會發(fā)生 out of memory。盡管這些都有方案緩解,但并不是所有云廠商都有這方面的投入。
- 商業(yè)策略。較多用戶使用 RedisShake 是為了下云或者換云,所以部分云廠商并不希望用戶使用 RedisShake,從而屏蔽了 PSync 協(xié)議。
下文會結合實踐經(jīng)驗,介紹一些特殊場景下的 RedisShake 使用方案。
阿里云「云數(shù)據(jù)庫 Redis」與「云原生內(nèi)存數(shù)據(jù)庫Tair」
「云數(shù)據(jù)庫 Redis」與「云原生內(nèi)存數(shù)據(jù)庫Tair」都支持 PSync 協(xié)議,推薦使用 sync_reader。用戶需要創(chuàng)建一個具有復制權限的賬號(可以執(zhí)行 PSync 命令),RedisShake 使用該賬號進行數(shù)據(jù)同步,具體創(chuàng)建步驟見 創(chuàng)建與管理賬號。
例外情況:
- 2.8 版本的 Redis 實例不支持創(chuàng)建復制權限的賬號,需要 升級大版本。
- 集群架構的 Reids 與 Tair 實例在 代理模式 下不支持 PSync 協(xié)議。
- 讀寫分離架構不支持 PSync 協(xié)議。
在不支持 PSync 協(xié)議的場景下,可以使用 scan_reader。需要注意的是,scan_reader 會對源庫造成較大的壓力。
AWS ElastiCache and MemoryDB
優(yōu)選 sync_reader, AWS ElastiCache and MemoryDB 默認情況下沒有開啟 PSync 協(xié)議,但是可以通過提交工單的方式請求開啟 PSync 協(xié)議。AWS 會在工單中給出一份重命名的 PSync 命令,比如 xhma21yfkssync 和 nmfu2bl5osync。此命令效果等同于 psync 命令,只是名字不一樣。 用戶修改 RedisShake 配置文件中的 aws_psync 配置項即可。對于單實例只寫一對 ip:port@cmd 即可,對于集群實例,需要寫上所有的 ip:port@cmd,以逗號分隔。
不方便提交工單時,可以使用 scan_reader。需要注意的是,scan_reader 會對源庫造成較大的壓力。
安裝
Wget https://github.com/tair-opensource/RedisShake/archive/refs/tags/v4.0.5.tar.gz
yum install go
tar -xvf redis-shake-linux-amd64.tar.gz
cd redis-shark
[root@ redis-shark]# ls
1shake_online.toml 2shake_rdb.toml data redis-shake shake.toml.bak
遷移配置
在線遷移SYNC模式
[root@idc-zabbix12 redis-shark]# vim 1shake_online.toml
function = ""
[sync_reader]
version = "7.0" ###source集群版本
cluster = true ###集群
address = "10.0.0.8:9101" ###source只需填寫一個主節(jié)點地址即可
username = ""
password = "Bj***w"
tls = false
sync_rdb = true
sync_aof = true
prefer_replica = true
# [scan_reader]
# cluster = false # set to true if source is a redis cluster
# address = "127.0.0.1:6379" # when cluster is true, set address to one of the cluster node
# username = "" # keep empty if not using ACL
# password = "" # keep empty if no authentication is required
# ksn = false # set to true to enabled Redis keyspace notifications (KSN) subscription
# tls = false
# dbs = [] # set you want to scan dbs such as [1,5,7], if you don't want to scan all
# prefer_replica = true # set to true if you want to sync from replica node
##文件依次執(zhí)行
[rdb_reader]
#filepath = "/opt/bakredis-shake.bak/local_dump.0"
#filepath = "/opt/bakredis-shake.bak/local_dump.1"
#filepath = "/opt/bakredis-shake.bak/local_dump.2"
# [aof_reader]
# filepath = "/tmp/.aof"
# timestamp = 0 # subsecond
[redis_writer]
version = "7.0" ###目標集群版本
cluster = true ###集群
address = "10.28.29.51:9101" ###target集群只需填寫一個主節(jié)點地址即可
username = ""
password = "B***Zw"
tls = false
off_reply = false
[advanced]
dir = "data" ###生成的數(shù)據(jù)目錄
ncpu = 4 # runtime.GOMAXPROCS, 0 means use runtime.NumCPU() cpu cores
pprof_port = 0 # pprof port, 0 means disable
status_port = 0 # status port, 0 means disable
# log
log_file = "shake.log" ###日志文件
log_level = "info" # debug, info or warn
log_interval = 5 # in seconds
rdb_restore_command_behavior = "panic" # panic, rewrite or skip
pipeline_count_limit = 1024
target_redis_client_max_querybuf_len = 1024_000_000
target_redis_proto_max_bulk_len = 512_000_000
aws_psync = "" # example: aws_psync = "10.0.0.1:6379@nmfu2sl5osync,10.0.0.1:6379@xhma21xfkssync"
empty_db_before_sync = false
[module]
target_mbbloom_version = 20603
執(zhí)行:
[root@idc-zabbix12 redis-shark]# ./redis-shake 1shake_online.toml
RDB文件導入模式
[root@idc-zabbix12 redis-shark]# cat 2shake_rdb.toml
function = ""
#[sync_reader]
#version = "7.0"
#cluster = true
#address = "10.28.29.8:9101"
#username = ""
#password = "Bjmr0cakP7Zw"
#tls = false
#sync_rdb = true
#sync_aof = true
#prefer_replica = true
# [scan_reader]
# cluster = false # set to true if source is a redis cluster
# address = "127.0.0.1:6379" # when cluster is true, set address to one of the cluster node
# username = "" # keep empty if not using ACL
# password = "" # keep empty if no authentication is required
# ksn = false # set to true to enabled Redis keyspace notifications (KSN) subscription
# tls = false
# dbs = [] # set you want to scan dbs such as [1,5,7], if you don't want to scan all
# prefer_replica = true # set to true if you want to sync from replica node
##文件依次執(zhí)行 0、1、2
[rdb_reader]
filepath = "/opt/bakredis-shake.bak/local_dump.0" ###rdb文件,source集群是3分片集群
#filepath = "/opt/bakredis-shake.bak/local_dump.1"
#filepath = "/opt/bakredis-shake.bak/local_dump.2"
# [aof_reader]
# filepath = "/tmp/.aof"
# timestamp = 0 # subsecond
[redis_writer]
version = "7.0"
cluster = true
address = "10.0.0.51:9101" ####目標導入集群
username = ""
password = "Bj***Zw"
tls = false
off_reply = false
[advanced]
dir = "data"
ncpu = 4 # runtime.GOMAXPROCS, 0 means use runtime.NumCPU() cpu cores
pprof_port = 0 # pprof port, 0 means disable
status_port = 0 # status port, 0 means disable
# log
log_file = "shake.log"
log_level = "info" # debug, info or warn
log_interval = 5 # in seconds
rdb_restore_command_behavior = "panic" # panic, rewrite or skip
pipeline_count_limit = 1024
target_redis_client_max_querybuf_len = 1024_000_000
target_redis_proto_max_bulk_len = 512_000_000
aws_psync = "" # example: aws_psync = "10.0.0.1:6379@nmfu2sl5osync,10.0.0.1:6379@xhma21xfkssync"
empty_db_before_sync = false
[module]
target_mbbloom_version = 20603
執(zhí)行:
恢復/opt/bakredis-shake.bak/local_dump.0文件
[root@idc-zabbix12 redis-shark]# ./redis-shake 2shake_rdb.toml
恢復/opt/bakredis-shake.bak/local_dump.1文件
[root@idc-zabbix12 redis-shark]# ./redis-shake 2shake_rdb.toml
恢復/opt/bakredis-shake.bak/local_dump.2文件
[root@idc-zabbix12 redis-shark]# ./redis-shake 2shake_rdb.toml
scan_reader
scan_reader 通過 SCAN 命令遍歷源端數(shù)據(jù)庫中的所有 Key,并使用 DUMP 與 RESTORE 命令來讀取與寫入 Key 的內(nèi)容。
注意:
Redis 的 SCAN 命令只保證 SCAN 的開始與結束之前均存在的 Key 一定會被返回,但是新寫入的 Key 有可能會被遺漏,期間刪除的 Key 也可能已經(jīng)被寫入目的端。這可以通過 ksn 配置解決。
SCAN 命令與 DUMP 命令會占用源端數(shù)據(jù)庫較多的 CPU 資源。
[scan_reader]
cluster = false # set to true if source is a redis cluster
address = "127.0.0.1:6379" # when cluster is true, set address to one of the cluster node
username = "" # keep empty if not using ACL
password = "" # keep empty if no authentication is required
tls = false
ksn = false # set to true to enabled Redis keyspace notifications (KSN) subscription
dbs = [] # set you want to scan dbs, if you don't want to scan all
其中:
cluster:源端是否為集群。
address:源端地址,當源端為集群時,address 為集群中的任意一個節(jié)點即可。
鑒權:
當源端使用 ACL 賬號時,配置 username 和 password。
當源端使用傳統(tǒng)賬號時,僅配置 password。
當源端無鑒權時,不配置 username 和 password。
tls:源端是否開啟 TLS/SSL,不需要配置證書。因為 RedisShake 沒有校驗服務器證書。
ksn:開啟 ksn 參數(shù)后, RedisShake 會在 SCAN 之前使用 Redis keyspace notifications 能力來訂閱 Key 的變化。當 Key 發(fā)生變化時,RedisShake 會使用 DUMP 與 RESTORE 命令來從源端讀取 Key 的內(nèi)容,并寫入目標端。
dbs:源端為非集群模式時,支持指定DB庫。
WARNING
Redis keyspace notifications 不會感知到 FLUSHALL 與 FLUSHDB 命令,因此在使用 ksn 參數(shù)時,需要確保源端數(shù)據(jù)庫不會執(zhí)行這兩個命令。
function模式
RedisShake 通過提供 function 功能,實現(xiàn)了的 ETL(提取-轉(zhuǎn)換-加載) 中的 transform 能力。通過利用 function 可以實現(xiàn)類似功能:
- 更改數(shù)據(jù)所屬的 db,比如將源端的 db 0 寫入到目的端的 db 1。
- 對數(shù)據(jù)進行篩選,例如,只將 key 以 user: 開頭的源數(shù)據(jù)寫入到目標端。
- 改變 Key 的前綴,例如,將源端的 key prefix_old_key 寫入到目標端的 key prefix_new_key。
...
要使用 function 功能,只需編寫一份 lua 腳本。RedisShake 在從源端獲取數(shù)據(jù)后,會將數(shù)據(jù)轉(zhuǎn)換為 Redis 命令。然后,它會處理這些命令,從中解析出 KEYS、ARGV、SLOTS、GROUP 等信息,并將這些信息傳遞給 lua 腳本。lua 腳本會處理這些數(shù)據(jù),并返回處理后的命令。最后,RedisShake 會將處理后的數(shù)據(jù)寫入到目標端。
以下是一個具體的例子:
function = """
shake.log(DB)
if DB == 0
then
return
end
shake.call(DB, ARGV)
"""
[sync_reader]
address = "127.0.0.1:6379"
[redis_writer]
address = "127.0.0.1:6380"
DB 是 RedisShake 提供的信息,表示當前數(shù)據(jù)所屬的 db。shake.log 用于打印日志,shake.call 用于調(diào)用 Redis 命令。上述腳本的目的是丟棄源端 db 0 的數(shù)據(jù),將其他 db 的數(shù)據(jù)寫入到目標端。
除了 DB,還有其他信息如 KEYS、ARGV、SLOTS、GROUP 等,可供調(diào)用的函數(shù)有 shake.log 和 shake.call,具體請參考 function API。
function API
變量
因為有些命令中含有多個 key,比如 mset 等命令。所以,KEYS、KEY_INDEXES、SLOTS 這三個變量都是數(shù)組類型。如果確認命令只有一個 key,可以直接使用 KEYS[1]、KEY_INDEXES[1]、SLOTS[1]。
函數(shù)
- shake.call(DB, ARGV):返回一個 Redis 命令,RedisShake 會將該命令寫入目標端。
- shake.log(msg):打印日志。
最佳實踐
過濾 Key
local prefix = "user:"
local prefix_len = #prefix
if string.sub(KEYS[1], 1, prefix_len) ~= prefix then
return
end
shake.call(DB, ARGV)
效果是只將 key 以 user: 開頭的源數(shù)據(jù)寫入到目標端。沒有考慮 mset 等多 key 命令的情況。
過濾 DB
shake.log(DB)
if DB == 0
then
return
end
shake.call(DB, ARGV)
效果是丟棄源端 db 0 的數(shù)據(jù),將其他 db 的數(shù)據(jù)寫入到目標端。
過濾某類數(shù)據(jù)結構
可以通過 GROUP 變量來判斷數(shù)據(jù)結構類型,支持的數(shù)據(jù)結構類型有:STRING、LIST、SET、ZSET、HASH、SCRIPTING 等。
過濾 Hash 類型數(shù)據(jù)
if GROUP == "HASH" then
return
end
shake.call(DB, ARGV)
效果是丟棄源端的 hash 類型數(shù)據(jù),將其他數(shù)據(jù)寫入到目標端。
過濾 LUA 腳本
if GROUP == "SCRIPTING" then
return
end
shake.call(DB, ARGV)
效果是丟棄源端的 lua 腳本,將其他數(shù)據(jù)寫入到目標端。常見于主從同步至集群時,存在集群不支持的 LUA 腳本。
修改 Key 的前綴
local prefix_old = "prefix_old_"
local prefix_new = "prefix_new_"
shake.log("old=" .. table.concat(ARGV, " "))
for i, index in ipairs(KEY_INDEXES) do
local key = ARGV[index]
if string.sub(key, 1, #prefix_old) == prefix_old then
ARGV[index] = prefix_new .. string.sub(key, #prefix_old + 1)
end
end
shake.log("new=" .. table.concat(ARGV, " "))
shake.call(DB, ARGV)
效果是將源端的 key prefix_old_key 寫入到目標端的 key prefix_new_key。
交換 DB
local db1 = 1
local db2 = 2
if DB == db1 then
DB = db2
elseif DB == db2 then
DB = db1
end
shake.call(DB, ARGV)
效果是將源端的 db 1 寫入到目標端的 db 2,將源端的 db 2 寫入到目標端的 db 1, 其他 db 不變。
注意事項
- 不要在同一個目錄運行兩個 RedisShake 進程,因為運行時產(chǎn)生的臨時文件可能會被覆蓋,導致異常行為。
- 不要降低 Redis 版本,比如從 6.0 降到 5.0,因為 RedisShake 每個大版本都會引入一些新的命令和新的編碼方式,如果降低版本,可能會導致不兼容。
作者簡介
吳守陽,51CTO社區(qū)編輯,擁有8年DBA工作經(jīng)驗,熟練管理MySQL、Redis、MongoDB等開源數(shù)據(jù)庫。精通性能優(yōu)化、備份恢復和高可用性架構設計。善于故障排除和自動化運維,保障系統(tǒng)穩(wěn)定可靠。具備良好的團隊合作和溝通能力,致力于為企業(yè)提供高效可靠的數(shù)據(jù)庫解決方案。