vitess.io/vitess@v0.16.2/go/vt/vttablet/tabletserver/txthrottler/tx_throttler.go (about)

     1  /*
     2  Copyright 2019 The Vitess Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreedto in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package txthrottler
    18  
    19  import (
    20  	"fmt"
    21  	"strings"
    22  	"sync"
    23  	"time"
    24  
    25  	"google.golang.org/protobuf/proto"
    26  
    27  	"google.golang.org/protobuf/encoding/prototext"
    28  
    29  	"context"
    30  
    31  	"vitess.io/vitess/go/vt/discovery"
    32  	"vitess.io/vitess/go/vt/log"
    33  	"vitess.io/vitess/go/vt/throttler"
    34  	"vitess.io/vitess/go/vt/topo"
    35  	"vitess.io/vitess/go/vt/vttablet/tabletserver/tabletenv"
    36  
    37  	querypb "vitess.io/vitess/go/vt/proto/query"
    38  	throttlerdatapb "vitess.io/vitess/go/vt/proto/throttlerdata"
    39  	topodatapb "vitess.io/vitess/go/vt/proto/topodata"
    40  )
    41  
    42  // TxThrottler throttles transactions based on replication lag.
    43  // It's a thin wrapper around the throttler found in vitess/go/vt/throttler.
    44  // It uses a discovery.HealthCheck to send replication-lag updates to the wrapped throttler.
    45  //
    46  // Intended Usage:
    47  //
    48  //	// Assuming topoServer is a topo.Server variable pointing to a Vitess topology server.
    49  //	t := NewTxThrottler(config, topoServer)
    50  //
    51  //	// A transaction throttler must be opened before its first use:
    52  //	if err := t.Open(keyspace, shard); err != nil {
    53  //	  return err
    54  //	}
    55  //
    56  //	// Checking whether to throttle can be done as follows before starting a transaction.
    57  //	if t.Throttle() {
    58  //	  return fmt.Errorf("Transaction throttled!")
    59  //	} else {
    60  //	  // execute transaction.
    61  //	}
    62  //
    63  //	// To release the resources used by the throttler the caller should call Close().
    64  //	t.Close()
    65  //
    66  // A TxThrottler object is generally not thread-safe: at any given time at most one goroutine should
    67  // be executing a method. The only exception is the 'Throttle' method where multiple goroutines are
    68  // allowed to execute it concurrently.
    69  type TxThrottler struct {
    70  	// config stores the transaction throttler's configuration.
    71  	// It is populated in NewTxThrottler and is not modified
    72  	// since.
    73  	config *txThrottlerConfig
    74  
    75  	// state holds an open transaction throttler state. It is nil
    76  	// if the TransactionThrottler is closed.
    77  	state *txThrottlerState
    78  
    79  	target *querypb.Target
    80  }
    81  
    82  // NewTxThrottler tries to construct a TxThrottler from the
    83  // relevant fields in the tabletenv.Config object. It returns a disabled TxThrottler if
    84  // any error occurs.
    85  // This function calls tryCreateTxThrottler that does the actual creation work
    86  // and returns an error if one occurred.
    87  func NewTxThrottler(config *tabletenv.TabletConfig, topoServer *topo.Server) *TxThrottler {
    88  	txThrottler, err := tryCreateTxThrottler(config, topoServer)
    89  	if err != nil {
    90  		log.Errorf("Error creating transaction throttler. Transaction throttling will"+
    91  			" be disabled. Error: %v", err)
    92  		txThrottler, err = newTxThrottler(&txThrottlerConfig{enabled: false})
    93  		if err != nil {
    94  			panic("BUG: Can't create a disabled transaction throttler")
    95  		}
    96  	} else {
    97  		log.Infof("Initialized transaction throttler with config: %+v", txThrottler.config)
    98  	}
    99  	return txThrottler
   100  }
   101  
   102  // InitDBConfig initializes the target parameters for the throttler.
   103  func (t *TxThrottler) InitDBConfig(target *querypb.Target) {
   104  	t.target = proto.Clone(target).(*querypb.Target)
   105  }
   106  
   107  func tryCreateTxThrottler(config *tabletenv.TabletConfig, topoServer *topo.Server) (*TxThrottler, error) {
   108  	if !config.EnableTxThrottler {
   109  		return newTxThrottler(&txThrottlerConfig{enabled: false})
   110  	}
   111  
   112  	var throttlerConfig throttlerdatapb.Configuration
   113  	if err := prototext.Unmarshal([]byte(config.TxThrottlerConfig), &throttlerConfig); err != nil {
   114  		return nil, err
   115  	}
   116  
   117  	// Clone tsv.TxThrottlerHealthCheckCells so that we don't assume tsv.TxThrottlerHealthCheckCells
   118  	// is immutable.
   119  	healthCheckCells := make([]string, len(config.TxThrottlerHealthCheckCells))
   120  	copy(healthCheckCells, config.TxThrottlerHealthCheckCells)
   121  
   122  	return newTxThrottler(&txThrottlerConfig{
   123  		enabled:          true,
   124  		topoServer:       topoServer,
   125  		throttlerConfig:  &throttlerConfig,
   126  		healthCheckCells: healthCheckCells,
   127  	})
   128  }
   129  
   130  // txThrottlerConfig holds the parameters that need to be
   131  // passed when constructing a TxThrottler object.
   132  type txThrottlerConfig struct {
   133  	// enabled is true if the transaction throttler is enabled. All methods
   134  	// of a disabled transaction throttler do nothing and Throttle() always
   135  	// returns false.
   136  	enabled bool
   137  
   138  	topoServer      *topo.Server
   139  	throttlerConfig *throttlerdatapb.Configuration
   140  	// healthCheckCells stores the cell names in which running vttablets will be monitored for
   141  	// replication lag.
   142  	healthCheckCells []string
   143  }
   144  
   145  // ThrottlerInterface defines the public interface that is implemented by go/vt/throttler.Throttler
   146  // It is only used here to allow mocking out a throttler object.
   147  type ThrottlerInterface interface {
   148  	Throttle(threadID int) time.Duration
   149  	ThreadFinished(threadID int)
   150  	Close()
   151  	MaxRate() int64
   152  	SetMaxRate(rate int64)
   153  	RecordReplicationLag(time time.Time, th *discovery.TabletHealth)
   154  	GetConfiguration() *throttlerdatapb.Configuration
   155  	UpdateConfiguration(configuration *throttlerdatapb.Configuration, copyZeroValues bool) error
   156  	ResetConfiguration()
   157  }
   158  
   159  // TopologyWatcherInterface defines the public interface that is implemented by
   160  // discovery.LegacyTopologyWatcher. It is only used here to allow mocking out
   161  // go/vt/discovery.LegacyTopologyWatcher.
   162  type TopologyWatcherInterface interface {
   163  	Start()
   164  	Stop()
   165  }
   166  
   167  // txThrottlerState holds the state of an open TxThrottler object.
   168  type txThrottlerState struct {
   169  	// throttleMu serializes calls to throttler.Throttler.Throttle(threadId).
   170  	// That method is required to be called in serial for each threadId.
   171  	throttleMu      sync.Mutex
   172  	throttler       ThrottlerInterface
   173  	stopHealthCheck context.CancelFunc
   174  
   175  	healthCheck      discovery.HealthCheck
   176  	topologyWatchers []TopologyWatcherInterface
   177  }
   178  
   179  // These vars store the functions used to create the topo server, healthcheck,
   180  // topology watchers and go/vt/throttler. These are provided here so that they can be overridden
   181  // in tests to generate mocks.
   182  type healthCheckFactoryFunc func(topoServer *topo.Server, cell string, cellsToWatch []string) discovery.HealthCheck
   183  type topologyWatcherFactoryFunc func(topoServer *topo.Server, hc discovery.HealthCheck, cell, keyspace, shard string, refreshInterval time.Duration, topoReadConcurrency int) TopologyWatcherInterface
   184  type throttlerFactoryFunc func(name, unit string, threadCount int, maxRate, maxReplicationLag int64) (ThrottlerInterface, error)
   185  
   186  var (
   187  	healthCheckFactory     healthCheckFactoryFunc
   188  	topologyWatcherFactory topologyWatcherFactoryFunc
   189  	throttlerFactory       throttlerFactoryFunc
   190  )
   191  
   192  func init() {
   193  	resetTxThrottlerFactories()
   194  }
   195  
   196  func resetTxThrottlerFactories() {
   197  	healthCheckFactory = func(topoServer *topo.Server, cell string, cellsToWatch []string) discovery.HealthCheck {
   198  		return discovery.NewHealthCheck(context.Background(), discovery.DefaultHealthCheckRetryDelay, discovery.DefaultHealthCheckTimeout, topoServer, cell, strings.Join(cellsToWatch, ","))
   199  	}
   200  	topologyWatcherFactory = func(topoServer *topo.Server, hc discovery.HealthCheck, cell, keyspace, shard string, refreshInterval time.Duration, topoReadConcurrency int) TopologyWatcherInterface {
   201  		return discovery.NewCellTabletsWatcher(context.Background(), topoServer, hc, discovery.NewFilterByKeyspace([]string{keyspace}), cell, refreshInterval, true, topoReadConcurrency)
   202  	}
   203  	throttlerFactory = func(name, unit string, threadCount int, maxRate, maxReplicationLag int64) (ThrottlerInterface, error) {
   204  		return throttler.NewThrottler(name, unit, threadCount, maxRate, maxReplicationLag)
   205  	}
   206  }
   207  
   208  // TxThrottlerName is the name the wrapped go/vt/throttler object will be registered with
   209  // go/vt/throttler.GlobalManager.
   210  const TxThrottlerName = "TransactionThrottler"
   211  
   212  func newTxThrottler(config *txThrottlerConfig) (*TxThrottler, error) {
   213  	if config.enabled {
   214  		// Verify config.
   215  		err := throttler.MaxReplicationLagModuleConfig{Configuration: config.throttlerConfig}.Verify()
   216  		if err != nil {
   217  			return nil, err
   218  		}
   219  		if len(config.healthCheckCells) == 0 {
   220  			return nil, fmt.Errorf("empty healthCheckCells given. %+v", config)
   221  		}
   222  	}
   223  	return &TxThrottler{
   224  		config: config,
   225  	}, nil
   226  }
   227  
   228  // Open opens the transaction throttler. It must be called prior to 'Throttle'.
   229  func (t *TxThrottler) Open() error {
   230  	if !t.config.enabled {
   231  		return nil
   232  	}
   233  	if t.state != nil {
   234  		return nil
   235  	}
   236  	log.Info("TxThrottler: opening")
   237  	var err error
   238  	t.state, err = newTxThrottlerState(t.config, t.target.Keyspace, t.target.Shard, t.target.Cell)
   239  	return err
   240  }
   241  
   242  // Close closes the TxThrottler object and releases resources.
   243  // It should be called after the throttler is no longer needed.
   244  // It's ok to call this method on a closed throttler--in which case the method does nothing.
   245  func (t *TxThrottler) Close() {
   246  	if !t.config.enabled {
   247  		return
   248  	}
   249  	if t.state == nil {
   250  		return
   251  	}
   252  	t.state.deallocateResources()
   253  	t.state = nil
   254  	log.Info("TxThrottler: closed")
   255  }
   256  
   257  // Throttle should be called before a new transaction is started.
   258  // It returns true if the transaction should not proceed (the caller
   259  // should back off). Throttle requires that Open() was previously called
   260  // successfully.
   261  func (t *TxThrottler) Throttle() (result bool) {
   262  	if !t.config.enabled {
   263  		return false
   264  	}
   265  	if t.state == nil {
   266  		panic("BUG: Throttle() called on a closed TxThrottler")
   267  	}
   268  	return t.state.throttle()
   269  }
   270  
   271  func newTxThrottlerState(config *txThrottlerConfig, keyspace, shard, cell string) (*txThrottlerState, error) {
   272  	t, err := throttlerFactory(
   273  		TxThrottlerName,
   274  		"TPS",                           /* unit */
   275  		1,                               /* threadCount */
   276  		throttler.MaxRateModuleDisabled, /* maxRate */
   277  		config.throttlerConfig.MaxReplicationLagSec /* maxReplicationLag */)
   278  	if err != nil {
   279  		return nil, err
   280  	}
   281  	if err := t.UpdateConfiguration(config.throttlerConfig, true /* copyZeroValues */); err != nil {
   282  		t.Close()
   283  		return nil, err
   284  	}
   285  	result := &txThrottlerState{
   286  		throttler: t,
   287  	}
   288  	createTxThrottlerHealthCheck(config, result, cell)
   289  
   290  	result.topologyWatchers = make(
   291  		[]TopologyWatcherInterface, 0, len(config.healthCheckCells))
   292  	for _, cell := range config.healthCheckCells {
   293  		result.topologyWatchers = append(
   294  			result.topologyWatchers,
   295  			topologyWatcherFactory(
   296  				config.topoServer,
   297  				result.healthCheck,
   298  				cell,
   299  				keyspace,
   300  				shard,
   301  				discovery.DefaultTopologyWatcherRefreshInterval,
   302  				discovery.DefaultTopoReadConcurrency))
   303  	}
   304  	return result, nil
   305  }
   306  
   307  func createTxThrottlerHealthCheck(config *txThrottlerConfig, result *txThrottlerState, cell string) {
   308  	ctx, cancel := context.WithCancel(context.Background())
   309  	result.stopHealthCheck = cancel
   310  	result.healthCheck = healthCheckFactory(config.topoServer, cell, config.healthCheckCells)
   311  	ch := result.healthCheck.Subscribe()
   312  	go func(ctx context.Context) {
   313  		for {
   314  			select {
   315  			case <-ctx.Done():
   316  				return
   317  			case th := <-ch:
   318  				result.StatsUpdate(th)
   319  			}
   320  		}
   321  	}(ctx)
   322  }
   323  
   324  func (ts *txThrottlerState) throttle() bool {
   325  	if ts.throttler == nil {
   326  		panic("BUG: throttle called after deallocateResources was called.")
   327  	}
   328  	// Serialize calls to ts.throttle.Throttle()
   329  	ts.throttleMu.Lock()
   330  	defer ts.throttleMu.Unlock()
   331  	return ts.throttler.Throttle(0 /* threadId */) > 0
   332  }
   333  
   334  func (ts *txThrottlerState) deallocateResources() {
   335  	// We don't really need to nil out the fields here
   336  	// as deallocateResources is not expected to be called
   337  	// more than once, but it doesn't hurt to do so.
   338  	for _, watcher := range ts.topologyWatchers {
   339  		watcher.Stop()
   340  	}
   341  	ts.topologyWatchers = nil
   342  
   343  	ts.healthCheck.Close()
   344  	ts.healthCheck = nil
   345  
   346  	// After ts.healthCheck is closed txThrottlerState.StatsUpdate() is guaranteed not
   347  	// to be executing, so we can safely close the throttler.
   348  	ts.throttler.Close()
   349  	ts.throttler = nil
   350  }
   351  
   352  // StatsUpdate updates the health of a tablet with the given healthcheck.
   353  func (ts *txThrottlerState) StatsUpdate(tabletStats *discovery.TabletHealth) {
   354  	// Ignore PRIMARY and RDONLY stats.
   355  	// We currently do not monitor RDONLY tablets for replication lag. RDONLY tablets are not
   356  	// candidates for becoming primary during failover, and it's acceptable to serve somewhat
   357  	// stale date from these.
   358  	// TODO(erez): If this becomes necessary, we can add a configuration option that would
   359  	// determine whether we consider RDONLY tablets here, as well.
   360  	if tabletStats.Target.TabletType != topodatapb.TabletType_REPLICA {
   361  		return
   362  	}
   363  	ts.throttler.RecordReplicationLag(time.Now(), tabletStats)
   364  }