Skip to content

Commit acffdb5

Browse files
committed
Create a v2 snapshot when running etcdutl migrate command
Also added test to cover the etcdutl migrate command Signed-off-by: Benjamin Wang <[email protected]>
1 parent a03bdaa commit acffdb5

File tree

4 files changed

+256
-45
lines changed

4 files changed

+256
-45
lines changed

etcdutl/etcdutl/common.go

+88
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,19 @@ package etcdutl
1616

1717
import (
1818
"errors"
19+
"fmt"
1920

2021
"go.uber.org/zap"
2122
"go.uber.org/zap/zapcore"
2223

2324
"go.etcd.io/etcd/client/pkg/v3/logutil"
2425
"go.etcd.io/etcd/pkg/v3/cobrautl"
26+
"go.etcd.io/etcd/server/v3/etcdserver"
27+
"go.etcd.io/etcd/server/v3/etcdserver/api/membership"
2528
"go.etcd.io/etcd/server/v3/etcdserver/api/snap"
29+
"go.etcd.io/etcd/server/v3/storage/backend"
2630
"go.etcd.io/etcd/server/v3/storage/datadir"
31+
"go.etcd.io/etcd/server/v3/storage/schema"
2732
"go.etcd.io/etcd/server/v3/storage/wal"
2833
"go.etcd.io/etcd/server/v3/storage/wal/walpb"
2934
"go.etcd.io/raft/v3/raftpb"
@@ -68,3 +73,86 @@ func getLatestV2Snapshot(lg *zap.Logger, dataDir string) (*raftpb.Snapshot, erro
6873

6974
return snapshot, nil
7075
}
76+
77+
func createV2SnapshotFromV3Store(dataDir string, be backend.Backend) error {
78+
var (
79+
lg = GetLogger()
80+
81+
snapDir = datadir.ToSnapDir(dataDir)
82+
walDir = datadir.ToWALDir(dataDir)
83+
)
84+
85+
ci, term := schema.ReadConsistentIndex(be.ReadTx())
86+
87+
cl := membership.NewCluster(lg)
88+
cl.SetBackend(schema.NewMembershipBackend(lg, be))
89+
cl.UnsafeLoad()
90+
91+
latestWALSnap, err := getLatestWALSnap(lg, dataDir)
92+
if err != nil {
93+
return err
94+
}
95+
96+
// Each time before creating the v2 snapshot, etcdserve always flush
97+
// the backend storage (bbolt db), so the consistent index should never
98+
// less than the Index or term of the latest snapshot.
99+
if ci < latestWALSnap.Index || term < latestWALSnap.Term {
100+
// This should never happen
101+
return fmt.Errorf("consistent_index [Index: %d, Term: %d] is less than the latest snapshot [Index: %d, Term: %d]", ci, term, latestWALSnap.Index, latestWALSnap.Term)
102+
}
103+
104+
if ci == latestWALSnap.Index {
105+
lg.Info("The latest snapshot is already up to date", zap.Uint64("consistent_index", ci))
106+
return nil
107+
}
108+
109+
voters, learners := getVotersAndLearners(cl)
110+
confState := raftpb.ConfState{
111+
Voters: voters,
112+
Learners: learners,
113+
}
114+
115+
// create the v2 snaspshot file
116+
raftSnap := raftpb.Snapshot{
117+
Data: etcdserver.GetMembershipInfoInV2Format(lg, cl),
118+
Metadata: raftpb.SnapshotMetadata{
119+
Index: ci,
120+
Term: term,
121+
ConfState: confState,
122+
},
123+
}
124+
sn := snap.New(lg, snapDir)
125+
if err = sn.SaveSnap(raftSnap); err != nil {
126+
return err
127+
}
128+
129+
// save WAL snapshot record
130+
w, err := wal.Open(lg, walDir, latestWALSnap)
131+
if err != nil {
132+
return err
133+
}
134+
defer w.Close()
135+
// We must read all records to locate the tail of the last valid WAL file.
136+
if _, _, _, err = w.ReadAll(); err != nil {
137+
return err
138+
}
139+
140+
return w.SaveSnapshot(walpb.Snapshot{Index: ci, Term: term, ConfState: &confState})
141+
}
142+
143+
func getVotersAndLearners(cl *membership.RaftCluster) ([]uint64, []uint64) {
144+
var (
145+
voters []uint64
146+
learners []uint64
147+
)
148+
for _, m := range cl.Members() {
149+
if m.IsLearner {
150+
learners = append(learners, uint64(m.ID))
151+
continue
152+
}
153+
154+
voters = append(voters, uint64(m.ID))
155+
}
156+
157+
return voters, learners
158+
}

etcdutl/etcdutl/migrate_command.go

+61-19
Original file line numberDiff line numberDiff line change
@@ -74,8 +74,9 @@ func (o *migrateOptions) AddFlags(cmd *cobra.Command) {
7474

7575
func (o *migrateOptions) Config() (*migrateConfig, error) {
7676
c := &migrateConfig{
77-
force: o.force,
78-
lg: GetLogger(),
77+
force: o.force,
78+
dataDir: o.dataDir,
79+
lg: GetLogger(),
7980
}
8081
var err error
8182
dotCount := strings.Count(o.targetVersion, ".")
@@ -90,47 +91,75 @@ func (o *migrateOptions) Config() (*migrateConfig, error) {
9091
return nil, fmt.Errorf(`target version %q not supported. Minimal "3.5"`, storageVersionToString(c.targetVersion))
9192
}
9293

93-
dbPath := datadir.ToBackendFileName(o.dataDir)
94-
c.be = backend.NewDefaultBackend(GetLogger(), dbPath)
94+
return c, nil
95+
}
96+
97+
type migrateConfig struct {
98+
lg *zap.Logger
99+
be backend.Backend
100+
targetVersion *semver.Version
101+
walVersion schema.WALVersion
102+
dataDir string
103+
force bool
104+
}
95105

96-
walPath := datadir.ToWALDir(o.dataDir)
97-
walSnap, err := getLatestWALSnap(c.lg, o.dataDir)
106+
func (c *migrateConfig) finalize() error {
107+
walPath := datadir.ToWALDir(c.dataDir)
108+
walSnap, err := getLatestWALSnap(c.lg, c.dataDir)
98109
if err != nil {
99-
return nil, fmt.Errorf("failed to get the lastest snapshot: %w", err)
110+
return fmt.Errorf("failed to get the lastest snapshot: %w", err)
100111
}
101112
w, err := wal.OpenForRead(c.lg, walPath, walSnap)
102113
if err != nil {
103-
return nil, fmt.Errorf(`failed to open wal: %w`, err)
114+
return fmt.Errorf(`failed to open wal: %w`, err)
104115
}
105116
defer w.Close()
106117
c.walVersion, err = wal.ReadWALVersion(w)
107118
if err != nil {
108-
return nil, fmt.Errorf(`failed to read wal: %w`, err)
119+
return fmt.Errorf(`failed to read wal: %w`, err)
109120
}
110121

111-
return c, nil
112-
}
113-
114-
type migrateConfig struct {
115-
lg *zap.Logger
116-
be backend.Backend
117-
targetVersion *semver.Version
118-
walVersion schema.WALVersion
119-
force bool
122+
return nil
120123
}
121124

122125
func migrateCommandFunc(c *migrateConfig) error {
126+
dbPath := datadir.ToBackendFileName(c.dataDir)
127+
c.be = backend.NewDefaultBackend(GetLogger(), dbPath)
123128
defer c.be.Close()
129+
124130
tx := c.be.BatchTx()
125131
current, err := schema.DetectSchemaVersion(c.lg, c.be.ReadTx())
126132
if err != nil {
127-
c.lg.Error("failed to detect storage version. Please make sure you are using data dir from etcd v3.5 and older")
133+
c.lg.Error("failed to detect storage version. Please make sure you are using data dir from etcd v3.5 and older", zap.Error(err))
128134
return err
129135
}
130136
if current == *c.targetVersion {
131137
c.lg.Info("storage version up-to-date", zap.String("storage-version", storageVersionToString(&current)))
132138
return nil
133139
}
140+
141+
downgrade, err := isDowngrade(c.lg, tx, c.targetVersion)
142+
if err != nil {
143+
return err
144+
}
145+
if downgrade {
146+
// Update cluster version
147+
be := schema.NewMembershipBackend(c.lg, c.be)
148+
be.MustSaveClusterVersionToBackend(c.targetVersion)
149+
150+
// forcibly create a v2 snapshot file
151+
// TODO: remove in 3.8
152+
if err = createV2SnapshotFromV3Store(c.dataDir, c.be); err != nil {
153+
c.lg.Error("Failed to create v2 snapshot file", zap.Error(err))
154+
return err
155+
}
156+
}
157+
158+
if err = c.finalize(); err != nil {
159+
c.lg.Error("Failed to finalize config", zap.Error(err))
160+
return err
161+
}
162+
134163
err = schema.Migrate(c.lg, tx, c.walVersion, *c.targetVersion)
135164
if err != nil {
136165
if !c.force {
@@ -139,7 +168,9 @@ func migrateCommandFunc(c *migrateConfig) error {
139168
c.lg.Info("normal migrate failed, trying with force", zap.Error(err))
140169
migrateForce(c.lg, tx, c.targetVersion)
141170
}
171+
142172
c.be.ForceCommit()
173+
143174
return nil
144175
}
145176

@@ -156,6 +187,17 @@ func migrateForce(lg *zap.Logger, tx backend.BatchTx, target *semver.Version) {
156187
}
157188
}
158189

190+
func isDowngrade(lg *zap.Logger, tx backend.BatchTx, target *semver.Version) (bool, error) {
191+
tx.LockOutsideApply()
192+
defer tx.Unlock()
193+
ver, err := schema.UnsafeDetectSchemaVersion(lg, tx)
194+
if err != nil {
195+
lg.Error("Failed to detect current storage version", zap.Error(err))
196+
return false, err
197+
}
198+
return target.LessThan(ver), nil
199+
}
200+
159201
func storageVersionToString(ver *semver.Version) string {
160202
return fmt.Sprintf("%d.%d", ver.Major, ver.Minor)
161203
}

server/etcdserver/api/membership/cluster.go

+11-5
Original file line numberDiff line numberDiff line change
@@ -256,22 +256,28 @@ func (c *RaftCluster) SetVersionChangedNotifier(n *notify.Notifier) {
256256
c.versionChanged = n
257257
}
258258

259-
func (c *RaftCluster) Recover(onSet func(*zap.Logger, *semver.Version)) {
260-
c.Lock()
261-
defer c.Unlock()
262-
259+
func (c *RaftCluster) UnsafeLoad() {
263260
if c.be != nil {
264261
c.version = c.be.ClusterVersionFromBackend()
265262
c.members, c.removed = c.be.MustReadMembersFromBackend()
266263
} else {
267264
c.version = clusterVersionFromStore(c.lg, c.v2store)
268265
c.members, c.removed = membersFromStore(c.lg, c.v2store)
269266
}
270-
c.buildMembershipMetric()
271267

272268
if c.be != nil {
273269
c.downgradeInfo = c.be.DowngradeInfoFromBackend()
274270
}
271+
}
272+
273+
func (c *RaftCluster) Recover(onSet func(*zap.Logger, *semver.Version)) {
274+
c.Lock()
275+
defer c.Unlock()
276+
277+
c.UnsafeLoad()
278+
279+
c.buildMembershipMetric()
280+
275281
sv := semver.Must(semver.NewVersion(version.Version))
276282
if c.downgradeInfo != nil && c.downgradeInfo.Enabled {
277283
c.lg.Info(

0 commit comments

Comments
 (0)