안녕하세요. 쿠버네티스에서 파드를 분산 처리할 때 사용할 수 있는 토폴로지 분배 제약 조건(topology spread constraints) 활용 사례를 공유할 ContentsService2 팀 문범우입니다.
서비스의 가용성과 안전성을 높이기 위해 최근 많은 분들이 쿠버네티스를 다양하게 활용하고 있습니다. 쿠버네티스를 활용해 애 플리케이션을 관리할 때 매우 중요한 요소 중 하나가 파드(pod)의 분산 처리인데요. 파드 분산 처리는 단순히 리소스 활용을 최적화하는 것을 넘어 장애 복원력과 서비스 고가용성, 지속성을 보장하는 데 중요한 역할을 하기 때문입니다.
파드가 클러스터 내 특정 노드에 몰리면 해당 노드에 장애가 발생했을 때 애플리케이션의 전체 가용성이 위협받을 수 있으며, 성능에도 영향을 끼칠 수 있습니다. 따라서 파드를 클러스터 내 여러 노드에 고르게 분산할 필요가 있으며, 이를 통해 단일 장애 지점(single point of failure, SPOF) 발생을 최소화하고 클러스터의 전반적인 안정성과 리소스 사용 효율을 높일 수 있습니다.
쿠버네티스는 파드를 효과적으로 분산해 관리할 수 있는 다양한 방식을 제공하는데요. 그중에서도 파드 토폴로지 분배 제약 조건이라는 기능을 사용하면 효과적이면서도 간편하게 파드를 분산할 수 있습니다.
이 글에서는 파드를 분산 처리할 수 있는 세 가지 방식을 안내하고, 그중 토폴로지 분배 제약 조건의 장점과 활용 방법을 자세히 알아보겠습니다. 파드 분산 처리 영역은 쿠버네티스에 대한 전문적인 지식이 없더라도 충분히 이해할 수 있기 때문에 아직 쿠버네티스를 깊이 이해하지 않은 상태에서도 쉽게 이해할 수 있을 것이라고 생각합니다. 사실 서비스가 운영되고 있는 시점에서 쿠버네티스의 설정을 변경하는 것은 또 다른 리스크를 가져올 수 있기 때문에 오히려 쿠버네티스를 이제 막 도입하려고 준비하고 계신 분들이나 공부를 시작하신 분들께 더욱 큰 도움이 될 것이라고저 생각합니다.
파드 분산 처리가 필요한 이유
앞서 말씀드린 바와 같 이, 결론적으로 파드를 분산 처리하는 이유는 서비스의 가용성과 효율성을 높이기 위해서입니다. 쿠버네티스 스케줄러가 기본적으로 파드를 분산 배치해 주기는 하지만, 서비스가 정교해지고 거대해지면서 많은 파드와 노드를 활용하면 스케줄러가 자동으로 해주는 분산 처리만으로는 부족합니다. 보다 정확하고 정교한 파드 분산 처리가 필요해집니다.
'그럼 아직 소규모인 서비스 초기에는 쿠버네티스 스케줄러를 이용해 간단하게 시작하다가 나중에 정교한 파드 분산 처리가 필요할 때 쿠버네티스 설정을 바꾸면 되는 것 아니야?'라고 생각하실 수도 있는데요. 앞서 말씀드린 것처럼 서비스가 운영되고 있는 시점에 쿠버네티스의 설정을 변경하는 것은 그 자체로 또 하나의 리스크가 될 수 있기 때문에 초기부터 파드의 분산 처리를 고려해 설정하는 것을 추천합니다.
한 가지 예시를 들어보겠습니다. 어느 서비스에서 노드 2개와 파드 4개를 운영하고 있다고 가정해 보겠습니다. 쿠버네티스 스케줄러가 기본적인 파드 분산 처리를 해주기는 하지만 정교하고 완벽하지는 않기 때문에 아래 그림과 같이 하나의 노드에만 파드가 배치되는 극단적인 상황이 충분히 발생할 수 있습니다.
만약 이런 환경에서 아래와 같이 1번 노드에 장애가 발생하면 모든 파드에 장애 상황이 전파되면서 서비스에 접근할 수 없게 됩니다.
물론 쿠버네티스가 이를 감지하고 즉각 2번 노드에 파드를 재기동하겠지만, 파드 종료 후 재기동되기까지는 서비스가 다운되며 장애가 발생할 수밖에 없습니다. 이런 상황을 미연에 방지하기 위해서는 파드를 정교하게 분산 처리해야 합니다.
파드를 분산 처리해야 하는 이유는 다음과 같이 크게 세 가지로 나눠 생각해 볼 수 있습니다.
- 고가용성: 파드가 적절히 분산 처리되면 일부 노드에 장애가 발생하더라도 서비스를 계속 제공할 수 있습니다. 즉, 서비스의 가용성을 높일 수 있습니다.
- 리소스 최적화: 파드가 적절히 분산 처리되면 노드의 한정된 자원을 더 효율적으로, 최적화해서 사용할 수 있습니다.
- 성능 향상: 일부 노드에만 파드가 집중 배치되면 외부 요청이 특정 노드로 몰리면서 서비스 성능이 저하 상황을 예방할 수 있습니다.
이외에도 서비스를 운영하고 유지 보수하다 보면 파드를 분산 처리해야 할 다양한 이유가 발생할 수 있는데요. 대표적인 사례로 쿠버네티스 클러스터 버전 업그레이드 과정을 살펴보겠습니다.
분산 처리가 필요한 사례: 쿠버네티스 클러스터 버전 업그레이드
쿠버네티스는 공식적으로 3개월 또는 4개월마다 버전을 릴리스하고 있으며, 릴리스한 버전은 최대 9개월까지 지원합니다. 이를 사용자 입장에서 생각해 보면, 1년 전에 설정해 놓은 클러스터는 이미 공식 지원이 종료된 버전일 가능성이 매우 높다는 것인데요. 물론 아직 공식 지원이 종료된 클러스터 버전을 사용하면서 보안 관점에서 큰 이슈가 발생하거나 기존 기능을 사용하는 데 문제가 발생한 경우가 확인된 바는 없지만, 당연히 최신 버전으로 운영하는 것을 권고하고 있습니다.
그럼 쿠버네티스 클러스터 버전을 업그레이드하는 과정을 간단하게 살펴볼 텐데요. 아래와 같이 v1.19 버전의 노드를 운영하고 있는 상태에서 v1.20 버전으로 업그레이드하는 상황을 고려해 보겠습니다.
쿠버네티스에서 버전 업그레이드는 노드 단위로 실행됩니다. 따라서 아래 그림과 같이 업그레이드를 위해 1번 노드가 종료되면 1번 노드에 있던 파드가 다른 노드로 배치되는 상황이 발생할 수 있습니다.
1번 노드가 정상적으로 종료된 후 버전이 잘 업그레이드되면 다시 파드를 배치할 수 있습니다. 이후 다른 노드를 대상으로 위 순서가 반복됩니다.
이렇게 모든 노드의 버전이 순서대로 업그레이드되어 모든 노드가 동일한 버전이 되면 쿠버네티스 클러스터는 버전 업그레이드가 완료된 것으로 인식하는데요. 앞서 살펴본 과정에서 만약 파드가 제대로 분산 처리돼 있지 않았다면, 다시 말해 일부 노드에 상당히 많은 파드가 집중 배치돼 있었다면 해당 노드를 업그레이드하는 시점에 다수의 파드가 한 번에 종료될 수밖에 없습니다. 이는 서비스 성능 저하로 이어질 수 있고, 극단적인 경우에는 앞서 살펴본 것처럼 서비스 다운이 발생하며 장애로 이어질 수도 있습니다.
파드를 분산 처리하는 세 가지 방법
그럼 이 중요한 파드 분산 처리를 과연 어떻게 할 수 있을까요? 세 가지 방법을 살펴보겠습니다.
디플로이먼트 파드 분산 배치
첫 번째는 디플로이먼트(deployment)에서 자동으로 수행되는 스케줄러를 활용하는 방법입니다. 디플로이먼트를 이용해서 파드를 노드에 배치할 때는 쿠버네티스 스케줄러가 필터링과 스코어링, 두 단계에 걸쳐서 노드에 파드를 배치합니다. 먼저 파드를 배치할 수 있는 노드를 필터링하고, 다음으로 쿠버네티스 스케줄러 내부 기준에 따라 각 노드별로 점수를 매긴 뒤, 그중에서 점수가 가장 높은 노드로 파드를 배치하는 것이죠.
이 방법에서 가장 좋은 점은 디플로이먼트를 사용하기만 하면 개발자가 별도로 설정하지 않아도 자동으로 이 과정이 진행된다는 것입니다. 하지만 이 방법은 개발자나 시스템 운영자의 의도를 반영해 커스터마이징하기가 어렵습니다. 대규모 시스템을 운영할 때 필요한 정교한 파드 분산 처리가 어려운 것이죠. 서비스가 발전하고 고도화되면서 파드가 많아지고 운영하는 노드가 많아지면, 디플로이먼트를 통해서 쿠버네티스 스케줄러가 알아서 해주는 파드 분산 처리만을 믿고 서비스를 운영한다면 예상치 못한 서비스 장애 상황을 만날 수 있습니다.
어피니티 이용하기
두 번째는 어피니티(affinity)를 활용하는 방법입니다. 어피니티는 뒤에서 설명할 토폴로지 분산 제약보다 먼저 반영된 옵션입니다. 그 때문인지 어피니티에 익숙하신 분들이 더 많은 것 같은데요. 어피니티를 살펴보면서 토폴로지 분산 제약과 어떻게 다른지도 함께 살펴보겠습니다.
먼저 어피니티는 크게 노드 어피니티와 파드 어피니티, 두 가지가 존재합니다.
노드 어피니티는 파드와 노드 간 관계를 정의하는 옵션입니다. 대표적으로 노드에 존재하는 레이블을 기반으로 파드가 특정 레이블을 가진 노드에 배치되게 하거나(어피니티) 반대로 배치되지 않게(안티-어피니티) 설정할 수 있습니다.
반면 파드 어피니티는 파드와 노드 간의 관계가 아니라 파드와 파드 간의 관계를 정의합니다. 예를 들어 a 파드와 b 파드가 있다면, a 파드를 배치할 때 꼭 b 파드가 배치된 노드에 배치하도록 설정하거나(어피니티), 또는 b 파드와 다른 노드에 배치되도록 설정할 수 있습니다(안티-어피니티).
이와 같이 어피니티는 특정 노드 또는 파드와의 관계에 따라서 파드의 위치를 결정할 때 주로 사용합니다. 이 기능을 파드 분산 처리로도 활용할 수는 있는데요. 보통 어피니티는 파드의 위치를 보다 정교하게 결정하기 위해 사용하는 옵션이기 때문에, 단순히 파드를 분산 처리하기 위한 용도로 사용하기에는 아래 소개할 토폴로지 분산 제약보다 조금 더 복잡한 설정 작업을 거쳐야 한다는 단점이 있습니다.
토폴로지 분산 제약 이용하기
그럼 마지막으로 토폴로지 분산 제약(topology spread constraints)을 설명하겠습니다. 최신 쿠버네티스 버전을 보면 토폴로지 분산 제약에 다양한 하위 옵션이 준비돼 있는데요. v1.19 버전에서 최초에 도입됐을 때에는 주로 아래 캡처 화면에 보이는 네 가지 옵션(maxSkew
, topologyKey
, whenUnsatisfiable
, lableSelector
)이 필수 옵션으로 사용됐습니다.
현재 버전에서도 이 네 가지 옵션만으로도 충분히 파드를 분산 처리할 수 있기에 이 글에서는 이 옵션들만 확인해 보겠습니다. 나머지 옵션들은 더욱 정교하게 파드를 분산 처리해야 할 때 하나씩 직접 확인하면서 사용하시면 될 것 같습니다.
그럼 각 옵션이 무슨 역할을 하는지, 어떻게 쿠버네티스가 토폴로지 분산 제약으로 파드를 분산 처리해 주는지 살펴보겠습니다.
maxSkew
skew
값은 이후에 확인할 topologyKey
에 따라 파드를 분산할 때 각 토폴로지 도메인 간 파드 개수의 최대 차이값입니다. 조금 더 쉽게 생각해 보면, 저희가 분산 배치할 그룹(토폴로지)에 속한 각 노드에 배치된 최대 파드 개수에서 최소 파드 개수를 뺀 값입니다(토폴로지라는 개념이 낯선 분들은 이를 존(zone)이나 그룹이라고 생각하시면 될 것 같습니다).
예시와 함께 살펴보겠습니다. 아래와 같이 세 개의 노드가 같은 topologyKey
로 설정돼 있다고 가정하겠습니다(topologyKey
의 값, 즉 토폴로지 도메인은 모두 다른 상태로, 보다 자세한 설명은 아래 topologyKey
섹션에서 살펴보겠습니다). 현재 각 노드에 파드가 모두 두 개씩 배치돼 있기 때문에 최대 파드 개수도 2이고, 최소 파드 개수도 2입니다. 따라서 최대 파드 개수에서 최소 파드 개수를 뺀 skew
값은 0입니다.
아래 그림에서는 최대 파드 개수가 2이고 최소 파드 개수가 1이기 때문에 skew
값은 1입니다.
아래 그림에서는 최대 파드 개수는 2로 동일하지만 파드가 존재하지 않는 노드가 있어서 최소 파드 개수는 0이기 때문에 skew
값은 2입니다.
이제 감이 오셨을 텐데요. 토폴로지 분산 제약에서 의 maxSkew
값은 파드를 분산 배치할 때 허용할 최대 skew
값을 의미합니다. 당연하게도 해당 값을 크게 잡을수록 파드가 덜 고르게 분포되지만, 그렇다고 너무 작게 잡으면 파드 배치 조건이 타이트해지면서 스케줄링이 지연될 수 있습니다. 따라서 노드와 파드 규모를 고려해 적절하게 값을 설정해야 합니다.
topologyKey
위에서 skew
값을 계산할 때 토폴로지 도메인을 기반으로 계산한다고 말씀드렸습니다. 여기서 토폴로지 도메인의 기준으로 사용하는 값이 topologyKey
입니다.
노드에는 다양한 키-값의 레이블이 할당되는데요. 이때 topologyKey
에 입력된 값을 키로 갖고 있는 레이블이 있다면 지금 적용하고자 하는 토폴로지 분산 제약 옵션의 대상이 됩니다. 즉, 대상 토폴로지가 되는 것인데요. 이때 해당 키에 대한 값(value)이 토폴로지 도메인이 됩니다.
토폴로지 도메인은 하나의 그룹이라고 생각하시면 편합니다. 토폴로지 도메인이 같은 것끼리는 파드의 분산 처리 대상이 아니라 동일한 영역으로 고려되는 것입니다(물론 토폴로지라는 개념은 토폴로지 분산 제약의 옵션으로만 활용되는 것은 아닙니다).
예를 들어 topologyKey
를 domain
으로 설정했다고 가정해 보겠습니다. 아래 그림을 보면 세 개의 노드가 domain
이라는 키값을 갖고 있는데 그 값이 각각 A
, B
, C
로 서로 다릅니다. 이 경우 값이 모두 다르기 때문에 세 노드는 각각 토폴로지 도메인을 이루며, 파드는 세 개의 토폴로지 도메인을 대상으로 분산 처리됩니다.
아래 그림에서는 두 노드가 domain
에 B
라는 동일한 값을 갖고 있습니다. 따라서 좌측 상단 노드(domain=A
)가 이루는 토폴로지 도메인 하나와, 우측과 하단 노드(domain=B
)가 이루는 토폴로지 도메인 하나, 총 두 개의 토폴로지 도메인이 형성됩니다. 파드가 분산될 때에는 두 개의 토폴로지 도메인을 대상으로 분산 처리되기 때문에 우측 노드와 하단 노드만 봤을 때에는 둘 중 하나의 노드에 파드가 집중 배치될 수 있습니다.
그럼 이번엔 위에서 확인했던 skew
값과 함께 확인해 볼까요? 만약 아래 그림과 같은 상황이라면 skew
값은 1이 됩니다. 상단의 domain=A
로 구성된 토폴로지 도메인은 총 두 개의 파드를 갖고 있고, 하단의 domain=B
로 구성된 토폴로지 도메인은 총 세 개의 파드를 갖고 있기 때문입니다.
이와 같이 topologyKey
를 활용하면 노드에 붙은 레이블에 따라서 토폴로지 도메인이 구성되고, 각 토폴로지 도메인에 따라서 skew
값을 계산하도록 설정할 수 있습니다.
labelSelector
앞서 살펴본 topologyKey
가 노드에 대한 조건이라면, labelSelector
는 파드에 대한 조건입니다. 만약 토폴로지 분산 제약이 무조건 모든 파드에 적용된다면, 분산 배치가 별로 필요하지 않은 파드까지 관리해야 하는 번거로움이 생깁니다. 또는 A 기능을 하는 파드 그룹과 B 기능을 하는 파드 그룹 간에 서로 다른 분산 배치를 적용하기가 어려워지겠죠.
이를 방지하기 위해 토폴로지 분산 제약을 적용할 파드를 설정하는 조건이 labelSelector
입니다. 즉, labelSelector
옵션에 기입된 레이블이 존재하는 파드만 분산 배치를 적용하는 것입니다.
만약 아래 그림처럼 labelSelector
값으로 Key1=value1
과 Key2=value2
를 설정하면, 왼쪽 네 개 파드만 파드 분산 배치에 영향을 받습니다. 오른쪽 파드는 labelSelector
조건에 부합하지 않기 때문에 파드 분산 배치 대상에서 제외됩니다.
whenUnsatisfiable
whenUnsatisfiable
는 문자 그대로 앞서 설정한 옵션들을 만족할 수 없는 상황에서 파드를 어떻게 배치할 것인지를 정하기 위한 옵션입니다.
아래 그림을 보면 두 노드가 서로 다른 토폴로지 도메인으로 구성돼 있고, 해당 토폴로지의 skew
값은 1입니다. 이때 maxSkew
값이 1로 설정돼 있다면, 신규 파드를 배치할 때 오른쪽 노드(domain=B
)에 배치해야 합니다.
그런데 이때 오른쪽 노드에 이미 다른 파드가 배치돼 있어서 노드의 리소스가 부족해 새로운 파드를 배치할 수 없는 상황이 벌어질 수 있습니다. 이런 상황에서 파드를 어떻게 배치할 것인지를 결정하는 역할이 whenUnsatisfiable
옵션의 역할입니다.
whenUnsatisfiable
설정은 DoNotSchedule
과 ScheduleAnyway
의 두 가지 옵션을 제공합니다.
먼저 DoNotSchedule
은 이름 그대로 maxSkew
값을 넘어가는 파드는 배치하지 않고 대기하게 만드는 옵션입니다. 만약 위 상황에서 이 옵션으로 설정했다면 배치하려고 했던 파드는 자리가 날 때까지 무한정 대기할 것입니다.
반대로 ScheduleAnyway
는 maxSkew
값을 넘기더라도 일단 파드를 배치 가능한 노드에 배치해서 실행하는 옵션입니다. 이때 사용자에게 경고 메시지가 가거나 이후 별도 리밸런싱 작업 같은 것은 진행되지 않습니다. 또한 이후 다시 DoNotSchedule
로 변경하더라도 기존에 배치돼 있던 파드를 강제로 종료시키면서 리밸런싱 작업을 하지는 않습니다. 따라서 만약 파드를 정말 정교하고 정밀하게 분산 배치해야 하고 그렇지 않을 경우 이슈가 발생할 수 있는 상황이라면, ScheduleAnyway
대신 DoNotSchedule
를 사용하는 게 올바른 방향일 것입니다.
토폴로지 분산 제약 옵션 요약정리
마지막으로 각 옵션의 목적을 간단하게 정리해 보겠습니다.
maxSkew
: 허용 파드 간격 설정topologyKey
:skew
값을 계산할 토폴로지 도메인(노드 그룹) 설정, 파드 분산 배치 시 사용하는 토폴로지 기준labelSelector
: 계산 또는 관리에 포함할 대상 파드 설정whenUnsatisfiable
:maxSkew
값을 초과할 때 파드를 어떻게 배치할지 정책 설정
토폴로지 분산 제약 사용 사례
아래는 저희 팀에서 사용하고 있는 토폴로지 분산 제약 설정을 간소화해서 가져온 것입니다. 생각보다 간단해 보일 수 있는데요. 이 정도의 옵션만으로도 의도한 대로 정확하게 파드를 분산 처리할 수 있습니다. 물론 파드와 노드가 더 많아지면 최신 버전의 쿠버네티스에서 지원하는 다른 옵션들을 활용해 볼 수도 있을 것이고, 서비스의 특성에 따라 파드 배치 조건이 복잡해지는 경우 앞서 잠깐 살펴봤던 어피니티를 추가로 활용하는 것을 고려해 볼 수 있습니다.
topologySpreadConstraints:
- maxSkew: 1
whenUnsatisfiable: ScheduleAnyway
topologyKey: **********/hostname
labelSelector:
matchlabels:
app.kubernetes.io/name: api-real
위 설정을 간단히 살펴보겠습니다.
maxSkew
값은 1로 설정해서 모든 노드에 파드가 최대한 고르게 분산되도록 설정했습니다. 당연하게도 maxSkew
값은 1이 최소이고, 0으로 설정할 수는 없습니다. 0으로 설정하면 애초에 신규 파드가 배치될 때부터 조건을 만족할 수 없겠죠.
두 번째로 whenUnsatisfiable
은 ScheduleAnyway
로 설정했습니다. 만약 프로젝트의 상황이나 팀의 특성에 따라서 모든 파드를 모든 노드에 동일하게 분산해야 한다는 조건이 있었다면 DoNotSchedule
로 설정해 더 정확하게 분산 배치되도록 설정했을 텐데요. 현재 프로젝트에서는 그럴 필요가 없었고, 우선 파드가 노드에 배치돼 기동되는 것이 중요했기에 ScheduleAnyway
로 설정했습니다.
labelSelector
는 기본적으로 디플로이먼트를 활용할 때 쿠버네티스를 통해 생성되는 레이블을 선택했습니다. 만약 파드를 더 다양하게 관리하고 있는 프로젝트라면 조금 더 세분화해 볼 수 있을 것 같습니다.
마지막으로 topologyKey
로는 각 노드마다 유니크하게 생성되는 hostname
값을 사용했습니다. 기본적으로 노드마다 파드가 분산 배치되도록 하는 것이 의도이기 때문에 노드마다 토폴로지 도메인을 구성할 수 있도록 노드마다 유니크한 hostname
을 사용했습니다.
여기서 저희 팀은 휴먼 에러를 줄이기 위해 topologyKey
를 기본적으로 자동 생성되는 레이블 중에서 설정했습니다. 만약 자동으로 생성되는 레이블을 활용하지 않는다면 신규 노드를 생성할 때마다 저희가 설정한 topologyKey
값을 직접 적용해야 하는데요. 이 부분은 자동화하지 않으면 휴먼 에러가 발생할 수 있다고 판단해 시스템에서 자동으로 생성해 주는 키값을 활용했습니다.
마치며
이번 글에서는 파드를 분산 처리할 수 있는 세 가지 방법을 살펴보고, 그중에서 토폴로지 분산 제약 활용 방법을 자세히 설명드렸습니다. 향후 쿠버네티스를 통해 서버를 구현할 때 토폴로지 분산 제약 옵션을 활용해 쉽고 빠르게 파드 분산 배치를 구성하는 것을 고려해 보시면 좋을 것 같습니다. 또한 보다 구체적인 파드 배치가 필요할 때는 어피니티를 활용해 보는 것도 추천합니다. 다양한 환경에서 파드 분산 처리를 계획하고 계신 분들께 이 글이 도움이 되기를 바라며 이만 마치겠습니다.