TinySQL_单个DDL任务执行的流程

TinySQL_单个DDL任务执行的流程

前面的博客我们已经讲到了在 TiDB 中, 当一个 DDL 变更请求到达服务器时, Server 执行的流程, 包含了:

  1. 语法解析器配置需要执行的 DDL 任务的上下文信息.
  2. DDL Executor 在 Next() 函数中调用数据库表变更的函数 AlterTable. (TiDB 的 SQL Executor使用的是火山模型, 与 CMU15445 相同).
  3. AlterTable 函数调用具体的 DDL 变更的函数, 例如我们的例子, 在数据库表中新增一列, 使用的是 AddColumn 函数.
  4. 在后续的 doDDLJob 中, 会异步的通知服务器中的 worker 开始工作, 接下来我们看一下 TiDB 中 worker 工作流程是如何实现的.

Job 生成

在服务器节点收到 MySQL Client 的 DDL 操作请求后, Parser 解析器会将 SQL DDL 语言解析为需要执行的 Job, 这里的步骤与前面 Parser 解析器解析 SELECT 等其他 MySQL 语句类似, 不同点是 DDL 操作是管理员类型操作, 操作类型定义如下:

// AdminStmt is the struct for Admin statement.
type AdminStmt struct {
	stmtNode
	Tp        AdminStmtType
	Tables    []*TableName
	JobNumber int64
	Where     ExprNode
}

在生成的语法解析器 parser.go 中, 构造出一个 DDL Job 的部分如下:

case 979:
  {
    stmt := &ast.AdminStmt{Tp: ast.AdminShowDDLJobs}
    if yyS[yypt-0].item != nil {
      stmt.Where = yyS[yypt-0].item.(ast.ExprNode)
    }
    parser.yyVAL.statement = stmt
  }
case 980:
  {
    stmt := &ast.AdminStmt{
      Tp:        ast.AdminShowDDLJobs,
      JobNumber: yyS[yypt-1].item.(int64),
    }
    if yyS[yypt-0].item != nil {
      stmt.Where = yyS[yypt-0].item.(ast.ExprNode)
    }
    parser.yyVAL.statement = stmt
  }

这部分生成的 Job, 也就是 DDL 操作的 Job 部分如下, 这个结构体就代表了一个 DDL 操作的 Job.

// Job is for a DDL operation.
type Job struct {
	ID         int64         `json:"id"`
	Type       ActionType    `json:"type"`
	SchemaID   int64         `json:"schema_id"`
	TableID    int64         `json:"table_id"`
	SchemaName string        `json:"schema_name"`
	State      JobState      `json:"state"`
	Error      *terror.Error `json:"err"`
	// ErrorCount will be increased, every time we meet an error when running job.
	ErrorCount int64 `json:"err_count"`
	// RowCount means the number of rows that are processed.
	RowCount int64         `json:"row_count"`
	Mu       sync.Mutex    `json:"-"`
	Args     []interface{} `json:"-"`
	// RawArgs : We must use json raw message to delay parsing special args.
	RawArgs     json.RawMessage `json:"raw_args"`
	SchemaState SchemaState     `json:"schema_state"`
	// SnapshotVer means snapshot version for this job.
	SnapshotVer uint64 `json:"snapshot_ver"`
	// StartTS uses timestamp allocated by TSO.
	// Now it's the TS when we put the job to TiKV queue.
	StartTS uint64 `json:"start_ts"`
	// DependencyID is the job's ID that the current job depends on.
	DependencyID int64 `json:"dependency_id"`
	// Query string of the ddl job.
	Query      string       `json:"query"`
	BinlogInfo *HistoryInfo `json:"binlog"`

	// Version indicates the DDL job version. For old jobs, it will be 0.
	Version int64 `json:"version"`

	// ReorgMeta is meta info of ddl reorganization.
	// This field is depreciated.
	ReorgMeta *DDLReorgMeta `json:"reorg_meta"`

	// Priority is only used to set the operation priority of adding indices.
	Priority int `json:"priority"`
}

Executor 执行 DDL 变更任务

在 TiDB 中, 每个节点的 worker 是在服务节点 Domain 初始化的时候开始运行 goroutine 的, 当一个 DDL 变更任务到达的时候, 我们以 AddColumn 为例看一下执行的逻辑.

首先会根据语法解析树解析出来的 DDL 操作的类别, 判断需要执行的函数, 我们可以从 DDL 操作的 Next() 函数中看到这部分内容

// Next implements the Executor Next interface.
func (e *DDLExec) Next(ctx context.Context, req *chunk.Chunk) (err error) {
  // DDLExec 是 DDL 类型任务的上下文, 会在前面的步骤封装好
	if e.done {
		return nil
	}
	e.done = true

	// For each DDL, we should commit the previous transaction and create a new transaction.
	if err = e.ctx.NewTxn(ctx); err != nil {
		return err
	}
	defer func() { e.ctx.GetSessionVars().StmtCtx.IsDDLJobInQueue = false }()

	switch x := e.stmt.(type) {
  // 判断 DDL 操作的类别, 这里是修改 Table 的操作, AddColumn
	case *ast.AlterTableStmt:
		err = e.executeAlterTable(x)
	case *ast.CreateIndexStmt:
		err = e.executeCreateIndex(x)
	case *ast.CreateDatabaseStmt:
		err = e.executeCreateDatabase(x)
	case *ast.CreateTableStmt:
		err = e.executeCreateTable(x)
	case *ast.DropIndexStmt:
		err = e.executeDropIndex(x)
	case *ast.DropDatabaseStmt:
		err = e.executeDropDatabase(x)
	case *ast.DropTableStmt:
		err = e.executeDropTableOrView(x)
	}
	if err != nil {
		// If the owner return ErrTableNotExists error when running this DDL, it may be caused by schema changed,
		// otherwise, ErrTableNotExists can be returned before putting this DDL job to the job queue.
		if (e.ctx.GetSessionVars().StmtCtx.IsDDLJobInQueue && infoschema.ErrTableNotExists.Equal(err)) ||
			!e.ctx.GetSessionVars().StmtCtx.IsDDLJobInQueue {
			return e.toErr(err)
		}
		return err

	}

	dom := domain.GetDomain(e.ctx)
	// Update InfoSchema in TxnCtx, so it will pass schema check.
	is := dom.InfoSchema()
	txnCtx := e.ctx.GetSessionVars().TxnCtx
	txnCtx.InfoSchema = is
	txnCtx.SchemaVersion = is.SchemaMetaVersion()
	// DDL will force commit old transaction, after DDL, in transaction status should be false.
	e.ctx.GetSessionVars().SetStatusFlag(mysql.ServerStatusInTrans, false)
	return nil
}

executeAlterTable 函数会使用本次 DDL 变更相关的上下文, 然后调用 DDL 的 AlterTable 操作, 执行真正的修改数据库表的操作.

func (e *DDLExec) executeAlterTable(s *ast.AlterTableStmt) error {
	ti := ast.Ident{Schema: s.Table.Schema, Name: s.Table.Name}
	err := domain.GetDomain(e.ctx).DDL().AlterTable(e.ctx, ti, s.Specs)
	return err
}

AlterTable 则是 DDL 在 ddl_api.go 中对外提供的 API 接口, 这个接口由结构体 ddl 实现, 可以看到其实现的功能, 主要功能就是根据得到的语法树中 DDL 上下文的信息判断执行的 DDL 操作, 然后执行具体的 DDL 操作函数. 这里我仅粘贴了部分 AlterTable 的功能.

func (d *ddl) AlterTable(ctx sessionctx.Context, ident ast.Ident, specs []*ast.AlterTableSpec) (err error) {
	validSpecs, err := resolveAlterTableSpec(ctx, specs)
	if err != nil {
		return errors.Trace(err)
	}

	for _, spec := range validSpecs {
		switch spec.Tp {
		case ast.AlterTableAddColumns:
			if len(spec.NewColumns) != 1 {
				return errRunMultiSchemaChanges
			}
			err = d.AddColumn(ctx, ident, spec)
		case ast.AlterTableDropColumn:
			err = d.DropColumn(ctx, ident, spec)
		default:
			// Nothing to do now.
		}

		if err != nil {
			return errors.Trace(err)
		}
	}

	return nil
}

终于, 我们调用了 AddColumn 函数, 但是这个函数并不是直接执行在数据库中添加一列的操作, 这个函数如下:

// AddColumn will add a new column to the table.
func (d *ddl) AddColumn(ctx sessionctx.Context, ti ast.Ident, spec *ast.AlterTableSpec) error {
	specNewColumn := spec.NewColumns[0]
  /**
   * 这里省略了很多校验的部分, 这些校验的部分就是前面我们提到了错误的情况直接返回
   * 不执行该 DDL 变更任务
  */

  // 这一步很关键, 构造一个具体的 DDL 类型的 Job
	job := &model.Job{
		SchemaID:   schema.ID,
		TableID:    t.Meta().ID,
		SchemaName: schema.Name.L,
		Type:       model.ActionAddColumn,
		BinlogInfo: &model.HistoryInfo{},
		Args:       []interface{}{col, 0},
	}

  // 执行构造得到的 DDL 变更 job, 但不是直接执行, 而是将这个 job 添加到执行队列中去
	err = d.doDDLJob(ctx, job)
	// column exists, but if_not_exists flags is true, so we ignore this error.
	if infoschema.ErrColumnExists.Equal(err) && spec.IfNotExists {
		ctx.GetSessionVars().StmtCtx.AppendNote(err)
		return nil
	}
	err = d.callHookOnChanged(err)
	return errors.Trace(err)
}

doDDLJob 中也有一些重要的步骤, 这个函数如下:

func (d *ddl) doDDLJob(ctx sessionctx.Context, job *model.Job) error {
	// Get a global job ID and put the DDL job in the queue.
	// 将 DDL 任务添加到任务队列中
	err := d.addDDLJob(ctx, job)
	if err != nil {
		return errors.Trace(err)
	}
	// 修改上下文
	ctx.GetSessionVars().StmtCtx.IsDDLJobInQueue = true

	// Notice worker that we push a new job and wait the job done.
	// 这里还会通知 worker 工作协程, worker 收到 channel 的信号之后开始工作
	d.asyncNotifyWorker(job.Type)
	logutil.BgLogger().Info("[ddl] start DDL job", zap.String("job", job.String()), zap.String("query", job.Query))

	var historyJob *model.Job
	jobID := job.ID
	// For a job from start to end, the state of it will be none -> delete only -> write only -> reorganization -> public
	// For every state changes, we will wait as lease 2 * lease time, so here the ticker check is 10 * lease.
	// But we use etcd to speed up, normally it takes less than 0.5s now, so we use 0.5s or 1s or 3s as the max value.
	ticker := time.NewTicker(chooseLeaseTime(10*d.lease, checkJobMaxInterval(job)))
	// 在这个任务最多花费的时间段之后检查该 DDL 任务是否已经完成
	defer func() {
		ticker.Stop()
	}()
	for {
		select {
		case <-d.ddlJobDoneCh:
		case <-ticker.C:
		}

		historyJob, err = d.getHistoryDDLJob(jobID)
		if err != nil {
			logutil.BgLogger().Error("[ddl] get history DDL job failed, check again", zap.Error(err))
			continue
		} else if historyJob == nil {
			logutil.BgLogger().Debug("[ddl] DDL job is not in history, maybe not run", zap.Int64("jobID", jobID))
			continue
		}

		// If a job is a history job, the state must be JobStateSynced or JobStateRollbackDone or JobStateCancelled.
		if historyJob.IsSynced() {
			logutil.BgLogger().Info("[ddl] DDL job is finished", zap.Int64("jobID", jobID))
			return nil
		}

		if historyJob.Error != nil {
			return errors.Trace(historyJob.Error)
		}
		panic("When the state is JobStateRollbackDone or JobStateCancelled, historyJob.Error should never be nil")
	}
}

这里我们总结了当一个节点接收到了一条 MySQL 的 DDL 语句之后的执行步骤, 从 Executor 开始, 判断操作的类别, 到执行具体的变更, 最后是将 DDL 任务添加到任务队列中, 然后通知 worker 协程执行这个任务. 那么当一个节点执行完该任务之后又会发生什么呢?

posted @ 2025-06-19 10:48  虾野百鹤  阅读(12)  评论(0)    收藏  举报