어피니티
개요
어피니티는 스케줄링 시 사용할 수 있는 대표적인 기법 중 하나이다.
문자 그대로 친화도, 선호도를 나타내는데, 그래서 원하는 노드를, 혹은 파드를 선호하게 만들 수 있다.
반대로 안티 어피니티라 하여 꺼리게 만드는 것도 가능하다.
노드 셀렉터와 비교했을 때, 훨씬 표현성이 높다.
선호도를 표현할 수 있다.
즉, 희망하는 노드를 지정할 수 있어서
유형
구체적으로 어피니티는 3가지 유형으로 분류되는데 이들은 양식 작성법이 비슷하면서도 살짝 다르다.
대충 봤다간 헷갈리기 매우 쉽기 때문에 각각의 양식 작성법도 따로 구분하여 작성한다.
일단 한가지 미리 알아둘 것은 두 필드에 대한 값이다.
- requiredDuringSchedulingIgnoredDuringExecution
- required, 즉 어피니티로서 반드시 요구되는 제약사항을 나타내는 필드이다.
- 스케줄링에 있어 이 값은 필터링으로 작용한다.
- preferredDuringSchedulingIgnoredDuringExecution
- prefer, 즉 어피니티로서 선호를 나타내는 제약사항을 나타내는 필드이다.
- 스케줄링에서는 스코어링으로서 작용하기에, 가중치 값을 넣는 식으로 설정한다.
모든 유형의 어피니티가 이 필드들을 활용하는데, 각 유형에 따라 이것을 설정하는 방법이 조금씩 달라서 자칫 혼동이 오기 쉽다.
이 각 필드를 어떻게 활용하는지는 아래에서 본격적으로 다루겠다.
nodeAffinity
apiVersion: v1
kind: Pod
metadata:
name: with-node-affinity
spec:
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: topology.kubernetes.io/zone
operator: In
values:
- antarctica-east1
- antarctica-west1
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 1
preference:
matchExpressions:
- key: another-node-label-key
operator: In
values:
- another-node-label-value
containers:
- name: with-node-affinity
image: registry.k8s.io/pause:3.8
노드 어피니티는 어떤 노드에 배치되도록할지 지정하는 어피니티이다.
파드 스펙에 .affinity.nodeAffinity
필드에 작성한다.
노드 어피니티에 있어 required는 잘 생각해보면 스케줄링#nodeSelector와 비슷하게 동작할 것이라 생각할 수 있다.
대신 그보다 훨씬 세밀하게 설정을 할 수 있다는 것이 장점이다.
nodeSelectorTerms
를 두고 그 아래로 여러 라벨 셀렉터를 넣어줄 수 있는데, 이 원소 간에는 OR 연산이 일어난다.
그리고 그 라벨 셀렉터 안속에 다시 한번 리스트가 들어가는데, 이건 AND 연산이 이뤄진다.
각 리스트에는 라벨 셀렉터로 또 리스트가 들어가면 된다.
전자의 리스트는 OR 연산, 후자의 리스트는 AND 연산이라는 것을 참고하자.
prefer에서는 가중치 값을 weight
로 작성한다.
이 값은 스코어링을 할 때 반영되는데, 적힌 값만큼 점수에 정직하게 합산된다!
스코어링을 할 때는 다른 스케줄링 기법들의 점수도 고려되는데, 값의 범위가 대충 2,3 정도 수준이다.
그러니까 여기에 100같은 값을 때려박으면 해당 기준에 매칭되는 노드는 거의 무조건 선택되는 거라 보면 된다.
prefer 부분은 nodeSelectorTerms
를 뒀던 require와 달리 바로 리스트를 작성한다.
이 리스트에 각각의 조건과 선호도를 작성해주면 된다.
그리고 각 원소의 preference
필드 라벨 셀렉터 하위로 다시금 리스트로 선택할 라벨들을 적어주면 된다.
위와 같이 여기의 라벨 리스트는 AND 연산이 일어난다.
참고로 matchExpressions.operator
는 다음의 연산자를 지원한다.
- In, NotIn - 해당 라벨 키에 지정한 값이 들어가는지 체크
- NotIn 때문에 언급하는데, a 라벨이 없는 파드는
a :
로 간주되니 주의하자.
- NotIn 때문에 언급하는데, a 라벨이 없는 파드는
- Exists, DoesNotExists - 해당 라벨 키가 존재하는지 체크
- 위의 케이스로 인해 NotIn을 쓸 때는 Exists를 같이 써주는 게 아무래도 좋다.
이건 아래 유형들에도 적용되는데, 노드 어피니티의 경우엔 다음의 추가 연산자도 지원한다.
- Gt, Lt - 정수 비교 연산자..
라고 처음에 생각했다.
근데 다시 생각해보면 노드에 대해서는 이 값이 충분히 의미가 있다.
가령 EKS만 하더라도 노드에 인스턴스 타입, 스펙 정보들을 추가 라벨로 달아둔다.
그럼 이걸 기반으로 노드를 선택하도록 유도하는 것이 가능한 것이다!
가령 cpu가 8개 이상인 노드에 배치하도록 하고 싶다면 이 연산자를 쓰면 되겠다.
podAffinity & podAntiAffinity
파드 어피니티는 어떤 파드가 있는 노드에 배치되도록 할지 지정하는 어피니티이다.
가령 웹서버 파드는 WAS서버 파드가 있는 노드에 배치돼야 상대적으로 통신이 빠르게 이뤄질 것이다.
그럴 때 이런 식으로 넣어주면 된다.
반대로 파드 안티 어피니티는 어떤 파드가 있는 노드에 배치되지 않도록 할지 지정한다.
이 어피니티들에 대한 설정은 다음과 같은 식으로 해석된다.
Y 기준을 만족하는 파드들이 있는 X 토폴로지를 가진 노드에 배치하도록 하는 어피니티
말이 벌써 어려운데, 일단 Y라는 것은 그냥 파드 라벨에 대한 정보를 적어주는 부분이다.
X가 무엇이냐가 살짝 중요한데, 이것은 노드의 그룹을 나타내는 토폴로지 정보이다.
위 그림처럼 스케줄링에 있어서 먼저 고려되는 것은 토폴로지 정보로, 토폴로지를 기준으로 노드들을 뭉텅이로 취급한다.
그 다음에 비로소 파드의 기준을 따지며 조건을 만족하는 파드가 있는 토폴로지 그룹에 스케줄링을 한다는 것이다.
즉, 노드를 그룹화시켜서 하나로 바라보는 것이 바로 토폴로지라고 보면 되겠다.
그룹화가 되면 실상 한 노드에는 조건을 만족하는 파드가 없더라도 그 노드에 파드가 배치되는 것도 가능한 일이다.
affinity:
podAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchExpressions:
- key: security
operator: In
values:
- S1
topologyKey: topology.kubernetes.io/zone
podAntiAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 100
podAffinityTerm:
labelSelector:
matchExpressions:
- key: security
operator: In
values:
- S2
topologyKey: topology.kubernetes.io/zone
이제 양식 작성법을 볼텐데, [[#nodeAffinity]]와 다르게 require에서 nodeSelectorTerms
없이 바로 라벨 셀렉터 리스트가 나온다.
사용 방식 자체는 동일한데, 이번에는 topologyKey
라는 필드를 통해 노드 토폴로지를 지정한다.
prefer 부분을 보면 또 골때리는 게, 이번에는 preference
가 아니라 podAffinityTerm.labelSelector
을 쓴다..
사용법은 또 똑같으면서...
왜 어피니티마다 살짝살짝 필드 이름이 바뀌고 사용법도 바뀌는 것인가?
아직까지는 이런 식으로 필드 명을 구분했어야 했던 이유를 모르겠다.
그냥 혼란만 가중시키는 방식이라고 생각한다.
아무튼 파드 어피니티를 쓸 때 토폴로지키는 필수값이다.
파드 어피니티는 연산량을 많이 소모할 수 있어서 큰 규모의 클러스터에서 성능 저하가 일어날 수 있다.
이건 기본적으로는 이미 배치된 파드를 대상으로 거는 것이지만 한꺼번에 같은 파드가 배치될 때도 값이 고려된다.
가령 워크로드를 통해 같은 어피니티 정보를 가진 파드가 배치된다고 했을 때, 결국 파드는 하나씩 스케줄링된다.
그래서 한 파드가 먼저 배치되면 다음 파드는 그 정보를 고려하여 스케줄링된다는 것이다.
namespaces & namespaceSelector
podAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchExpressions:
- key: app
operator: In
values:
- some-app
topologyKey: "kubernetes.io/hostname"
namespaceSelector:
matchLabels:
project: myproject
namespaces: ['default', 'kube-system']
이제 슬슬 골이 아파진다..
파드를 고르는 건 좋은데, 네임스페이스 별로 같은 이름의 파드를 가지고 있거나 하는 이슈가 있을 수 있고, 또 스케줄링하는 파드의 네임스페이스에 없는 다른 파드를 이용해 스케줄링을 하고 싶을 수 있다.
이럴 때 네임스페이스 관련 필드를 써주면 된다.[1]
여기에는 두 가지 방식이 가능한데, 하나는 namespaceSelector
로 네임스페이스의 라벨을 이용해 선택하는 것이다.
그리고 다른 하나는 namespaces
를 이용해 정직하게 네임스페이스 이름을 적어주는 것이다.
이 필드를 넣고 나서 빈 값(namspaces: []
나 namespaceSelector: {}
)을 넣으면 모든 네임스페이스를 고려하겠다는 뜻이다.
아예 이 필드를 생략한다면 현재 스케줄링되는 파드의 네임스페이스만 고려하겠다는 뜻이 된다.
matchLabelKeys
이제 골이 더 아파진다!
이 matchLabelKeys
, misMatchLabelKeys
는 나온지 얼마 안 된 기능인데, 나오게 된 경위부터 살펴보자.[2]
podAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchExpressions:
- key: app
operator: In
values:
- database
topologyKey: topology.kubernetes.io/zone
이런 식으로 어피니티가 짜여진 디플로이먼트를 업데이트하는 상황을 생각해보자.
이 디플로이먼트는 파드들이 서로 가까이 있는 것이 요구되는 어플리케이션을 실행하고 있는 것이다.
근데 업데이트 간에는 레플리카셋을 통해 파드는 구버전과 새버전이 공존하는 기간이 생긴다.
이때 문제가 발생하는데, 스케줄러는 파드들의 버전들의 차이를 인지할 수 없어 최적의 스케줄링을 해주지 못한다.
- 새 버전의 파드들이 최대한 같은 곳에 배치돼야 하는데, 구버전과 신버전이 마구 혼합되어 배치돼서 원하는 대로 스케줄링 안 됨
- 만약 안티 어피니티도 걸려있었다면 구버전 파드가 널리널리 퍼져 있어서 신버전 파드가 스케줄링 안 될 수도 있음
그럼 어떻게 하지?
사실 여기에 한 가지 해결책이 있다.
podAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchExpressions:
- key: pod-template-hash
operator: In
values:
- ???
topologyKey: topology.kubernetes.io/zone
모든 디플로이먼트는 각 버전에 대해서 pod-template-hash
라는 라벨을 추가적으로 두어 버전 별 파드들이 엮이지 않도록 만든다.
그럼 해당 라벨을 이용해 어피니티를 만들면 문제가 없을 것이다!
그런데... 이 값은 디플로이먼트가 업데이트될 때나 생성될 때 만들어지기 때문에, 디플 조작 명령만 내리는 우리로선 디플의 동작이 수행되기 이전까지 pod-template-hash
라는 키에 대한 값을 알 수가 없다..
affinity:
podAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchExpressions:
- key: app
operator: In
values:
- database
topologyKey: topology.kubernetes.io/zone
matchLabelKeys:
- pod-template-hash
이 문제를 해결하기 위해 나온 것이 바로 matchLabelKeys
이다.
스케줄러는 명시된 키에 대한 값에 대해서는 확실하게 값이 라벨셀렉터로 제대로 들어가도록 보장해준다.
디플로이먼트 업데이트 명령이 api서버에 들어와서 etcd에 해당 정보가 저장됐다.
그럼 kube-controller-manager의 워크로드 컨트롤러가 동작하여 업데이트 간 새롭게 만들어져야 할 파드, 없어져야 할 파드들을 etcd에 업데이트하도록 api서버에 요청을 날릴 것이다.
이때 pod-template-hash
키가 새로운 파드들에 채워질 것이다.
kind: Pod
metadata:
name: application-server
labels:
pod-template-hash: xyz
...
affinity:
podAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchExpressions:
- key: app
operator: In
values:
- database
- key: pod-template-hash
operator: In
values:
- xyz
topologyKey: topology.kubernetes.io/zone
matchLabelKeys:
- pod-template-hash
스케줄러는 matchLabelKeys
에 나온 키를 보고 해당하는 라벨에 대한 값을 labelSelector
에 추가해준다.
위 예제를 보면 파드 라벨에 pod-template-hash
가 생겼고, 이 값을 스케줄러가 그대로 labelSelector
에 등록을 한 것이 보인다.
사용케이스를 기반으로 정리하자면 matchLabelKeys
는 워크로드 업데이트 간 다른 버전 간 구분을 명확히 하기 위해 사용한다.
misMatchLabelKeys
위의 필드의 반대로 동작하는 것이 misMatchLabelKeys
이다.
모든 파드들이 테넌트 간 분리를 명확히 하기 위해 tenant라는 키를 기반으로 라벨을 가진다고 쳐보자.
구체적으로 tenant라는 키 뒤에 어떤 값이 들어올지를 모르더라도 테넌트 간 명확한 노드 분리를 보장하고 싶다면 다음과 같이 필드를 사용하면 된다.
apiVersion: v1
kind: Pod
metadata:
labels:
# 관련된 모든 파드가 이 라벨의 키는 가지고 있다.
tenant: tenant-a
...
spec:
affinity:
podAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
# 같은 테넌트끼리는 무조건 같은 토폴로지 그룹에 배치된다.
- matchLabelKeys:
- tenant
topologyKey: node-pool
podAntiAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
# 다른 테넌트끼리는 다른 토폴로지 그룹에 배치되도록 하기 위한 설정
# 이 파드가 실제 어떤 테넌트인지는 관리자가 모르더라도 간단하게 테넌트 간 분리를 달성할 수 있다.
- mismatchLabelKeys:
- tenant
# tenent 라벨이 존재하는 파드들만 고려하기 위해서 이 필드를 또 넣어준다.
# 이게 있어야 하는 이유는 아래에서 후술
labelSelector:
matchExpressions:
- key: tenant
operator: Exists
topologyKey: node-pool
이렇게 세팅을 한다면 현재 내가 만드는 파드가 어떤 테넌트인지 일일히 신경쓰면서 양식을 작성하지 않아도 알아서 테넌트 간 분리가 달성된다.
labelSelector:
matchExpressions:
- key: tenant
operator: NotIn
values:
- tenant-a
misLabelMatchKeys
는 이렇게 라벨 셀렉터를 추가해준다.
위에서 라벨셀렉터 추가 조건으로 tenant라는 키가 있어야 한다는 규칙을 넣었는데, 이게 바로 그 이유다.
저 규칙이 없으면 tenant란 라벨에 tenant-a가 없는 모든 파드들에 대해 안티어피니티가 설정되는데, 이건 테넌트 라벨이 붙지 않는 모든 파드들과 격리되도록 설정하는 꼴이다.
그래서 테넌트 라벨을 붙이지 않은, 그냥 노드 세팅을 위해 존재하는 데몬셋 파드까지 고려대상이 되어버리는 불상사가 발생할 수 있다.
사용 케이스를 기반으로 정리하자면 misLabelMatchKeys
는 휴먼 에러 없이 명확하게 노드를 테넌시 별로 구분하여 사용하기 위해 사용한다.
스케줄링 프로필에서
apiVersion: kubescheduler.config.k8s.io/v1beta3
kind: KubeSchedulerConfiguration
profiles:
- schedulerName: default-scheduler
- schedulerName: foo-scheduler
pluginConfig:
- name: NodeAffinity
args:
addedAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: scheduler-profile
operator: In
values:
- foo
이렇게 addedAffinity
필드에 똑같은 양식으로 적어주면 된다.
관련 문서
이름 | noteType | created |
---|