Kubernetes에서 커스텀 스케줄러 확장하기: Scheduler Extender 활용

Kubernetes에서 Scheduler Extender를 통해 커스텀 스케줄링 로직을 구현하는 방법을 알아봅니다.

  1. 커스텀 스케줄링 로직이 필요한 이유

스케줄링이란?

  • 스케줄링은 Pod 객체의 spec.nodeName에 값을 할당하는 것을 의미합니다.
  • 대상 Pod는 모든 spec.nodeName이 비어 있는 Pod입니다.
  • 스케줄링 과정은 현재 Pod에 대해 클러스터 내 Node들 중 가장 적합한 하나를 선택하는 작업입니다.

Pod에는 일반적으로 주목하지 않는 속성인 spec.schedulerName이 있습니다. 이 속성은 해당 Pod이 어떤 스케줄러로 스케줄링될지를 지정합니다.

그렇다면 보통 spec.schedulerName을 설정하지 않아도 어떻게 스케줄링이 되는 걸까요?

기본 kube-scheduler는 spec.schedulerName이 비어 있거나 "default"인 Pod을 처리할 수 있도록 설계되어 있기 때문입니다.

왜 커스텀 스케줄링 로직이 필요할까요?

특정 상황과 요구사항을 해결하고, 클러스터 자원을 더 효율적으로 사용하기 위해서 커스텀 스케줄링 로직이 필요합니다.

예시:

  • 다양한 워크로드가 특정 리소스를 요구함: 예를 들어 GPU 또는 NPU와 같은 특수 리소스가 필요한 경우, 이러한 조건을 만족하는 노드로만 Pod이 배치되도록 해야 합니다.
  • 일부 클러스터는 리소스 소비 균형을 유지해야 할 수도 있습니다.
  • 지연 시간을 줄이기 위해 특정 지역의 노드로 Pod을 배치해야 하는 경우도 있습니다.
  • 특정 애플리케이션들은 다른 애플리케이션들과 격리된 상태에서 실행되어야 할 수 있습니다.

따라서 이러한 다양한 요구사항을 충족하기 위해 커스텀 스케줄러를 구현할 필요가 있습니다.

  1. 커스텀 스케줄링 로직 추가 방법

커스텀 스케줄러를 구현하는 몇 가지 방법

커스텀 스케줄링 로직을 추가하는 것은 복잡하지 않습니다. Kubernetes의 전체 스케줄링 프로세스는 플러그인화되어 있으므로 새 스케줄러를 처음부터 구현할 필요 없이 단순히 스케줄링 과정의 각 단계에 커스텀 로직을 추가하면 됩니다.

전반적으로 다음과 같은 방향으로 나눌 수 있습니다:

1) 새로운 스케줄러 추가

  • **스케줄링 프레임워크(Scheduling Framework)**를 사용하여 scheduler-plugins을 기반으로 커스텀 스케줄러 개발을 간소화할 수 있습니다.

2) 기존 스케줄러 확장

  • Scheduler Extender를 통해 기존 스케줄러를 확장할 수 있습니다. 별도의 HTTP 서비스를 생성하고 해당 인터페이스를 구현하면 됩니다.

3) 기타 비주류 방법

  • Webhook을 사용하여 미스케줄링된 Pod의 spec.nodeName 필드를 직접 수정하는 방법도 가능합니다.

각 방법의 장단점은 아래 표를 참고하세요.

장점 단점
새로운 스케줄러 성능 우수: 외부 플러그인이나 HTTP 호출에 의존하지 않으므로 지연이 적습니다. 재사용성 높음: 기존 스케줄링 플러그인을 재사용할 수 있어 개발 난이도가 낮습니다. 스케줄러 간 충돌 가능성: 여러 스케줄러가 동시에 동일한 Pod을 스케줄링할 경우 문제가 발생할 수 있습니다.
스케줄러 확장 구현 간편: 스케줄러 재컴파일이 필요 없으며 KubeSchedulerConfiguration을 통해 외부 HTTP 서비스를 구성할 수 있습니다. 비침습적: 핵심 코드를 변경하거나 재구성할 필요가 없습니다. 유연성 높음: 원래 스케줄러와 커스텀 로직이 독립적이어서 유지보수가 용이합니다. 성능 저하: HTTP 호출이 필요해 성능에 영향을 미칠 수 있습니다.

일반적으로 추가해야 할 로직이 많지 않을 때는 Scheduler Extender를 사용하는 것이 간단합니다.

스케줄러 구성

스케줄러의 구성은 ConfigMap에 저장된 KubeSchedulerConfiguration 객체로 관리됩니다.

아래는 KubeSchedulerConfiguration의 YAML 예제입니다.

apiVersion: v1
data:
  config.yaml: |
    apiVersion: kubescheduler.config.k8s.io/v1beta2
    kind: KubeSchedulerConfiguration
    leaderElection:
      leaderElect: false
    profiles:
    - schedulerName: hami-scheduler
    extenders:
    - urlPrefix: "https://127.0.0.1:443"
      filterVerb: filter
      bindVerb: bind
      nodeCacheCapable: true
      weight: 1
      httpTimeout: 30s
      enableHTTPS: true
      tlsConfig:
        insecure: true
      managedResources:
      - name: nvidia.com/gpu
        ignoredByScheduler: true

기본 구성

기본 구성에서는 스케줄러 이름을 지정하는 것입니다.

apiVersion: v1
kind: ConfigMap
metadata:
  name: my-scheduler-config
  namespace: kube-system
data:
  my-scheduler-config.yaml: |
    apiVersion: kubescheduler.config.k8s.io/v1beta2
    kind: KubeSchedulerConfiguration
    profiles:
      - schedulerName: my-scheduler
    leaderElection:
      leaderElect: false   

Extender 구성

Extender 구성은 외부 HTTP 서버를 추가로 지정하여 커스텀 스케줄링 로직을 구현합니다.

apiVersion: v1
kind: ConfigMap
metadata:
  name: i-scheduler-extender
  namespace: kube-system
data:
  i-scheduler-extender.yaml: |
    apiVersion: kubescheduler.config.k8s.io/v1
    kind: KubeSchedulerConfiguration
    profiles:
      - schedulerName: i-scheduler-extender
    leaderElection:
      leaderElect: false
    extenders:
    - urlPrefix: "http://localhost:8080"
      enableHTTPS: false
      filterVerb: "filter"
      prioritizeVerb: "prioritize"
      bindVerb: "bind"
      weight: 1
      nodeCacheCapable: true

핵심 파라미터:

  • urlPrefix: 외부 스케줄링 서비스의 접근 주소.
  • filterVerb, prioritizeVerb, bindVerb: 각 단계의 HTTP API 경로.

ManagedResources 구성

ManagedResources는 특정 리소스를 요청한 Pod만 Extender 스케줄링 로직을 거치도록 제한합니다.

apiVersion: v1
data:
  config.yaml: |
    apiVersion: kubescheduler.config.k8s.io/v1
    kind: KubeSchedulerConfiguration
    leaderElection:
      leaderElect: false
    profiles:
    - schedulerName: hami-scheduler
    extenders:
    - urlPrefix: "https://127.0.0.1:443"
      filterVerb: filter
      bindVerb: bind
      nodeCacheCapable: false
      enableHTTPS: false
      managedResources:
      - name: nvidia.com/gpu
        ignoredByScheduler: true
  1. Scheduler Extender 스펙

Scheduler Extender는 HTTP 요청을 통해 스케줄링 결정을 외부 서비스에 위임합니다.

Filter, Prioritize, Bind 세 단계에서 각각 다음과 같은 HTTP 인터페이스를 구현할 수 있습니다.

Filter

요청 파라미터:

type ExtenderArgs struct {
    Pod *v1.Pod
    Nodes *v1.NodeList
    NodeNames *[]string
}

응답 결과:

type ExtenderFilterResult struct {
    Nodes *v1.NodeList
    NodeNames *[]string
    FailedNodes FailedNodesMap
    FailedAndUnresolvableNodes FailedNodesMap
    Error string
}

Prioritize

요청 파라미터:

type ExtenderArgs struct {
    Pod *v1.Pod
    Nodes *v1.NodeList
    NodeNames *[]string
}

응답 결과:

type HostPriority struct {
    Host string
    Score int64
}

type HostPriorityList []HostPriority

Bind

요청 파라미터:

type ExtenderBindingArgs struct {
    PodName string
    PodNamespace string
    PodUID types.UID
    Node string
}

응답 결과:

type ExtenderBindingResult struct {
    Error string
}
  1. 데모

간단한 Extender를 구현하는 예제입니다.

main.go

var handler *server.Handler

func init() {
    handler = server.NewHandler(extender.NewExtender())
}

func main() {
    http.HandleFunc("/filter", handler.Filter)
    http.HandleFunc("/priority", handler.Prioritize)
    http.HandleFunc("/bind", handler.Bind)
    http.ListenAndServe(":8080", nil)
}

Filter 구현

func (ex *Extender) Filter(args extenderv1.ExtenderArgs) *extenderv1.ExtenderFilterResult {
    filtered := make([]v1.Node, 0)

    for _, node := range args.Nodes.Items {
        if value, exists := node.Labels["custom.priority"]; exists && value != "" {
            filtered = append(filtered, node)
        }
    }

    if len(filtered) == 0 {
        return &extenderv1.ExtenderFilterResult{Error: "No nodes match the label"}
    }

    args.Nodes.Items = filtered

    return &extenderv1.ExtenderFilterResult{
        Nodes: args.Nodes,
    }
}

Prioritize 구현

func (ex *Extender) Prioritize(args extenderv1.ExtenderArgs) *extenderv1.HostPriorityList {
    var priorities extenderv1.HostPriorityList

    for _, node := range args.Nodes.Items {
        if priorityStr, exists := node.Labels["custom.priority"]; exists {
            if priority, err := strconv.Atoi(priorityStr); err == nil {
                priorities = append(priorities, extenderv1.HostPriority{
                    Host:  node.Name,
                    Score: int64(priority),
                })
            }
        }
    }

    return &priorities
}

Bind 구현

func (ex *Extender) Bind(args extenderv1.ExtenderBindingArgs) *extenderv1.ExtenderBindingResult {
    binding := &corev1.Binding{
        ObjectMeta: metav1.ObjectMeta{Name: args.PodName, Namespace: args.PodNamespace, UID: args.PodUID},
        Target:     corev1.ObjectReference{Kind: "Node", APIVersion: "v1", Name: args.Node},
    }

    err := ex.ClientSet.CoreV1().Pods(args.PodNamespace).Bind(context.Background(), binding, metav1.CreateOptions{})
    if err != nil {
        return &extenderv1.ExtenderBindingResult{Error: err.Error()}
    }

    return &extenderv1.ExtenderBindingResult{}
}
  1. 결론

이 문서에서는 Scheduler Extender를 통해 커스텀 스케줄링 로직을 구현하는 방법을 다루었습니다.

Extender는 HTTP 서버로 Filter, Prioritize, Bind 세 가지 인터페이스를 구현할 수 있으며, 이를 통해 기존 스케줄러를 확장할 수 있습니다.

기본적인 단계는 다음과 같습니다:

  1. HTTP 서비스를 생성하고 인터페이스를 구현합니다.
  2. KubeSchedulerConfiguration을 수정하여 Extender를 등록합니다.

또한 ManagedResources 구성 옵션을 사용하여 특정 Pod만 Extender 로직을 거치도록 제한할 수 있습니다.

태그: kubernetes scheduler-extender custom-scheduler

5월 21일 20:07에 게시됨