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 }