深入理解Moby Buildkit系列 #27 - 编排高手Scheduler

641 阅读4分钟

这是我参与11月更文挑战的第27天,活动详情查看:2021最后一次更文挑战

scheduler.build

现在有两个Edge,Scheduler会怎么编排任务呢? Scheduler开始构建的地方:

// build evaluates edge into a result
func (s *scheduler) build(ctx context.Context, edge Edge) (CachedResult, error) {
   s.mu.Lock()
   e := s.ef.getEdge(edge)
   ...
   wait := make(chan struct{})
   var p *pipe.Pipe
   p = s.newPipe(e, nil, pipe.Request{Payload: &edgeRequest{desiredState: edgeStatusComplete}})
   p.OnSendCompletion = func() {
      p.Receiver.Receive()
      if p.Receiver.Status().Completed {
         close(wait)
      }
   }
   ...
   <-wait
   ...
}

可以看到,最先获取edge,然后创建EdgePipe来处理Edge,这里的onSendCompletion里最终要完成的任务就是close(wait),就是说我们这是最后一个Edge,当OnSendCompletion完成后,也就意味着这一次的构建全部完成。

scheduler newPipe

再来看看newPipe

// newPipe creates a new request pipe between two edges
func (s *scheduler) newPipe(target, from *edge, req pipe.Request) *pipe.Pipe {
   p := &edgePipe{
      Pipe:   pipe.New(req),
      Target: target,
      From:   from,
   }

   s.signal(target)
   if from != nil {
      p.OnSendCompletion = func() {
         p.mu.Lock()
         defer p.mu.Unlock()
         s.signal(p.From)
      }
      s.outgoing[from] = append(s.outgoing[from], p)
   }
   s.incoming[target] = append(s.incoming[target], p)
   p.OnReceiveCompletion = func() {
      p.mu.Lock()
      defer p.mu.Unlock()
      s.signal(p.Target)
   }
   return p.Pipe
}

从参数可以看出,有两个edge传入,一个是target,一个是from,也就是第一个是待构建的edge,而from则是来源于哪个edge构建的请求,从build里的调用可以看出,from为nil,也就是第一构建的edge就是最后一个edge。 当from不为空时,OnSendCompletion会调用s.signal(p.From),发送from也就是源的信号。 OnReceiveCompletion则会发送target的信号。 同时将from,target分别添加到s.outgoign, s.incoming数组中。

scheduler.signal

// signal notifies that an edge needs to be processed again
func (s *scheduler) signal(e *edge) {
   s.muQ.Lock()
   if _, ok := s.waitq[e]; !ok {
      d := &dispatcher{e: e}
      if s.last == nil {
         s.next = d
      } else {
         s.last.next = d
      }
      s.last = d
      s.waitq[e] = struct{}{}
      s.cond.Signal()
   }
   s.muQ.Unlock()
}

根据信号edge创建新的&dispatcher,加入到s.last,并触发信号。 那发出的信号被谁接收到了,又做了什么样的处理呢?

func (s *scheduler) loop() {
   defer func() {
      close(s.closed)
   }()

   go func() {
      <-s.stopped
      s.mu.Lock()
      s.cond.Signal()
      s.mu.Unlock()
   }()

   s.mu.Lock()
   for {
      select {
      case <-s.stopped:
         s.mu.Unlock()
         return
      default:
      }
      s.muQ.Lock()
      l := s.next
      if l != nil {
         if l == s.last {
            s.last = nil
         }
         s.next = l.next
         delete(s.waitq, l.e)
      }
      s.muQ.Unlock()
      if l == nil {
         s.cond.Wait()
         continue
      }
      s.dispatch(l.e)
   }
}

记得scheduler在被jobs初始化的时候,就触发了这个loop,会一直监听最新的dispatch事件,最终分发事件s.dispatch(l.e)

// dispatch schedules an edge to be processed
func (s *scheduler) dispatch(e *edge) {
   ...

   pf := &pipeFactory{s: s, e: e}

   // unpark the edge
   e.unpark(inc, updates, out, pf)
   ...
}

也就是在分发edge的时候,调用了e.unpark,这就是真正处理edge的地方:

func (e *edge) unpark(incoming []pipe.Sender, updates, allPipes []pipe.Receiver, f *pipeFactory) {
   ...

   desiredState, done := e.respondToIncoming(incoming, allPipes)
   if done {
      return
   }

   cacheMapReq := false
   // set up new outgoing requests if needed
   if e.cacheMapReq == nil && (e.cacheMap == nil || len(e.cacheRecords) == 0) {
      index := e.cacheMapIndex
      e.cacheMapReq = f.NewFuncRequest(func(ctx context.Context) (interface{}, error) {
         cm, err := e.op.CacheMap(ctx, index)
         return cm, errors.Wrap(err, "failed to load cache key")
      })
      cacheMapReq = true
   }
   ...
   if e.execReq == nil {
      if added := e.createInputRequests(desiredState, f, false); !added && !e.hasActiveOutgoing && !cacheMapReq {
         bklog.G(context.TODO()).Errorf("buildkit scheluding error: leaving incoming open. forcing solve. Please report this with BUILDKIT_SCHEDULER_DEBUG=1")
         debugSchedulerPreUnpark(e, incoming, updates, allPipes)
         e.createInputRequests(desiredState, f, true)
      }
   }

}

在这里:

  • desiredState, done := e.respondToIncoming(incoming, allPipes),判断edge构建是否完成
  • e.cacheMapReq = f.NewFuncRequest(...),创建获取op.CacheMap的操作,可以看到这里又创建了新的pipe
  • added := e.createInputRequests(...),如果是可执行的Op,要看看依赖有没有准备好,也就是自己的Inputs,又会创建新的pipe。

这一环环的梳理下来后,如何更直观的理解呢? Edges inputs - edge pipes.jpg

  • 从Edge0开始构建,创建了一个只关心close(wait)的pipe0
  • 进入到unpark后,会检查自己Op的CacheMap,也就是这里的第二步,创建的pipe1,pipe1是从pipe0来,所以发送完成的时候会发送edge0信号,这样再次分发的dispatch事件就可以让edge0的pipe获取所需要内容,看看是否已经完成
  • edge0是有inputs()的,也就是edge1,所以也要为edge1创建构建事件,创建了pipe2,也就是步骤3
  • 在edge1开始构建后,和edge1一样,先看看自己的Op是否在CacheMap中,创建了pipe3,也就是第4步,但这里pipe3发送完成的时候是要通知edge1的

可以看出这也是一个递归,而pipe的作用就是将大家都关联起来,可以触发回调事件,这里是s.dispatchs.loop就像事件总线,负责监听新事件,并发送。

既然是递归,那就有了口,那edge的出口又在哪儿呢,是怎么判断自己完成了呢? Edges inputs - edge pipes (1).jpg 还记得s.incoming, s.outgoing数组吗? 里面存储了所有的pipe,而edge递归构建的出口就在:

desiredState, done := e.respondToIncoming(incoming, allPipes)

在这里和e0相关的pipe有p0, p1, p2,也就是说这三个pipe都完成后,e0才算构建完成。 同理,和e1相关的有p2, p3,只有p2, p3都完成了才算构建完成。

原来如此! Scheduler真是一位好管家,将所有的事情管理的有条不紊。

下一篇:深入理解Moby Buildkit系列 #28 - SourceOp CacheMap