github.com/cockroachdb/cockroach@v20.2.0-alpha.1+incompatible/pkg/kv/kvserver/client_replica_backpressure_test.go (about)

     1  // Copyright 2020 The Cockroach Authors.
     2  //
     3  // Use of this software is governed by the Business Source License
     4  // included in the file licenses/BSL.txt.
     5  //
     6  // As of the Change Date specified in that file, in accordance with
     7  // the Business Source License, use of this software will be governed
     8  // by the Apache License, Version 2.0, included in the file
     9  // licenses/APL.txt.
    10  
    11  package kvserver_test
    12  
    13  import (
    14  	"context"
    15  	"fmt"
    16  	"net/url"
    17  	"sync"
    18  	"sync/atomic"
    19  	"testing"
    20  	"time"
    21  
    22  	"github.com/cockroachdb/cockroach/pkg/base"
    23  	"github.com/cockroachdb/cockroach/pkg/keys"
    24  	"github.com/cockroachdb/cockroach/pkg/kv/kvserver"
    25  	"github.com/cockroachdb/cockroach/pkg/roachpb"
    26  	"github.com/cockroachdb/cockroach/pkg/testutils"
    27  	"github.com/cockroachdb/cockroach/pkg/testutils/sqlutils"
    28  	"github.com/cockroachdb/cockroach/pkg/testutils/testcluster"
    29  	"github.com/cockroachdb/cockroach/pkg/util/leaktest"
    30  	"github.com/cockroachdb/cockroach/pkg/util/randutil"
    31  	"github.com/cockroachdb/errors"
    32  	"github.com/jackc/pgx"
    33  	"github.com/stretchr/testify/require"
    34  )
    35  
    36  // Test that mitigations to backpressure when reducing the range size work.
    37  func TestBackpressureNotAppliedWhenReducingRangeSize(t *testing.T) {
    38  	defer leaktest.AfterTest(t)()
    39  
    40  	rRand, _ := randutil.NewPseudoRand()
    41  	ctx := context.Background()
    42  
    43  	// Some arbitrary data sizes we'll load into a table and then use to derive
    44  	// range size parameters. We want something not too tiny but also not too big
    45  	// that it takes a while to load.
    46  	const (
    47  		rowSize  = 16 << 10  // 16 KiB
    48  		dataSize = 512 << 10 // 512 KiB
    49  		numRows  = dataSize / rowSize
    50  	)
    51  
    52  	// setup will set up a testcluster with a table filled with data. All splits
    53  	// will be blocked until the returned closure is called.
    54  	setup := func(t *testing.T, numServers int) (
    55  		tc *testcluster.TestCluster,
    56  		args base.TestClusterArgs,
    57  		tdb *sqlutils.SQLRunner,
    58  		tablePrefix roachpb.Key,
    59  		unblockSplit func(),
    60  		waitForBlockedRange func(id roachpb.RangeID),
    61  	) {
    62  		// Add a testing knob to block split transactions which we'll enable before
    63  		// we return from setup.
    64  		var allowSplits atomic.Value
    65  		allowSplits.Store(true)
    66  		unblockCh := make(chan struct{}, 1)
    67  		var rangesBlocked sync.Map
    68  		args = base.TestClusterArgs{
    69  			ServerArgs: base.TestServerArgs{
    70  				Knobs: base.TestingKnobs{
    71  					Store: &kvserver.StoreTestingKnobs{
    72  						TestingRequestFilter: func(ctx context.Context, ba roachpb.BatchRequest) *roachpb.Error {
    73  							if ba.Header.Txn != nil && ba.Header.Txn.Name == "split" && !allowSplits.Load().(bool) {
    74  								rangesBlocked.Store(ba.Header.RangeID, true)
    75  								defer rangesBlocked.Delete(ba.Header.RangeID)
    76  								select {
    77  								case <-unblockCh:
    78  									return roachpb.NewError(errors.Errorf("splits disabled"))
    79  								case <-ctx.Done():
    80  									<-ctx.Done()
    81  								}
    82  							}
    83  							return nil
    84  						},
    85  					},
    86  				},
    87  			},
    88  		}
    89  		tc = testcluster.StartTestCluster(t, numServers, args)
    90  		require.NoError(t, tc.WaitForFullReplication())
    91  
    92  		// Create the table, split it off, and load it up with data.
    93  		tdb = sqlutils.MakeSQLRunner(tc.ServerConn(0))
    94  		tdb.Exec(t, "CREATE TABLE foo (k INT PRIMARY KEY, v BYTES NOT NULL)")
    95  
    96  		var tableID int
    97  		tdb.QueryRow(t, "SELECT table_id FROM crdb_internal.tables WHERE name = 'foo'").Scan(&tableID)
    98  		require.NotEqual(t, 0, tableID)
    99  		tablePrefix = keys.SystemSQLCodec.TablePrefix(uint32(tableID))
   100  		tc.SplitRangeOrFatal(t, tablePrefix)
   101  		require.NoError(t, tc.WaitForSplitAndInitialization(tablePrefix))
   102  
   103  		for i := 0; i < dataSize/rowSize; i++ {
   104  			tdb.Exec(t, "UPSERT INTO foo VALUES ($1, $2)",
   105  				rRand.Intn(numRows), randutil.RandBytes(rRand, rowSize))
   106  		}
   107  
   108  		// Block splits and return.
   109  		allowSplits.Store(false)
   110  		var closeOnce sync.Once
   111  		unblockSplit = func() {
   112  			closeOnce.Do(func() {
   113  				allowSplits.Store(true)
   114  				close(unblockCh)
   115  			})
   116  		}
   117  		waitForBlockedRange = func(id roachpb.RangeID) {
   118  			testutils.SucceedsSoon(t, func() error {
   119  				if _, ok := rangesBlocked.Load(id); !ok {
   120  					return errors.Errorf("waiting for %v to be blocked", id)
   121  				}
   122  				return nil
   123  			})
   124  		}
   125  		return tc, args, tdb, tablePrefix, unblockSplit, waitForBlockedRange
   126  	}
   127  
   128  	waitForZoneConfig := func(t *testing.T, tc *testcluster.TestCluster, tablePrefix roachpb.Key, exp int64) {
   129  		testutils.SucceedsSoon(t, func() error {
   130  			for i := 0; i < tc.NumServers(); i++ {
   131  				s := tc.Server(i)
   132  				_, r := getFirstStoreReplica(t, s, tablePrefix)
   133  				_, zone := r.DescAndZone()
   134  				if *zone.RangeMaxBytes != exp {
   135  					return fmt.Errorf("expected %d, got %d", exp, *zone.RangeMaxBytes)
   136  				}
   137  			}
   138  			return nil
   139  		})
   140  	}
   141  
   142  	moveTableToNewStore := func(t *testing.T, tc *testcluster.TestCluster, args base.TestClusterArgs, tablePrefix roachpb.Key) {
   143  		tc.AddServer(t, args.ServerArgs)
   144  		testutils.SucceedsSoon(t, func() error {
   145  			desc, err := tc.LookupRange(tablePrefix)
   146  			require.NoError(t, err)
   147  			voters := desc.Replicas().Voters()
   148  			if len(voters) == 1 && voters[0].NodeID == tc.Server(1).NodeID() {
   149  				return nil
   150  			}
   151  			if len(voters) == 1 {
   152  				desc, err = tc.AddReplicas(tablePrefix, tc.Target(1))
   153  				if err != nil {
   154  					return err
   155  				}
   156  			}
   157  			if err = tc.TransferRangeLease(desc, tc.Target(1)); err != nil {
   158  				return err
   159  			}
   160  			_, err = tc.RemoveReplicas(tablePrefix, tc.Target(0))
   161  			return err
   162  		})
   163  	}
   164  
   165  	t.Run("no backpressure when much larger on existing node", func(t *testing.T) {
   166  		tc, _, tdb, tablePrefix, unblockSplits, _ := setup(t, 1)
   167  		defer tc.Stopper().Stop(ctx)
   168  		defer unblockSplits()
   169  
   170  		tdb.Exec(t, "ALTER TABLE foo CONFIGURE ZONE USING "+
   171  			"range_max_bytes = $1, range_min_bytes = $2", dataSize/5, dataSize/10)
   172  		waitForZoneConfig(t, tc, tablePrefix, dataSize/5)
   173  
   174  		// Don't observe backpressure.
   175  		tdb.Exec(t, "UPSERT INTO foo VALUES ($1, $2)",
   176  			rRand.Intn(10000000), randutil.RandBytes(rRand, rowSize))
   177  	})
   178  
   179  	t.Run("no backpressure when much larger on new node", func(t *testing.T) {
   180  		tc, args, tdb, tablePrefix, unblockSplits, _ := setup(t, 1)
   181  		defer tc.Stopper().Stop(ctx)
   182  		defer unblockSplits()
   183  
   184  		// We didn't want to have to load too much data into these ranges because
   185  		// it makes the testing slower so let's lower the threshold at which we'll
   186  		// consider the range to be way over the backpressure limit from megabytes
   187  		// down to kilobytes.
   188  		tdb.Exec(t, "SET CLUSTER SETTING kv.range.backpressure_byte_tolerance = '1 KiB'")
   189  
   190  		tdb.Exec(t, "ALTER TABLE foo CONFIGURE ZONE USING "+
   191  			"range_max_bytes = $1, range_min_bytes = $2", dataSize/5, dataSize/10)
   192  		waitForZoneConfig(t, tc, tablePrefix, dataSize/5)
   193  
   194  		// Then we'll add a new server and move the table there.
   195  		moveTableToNewStore(t, tc, args, tablePrefix)
   196  
   197  		// Don't observe backpressure.
   198  		tdb.Exec(t, "UPSERT INTO foo VALUES ($1, $2)",
   199  			rRand.Intn(10000000), randutil.RandBytes(rRand, rowSize))
   200  	})
   201  
   202  	t.Run("no backpressure when near limit on existing node", func(t *testing.T) {
   203  		tc, _, tdb, tablePrefix, unblockSplits, _ := setup(t, 1)
   204  		defer tc.Stopper().Stop(ctx)
   205  		defer unblockSplits()
   206  
   207  		// We didn't want to have to load too much data into these ranges because
   208  		// it makes the testing slower so let's lower the threshold at which we'll
   209  		// consider the range to be way over the backpressure limit from megabytes
   210  		// down to kilobytes.
   211  		tdb.Exec(t, "SET CLUSTER SETTING kv.range.backpressure_byte_tolerance = '128 KiB'")
   212  
   213  		// Now we'll change the range_max_bytes to be half the range size less a bit
   214  		// so that the range size is above the backpressure threshold but within the
   215  		// backpressureByteTolerance. We won't see backpressure because the range
   216  		// will remember its previous zone config setting.
   217  		s, repl := getFirstStoreReplica(t, tc.Server(0), tablePrefix.Next())
   218  		newMax := repl.GetMVCCStats().Total()/2 - 32<<10
   219  		newMin := newMax / 4
   220  		tdb.Exec(t, "ALTER TABLE foo CONFIGURE ZONE USING "+
   221  			"range_max_bytes = $1, range_min_bytes = $2", newMax, newMin)
   222  		waitForZoneConfig(t, tc, tablePrefix, newMax)
   223  
   224  		// Don't observe backpressure because we remember the previous max size on
   225  		// this node.
   226  		tdb.Exec(t, "UPSERT INTO foo VALUES ($1, $2)",
   227  			rRand.Intn(10000000), randutil.RandBytes(rRand, rowSize))
   228  
   229  		// Allow one split to occur and make sure that the remembered value is
   230  		// cleared.
   231  		unblockSplits()
   232  
   233  		testutils.SucceedsSoon(t, func() error {
   234  			if size := repl.LargestPreviousMaxRangeSizeBytes(); size != 0 {
   235  				_ = s.ForceSplitScanAndProcess()
   236  				return errors.Errorf("expected LargestPreviousMaxRangeSizeBytes to be 0, got %d", size)
   237  			}
   238  			return nil
   239  		})
   240  	})
   241  	// This case is very similar to the above case but differs in that the range
   242  	// is moved to a new node after the range size is decreased. This new node
   243  	// never knew about the old, larger range size, and thus will backpressure
   244  	// writes. This is the one case that is not mitigated by either
   245  	// backpressureByteTolerance or largestPreviousMaxRangeSizeBytes.
   246  	t.Run("backpressure when near limit on new node", func(t *testing.T) {
   247  		tc, args, tdb, tablePrefix, unblockSplits, waitForBlocked := setup(t, 1)
   248  		defer tc.Stopper().Stop(ctx)
   249  		defer unblockSplits()
   250  
   251  		// Now we'll change the range_max_bytes to be half the range size less a
   252  		// bit. This is the range where we expect to observe backpressure.
   253  		_, repl := getFirstStoreReplica(t, tc.Server(0), tablePrefix.Next())
   254  		newMax := repl.GetMVCCStats().Total()/2 - 32<<10
   255  		newMin := newMax / 4
   256  		tdb.Exec(t, "ALTER TABLE foo CONFIGURE ZONE USING "+
   257  			"range_max_bytes = $1, range_min_bytes = $2", newMax, newMin)
   258  		waitForZoneConfig(t, tc, tablePrefix, newMax)
   259  
   260  		// Then we'll add a new server and move the table there.
   261  		moveTableToNewStore(t, tc, args, tablePrefix)
   262  
   263  		s, repl := getFirstStoreReplica(t, tc.Server(1), tablePrefix)
   264  		s.SetReplicateQueueActive(false)
   265  		require.Len(t, repl.Desc().Replicas().All(), 1)
   266  		// We really need to make sure that the split queue has hit this range,
   267  		// otherwise we'll fail to backpressure.
   268  		go func() { _ = s.ForceSplitScanAndProcess() }()
   269  		waitForBlocked(repl.RangeID)
   270  
   271  		// Observe backpressure now that the range is just over the limit.
   272  		// Use pgx so that cancellation does something reasonable.
   273  		url, cleanup := sqlutils.PGUrl(t, tc.Server(1).ServingSQLAddr(), "", url.User("root"))
   274  		defer cleanup()
   275  		conf, err := pgx.ParseConnectionString(url.String())
   276  		require.NoError(t, err)
   277  		c, err := pgx.Connect(conf)
   278  		require.NoError(t, err)
   279  		ctxWithCancel, cancel := context.WithCancel(ctx)
   280  		defer cancel()
   281  		upsertErrCh := make(chan error)
   282  		go func() {
   283  			_, err := c.ExecEx(ctxWithCancel, "UPSERT INTO foo VALUES ($1, $2)",
   284  				nil /* options */, rRand.Intn(numRows), randutil.RandBytes(rRand, rowSize))
   285  			upsertErrCh <- err
   286  		}()
   287  
   288  		select {
   289  		case <-time.After(10 * time.Millisecond):
   290  			cancel()
   291  		case err := <-upsertErrCh:
   292  			t.Fatalf("expected no error because the request should hang, got %v", err)
   293  		}
   294  		require.Equal(t, context.Canceled, <-upsertErrCh)
   295  	})
   296  }