feat: improve sentinel support (#902)

This commit is contained in:
suxb201 2024-12-12 17:47:54 +08:00 committed by GitHub
parent 485fd11241
commit 7675749b67
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 161 additions and 77 deletions

View File

@ -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

View File

@ -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

View File

@ -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 服务

View File

@ -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
}

View File

@ -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

View File

@ -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)
}

View File

@ -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 {

View File

@ -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