// Copyright 2018 The etcd Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package tester import ( "context" "fmt" "math/rand" "reflect" "sync" "sync/atomic" "time" "github.com/coreos/etcd/clientv3" "github.com/coreos/etcd/etcdserver" "github.com/coreos/etcd/etcdserver/api/v3rpc/rpctypes" "github.com/coreos/etcd/functional/rpcpb" "go.uber.org/zap" "golang.org/x/time/rate" "google.golang.org/grpc" ) type keyStresser struct { stype rpcpb.Stresser lg *zap.Logger m *rpcpb.Member keySize int keyLargeSize int keySuffixRange int keyTxnSuffixRange int keyTxnOps int rateLimiter *rate.Limiter wg sync.WaitGroup clientsN int ctx context.Context cancel func() cli *clientv3.Client emu sync.RWMutex ems map[string]int paused bool // atomicModifiedKeys records the number of keys created and deleted by the stresser. atomicModifiedKeys int64 stressTable *stressTable } func (s *keyStresser) Stress() error { var err error s.cli, err = s.m.CreateEtcdClient(grpc.WithBackoffMaxDelay(1 * time.Second)) if err != nil { return fmt.Errorf("%v (%q)", err, s.m.EtcdClientEndpoint) } s.ctx, s.cancel = context.WithCancel(context.Background()) s.wg.Add(s.clientsN) var stressEntries = []stressEntry{ {weight: 0.7, f: newStressPut(s.cli, s.keySuffixRange, s.keySize)}, { weight: 0.7 * float32(s.keySize) / float32(s.keyLargeSize), f: newStressPut(s.cli, s.keySuffixRange, s.keyLargeSize), }, {weight: 0.07, f: newStressRange(s.cli, s.keySuffixRange)}, {weight: 0.07, f: newStressRangeInterval(s.cli, s.keySuffixRange)}, {weight: 0.07, f: newStressDelete(s.cli, s.keySuffixRange)}, {weight: 0.07, f: newStressDeleteInterval(s.cli, s.keySuffixRange)}, } if s.keyTxnSuffixRange > 0 { // adjust to make up ±70% of workloads with writes stressEntries[0].weight = 0.35 stressEntries = append(stressEntries, stressEntry{ weight: 0.35, f: newStressTxn(s.cli, s.keyTxnSuffixRange, s.keyTxnOps), }) } s.stressTable = createStressTable(stressEntries) s.emu.Lock() s.paused = false s.ems = make(map[string]int, 100) s.emu.Unlock() for i := 0; i < s.clientsN; i++ { go s.run() } s.lg.Info( "stress START", zap.String("stress-type", s.stype.String()), zap.String("endpoint", s.m.EtcdClientEndpoint), ) return nil } func (s *keyStresser) run() { defer s.wg.Done() for { if err := s.rateLimiter.Wait(s.ctx); err == context.Canceled { return } // TODO: 10-second is enough timeout to cover leader failure // and immediate leader election. Find out what other cases this // could be timed out. sctx, scancel := context.WithTimeout(s.ctx, 10*time.Second) err, modifiedKeys := s.stressTable.choose()(sctx) scancel() if err == nil { atomic.AddInt64(&s.atomicModifiedKeys, modifiedKeys) continue } switch rpctypes.ErrorDesc(err) { case context.DeadlineExceeded.Error(): // This retries when request is triggered at the same time as // leader failure. When we terminate the leader, the request to // that leader cannot be processed, and times out. Also requests // to followers cannot be forwarded to the old leader, so timing out // as well. We want to keep stressing until the cluster elects a // new leader and start processing requests again. case etcdserver.ErrTimeoutDueToLeaderFail.Error(), etcdserver.ErrTimeout.Error(): // This retries when request is triggered at the same time as // leader failure and follower nodes receive time out errors // from losing their leader. Followers should retry to connect // to the new leader. case etcdserver.ErrStopped.Error(): // one of the etcd nodes stopped from failure injection // case transport.ErrConnClosing.Desc: // // server closed the transport (failure injected node) case rpctypes.ErrNotCapable.Error(): // capability check has not been done (in the beginning) case rpctypes.ErrTooManyRequests.Error(): // hitting the recovering member. case context.Canceled.Error(): // from stresser.Cancel method: return case grpc.ErrClientConnClosing.Error(): // from stresser.Cancel method: return default: s.lg.Warn( "stress run exiting", zap.String("stress-type", s.stype.String()), zap.String("endpoint", s.m.EtcdClientEndpoint), zap.String("error-type", reflect.TypeOf(err).String()), zap.Error(err), ) return } // only record errors before pausing stressers s.emu.Lock() if !s.paused { s.ems[err.Error()]++ } s.emu.Unlock() } } func (s *keyStresser) Pause() map[string]int { return s.Close() } func (s *keyStresser) Close() map[string]int { s.cancel() s.cli.Close() s.wg.Wait() s.emu.Lock() s.paused = true ess := s.ems s.ems = make(map[string]int, 100) s.emu.Unlock() s.lg.Info( "stress STOP", zap.String("stress-type", s.stype.String()), zap.String("endpoint", s.m.EtcdClientEndpoint), ) return ess } func (s *keyStresser) ModifiedKeys() int64 { return atomic.LoadInt64(&s.atomicModifiedKeys) } type stressFunc func(ctx context.Context) (err error, modifiedKeys int64) type stressEntry struct { weight float32 f stressFunc } type stressTable struct { entries []stressEntry sumWeights float32 } func createStressTable(entries []stressEntry) *stressTable { st := stressTable{entries: entries} for _, entry := range st.entries { st.sumWeights += entry.weight } return &st } func (st *stressTable) choose() stressFunc { v := rand.Float32() * st.sumWeights var sum float32 var idx int for i := range st.entries { sum += st.entries[i].weight if sum >= v { idx = i break } } return st.entries[idx].f } func newStressPut(cli *clientv3.Client, keySuffixRange, keySize int) stressFunc { return func(ctx context.Context) (error, int64) { _, err := cli.Put( ctx, fmt.Sprintf("foo%016x", rand.Intn(keySuffixRange)), string(randBytes(keySize)), ) return err, 1 } } func newStressTxn(cli *clientv3.Client, keyTxnSuffixRange, txnOps int) stressFunc { keys := make([]string, keyTxnSuffixRange) for i := range keys { keys[i] = fmt.Sprintf("/k%03d", i) } return writeTxn(cli, keys, txnOps) } func writeTxn(cli *clientv3.Client, keys []string, txnOps int) stressFunc { return func(ctx context.Context) (error, int64) { ks := make(map[string]struct{}, txnOps) for len(ks) != txnOps { ks[keys[rand.Intn(len(keys))]] = struct{}{} } selected := make([]string, 0, txnOps) for k := range ks { selected = append(selected, k) } com, delOp, putOp := getTxnOps(selected[0], "bar00") thenOps := []clientv3.Op{delOp} elseOps := []clientv3.Op{putOp} for i := 1; i < txnOps; i++ { // nested txns k, v := selected[i], fmt.Sprintf("bar%02d", i) com, delOp, putOp = getTxnOps(k, v) txnOp := clientv3.OpTxn( []clientv3.Cmp{com}, []clientv3.Op{delOp}, []clientv3.Op{putOp}, ) thenOps = append(thenOps, txnOp) elseOps = append(elseOps, txnOp) } _, err := cli.Txn(ctx). If(com). Then(thenOps...). Else(elseOps...). Commit() return err, int64(txnOps) } } func getTxnOps(k, v string) ( cmp clientv3.Cmp, dop clientv3.Op, pop clientv3.Op) { // if key exists (version > 0) cmp = clientv3.Compare(clientv3.Version(k), ">", 0) dop = clientv3.OpDelete(k) pop = clientv3.OpPut(k, v) return cmp, dop, pop } func newStressRange(cli *clientv3.Client, keySuffixRange int) stressFunc { return func(ctx context.Context) (error, int64) { _, err := cli.Get(ctx, fmt.Sprintf("foo%016x", rand.Intn(keySuffixRange))) return err, 0 } } func newStressRangeInterval(cli *clientv3.Client, keySuffixRange int) stressFunc { return func(ctx context.Context) (error, int64) { start := rand.Intn(keySuffixRange) end := start + 500 _, err := cli.Get( ctx, fmt.Sprintf("foo%016x", start), clientv3.WithRange(fmt.Sprintf("foo%016x", end)), ) return err, 0 } } func newStressDelete(cli *clientv3.Client, keySuffixRange int) stressFunc { return func(ctx context.Context) (error, int64) { _, err := cli.Delete(ctx, fmt.Sprintf("foo%016x", rand.Intn(keySuffixRange))) return err, 1 } } func newStressDeleteInterval(cli *clientv3.Client, keySuffixRange int) stressFunc { return func(ctx context.Context) (error, int64) { start := rand.Intn(keySuffixRange) end := start + 500 resp, err := cli.Delete(ctx, fmt.Sprintf("foo%016x", start), clientv3.WithRange(fmt.Sprintf("foo%016x", end)), ) if err == nil { return nil, resp.Deleted } return err, 0 } }