vitess.io/vitess@v0.16.2/go/vt/vttablet/tabletserver/repltracker/writer.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 agreed to 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 repltracker
    18  
    19  import (
    20  	"fmt"
    21  	"sync"
    22  	"sync/atomic"
    23  	"time"
    24  
    25  	"google.golang.org/protobuf/proto"
    26  
    27  	"context"
    28  
    29  	"vitess.io/vitess/go/sqltypes"
    30  	"vitess.io/vitess/go/timer"
    31  	"vitess.io/vitess/go/vt/dbconnpool"
    32  	"vitess.io/vitess/go/vt/log"
    33  	"vitess.io/vitess/go/vt/logutil"
    34  	"vitess.io/vitess/go/vt/mysqlctl"
    35  	"vitess.io/vitess/go/vt/sqlparser"
    36  	"vitess.io/vitess/go/vt/vttablet/tabletserver/tabletenv"
    37  
    38  	querypb "vitess.io/vitess/go/vt/proto/query"
    39  	topodatapb "vitess.io/vitess/go/vt/proto/topodata"
    40  )
    41  
    42  const (
    43  	sqlUpsertHeartbeat = "INSERT INTO %s.heartbeat (ts, tabletUid, keyspaceShard) VALUES (%a, %a, %a) ON DUPLICATE KEY UPDATE ts=VALUES(ts), tabletUid=VALUES(tabletUid)"
    44  )
    45  
    46  // heartbeatWriter runs on primary tablets and writes heartbeats to the _vt.heartbeat
    47  // table at a regular interval, defined by heartbeat_interval.
    48  type heartbeatWriter struct {
    49  	env tabletenv.Env
    50  
    51  	enabled       bool
    52  	interval      time.Duration
    53  	tabletAlias   *topodatapb.TabletAlias
    54  	keyspaceShard string
    55  	now           func() time.Time
    56  	errorLog      *logutil.ThrottledLogger
    57  
    58  	mu           sync.Mutex
    59  	isOpen       bool
    60  	appPool      *dbconnpool.ConnectionPool
    61  	allPrivsPool *dbconnpool.ConnectionPool
    62  	ticks        *timer.Timer
    63  
    64  	onDemandDuration            time.Duration
    65  	onDemandMu                  sync.Mutex
    66  	concurrentHeartbeatRequests int64
    67  	onDemandRequestTicks        int64
    68  	onDemandLastRequestTick     int64
    69  }
    70  
    71  // newHeartbeatWriter creates a new heartbeatWriter.
    72  func newHeartbeatWriter(env tabletenv.Env, alias *topodatapb.TabletAlias) *heartbeatWriter {
    73  	config := env.Config()
    74  
    75  	// config.EnableLagThrottler is a feature flag for the throttler; if throttler runs, then heartbeat must also run
    76  	if config.ReplicationTracker.Mode != tabletenv.Heartbeat && !config.EnableLagThrottler && config.ReplicationTracker.HeartbeatOnDemandSeconds.Get() == 0 {
    77  		return &heartbeatWriter{}
    78  	}
    79  	heartbeatInterval := config.ReplicationTracker.HeartbeatIntervalSeconds.Get()
    80  	w := &heartbeatWriter{
    81  		env:              env,
    82  		enabled:          true,
    83  		tabletAlias:      proto.Clone(alias).(*topodatapb.TabletAlias),
    84  		now:              time.Now,
    85  		interval:         heartbeatInterval,
    86  		onDemandDuration: config.ReplicationTracker.HeartbeatOnDemandSeconds.Get(),
    87  		ticks:            timer.NewTimer(heartbeatInterval),
    88  		errorLog:         logutil.NewThrottledLogger("HeartbeatWriter", 60*time.Second),
    89  		// We make this pool size 2; to prevent pool exhausted
    90  		// stats from incrementing continually, and causing concern
    91  		appPool:      dbconnpool.NewConnectionPool("HeartbeatWriteAppPool", 2, mysqlctl.DbaIdleTimeout, 0, mysqlctl.PoolDynamicHostnameResolution),
    92  		allPrivsPool: dbconnpool.NewConnectionPool("HeartbeatWriteAllPrivsPool", 2, mysqlctl.DbaIdleTimeout, 0, mysqlctl.PoolDynamicHostnameResolution),
    93  	}
    94  	if w.onDemandDuration > 0 {
    95  		// see RequestHeartbeats() for use of onDemandRequestTicks
    96  		// it's basically a mechnism to rate limit operation RequestHeartbeats().
    97  		// and selectively drop excessive requests.
    98  		w.allowNextHeartbeatRequest()
    99  		go func() {
   100  			// this will allow up to 1 request per (w.onDemandDuration / 4) to pass through
   101  			tick := time.NewTicker(w.onDemandDuration / 4)
   102  			defer tick.Stop()
   103  			for range tick.C {
   104  				w.allowNextHeartbeatRequest()
   105  			}
   106  		}()
   107  	}
   108  	return w
   109  }
   110  
   111  // InitDBConfig initializes the target name for the heartbeatWriter.
   112  func (w *heartbeatWriter) InitDBConfig(target *querypb.Target) {
   113  	w.keyspaceShard = fmt.Sprintf("%s:%s", target.Keyspace, target.Shard)
   114  }
   115  
   116  // Open sets up the heartbeatWriter's db connection and launches the ticker
   117  // responsible for periodically writing to the heartbeat table.
   118  func (w *heartbeatWriter) Open() {
   119  	if !w.enabled {
   120  		return
   121  	}
   122  	w.mu.Lock()
   123  	defer w.mu.Unlock()
   124  	if w.isOpen {
   125  		return
   126  	}
   127  	log.Info("Hearbeat Writer: opening")
   128  
   129  	// We cannot create the database and tables in this Open function
   130  	// since, this is run when a tablet changes to Primary type. The other replicas
   131  	// might not have started replication. So if we run the create commands, it will
   132  	// block this thread, and we could end up in a deadlock.
   133  	// Instead, we try creating the database and table in each tick which runs in a go routine
   134  	// keeping us safe from hanging the main thread.
   135  	w.appPool.Open(w.env.Config().DB.AppWithDB())
   136  	w.allPrivsPool.Open(w.env.Config().DB.AllPrivsWithDB())
   137  	if w.onDemandDuration == 0 {
   138  		w.enableWrites(true)
   139  		// when onDemandDuration > 0 we only enable writes per request
   140  	} else {
   141  		// A one-time kick off of heartbeats upon Open()
   142  		go w.RequestHeartbeats()
   143  	}
   144  
   145  	w.isOpen = true
   146  }
   147  
   148  // Close closes the heartbeatWriter's db connection and stops the periodic ticker.
   149  func (w *heartbeatWriter) Close() {
   150  	if !w.enabled {
   151  		return
   152  	}
   153  	w.mu.Lock()
   154  	defer w.mu.Unlock()
   155  	if !w.isOpen {
   156  		return
   157  	}
   158  
   159  	w.enableWrites(false)
   160  	w.appPool.Close()
   161  	w.allPrivsPool.Close()
   162  	w.isOpen = false
   163  	log.Info("Hearbeat Writer: closed")
   164  }
   165  
   166  // bindHeartbeatVars takes a heartbeat write (insert or update) and
   167  // adds the necessary fields to the query as bind vars. This is done
   168  // to protect ourselves against a badly formed keyspace or shard name.
   169  func (w *heartbeatWriter) bindHeartbeatVars(query string) (string, error) {
   170  	bindVars := map[string]*querypb.BindVariable{
   171  		"ks":  sqltypes.StringBindVariable(w.keyspaceShard),
   172  		"ts":  sqltypes.Int64BindVariable(w.now().UnixNano()),
   173  		"uid": sqltypes.Int64BindVariable(int64(w.tabletAlias.Uid)),
   174  	}
   175  	parsed := sqlparser.BuildParsedQuery(query, "_vt", ":ts", ":uid", ":ks")
   176  	bound, err := parsed.GenerateQuery(bindVars, nil)
   177  	if err != nil {
   178  		return "", err
   179  	}
   180  	return bound, nil
   181  }
   182  
   183  // writeHeartbeat updates the heartbeat row for this tablet with the current time in nanoseconds.
   184  func (w *heartbeatWriter) writeHeartbeat() {
   185  	if err := w.write(); err != nil {
   186  		w.recordError(err)
   187  		return
   188  	}
   189  	writes.Add(1)
   190  }
   191  
   192  func (w *heartbeatWriter) write() error {
   193  	defer w.env.LogError()
   194  	ctx, cancel := context.WithDeadline(context.Background(), w.now().Add(w.interval))
   195  	defer cancel()
   196  	allPrivsConn, err := w.allPrivsPool.Get(ctx)
   197  	if err != nil {
   198  		return err
   199  	}
   200  	defer allPrivsConn.Recycle()
   201  
   202  	upsert, err := w.bindHeartbeatVars(sqlUpsertHeartbeat)
   203  	if err != nil {
   204  		return err
   205  	}
   206  	appConn, err := w.appPool.Get(ctx)
   207  	if err != nil {
   208  		return err
   209  	}
   210  	defer appConn.Recycle()
   211  	_, err = appConn.ExecuteFetch(upsert, 1, false)
   212  	if err != nil {
   213  		return err
   214  	}
   215  	return nil
   216  }
   217  
   218  func (w *heartbeatWriter) recordError(err error) {
   219  	w.errorLog.Errorf("%v", err)
   220  	writeErrors.Add(1)
   221  }
   222  
   223  // enableWrites actives or deactives heartbeat writes
   224  func (w *heartbeatWriter) enableWrites(enable bool) {
   225  	if w.ticks == nil {
   226  		return
   227  	}
   228  	switch enable {
   229  	case true:
   230  		// We must combat a potential race condition: the writer is Open, and a request comes
   231  		// to enableWrites(true), but simultaneously the writes gets Close()d.
   232  		// We must not send any more ticks while the writer is closed.
   233  		go func() {
   234  			w.mu.Lock()
   235  			defer w.mu.Unlock()
   236  			if !w.isOpen {
   237  				return
   238  			}
   239  			w.ticks.Start(w.writeHeartbeat)
   240  		}()
   241  	case false:
   242  		w.ticks.Stop()
   243  		if w.onDemandDuration > 0 {
   244  			// Let the next RequestHeartbeats() go through
   245  			w.allowNextHeartbeatRequest()
   246  		}
   247  	}
   248  }
   249  
   250  // allowNextHeartbeatRequest ensures that the next call to RequestHeartbeats() passes through and
   251  // is not dropped.
   252  func (w *heartbeatWriter) allowNextHeartbeatRequest() {
   253  	atomic.AddInt64(&w.onDemandRequestTicks, 1)
   254  }
   255  
   256  // RequestHeartbeats implements heartbeat.HeartbeatWriter.RequestHeartbeats()
   257  // A client (such as the throttler) may call this function as frequently as it wishes, to request
   258  // for a heartbeat "lease".
   259  // This function will selectively and silently drop some such requests, depending on arrival rate.
   260  // This function is safe to call concurrently from goroutines
   261  func (w *heartbeatWriter) RequestHeartbeats() {
   262  	if w.onDemandDuration == 0 {
   263  		// heartbeats are not by demand. Therefore they are just coming in on their own (if enabled)
   264  		return
   265  	}
   266  	// In this function we're going to create a timer to activate heartbeats by-demand. Creating a timer has a cost.
   267  	// Now, this function can be spammed by clients (the lag throttler). We therefore only allow this function to
   268  	// actually operate once per X seconds (1/4 of onDemandDuration as a reasonable oversampling value):
   269  	if atomic.LoadInt64(&w.onDemandLastRequestTick) >= atomic.LoadInt64(&w.onDemandRequestTicks) {
   270  		// Too many requests. We're dropping this one.
   271  		return
   272  	}
   273  	atomic.StoreInt64(&w.onDemandLastRequestTick, atomic.LoadInt64(&w.onDemandRequestTicks))
   274  
   275  	// OK, the request passed through.
   276  
   277  	w.onDemandMu.Lock()
   278  	defer w.onDemandMu.Unlock()
   279  
   280  	// Now for the actual logic. A client requests heartbeats. If it were only this client, we would
   281  	// activate heartbeats for the duration of onDemandDuration, and then turn heartbeats off.
   282  	// However, there may be multiple clients interested in heartbeats, or maybe the same single client
   283  	// requesting heartbeats again and again. So we keep track of how many _concurrent_ requests we have.
   284  	// We enable heartbeats as soon as we have a request; we turn heartbeats off once
   285  	// we have zero concurrent requests
   286  	w.enableWrites(true)
   287  	w.concurrentHeartbeatRequests++
   288  
   289  	time.AfterFunc(w.onDemandDuration, func() {
   290  		w.onDemandMu.Lock()
   291  		defer w.onDemandMu.Unlock()
   292  		w.concurrentHeartbeatRequests--
   293  		if w.concurrentHeartbeatRequests == 0 {
   294  			// means there are currently no more clients interested in heartbeats
   295  			w.enableWrites(false)
   296  		}
   297  		w.allowNextHeartbeatRequest()
   298  	})
   299  }