Skip to content

Commit d549e24

Browse files
1gtmtamalsaha
andauthored
[cherry-pick] Support backup and restore for redis clusters (#164) (#166)
/cherry-pick Signed-off-by: Tamal Saha <tamal@appscode.com> Co-authored-by: Tamal Saha <tamal@appscode.com>
1 parent 7803cc9 commit d549e24

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

58 files changed

+10093
-120
lines changed

Makefile

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ else
4343
endif
4444

4545
RESTIC_VER := 0.13.1
46-
REDIS_DUMP_VER := 0.7.2-ac
46+
REDIS_DUMP_VER := 0.8.0-ac
4747

4848
###
4949
### These variables should not need tweaking.
@@ -59,8 +59,8 @@ BIN_PLATFORMS := $(DOCKER_PLATFORMS)
5959
OS := $(if $(GOOS),$(GOOS),$(shell go env GOOS))
6060
ARCH := $(if $(GOARCH),$(GOARCH),$(shell go env GOARCH))
6161

62-
BASEIMAGE_PROD ?= redis:6.2.5
63-
BASEIMAGE_DBG ?= redis:6.2.5
62+
BASEIMAGE_PROD ?= redis:7.0.5
63+
BASEIMAGE_DBG ?= redis:7.0.5
6464

6565
IMAGE := $(REGISTRY)/$(BIN)
6666
VERSION_PROD := $(VERSION)

go.mod

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@ module stash.appscode.dev/redis
33
go 1.18
44

55
require (
6+
github.com/mediocregopher/radix/v3 v3.8.0
67
github.com/spf13/cobra v1.6.0
8+
github.com/yannh/redis-dump-go v0.0.0-00010101000000-000000000000
79
go.bytebuilders.dev/license-verifier/kubernetes v0.12.0
810
gomodules.xyz/flags v0.1.3
911
gomodules.xyz/go-sh v0.1.0
@@ -17,7 +19,7 @@ require (
1719
kmodules.xyz/custom-resources v0.25.0
1820
kmodules.xyz/offshoot-api v0.25.0
1921
kubedb.dev/apimachinery v0.28.4-0.20220918021210-a0b96812228b
20-
stash.appscode.dev/apimachinery v0.28.0
22+
stash.appscode.dev/apimachinery v0.28.1-0.20230429131740-425734e18c7c
2123
)
2224

2325
require (
@@ -69,6 +71,7 @@ require (
6971
golang.org/x/term v0.5.0 // indirect
7072
golang.org/x/text v0.7.0 // indirect
7173
golang.org/x/time v0.1.0 // indirect
74+
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
7275
gomodules.xyz/clock v0.0.0-20200817085942-06523dba733f // indirect
7376
gomodules.xyz/jsonpatch/v2 v2.2.0 // indirect
7477
gomodules.xyz/mergo v0.3.13 // indirect
@@ -93,3 +96,5 @@ require (
9396
sigs.k8s.io/structured-merge-diff/v4 v4.2.3 // indirect
9497
sigs.k8s.io/yaml v1.3.0 // indirect
9598
)
99+
100+
replace github.com/yannh/redis-dump-go => github.com/kubedb/redis-dump-go v0.8.1-0.20230429151509-2f2a7ce60763

go.sum

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -293,6 +293,8 @@ github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfn
293293
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
294294
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
295295
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
296+
github.com/kubedb/redis-dump-go v0.8.1-0.20230429151509-2f2a7ce60763 h1:HyNjcmSJSLEPXN+y6wNNLtNPfEcZiPotGPHbnkhj1g0=
297+
github.com/kubedb/redis-dump-go v0.8.1-0.20230429151509-2f2a7ce60763/go.mod h1:u6sFg98XPtTAaIyUv5oq+4D8D6krErkijf78cV30VOA=
296298
github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
297299
github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
298300
github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
@@ -304,6 +306,8 @@ github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNx
304306
github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
305307
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
306308
github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 h1:I0XW9+e1XWDxdcEniV4rQAIOPUGDq67JSCiRCgGCZLI=
309+
github.com/mediocregopher/radix/v3 v3.8.0 h1:HI8EgkaM7WzsrFpYAkOXIgUKbjNonb2Ne7K6Le61Pmg=
310+
github.com/mediocregopher/radix/v3 v3.8.0/go.mod h1:8FL3F6UQRXHXIBSPUs5h0RybMF8i4n7wVopoX3x7Bv8=
307311
github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
308312
github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
309313
github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
@@ -673,6 +677,7 @@ golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
673677
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
674678
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
675679
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
680+
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
676681
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
677682
gomodules.xyz/clock v0.0.0-20200817085942-06523dba733f h1:hTyhR4r+tj1Uq7/PpFxLTzbeA0LhMVp7bEYfhkzFjdY=
678683
gomodules.xyz/clock v0.0.0-20200817085942-06523dba733f/go.mod h1:K3m7N+nBOlf91/tpv8REUGwsAgaKFwElQCuiLhm12AQ=
@@ -909,5 +914,5 @@ sigs.k8s.io/structured-merge-diff/v4 v4.2.3 h1:PRbqxJClWWYMNV1dhaG4NsibJbArud9kF
909914
sigs.k8s.io/structured-merge-diff/v4 v4.2.3/go.mod h1:qjx8mGObPmV2aSZepjQjbmb2ihdVs8cGKBraizNC69E=
910915
sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo=
911916
sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8=
912-
stash.appscode.dev/apimachinery v0.28.0 h1:Wy/u6/m1O7qNgNSJs/4T2Eeg1drODv2mkQJndu9dtaQ=
913-
stash.appscode.dev/apimachinery v0.28.0/go.mod h1:7ao2U0Jgntc10uRf9KRmzXuabO12Nm7+2WpcRE/ZXWo=
917+
stash.appscode.dev/apimachinery v0.28.1-0.20230429131740-425734e18c7c h1:X5nxy4AApyos2sHrIzoZpEcsT8vMdzqdusz2U+BhlKY=
918+
stash.appscode.dev/apimachinery v0.28.1-0.20230429131740-425734e18c7c/go.mod h1:7ao2U0Jgntc10uRf9KRmzXuabO12Nm7+2WpcRE/ZXWo=

pkg/backup.go

Lines changed: 21 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import (
2727
api_util "stash.appscode.dev/apimachinery/pkg/util"
2828

2929
"github.com/spf13/cobra"
30+
"github.com/yannh/redis-dump-go/pkg/redisdump"
3031
license "go.bytebuilders.dev/license-verifier/kubernetes"
3132
"gomodules.xyz/flags"
3233
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -192,39 +193,46 @@ func (opt *redisOptions) backupRedis(targetRef api_v1beta1.TargetRef) (*restic.B
192193
return nil, err
193194
}
194195

195-
session := opt.newSessionWrapper(RedisDumpCMD)
196-
197-
err = session.setDatabaseCredentials(opt.kubeClient, appBinding)
196+
hostname, err := appBinding.Hostname()
198197
if err != nil {
199198
return nil, err
200199
}
201-
202-
err = opt.setTLSParameters(appBinding, session.cmd)
200+
port, err := appBinding.Port()
203201
if err != nil {
204202
return nil, err
205203
}
206-
207-
err = session.waitForDBReady(appBinding)
204+
username, password, err := getDatabaseCredentials(opt.kubeClient, appBinding)
208205
if err != nil {
209206
return nil, err
210207
}
211208

212-
hostname, err := appBinding.Hostname()
209+
s := redisdump.Host{
210+
Host: hostname,
211+
Port: int(port),
212+
Username: username,
213+
Password: password,
214+
TlsHandler: nil, // TODO(Shaad7): Add support for tls protected redis
215+
}
216+
217+
session := opt.newSessionWrapper(RedisDumpCMD)
218+
session.setDatabaseCredentials(password)
219+
220+
err = opt.setTLSParameters(appBinding, session.cmd)
213221
if err != nil {
214222
return nil, err
215223
}
216224

217-
session.cmd.Args = append(session.cmd.Args, "-host", hostname)
218-
219-
port, err := appBinding.Port()
225+
err = session.waitForDBReady(s)
220226
if err != nil {
221227
return nil, err
222228
}
223229

230+
session.cmd.Args = append(session.cmd.Args, "-host", s.Host)
224231
// if port is specified, append port in the arguments
225-
if port != 0 {
226-
session.cmd.Args = append(session.cmd.Args, "-port", strconv.Itoa(int(port)))
232+
if s.Port != 0 {
233+
session.cmd.Args = append(session.cmd.Args, "-port", strconv.Itoa(s.Port))
227234
}
235+
session.cmd.Args = append(session.cmd.Args, "--set-total-keys")
228236

229237
session.setUserArgs(opt.redisArgs)
230238
// add backup command in the pipeline

pkg/restore.go

Lines changed: 144 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,18 @@ package pkg
1818

1919
import (
2020
"context"
21+
"fmt"
2122
"path/filepath"
2223
"strconv"
24+
"time"
2325

2426
api_v1beta1 "stash.appscode.dev/apimachinery/apis/stash/v1beta1"
2527
"stash.appscode.dev/apimachinery/pkg/restic"
2628

29+
"github.com/mediocregopher/radix/v3"
2730
"github.com/spf13/cobra"
31+
"github.com/yannh/redis-dump-go/pkg/config"
32+
"github.com/yannh/redis-dump-go/pkg/redisdump"
2833
license "go.bytebuilders.dev/license-verifier/kubernetes"
2934
"gomodules.xyz/flags"
3035
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -134,6 +139,8 @@ func NewCmdRestore() *cobra.Command {
134139

135140
cmd.Flags().StringVar(&opt.outputDir, "output-dir", opt.outputDir, "Directory where output.json file will be written (keep empty if you don't need to write output in file)")
136141

142+
cmd.Flags().IntVar(&opt.NWorkers, "n", 10, "Parallel workers")
143+
137144
return cmd
138145
}
139146

@@ -164,48 +171,157 @@ func (opt *redisOptions) restoreRedis(targetRef api_v1beta1.TargetRef) (*restic.
164171
return nil, err
165172
}
166173

167-
session := opt.newSessionWrapper(RedisRestoreCMD)
168-
169-
err = session.setDatabaseCredentials(opt.kubeClient, appBinding)
174+
hostname, err := appBinding.Hostname()
170175
if err != nil {
171176
return nil, err
172177
}
173-
174-
err = opt.setTLSParameters(appBinding, session.cmd)
178+
port, err := appBinding.Port()
175179
if err != nil {
176180
return nil, err
177181
}
178-
179-
err = session.waitForDBReady(appBinding)
182+
username, password, err := getDatabaseCredentials(opt.kubeClient, appBinding)
180183
if err != nil {
181184
return nil, err
182185
}
183186

184-
session.cmd.Args = append(session.cmd.Args, "--pipe")
185-
186-
hostname, err := appBinding.Hostname()
187-
if err != nil {
188-
return nil, err
187+
s := redisdump.Host{
188+
Host: hostname,
189+
Port: int(port),
190+
Username: username,
191+
Password: password,
192+
TlsHandler: nil, // TODO(Shaad7): Add support for tls protected redis
189193
}
190-
session.cmd.Args = append(session.cmd.Args, "-h", hostname)
191194

192-
port, err := appBinding.Port()
193-
if err != nil {
195+
if hosts, err := redisdump.GetHosts(s, opt.NWorkers); err != nil {
194196
return nil, err
195-
}
196-
// if port is specified, append port in the arguments
197-
if port != 0 {
198-
session.cmd.Args = append(session.cmd.Args, "-p", strconv.Itoa(int(port)))
199-
}
197+
} else {
198+
redisCluster := len(hosts) > 1
199+
// Start clock to measure total restore duration
200+
startTime := time.Now()
201+
beforeKeys := 0
202+
afterKeys := 0
200203

201-
session.setUserArgs(opt.redisArgs)
204+
for _, host := range hosts {
205+
session := opt.newSessionWrapper(RedisRestoreCMD)
202206

203-
// append the restore command to the pipeline
204-
opt.dumpOptions.StdoutPipeCommands = append(opt.dumpOptions.StdoutPipeCommands, *session.cmd)
205-
resticWrapper, err := restic.NewResticWrapperFromShell(opt.setupOptions, session.sh)
206-
if err != nil {
207-
return nil, err
207+
session.setDatabaseCredentials(host.Password)
208+
if err != nil {
209+
return nil, err
210+
}
211+
212+
err = opt.setTLSParameters(appBinding, session.cmd)
213+
if err != nil {
214+
return nil, err
215+
}
216+
217+
err = session.waitForDBReady(host)
218+
if err != nil {
219+
return nil, err
220+
}
221+
222+
session.cmd.Args = append(session.cmd.Args, "--pipe")
223+
224+
session.cmd.Args = append(session.cmd.Args, "-h", host.Host)
225+
226+
// if port is specified, append port in the arguments
227+
if host.Port != 0 {
228+
session.cmd.Args = append(session.cmd.Args, "-p", strconv.Itoa(host.Port))
229+
}
230+
231+
session.setUserArgs(opt.redisArgs)
232+
233+
// append the restore command to the pipeline
234+
opt.dumpOptions.StdoutPipeCommands = []restic.Command{*session.cmd}
235+
resticWrapper, err := restic.NewResticWrapperFromShell(opt.setupOptions, session.sh)
236+
if err != nil {
237+
return nil, err
238+
}
239+
240+
// if source host is not specified then use current host as source host
241+
if opt.dumpOptions.SourceHost == "" {
242+
opt.dumpOptions.SourceHost = opt.dumpOptions.Host
243+
}
244+
245+
var client *radix.Pool
246+
if redisCluster {
247+
client, err = redisdump.NewClient(host, nil, opt.NWorkers)
248+
if err != nil {
249+
return nil, err
250+
}
251+
if size, err := DBSize(client); err != nil {
252+
return nil, err
253+
} else {
254+
beforeKeys += size
255+
}
256+
}
257+
258+
// Run dump
259+
// Redis cluster restore will always return error. So, ignore error for redis clusters
260+
_, err = resticWrapper.DumpOnce(opt.dumpOptions)
261+
if !redisCluster && err != nil {
262+
return nil, err
263+
}
264+
265+
if redisCluster {
266+
if size, err := DBSize(client); err != nil {
267+
return nil, err
268+
} else {
269+
afterKeys += size
270+
}
271+
client.Close()
272+
}
273+
}
274+
275+
if redisCluster {
276+
client, err := redisdump.NewCluster(hosts)
277+
if err != nil {
278+
return nil, err
279+
}
280+
defer client.Close()
281+
282+
var strBackedupKeys string
283+
err = client.Do(radix.Cmd(&strBackedupKeys, "GET", config.KeyTotalKeys))
284+
if err != nil {
285+
return nil, err
286+
}
287+
backedupKeys, err := strconv.Atoi(strBackedupKeys)
288+
if err != nil {
289+
return nil, err
290+
}
291+
fmt.Printf("Total keys found in backuped data: %d\n", backedupKeys)
292+
fmt.Printf("Total keys in redis before restore: %d, after restore: %d\n", beforeKeys, afterKeys)
293+
294+
_ = client.Do(radix.Cmd(nil, "DEL", config.KeyTotalKeys))
295+
} else {
296+
client, err := redisdump.NewClient(s, nil, opt.NWorkers)
297+
if err != nil {
298+
return nil, err
299+
}
300+
defer client.Close()
301+
302+
_ = client.Do(radix.Cmd(nil, "DEL", config.KeyTotalKeys))
303+
}
304+
305+
restoreStats := api_v1beta1.HostRestoreStats{
306+
Hostname: opt.dumpOptions.Host,
307+
}
308+
// Dump successful. Now, calculate total session duration.
309+
restoreStats.Duration = time.Since(startTime).String()
310+
restoreStats.Phase = api_v1beta1.HostRestoreSucceeded
311+
restoreOutput := &restic.RestoreOutput{
312+
RestoreTargetStatus: api_v1beta1.RestoreMemberStatus{
313+
Ref: targetRef,
314+
Stats: []api_v1beta1.HostRestoreStats{restoreStats},
315+
},
316+
}
317+
return restoreOutput, nil
318+
}
319+
}
320+
321+
func DBSize(client *radix.Pool) (int, error) {
322+
var dbSize string
323+
if err := client.Do(radix.Cmd(&dbSize, "dbsize")); err != nil {
324+
return 0, err
208325
}
209-
// Run dump
210-
return resticWrapper.Dump(opt.dumpOptions, targetRef)
326+
return strconv.Atoi(dbSize)
211327
}

0 commit comments

Comments
 (0)