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 需要开始工作了.