TiDB 中 Server 上 Worker 的执行原理

TiDB 中 Server 上 Worker 的执行原理

本章我们来介绍一下 TiDB 中后台执行 DDL 变更的 worker 是如何实现的.

DDL worker 的启动

数据库的 DDL 相关的信息是属于数据库的元信息, 在 TiDB 中, 每一个 Server 使用 Domain 来管理这些元信息结构.Domain 是 TiDB Server 的元数据管理模块, 用于协调 Schema 信息缓存、DDL 执行、Stats 统计信息 和 GC 信息 等. 实际实现中每个 Domain(对应每一个节点 Server) 都有一份自己的 Schema 缓存, 但通过 etcd 协调 DDL 和 schema 同步, 从而实现一致性.

Domain 的初始化步骤如下, 我们可以在 Domain 初始化中看到对 Schema 变更(DDL 操作)的特殊处理:

// Init initializes a domain.
func (do *Domain) Init(ddlLease time.Duration, sysFactory func(*Domain) (pools.Resource, error)) error {
  // 判断 KV 存储引擎是否使用 tinykv
	if ebd, ok := do.store.(tikv.EtcdBackend); ok {
		if addrs := ebd.EtcdAddrs(); addrs != nil {
			cli, err := clientv3.New(clientv3.Config{
				Endpoints:        addrs,
				AutoSyncInterval: 30 * time.Second,
				DialTimeout:      5 * time.Second,
				DialOptions: []grpc.DialOption{
					grpc.WithBackoffMaxDelay(time.Second * 3),
					grpc.WithKeepaliveParams(keepalive.ClientParameters{
						Time:                time.Duration(10) * time.Second,
						Timeout:             time.Duration(3) * time.Second,
						PermitWithoutStream: true,
					}),
				},
				TLS: ebd.TLSConfig(),
			})
			if err != nil {
				return errors.Trace(err)
			}
			do.etcdClient = cli
		}
	}

	// TODO: Here we create new sessions with sysFac in DDL,
	// which will use `do` as Domain instead of call `domap.Get`.
	// That's because `domap.Get` requires a lock, but before
	// we initialize Domain finish, we can't require that again.
	// After we remove the lazy logic of creating Domain, we
	// can simplify code here.
  // 初始化系统 session 的资源池
	sysFac := func() (pools.Resource, error) {
		return sysFactory(do)
	}
	sysCtxPool := pools.NewResourcePool(sysFac, 2, 2, resourceIdleTimeout)
  // // 备份旧的 DDL 实例(用于 failpoint 测试)
	ctx := context.Background()
	callback := &ddlCallback{do: do}
	d := do.ddl
  // 创建新的 DDL 对象, 在创建时就会启动 DDL 中的 worker 的 goroutine
	do.ddl = ddl.NewDDL(
		ctx,
		ddl.WithEtcdClient(do.etcdClient),
		ddl.WithStore(do.store),
		ddl.WithInfoHandle(do.infoHandle),
		ddl.WithHook(callback),
		ddl.WithLease(ddlLease),
		ddl.WithResourcePool(sysCtxPool),
	)
  // 测试钩子 failpoint(非核心功能)
	failpoint.Inject("MockReplaceDDL", func(val failpoint.Value) {
		if val.(bool) {
			if err := do.ddl.Stop(); err != nil {
				logutil.BgLogger().Error("stop DDL failed", zap.Error(err))
			}
			do.ddl = d
		}
	})
  // 初始化 DDL 的 schema 同步模块, 它会启动 schema 同步器(SchemaSyncer), 
  // 会监听 etcd 中 DDL 的变更事件, 收到变更时会拉取最新 schema 并更新缓存
	err := do.ddl.SchemaSyncer().Init(ctx)
	if err != nil {
		return err
	}
  // 从 KV 中拉取当前最新 schema, 加载到本地的 infoSchema 缓存中
	err = do.Reload()
	if err != nil {
		return err
	}

	// Only when the store is local that the lease value is 0.
	// If the store is local, it doesn't need loadSchemaInLoop.
	if ddlLease > 0 {
		do.wg.Add(1)
		// Local store needs to get the change information for every DDL state in each session.
		go do.loadSchemaInLoop(ddlLease)
	}
	return nil
}

每一个 Server 中都会创建一个新的 DDL 实例, 这个 DDL 实例创建之后就是启动处理 DDL 任务的 worker,

func newDDL(ctx context.Context, options ...Option) *ddl {
	opt := &Options{
		Hook: &BaseCallback{},
	}
	for _, o := range options {
		o(opt)
	}

	id := uuid.New().String()
	ctx, cancelFunc := context.WithCancel(ctx)
	var manager owner.Manager
	var syncer util.SchemaSyncer
	if etcdCli := opt.EtcdCli; etcdCli == nil {
		// The etcdCli is nil if the store is localstore which is only used for testing.
		// So we use mockOwnerManager and MockSchemaSyncer.
		manager = owner.NewMockManager(id, cancelFunc)
		syncer = NewMockSchemaSyncer()
	} else {
		manager = owner.NewOwnerManager(etcdCli, ddlPrompt, id, DDLOwnerKey, cancelFunc)
		syncer = util.NewSchemaSyncer(etcdCli, id, manager)
	}
	// 构建处理 DDL 的上下文信息, 也就是 DDL 实例的一些属性信息
	ddlCtx := &ddlCtx{
		uuid:         id,
		store:        opt.Store,
		lease:        opt.Lease,
		ddlJobDoneCh: make(chan struct{}, 1),
		ownerManager: manager,
		schemaSyncer: syncer,
		infoHandle:   opt.InfoHandle,
	}
	ddlCtx.mu.hook = opt.Hook
	ddlCtx.mu.interceptor = &BaseInterceptor{}
	d := &ddl{
		ddlCtx: ddlCtx,
	}
	// 启动该 DDL 实例
	d.start(ctx, opt.ResourcePool)
	variable.RegisterStatistics(d)

	return d
}

worker 的实现

worker 的实现主要在文件 ddl/ddl_worker.go 文件中, 我们知道, 处理 DDL Jobs 的 workers 是 TiDB Server 节点的 Domain 初始化的时候就开始执行的, 在 newDDL 函数初始化一个 DDL 实例, 然后这个 DDL 实例会启动执行, 如下:

// start campaigns the owner and starts workers.
// ctxPool is used for the worker's delRangeManager and creates sessions.
func (d *ddl) start(ctx context.Context, ctxPool *pools.ResourcePool) {
	// 日志开始执行任务
	logutil.BgLogger().Info("[ddl] start DDL", zap.String("ID", d.uuid))
	// 创建通知 channel, 用于通知 DDL worker 退出
	d.quitCh = make(chan struct{})
	// 当前服务器竞争成为 DDL 的 owner, 只有成为 owner 的服务器才能执行 DDL 任务
	err := d.ownerManager.CampaignOwner(ctx)
	terror.Log(errors.Trace(err))
	// 创建这个 DDL 的上下文, 用于 DDL worker 执行任务
	d.workers = make(map[workerType]*worker, 2)
	d.sessPool = newSessionPool(ctxPool)
	// 新建两个 worker, 一个用于处理一般的 DDL 任务, 另一个用于处理添加索引的 DDL 任务
	d.workers[generalWorker] = newWorker(generalWorker, d.sessPool)
	d.workers[addIdxWorker] = newWorker(addIdxWorker, d.sessPool)
	for _, worker := range d.workers {
		worker.wg.Add(1)
		w := worker
		// 这是带有恢复的 goroutine, 如果 panic 了, 会打印日志
		go tidbutil.WithRecovery(
			func() { w.start(d.ddlCtx) },
			func(r interface{}) {
				if r != nil {
					logutil.Logger(w.logCtx).Error("[ddl] DDL worker meet panic", zap.String("ID", d.uuid))

				}
			})

		// When the start function is called, we will send a fake job to let worker
		// checks owner firstly and try to find whether a job exists and run.
		asyncNotify(worker.ddlJobCh)
	}
	// 启动清理任务, 收到通知后会清理过期的 DDL 任务
	go tidbutil.WithRecovery(
		func() { d.schemaSyncer.StartCleanWork() },
		func(r interface{}) {
			if r != nil {
				logutil.BgLogger().Error("[ddl] DDL syncer clean worker meet panic",
					zap.String("ID", d.uuid), zap.Reflect("r", r), zap.Stack("stack trace"))

			}
		})
}

会启用一个 goroutine 来执行 worker 的 start() 函数, 这个 start 函数如下:

// start is used for async online schema changing, it will try to become the owner firstly,
// then wait or pull the job queue to handle a schema change job.
func (w *worker) start(d *ddlCtx) {
	logutil.Logger(w.logCtx).Info("[ddl] start DDL worker")
	defer w.wg.Done()

	// We use 4 * lease time to check owner's timeout, so here, we will update owner's status
	// every 2 * lease time. If lease is 0, we will use default 1s.
	// But we use etcd to speed up, normally it takes less than 1s now, so we use 1s as the max value.
	checkTime := chooseLeaseTime(2*d.lease, 1*time.Second)
	// 设置定时任务, 当前 worker 每隔 checkTime 检查一次 DDL 状态
	ticker := time.NewTicker(checkTime)
	defer ticker.Stop()

	for {
		select {
		// 周期性检查 DDL 状态
		case <-ticker.C:
			logutil.Logger(w.logCtx).Debug("[ddl] wait to check DDL status again", zap.Duration("interval", checkTime))
		// 有新的 DDL 任务到来, 通知 worker 处理, 主动触发
		case <-w.ddlJobCh:
		case <-w.quitCh:
			return
		}
		// worker 进程根据上下文处理队列中的第一个 DDL 变更任务, 如果有的话
		err := w.handleDDLJobQueue(d)
		if err != nil {
			logutil.Logger(w.logCtx).Error("[ddl] handle DDL job failed", zap.Error(err))
		}
	}
}

因此可以知道 worker 实际上是一个在后台执行的 goroutine, 它会接受来自定时任务, 或者新任务到来时的通知, 来唤醒 worker 执行, 例如, 在上面的启动一个新的 DDL 任务中, 执行的 asyncNotify(worker.ddlJobCh) 步骤, 就是异步通知 worker 需要开始工作了.

posted @ 2025-06-18 17:10  虾野百鹤  阅读(14)  评论(0)    收藏  举报