Análise Técnica do Fluxo de Escalonamento no Kubernetes: Desvendando o kube-scheduler

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).

  1. PreFilter/Filter: Verifica recursos (CPU/RAM), seletores de nós e afinidades.
  2. Prioritize: Atribui uma nota a cada nó que passou nos filtros.
  3. 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.

Tags: kubernetes go kube-scheduler cloud-native Distributed-Systems

Publicado em 9-10 05:21