Kubernetes에서 Scheduler Extender를 통해 커스텀 스케줄링 로직을 구현하는 방법을 알아봅니다.
- 커스텀 스케줄링 로직이 필요한 이유
스케줄링이란?
- 스케줄링은 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을 배치해야 하는 경우도 있습니다.
- 특정 애플리케이션들은 다른 애플리케이션들과 격리된 상태에서 실행되어야 할 수 있습니다.
따라서 이러한 다양한 요구사항을 충족하기 위해 커스텀 스케줄러를 구현할 필요가 있습니다.
- 커스텀 스케줄링 로직 추가 방법
커스텀 스케줄러를 구현하는 몇 가지 방법
커스텀 스케줄링 로직을 추가하는 것은 복잡하지 않습니다. 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
- 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
}
- 데모
간단한 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{}
}
- 결론
이 문서에서는 Scheduler Extender를 통해 커스텀 스케줄링 로직을 구현하는 방법을 다루었습니다.
Extender는 HTTP 서버로 Filter, Prioritize, Bind 세 가지 인터페이스를 구현할 수 있으며, 이를 통해 기존 스케줄러를 확장할 수 있습니다.
기본적인 단계는 다음과 같습니다:
- HTTP 서비스를 생성하고 인터페이스를 구현합니다.
- KubeSchedulerConfiguration을 수정하여 Extender를 등록합니다.
또한 ManagedResources 구성 옵션을 사용하여 특정 Pod만 Extender 로직을 거치도록 제한할 수 있습니다.