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 }