diff --git a/cmd/redis-shake/main.go b/cmd/redis-shake/main.go index 7dbe33d9..a6fafce5 100644 --- a/cmd/redis-shake/main.go +++ b/cmd/redis-shake/main.go @@ -1,10 +1,12 @@ package main import ( + "RedisShake/internal/client" "context" _ "net/http/pprof" "os" "os/signal" + "strings" "sync/atomic" "syscall" "time" @@ -66,11 +68,23 @@ func main() { log.Panicf("failed to read the SyncReader config entry. err: %v", err) } if opts.Cluster { + log.Infof("create SyncClusterReader") + log.Infof("* address (should be the address of one node in the Redis cluster): %s", opts.Address) + log.Infof("* username: %s", opts.Username) + log.Infof("* password: %s", strings.Repeat("*", len(opts.Password))) + log.Infof("* tls: %v", opts.Tls) theReader = reader.NewSyncClusterReader(ctx, opts) - log.Infof("create SyncClusterReader: %v", opts.Address) } else { + if opts.Sentinel.Address != "" { + address := client.FetchAddressFromSentinel(&opts.Sentinel) + opts.Address = address + } + log.Infof("create SyncStandaloneReader") + log.Infof("* address: %s", opts.Address) + log.Infof("* username: %s", opts.Username) + log.Infof("* password: %s", strings.Repeat("*", len(opts.Password))) + log.Infof("* tls: %v", opts.Tls) theReader = reader.NewSyncStandaloneReader(ctx, opts) - log.Infof("create SyncStandaloneReader: %v", opts.Address) } case v.IsSet("scan_reader"): opts := new(reader.ScanReaderOptions) @@ -80,11 +94,19 @@ func main() { log.Panicf("failed to read the ScanReader config entry. err: %v", err) } if opts.Cluster { + log.Infof("create ScanClusterReader") + log.Infof("* address (should be the address of one node in the Redis cluster): %s", opts.Address) + log.Infof("* username: %s", opts.Username) + log.Infof("* password: %s", strings.Repeat("*", len(opts.Password))) + log.Infof("* tls: %v", opts.Tls) theReader = reader.NewScanClusterReader(ctx, opts) - log.Infof("create ScanClusterReader: %v", opts.Address) } else { + log.Infof("create ScanStandaloneReader") + log.Infof("* address: %s", opts.Address) + log.Infof("* username: %s", opts.Username) + log.Infof("* password: %s", strings.Repeat("*", len(opts.Password))) + log.Infof("* tls: %v", opts.Tls) theReader = reader.NewScanStandaloneReader(ctx, opts) - log.Infof("create ScanStandaloneReader: %v", opts.Address) } case v.IsSet("rdb_reader"): opts := new(reader.RdbReaderOptions) @@ -121,14 +143,23 @@ func main() { log.Panicf("the RDBRestoreCommandBehavior can't be 'panic' when the server not reply to commands") } if opts.Cluster { + log.Infof("create RedisClusterWriter") + log.Infof("* address (should be the address of one node in the Redis cluster): %s", opts.Address) + log.Infof("* username: %s", opts.Username) + log.Infof("* password: %s", strings.Repeat("*", len(opts.Password))) + log.Infof("* tls: %v", opts.Tls) theWriter = writer.NewRedisClusterWriter(ctx, opts) - log.Infof("create RedisClusterWriter: %v", opts.Address) - } else if opts.Sentinel { - theWriter = writer.NewRedisSentinelWriter(ctx, opts) - log.Infof("create RedisSentinelWriter: %v", opts.Address) } else { + if opts.Sentinel.Address != "" { + address := client.FetchAddressFromSentinel(&opts.Sentinel) + opts.Address = address + } + log.Infof("create RedisStandaloneWriter") + log.Infof("* address: %s", opts.Address) + log.Infof("* username: %s", opts.Username) + log.Infof("* password: %s", strings.Repeat("*", len(opts.Password))) + log.Infof("* tls: %v", opts.Tls) theWriter = writer.NewRedisStandaloneWriter(ctx, opts) - log.Infof("create RedisStandaloneWriter: %v", opts.Address) } if config.Opt.Advanced.EmptyDBBeforeSync { // exec FLUSHALL command to flush db diff --git a/docs/src/en/guide/mode.md b/docs/src/en/guide/mode.md index 711361da..cf160d42 100644 --- a/docs/src/en/guide/mode.md +++ b/docs/src/en/guide/mode.md @@ -20,9 +20,40 @@ When the source Redis is deployed in a cluster architecture, you can use `sync_r ## Redis Sentinel Architecture -When the source Redis is deployed in a sentinel architecture and RedisShake uses `sync_reader` to connect to the master, it will be treated as a slave by the master and may be elected as the new master by the sentinel. +1. Typically, you can ignore the Sentinel component and directly write the Redis connection information into the RedisShake configuration file. +::: warning +Note that when using `sync_reader` to connect to a Redis Master node managed by Sentinel, RedisShake will be treated as a Slave node by Sentinel, which may cause unexpected issues. Therefore, in such scenarios, it is recommended to choose a replica as the source. +::: +2. If it is not convenient to directly obtain the Redis connection information, you can configure the Sentinel information in the RedisShake configuration file. RedisShake will automatically obtain the master node address from Sentinel. Configuration reference: +```toml +[sync_reader] +cluster = false +address = "" # The source Redis address will be obtained from Sentinel +username = "" +password = "redis6380password" +tls = false +[sync_reader.sentinel] +master_name = "mymaster" +address = "127.0.0.1:26380" +username = "" +password = "" +tls = false + +[redis_writer] +cluster = false +address = "" # The target Redis address will be obtained from Sentinel +username = "" +password = "redis6381password" +tls = false +[redis_writer.sentinel] +master_name = "mymaster1" +address = "127.0.0.1:26380" +username = "" +password = "" +tls = false + +``` -To avoid this situation, you should choose a replica as the source. ## Cloud Redis Services diff --git a/docs/src/zh/guide/mode.md b/docs/src/zh/guide/mode.md index 84a61d7e..90d85eb6 100644 --- a/docs/src/zh/guide/mode.md +++ b/docs/src/zh/guide/mode.md @@ -22,9 +22,40 @@ outline: deep ## Redis Sentinel 架构 -当源端 Redis 以 sentinel 架构部署且 RedisShake 使用 `sync_reader` 连接主库时,会被主库当做 slave,从而有可能被 sentinel 选举为新的 master。 - -为了避免这种情况,应选择备库作为源端。 +1. 通常情况下,忽略 Sentinel 组件,直接将 Redis 的连接信息写入 RedisShake 配置文件即可。 +::: warning +需要注意的是使用 `sync_reader` 连接被 Sentinel 接管的 Redis Master 节点时,RedisShake 会被 Sentinel 当做 Slave 节点,从而引发非预期内问题。 +所以此类场景应尽量选择备库作为源端。 +::: +2. 如果不方便直接获取 Redis 的连接信息([#888](https://github.com/tair-opensource/RedisShake/pull/888#issuecomment-2513984861)),可以将 Sentinel 的信息配置在 RedisShake 配置文件中,RedisShake 会自动从 Sentinel 中获取主节点地址。配置参考: +```toml +[sync_reader] +cluster = false +address = "" # 源端 Redis 的地址会从 Sentinel 中获取 +username = "" +password = "redis6380password" +tls = false +[sync_reader.sentinel] +master_name = "mymaster" +address = "127.0.0.1:26380" +username = "" +password = "" +tls = false + +[redis_writer] +cluster = false +address = "" # 目标端 Redis 的地址会从 Sentinel 中获取 +username = "" +password = "redis6381password" +tls = false +[redis_writer.sentinel] +master_name = "mymaster1" +address = "127.0.0.1:26380" +username = "" +password = "" +tls = false + +``` ## 云 Redis 服务 diff --git a/internal/client/sentinel.go b/internal/client/sentinel.go new file mode 100644 index 00000000..91306a3a --- /dev/null +++ b/internal/client/sentinel.go @@ -0,0 +1,28 @@ +package client + +import ( + "RedisShake/internal/log" + "context" + "fmt" +) + +type SentinelOptions struct { + MasterName string `mapstructure:"master_name" default:""` + Address string `mapstructure:"address" default:""` + Username string `mapstructure:"username" default:""` + Password string `mapstructure:"password" default:""` + Tls bool `mapstructure:"tls" default:"false"` +} + +func FetchAddressFromSentinel(opts *SentinelOptions) string { + log.Infof("fetching master address from sentinel. sentinel address: %s, master name: %s", opts.Address, opts.MasterName) + + ctx := context.Background() + c := NewRedisClient(ctx, opts.Address, opts.Username, opts.Password, opts.Tls, false) + defer c.Close() + c.Send("SENTINEL", "GET-MASTER-ADDR-BY-NAME", opts.MasterName) + hostport := ArrayString(c.Receive()) + address := fmt.Sprintf("%s:%s", hostport[0], hostport[1]) + log.Infof("fetched master address: %s", address) + return address +} diff --git a/internal/reader/sync_standalone_reader.go b/internal/reader/sync_standalone_reader.go index 024b0164..e495bd0e 100644 --- a/internal/reader/sync_standalone_reader.go +++ b/internal/reader/sync_standalone_reader.go @@ -26,15 +26,16 @@ import ( ) type SyncReaderOptions struct { - Cluster bool `mapstructure:"cluster" default:"false"` - Address string `mapstructure:"address" default:""` - Username string `mapstructure:"username" default:""` - Password string `mapstructure:"password" default:""` - Tls bool `mapstructure:"tls" default:"false"` - SyncRdb bool `mapstructure:"sync_rdb" default:"true"` - SyncAof bool `mapstructure:"sync_aof" default:"true"` - PreferReplica bool `mapstructure:"prefer_replica" default:"false"` - TryDiskless bool `mapstructure:"try_diskless" default:"false"` + Cluster bool `mapstructure:"cluster" default:"false"` + Address string `mapstructure:"address" default:""` + Username string `mapstructure:"username" default:""` + Password string `mapstructure:"password" default:""` + Tls bool `mapstructure:"tls" default:"false"` + SyncRdb bool `mapstructure:"sync_rdb" default:"true"` + SyncAof bool `mapstructure:"sync_aof" default:"true"` + PreferReplica bool `mapstructure:"prefer_replica" default:"false"` + TryDiskless bool `mapstructure:"try_diskless" default:"false"` + Sentinel client.SentinelOptions `mapstructure:"sentinel"` } type State string diff --git a/internal/writer/redis_sentinel_writer.go b/internal/writer/redis_sentinel_writer.go deleted file mode 100644 index 36f2c4c8..00000000 --- a/internal/writer/redis_sentinel_writer.go +++ /dev/null @@ -1,31 +0,0 @@ -package writer - -import ( - "RedisShake/internal/client" - "RedisShake/internal/log" - "context" - "fmt" -) - -func NewRedisSentinelWriter(ctx context.Context, opts *RedisWriterOptions) Writer { - sentinel := client.NewSentinelMasterClient(ctx, opts.Address, opts.SentinelUsername, opts.SentinelPassword, opts.Tls) - sentinel.Send("SENTINEL", "GET-MASTER-ADDR-BY-NAME", opts.Master) - addr, err := sentinel.Receive() - if err != nil { - log.Panicf(err.Error()) - } - hostport := addr.([]interface{}) - address := fmt.Sprintf("%s:%s", hostport[0].(string), hostport[1].(string)) - sentinel.Close() - - redisOpt := &RedisWriterOptions{ - Address: address, - Username: opts.Username, - Password: opts.Password, - Tls: opts.Tls, - OffReply: opts.OffReply, - BuffSend: opts.BuffSend, - } - log.Infof("connecting to master node at %s", redisOpt.Address) - return NewRedisStandaloneWriter(ctx, redisOpt) -} diff --git a/internal/writer/redis_standalone_writer.go b/internal/writer/redis_standalone_writer.go index 08ab7c8c..c04fcb98 100644 --- a/internal/writer/redis_standalone_writer.go +++ b/internal/writer/redis_standalone_writer.go @@ -18,17 +18,14 @@ import ( ) type RedisWriterOptions struct { - Cluster bool `mapstructure:"cluster" default:"false"` - Sentinel bool `mapstructure:"sentinel" default:"false"` - Master string `mapstructure:"master" default:""` - Address string `mapstructure:"address" default:""` - Username string `mapstructure:"username" default:""` - Password string `mapstructure:"password" default:""` - SentinelUsername string `mapstructure:"sentinel_username" default:""` - SentinelPassword string `mapstructure:"sentinel_password" default:""` - Tls bool `mapstructure:"tls" default:"false"` - OffReply bool `mapstructure:"off_reply" default:"false"` - BuffSend bool `mapstructure:"buff_send" default:"false"` + Cluster bool `mapstructure:"cluster" default:"false"` + Address string `mapstructure:"address" default:""` + Username string `mapstructure:"username" default:""` + Password string `mapstructure:"password" default:""` + Tls bool `mapstructure:"tls" default:"false"` + OffReply bool `mapstructure:"off_reply" default:"false"` + BuffSend bool `mapstructure:"buff_send" default:"false"` + Sentinel client.SentinelOptions `mapstructure:"sentinel"` } type redisStandaloneWriter struct { diff --git a/shake.toml b/shake.toml index 180495d3..6d8482f6 100644 --- a/shake.toml +++ b/shake.toml @@ -1,13 +1,13 @@ [sync_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 # -sync_rdb = true # set to false if you don't want to sync rdb -sync_aof = true # set to false if you don't want to sync aof -prefer_replica = false # set to true if you want to sync from replica node -try_diskless = false # set to true if you want to sync by socket and source repl-diskless-sync=yes +cluster = false # Set to true if the source is a Redis cluster +address = "127.0.0.1:6379" # For clusters, specify the address of any cluster node; use the master or slave address in master-slave mode +username = "" # Keep empty if ACL is not in use +password = "" # Keep empty if no authentication is required +tls = false # Set to true to enable TLS if needed +sync_rdb = true # Set to false if RDB synchronization is not required +sync_aof = true # Set to false if AOF synchronization is not required +prefer_replica = false # Set to true to sync from a replica node +try_diskless = false # Set to true for diskless sync if the source has repl-diskless-sync=yes #[scan_reader] #cluster = false # set to true if source is a redis cluster @@ -29,13 +29,9 @@ try_diskless = false # set to true if you want to sync by socket and sourc [redis_writer] cluster = false # set to true if target is a redis cluster -sentinel = false # set to true if target is a redis sentinel -master = "" # set to master name if target is a redis sentinel address = "127.0.0.1:6380" # 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 -sentinel_username = "" # keep empty if not using sentinel ACL -sentinel_password = "" # keep empty if sentinel no authentication is required tls = false off_reply = false # turn off the server reply buff_send = false # buffer send, default false. may be a sync delay when true, but it can greatly improve the speed