Arquitetura de Inicialização do kube-scheduler
O kube-scheduler utiliza a biblioteca Cobra para gerenciar sua interface de linha de comando e ciclo de vida inicial. O ponto de entrada principle configura as opções globais e instancia os componentes necessários para o processo de decisão de escalonamento.
// Estrutura simplificada de inicialização do comando
func NovoComandoEscalonador() *cobra.Command {
opcoes := options.NewOptions()
cmd := &cobra.Command{
Use: "kube-scheduler",
RunE: func(cmd *cobra.Command, args []string) error {
return executarComando(cmd, opcoes)
},
}
return cmd
}
func executarComando(cmd *cobra.Command, opts *options.Options) error {
contexto := context.Background()
// Configura a estrutura base e a instância do escalonador
configCompleta, instanciaSched, err := PrepararConfiguracao(contexto, opts)
if err != nil {
return err
}
return Rodar(contexto, configCompleta, instanciaSched)
}
A função PrepararConfiguracao (Setup) é crucial, pois ela valida os argumentos de entrada e constrói o objeto Scheduler, injetando dependências como Informers e clientes de API.
func PrepararConfiguracao(ctx context.Context, opts *options.Options) (*config.CompletedConfig, *scheduler.Scheduler, error) {
// Validação de flags e parâmetros
if errs := opts.Validate(); len(errs) > 0 {
return nil, nil, utilerrors.NewAggregate(errs)
}
configBase, _ := opts.Config(ctx)
cfg := configBase.Complete()
// Inicialização da instância principal
instancia, err := scheduler.New(ctx,
cfg.Client,
cfg.InformerFactory,
recorderFactory,
scheduler.WithKubeConfig(cfg.KubeConfig),
scheduler.WithProfiles(cfg.ComponentConfig.Profiles...),
scheduler.WithParallelism(cfg.ComponentConfig.Parallelism),
)
return &cfg, instancia, err
}
Componentes Internos e Filas de Prioridade
Dentro do método scheduler.New, o sistema configura os mecanismos de cache e a SchedulingQueue. O escalonador utiliza três filas principais para gerenciar Pods que ainda não foram alocados:
- ActiveQ: Pods prontos para tentativa imediata de escalonamento.
- BackoffQ: Pods que falharam anteriormente e aguardam um tempo de espera (backoff).
- UnschedulablePods: Pods que não puderam ser alocados devido a restrições de recursos ou afinidade.
func New(ctx context.Context, ...) (*Scheduler, error) {
// Registro de plugins internos e externos
registroPlugins := frameworkplugins.NewInTreeRegistry()
// Configuração do cache de nós e snapshots
cacheSnapshot := internalcache.NewEmptySnapshot()
// Criação da fila de prioridade
filaPrioritaria := internalqueue.NewSchedulingQueue(
perfis[0].QueueSortFunc(),
informerFactory,
internalqueue.WithPodInitialBackoffDuration(time.Second * 5),
)
sc := &Scheduler{
Cache: internalcache.New(ctx, 30*time.Second),
SchedulingQueue: filaPrioritaria,
nodeInfoSnapshot: cacheSnapshot,
NextPod: filaPrioritaria.Pop,
}
return sc, nil
}
O Fluxo de Execução Principal (Run)
Ao rodar o escalonador, ele inicia os Informers para sincronizar o estado do cluster e entra em um loop infinito processando um Pod por vez através da função scheduleOne.
func (s *Scheduler) Run(ctx context.Context) {
// Inicia o processamento da fila
s.SchedulingQueue.Run(logger)
// Loop de escalonamento contínuo
wait.UntilWithContext(ctx, s.scheduleOne, 0)
}
Processo de Escalonamento Individual: scheduleOne
A lógica de scheduleOne é dividida em duas grandes fases: o Ciclo de Escalonamento (síncrono) e o Ciclo de Bind (assíncrono).
func (s *Scheduler) scheduleOne(ctx context.Context) {
// 1. Obtém o próximo Pod da ActiveQ
podInfo, _ := s.NextPod(logger)
// 2. Ciclo de Escalonamento: Decisão de qual nó usar
resultado, assumedPod, status := s.schedulingCycle(ctx, fwk, podInfo)
if !status.IsSuccess() {
s.FailureHandler(ctx, fwk, assumedPod, status)
return
}
// 3. Ciclo de Bind: Executado em goroutine separada para não travar o fluxo
go func() {
statusBind := s.bindingCycle(ctx, fwk, resultado, assumedPod)
if !statusBind.IsSuccess() {
s.handleBindingError(ctx, assumedPod, statusBind)
}
}()
}
Filtros e Pontuação (Scheduling Cycle)
O coração da decisão reside no schedulingCycle. Ele executa plugins de Filter (para descartar nós incapazes de rodar o Pod) e Score (para classificar os nós restantes).
- PreFilter/Filter: Verifica recursos (CPU/RAM), seletores de nós e afinidades.
- Prioritize: Atribui uma nota a cada nó que passou nos filtros.
- Assume: Atualiza o cache local assumindo que o Pod rodará no nó escolhido, permitindo que o próximo Pod seja processado sem esperar a confirmação da API.
func (s *Scheduler) findNodesThatFitPod(ctx context.Context, fwk framework.Framework, pod *v1.Pod) ([]*framework.NodeInfo, error) {
todosNos, _ := s.nodeInfoSnapshot.NodeInfos().List()
// Executa plugins de Filtro em paralelo
nosViaveis, _ := s.filtrarNos(ctx, fwk, pod, todosNos)
return nosViaveis, nil
}
Efetivando a Alocação (Binding Cycle)
Uma vez que um nó é selecionado, o escalonador entra na fase final. O ciclo de bind comunica a decisão ao kube-apiserver criando um objeto do tipo Binding.
func (s *Scheduler) bind(ctx context.Context, fwk framework.Framework, pod *v1.Pod, node string) *framework.Status {
// Chamada final para persistir a alocação no cluster
bindingObj := &v1.Binding{
ObjectMeta: metav1.ObjectMeta{Namespace: pod.Namespace, Name: pod.Name, UID: pod.UID},
Target: v1.ObjectReference{Kind: "Node", Name: node},
}
err := s.client.CoreV1().Pods(pod.Namespace).Bind(ctx, bindingObj, metav1.CreateOptions{})
return framework.AsStatus(err)
}
Este mecanismo desacoplado garante que o escalonador possa manter um alto rendimento (throughput), processando centenas de Pods por segundo, enquanto as operações de rede e persistência ocorrem em segundo plano.