Files
tidb/br/pkg/streamhelper/advancer.go
2023-04-12 23:23:01 +08:00

470 lines
14 KiB
Go

// Copyright 2022 PingCAP, Inc. Licensed under Apache-2.0.
package streamhelper
import (
"context"
"strings"
"sync"
"time"
"github.com/pingcap/errors"
backuppb "github.com/pingcap/kvproto/pkg/brpb"
"github.com/pingcap/log"
"github.com/pingcap/tidb/br/pkg/logutil"
"github.com/pingcap/tidb/br/pkg/streamhelper/config"
"github.com/pingcap/tidb/br/pkg/streamhelper/spans"
"github.com/pingcap/tidb/br/pkg/utils"
"github.com/pingcap/tidb/kv"
"github.com/pingcap/tidb/metrics"
"github.com/tikv/client-go/v2/oracle"
"go.uber.org/multierr"
"go.uber.org/zap"
"golang.org/x/sync/errgroup"
)
// CheckpointAdvancer is the central node for advancing the checkpoint of log backup.
// It's a part of "checkpoint v3".
// Generally, it scan the regions in the task range, collect checkpoints from tikvs.
/*
┌──────┐
┌────►│ TiKV │
│ └──────┘
┌──────────┐GetLastFlushTSOfRegion│ ┌──────┐
│ Advancer ├──────────────────────┼────►│ TiKV │
└────┬─────┘ │ └──────┘
│ │
│ │
│ │ ┌──────┐
│ └────►│ TiKV │
│ └──────┘
│ UploadCheckpointV3 ┌──────────────────┐
└─────────────────────►│ PD │
└──────────────────┘
*/
type CheckpointAdvancer struct {
env Env
// The concurrency accessed task:
// both by the task listener and ticking.
task *backuppb.StreamBackupTaskInfo
taskRange []kv.KeyRange
taskMu sync.Mutex
// the read-only config.
// once tick begin, this should not be changed for now.
cfg config.Config
// the cached last checkpoint.
// if no progress, this cache can help us don't to send useless requests.
lastCheckpoint uint64
checkpoints *spans.ValueSortedFull
checkpointsMu sync.Mutex
subscriber *FlushSubscriber
subscriberMu sync.Mutex
}
// NewCheckpointAdvancer creates a checkpoint advancer with the env.
func NewCheckpointAdvancer(env Env) *CheckpointAdvancer {
return &CheckpointAdvancer{
env: env,
cfg: config.Default(),
}
}
// UpdateConfig updates the config for the advancer.
// Note this should be called before starting the loop, because there isn't locks,
// TODO: support updating config when advancer starts working.
// (Maybe by applying changes at begin of ticking, and add locks.)
func (c *CheckpointAdvancer) UpdateConfig(newConf config.Config) {
c.cfg = newConf
}
// UpdateConfigWith updates the config by modifying the current config.
func (c *CheckpointAdvancer) UpdateConfigWith(f func(*config.Config)) {
cfg := c.cfg
f(&cfg)
c.UpdateConfig(cfg)
}
// Config returns the current config.
func (c *CheckpointAdvancer) Config() config.Config {
return c.cfg
}
// GetCheckpointInRange scans the regions in the range,
// collect them to the collector.
func (c *CheckpointAdvancer) GetCheckpointInRange(ctx context.Context, start, end []byte,
collector *clusterCollector) error {
log.Debug("scanning range", logutil.Key("start", start), logutil.Key("end", end))
iter := IterateRegion(c.env, start, end)
for !iter.Done() {
rs, err := iter.Next(ctx)
if err != nil {
return err
}
log.Debug("scan region", zap.Int("len", len(rs)))
for _, r := range rs {
err := collector.CollectRegion(r)
if err != nil {
log.Warn("meet error during getting checkpoint", logutil.ShortError(err))
return err
}
}
}
return nil
}
func (c *CheckpointAdvancer) recordTimeCost(message string, fields ...zap.Field) func() {
now := time.Now()
label := strings.ReplaceAll(message, " ", "-")
return func() {
cost := time.Since(now)
fields = append(fields, zap.Stringer("take", cost))
metrics.AdvancerTickDuration.WithLabelValues(label).Observe(cost.Seconds())
log.Debug(message, fields...)
}
}
// tryAdvance tries to advance the checkpoint ts of a set of ranges which shares the same checkpoint.
func (c *CheckpointAdvancer) tryAdvance(ctx context.Context, length int,
getRange func(int) kv.KeyRange) (err error) {
defer c.recordTimeCost("try advance", zap.Int("len", length))()
defer utils.PanicToErr(&err)
ranges := spans.Collapse(length, getRange)
workers := utils.NewWorkerPool(uint(config.DefaultMaxConcurrencyAdvance)*4, "sub ranges")
eg, cx := errgroup.WithContext(ctx)
collector := NewClusterCollector(ctx, c.env)
collector.SetOnSuccessHook(func(u uint64, kr kv.KeyRange) {
c.checkpointsMu.Lock()
defer c.checkpointsMu.Unlock()
c.checkpoints.Merge(spans.Valued{Key: kr, Value: u})
})
clampedRanges := utils.IntersectAll(ranges, utils.CloneSlice(c.taskRange))
for _, r := range clampedRanges {
r := r
workers.ApplyOnErrorGroup(eg, func() (e error) {
defer c.recordTimeCost("get regions in range")()
defer utils.PanicToErr(&e)
return c.GetCheckpointInRange(cx, r.StartKey, r.EndKey, collector)
})
}
err = eg.Wait()
if err != nil {
return err
}
_, err = collector.Finish(ctx)
if err != nil {
return err
}
return nil
}
func tsoBefore(n time.Duration) uint64 {
now := time.Now()
return oracle.ComposeTS(now.UnixMilli()-n.Milliseconds(), 0)
}
func (c *CheckpointAdvancer) WithCheckpoints(f func(*spans.ValueSortedFull)) {
c.checkpointsMu.Lock()
defer c.checkpointsMu.Unlock()
f(c.checkpoints)
}
func (c *CheckpointAdvancer) CalculateGlobalCheckpointLight(ctx context.Context,
threshold time.Duration) (uint64, error) {
var targets []spans.Valued
var minValue spans.Valued
c.WithCheckpoints(func(vsf *spans.ValueSortedFull) {
vsf.TraverseValuesLessThan(tsoBefore(threshold), func(v spans.Valued) bool {
targets = append(targets, v)
return true
})
minValue = vsf.Min()
})
log.Info("[log backup advancer hint] current last region",
zap.Stringer("min", minValue), zap.Int("for-polling", len(targets)),
zap.String("min-ts", oracle.GetTimeFromTS(minValue.Value).Format(time.RFC3339)))
if len(targets) == 0 {
return minValue.Value, nil
}
err := c.tryAdvance(ctx, len(targets), func(i int) kv.KeyRange { return targets[i].Key })
if err != nil {
return 0, err
}
return minValue.Value, nil
}
func (c *CheckpointAdvancer) consumeAllTask(ctx context.Context, ch <-chan TaskEvent) error {
for {
select {
case e, ok := <-ch:
if !ok {
return nil
}
log.Info("meet task event", zap.Stringer("event", &e))
if err := c.onTaskEvent(ctx, e); err != nil {
if errors.Cause(e.Err) != context.Canceled {
log.Error("listen task meet error, would reopen.", logutil.ShortError(err))
return err
}
return nil
}
default:
return nil
}
}
}
// beginListenTaskChange bootstraps the initial task set,
// and returns a channel respecting the change of tasks.
func (c *CheckpointAdvancer) beginListenTaskChange(ctx context.Context) (<-chan TaskEvent, error) {
ch := make(chan TaskEvent, 1024)
if err := c.env.Begin(ctx, ch); err != nil {
return nil, err
}
err := c.consumeAllTask(ctx, ch)
if err != nil {
return nil, err
}
return ch, nil
}
// StartTaskListener starts the task listener for the advancer.
// When no task detected, advancer would do nothing, please call this before begin the tick loop.
func (c *CheckpointAdvancer) StartTaskListener(ctx context.Context) {
cx, cancel := context.WithCancel(ctx)
var ch <-chan TaskEvent
for {
if cx.Err() != nil {
// make linter happy.
cancel()
return
}
var err error
ch, err = c.beginListenTaskChange(cx)
if err == nil {
break
}
log.Warn("failed to begin listening, retrying...", logutil.ShortError(err))
time.Sleep(c.cfg.BackoffTime)
}
go func() {
defer cancel()
for {
select {
case <-ctx.Done():
return
case e, ok := <-ch:
if !ok {
log.Info("[log backup advancer] Task watcher exits due to stream ends.")
return
}
log.Info("[log backup advancer] Meet task event", zap.Stringer("event", &e))
if err := c.onTaskEvent(ctx, e); err != nil {
if errors.Cause(e.Err) != context.Canceled {
log.Error("listen task meet error, would reopen.", logutil.ShortError(err))
time.AfterFunc(c.cfg.BackoffTime, func() { c.StartTaskListener(ctx) })
}
log.Info("[log backup advancer] Task watcher exits due to some error.",
logutil.ShortError(err))
return
}
}
}
}()
}
func (c *CheckpointAdvancer) onTaskEvent(ctx context.Context, e TaskEvent) error {
c.taskMu.Lock()
defer c.taskMu.Unlock()
switch e.Type {
case EventAdd:
utils.LogBackupTaskCountInc()
c.task = e.Info
c.taskRange = spans.Collapse(len(e.Ranges), func(i int) kv.KeyRange { return e.Ranges[i] })
c.checkpoints = spans.Sorted(spans.NewFullWith(e.Ranges, 0))
c.lastCheckpoint = e.Info.StartTs
p, err := c.env.BlockGCUntil(ctx, c.task.StartTs)
if err != nil {
log.Warn("failed to upload service GC safepoint, skipping.", logutil.ShortError(err))
}
log.Info("added event", zap.Stringer("task", e.Info),
zap.Stringer("ranges", logutil.StringifyKeys(c.taskRange)), zap.Uint64("current-checkpoint", p))
case EventDel:
utils.LogBackupTaskCountDec()
c.task = nil
c.taskRange = nil
c.checkpoints = nil
// This would be synced by `taskMu`, perhaps we'd better rename that to `tickMu`.
// Do the null check because some of test cases won't equip the advancer with subscriber.
if c.subscriber != nil {
c.subscriber.Clear()
}
if err := c.env.ClearV3GlobalCheckpointForTask(ctx, e.Name); err != nil {
log.Warn("failed to clear global checkpoint", logutil.ShortError(err))
}
if _, err := c.env.BlockGCUntil(ctx, 0); err != nil {
log.Warn("failed to remove service GC safepoint", logutil.ShortError(err))
}
metrics.LastCheckpoint.DeleteLabelValues(e.Name)
case EventErr:
return e.Err
}
return nil
}
func (c *CheckpointAdvancer) setCheckpoint(cp uint64) bool {
if cp < c.lastCheckpoint {
log.Warn("failed to update global checkpoint: stale",
zap.Uint64("old", c.lastCheckpoint), zap.Uint64("new", cp))
return false
}
if cp <= c.lastCheckpoint {
return false
}
c.lastCheckpoint = cp
metrics.LastCheckpoint.WithLabelValues(c.task.GetName()).Set(float64(c.lastCheckpoint))
return true
}
// advanceCheckpointBy advances the checkpoint by a checkpoint getter function.
func (c *CheckpointAdvancer) advanceCheckpointBy(ctx context.Context,
getCheckpoint func(context.Context) (uint64, error)) error {
start := time.Now()
cp, err := getCheckpoint(ctx)
if err != nil {
return err
}
if c.setCheckpoint(cp) {
log.Info("uploading checkpoint for task",
zap.Stringer("checkpoint", oracle.GetTimeFromTS(cp)),
zap.Uint64("checkpoint", cp),
zap.String("task", c.task.Name),
zap.Stringer("take", time.Since(start)))
}
return nil
}
func (c *CheckpointAdvancer) stopSubscriber() {
c.subscriberMu.Lock()
defer c.subscriberMu.Unlock()
c.subscriber.Drop()
c.subscriber = nil
}
func (c *CheckpointAdvancer) spawnSubscriptionHandler(ctx context.Context) {
c.subscriberMu.Lock()
defer c.subscriberMu.Unlock()
c.subscriber = NewSubscriber(c.env, c.env, WithMasterContext(ctx))
es := c.subscriber.Events()
go func() {
for {
select {
case <-ctx.Done():
return
case event, ok := <-es:
if !ok {
return
}
c.checkpointsMu.Lock()
log.Debug("Accepting region flush event.",
zap.Stringer("range", logutil.StringifyRange(event.Key)),
zap.Uint64("checkpoint", event.Value))
c.checkpoints.Merge(event)
c.checkpointsMu.Unlock()
}
}
}()
}
func (c *CheckpointAdvancer) subscribeTick(ctx context.Context) error {
if c.subscriber == nil {
return nil
}
if err := c.subscriber.UpdateStoreTopology(ctx); err != nil {
log.Warn("[log backup advancer] Error when updating store topology.", logutil.ShortError(err))
}
c.subscriber.HandleErrors(ctx)
return c.subscriber.PendingErrors()
}
func (c *CheckpointAdvancer) importantTick(ctx context.Context) error {
c.checkpointsMu.Lock()
c.setCheckpoint(c.checkpoints.MinValue())
c.checkpointsMu.Unlock()
if err := c.env.UploadV3GlobalCheckpointForTask(ctx, c.task.Name, c.lastCheckpoint); err != nil {
return errors.Annotate(err, "failed to upload global checkpoint")
}
p, err := c.env.BlockGCUntil(ctx, c.lastCheckpoint-1)
if err != nil {
return errors.Annotatef(err,
"failed to update service GC safe point, current checkpoint is %d, target checkpoint is %d",
c.lastCheckpoint-1, p)
}
if p <= c.lastCheckpoint-1 {
log.Info("updated log backup GC safe point.",
zap.Uint64("checkpoint", p), zap.Uint64("target", c.lastCheckpoint-1))
}
if p > c.lastCheckpoint-1 {
log.Warn("update log backup GC safe point failed: stale.",
zap.Uint64("checkpoint", p), zap.Uint64("target", c.lastCheckpoint-1))
}
return nil
}
func (c *CheckpointAdvancer) optionalTick(cx context.Context) error {
threshold := c.Config().GetDefaultStartPollThreshold()
if err := c.subscribeTick(cx); err != nil {
log.Warn("[log backup advancer] Subscriber meet error, would polling the checkpoint.",
logutil.ShortError(err))
threshold = c.Config().GetSubscriberErrorStartPollThreshold()
}
err := c.advanceCheckpointBy(cx, func(cx context.Context) (uint64, error) {
return c.CalculateGlobalCheckpointLight(cx, threshold)
})
if err != nil {
return err
}
return nil
}
func (c *CheckpointAdvancer) tick(ctx context.Context) error {
c.taskMu.Lock()
defer c.taskMu.Unlock()
if c.task == nil {
log.Debug("No tasks yet, skipping advancing.")
return nil
}
var errs error
cx, cancel := context.WithTimeout(ctx, c.Config().TickTimeout())
defer cancel()
err := c.optionalTick(cx)
if err != nil {
log.Warn("[log backup advancer] option tick failed.", logutil.ShortError(err))
errs = multierr.Append(errs, err)
}
err = c.importantTick(ctx)
if err != nil {
log.Warn("[log backup advancer] important tick failed.", logutil.ShortError(err))
errs = multierr.Append(errs, err)
}
return errs
}