TinySQL_单个DDL任务执行的流程
TinySQL_单个DDL任务执行的流程
前面的博客我们已经讲到了在 TiDB 中, 当一个 DDL 变更请求到达服务器时, Server 执行的流程, 包含了:
- 语法解析器配置需要执行的 DDL 任务的上下文信息.
- DDL Executor 在 Next() 函数中调用数据库表变更的函数
AlterTable
. (TiDB 的 SQL Executor使用的是火山模型, 与 CMU15445 相同). AlterTable
函数调用具体的 DDL 变更的函数, 例如我们的例子, 在数据库表中新增一列, 使用的是AddColumn
函数.- 在后续的
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 协程执行这个任务. 那么当一个节点执行完该任务之后又会发生什么呢?