LY Corporation Tech Blog

LY Corporation과 LY Corporation Group(LINE Plus, LINE Taiwan and LINE Vietnam)의 기술과 개발 문화를 알립니다.

쿠버네티스에서 파드를 분산 처리하기 위한 토폴로지 분배 제약 조건 활용 사례 소개

안녕하세요. 쿠버네티스에서 파드를 분산 처리할 때 사용할 수 있는 토폴로지 분배 제약 조건(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값은 동일한 토폴로지에 배치된 최대 파드 개수에서 최소 파드 개수를 뺀 값입니다. 만약 토폴로지라는 개념이 낯설다면 이를 존(zone)이나 그룹이라고 생각하시면 될 것 같습니다.

아래와 같이 세 개의 노드가 모두 동일한 토폴로지에 있다고 가정하겠습니다. 현재 각 노드에 파드가 모두 두 개씩 배치돼 있기 때문에 최대 파드 개수도 2이고 최소 파드 개수도 2입니다. 따라서 최대 파드 개수에서 최소 파드 개수를 뺀 skew값은 0입니다.

아래 그림에서는 최대 파드 개수가 2이고 최소 파드 개수가 1이기 때문에 skew값은 1입니다.

아래 그림에서는 최대 파드 개수는 2로 동일하지만 파드가 존재하지 않는 노드가 있어서 최소 파드 개수는 0이기 때문에 skew값은 2입니다.

이제 감이 오셨을 텐데요. 토폴로지 분산 제약에서의 maxSkew값은 파드를 분산 배치할 때 허용할 최대 skew값을 의미합니다. 당연하게도 해당 값을 크게 잡을수록 파드가 덜 고르게 분포되지만, 그렇다고 너무 작게 잡으면 파드 배치 조건이 타이트해지면서 스케줄링이 지연될 수 있습니다. 따라서 노드와 파드 규모를 고려해 적절하게 값을 설정해야 합니다.

topologyKey

위에서 skew값을 계산할 때 동일한 토폴로지를 기반으로 계산한다고 말씀드렸습니다. 여기서 '동일한 토폴로지'라는 기준으로 사용하는 값이 topologyKey입니다.

노드에는 다양한 키-값의 레이블이 할당되는데요. 이때 topologyKey에 입력된 값을 키로 갖고 있는 레이블을 찾아서, 그 값이 같은 노드들이 동일한 토폴로지가 됩니다(물론 토폴로지라는 개념은 토폴로지 분산 제약의 옵션으로만 활용되는 것은 아닙니다).

예를 들어, topologyKeydomain으로 설정했다고 가정해 보겠습니다. 아래 그림을 보면 세 개의 노드가 domain이라는 키값을 갖고 있는데 그 값이 각각 A, B, C로 서로 다릅니다. 이 경우 값이 모두 다르기 때문에 세 노드는 각각 토폴로지를 이루고 skew값도 각자 계산합니다.

아래 그림에서는 두 노드가 domainB라는 동일한 값을 갖고 있습니다. 따라서 두 노드가 동일한 토폴로지가 되고, skew값을 계산할 때 함께 계산합니다.

만약 아래 그림과 같은 상황이라면 domain=A 토폴로지의 skew값은 0, domain=B 토폴로지의 skew값은 3이 되겠죠.

이와 같이 topologyKey를 활용하면 노드에 붙은 레이블에 따라서 토폴로지를 나눠 skew값을 계산하도록 설정할 수 있습니다.

labelSelector

앞서 살펴본 topologyKey가 노드에 대한 조건이라면, labelSelector는 파드에 대한 조건입니다. 만약 토폴로지 분산 제약이 무조건 모든 파드에 적용된다면, 분산 배치가 별로 필요하지 않은 파드까지 관리해야 하는 번거로움이 생깁니다. 또는 A 기능을 하는 파드 그룹과 B 기능을 하는 파드 그룹 간에 서로 다른 분산 배치를 적용하기가 어려워지겠죠.

이를 방지하기 위해 토폴로지 분산 제약을 적용할 파드를 설정하는 조건이 labelSelector입니다. 즉, labelSelector 옵션에 기입된 레이블이 존재하는 파드만 분산 배치를 적용하는 것입니다.

만약 아래 그림처럼 labelSelector 값으로 Key1=value1Key2=value2를 설정하면, 왼쪽 네 개 파드만 파드 분산 배치에 영향을 받습니다. 오른쪽 파드는 labelSelector 조건에 부합하지 않기 때문에 파드 분산 배치 대상에서 제외됩니다. 

whenUnsatisfiable

whenUnsatisfiable는 문자 그대로 앞서 설정한 옵션들을 만족할 수 없는 상황에서 파드를 어떻게 배치할 것인지를 정하기 위한 옵션입니다. 

아래 그림을 보면 두 노드가 동일한 토폴로지에 속해 있고, 해당 토폴로지의 skew값은 1입니다. 이때 maxSkew값이 1로 설정돼 있다면, 신규 파드를 배치할 때 오른쪽 노드에 배치해야 합니다.

그런데 이때 오른쪽 노드에 이미 다른 파드가 배치돼 있어서 노드의 리소스가 부족해 새로운 파드를 배치할 수 없는 상황이 벌어질 수 있습니다. 이런 상황에서 파드를 어떻게 배치할 것인지를 결정하는 역할이 whenUnsatisfiable 옵션의 역할입니다.

whenUnsatisfiable 설정은 DoNotScheduleScheduleAnyway의 두 가지 옵션을 제공합니다.

먼저 DoNotSchedule은 이름 그대로 maxSkew값을 넘어가는 파드는 배치하지 않고 대기하게 만드는 옵션입니다. 만약 위 상황에서 이 옵션으로 설정했다면 배치하려고 했던 파드는 자리가 날 때까지 무한정 대기할 것입니다.

반대로 ScheduleAnywaymaxSkew값을 넘기더라도 일단 파드를 배치 가능한 노드에 배치해서 실행하는 옵션입니다. 이때 사용자에게 경고 메시지가 가거나 이후 별도 리밸런싱 작업 같은 것은 진행되지 않습니다. 또한 이후 다시 DoNotSchedule로 변경하더라도 기존에 배치돼 있던 파드를 강제로 종료시키면서 리밸런싱 작업을 하지는 않습니다. 따라서 만약 파드를 정말 정교하고 정밀하게 분산 배치해야 하고 그렇지 않을 경우 이슈가 발생할 수 있는 상황이라면, ScheduleAnyway 대신 DoNotSchedule를 사용하는 게 올바른 방향일 것입니다.

토폴로지 분산 제약 옵션 요약정리

마지막으로 각 옵션의 목적을 간단하게 정리해 보겠습니다.

  • maxSkew: 허용 파드 간격 설정
  • topologyKey: skew값을 계산할 토폴로지 도메인(노드 그룹) 설정
  • labelSelector: 계산 또는 관리에 포함할 대상 파드 설정
  • whenUnsatisfiable: maxSkew값을 초과할 때 파드를 어떻게 배치할지 정책 설정

토폴로지 분산 제약 사용 사례

아래는 저희 팀에서 사용하고 있는 토폴로지 분산 제약 설정을 간소화해서 가져온 것입니다. 생각보다 간단해 보일 수 있는데요. 이 정도의 옵션만으로도 의도한 대로 정확하게 파드를 분산 처리할 수 있습니다. 물론 파드와 노드가 더 많아지면 최신 버전의 쿠버네티스에서 지원하는 다른 옵션들을 활용해 볼 수도 있을 것이고, 서비스의 특성에 따라 파드 배치 조건이 복잡해지는 경우 앞서 잠깐 살펴봤던 어피니티를 추가로 활용하는 것을 고려해 볼 수 있습니다.

topologySpreadConstraints:
	- maxSkew: 1
	  whenUnsatisfiable: ScheduleAnyway
	  topologyKey: **********/nodepool
	  labelSelector:
	  	matchlabels:
	  	  app.kubernetes.io/name: api-real

위 설정을 간단히 살펴보겠습니다.

maxSkew값은 1로 설정해서 모든 노드에 파드가 최대한 고르게 분산되도록 설정했습니다. 당연하게도 maxSkew값은 1이 최소이고, 0으로 설정할 수는 없습니다. 0으로 설정하면 애초에 신규 파드가 배치될 때부터 조건을 만족할 수 없겠죠.

두 번째로 whenUnsatisfiableScheduleAnyway로 설정했습니다. 만약 프로젝트의 상황이나 팀의 특성에 따라서 모든 파드를 모든 노드에 동일하게 분산해야 한다는 조건이 있었다면 DoNotSchedule로 설정해 더 정확하게 분산 배치되도록 설정했을 텐데요. 현재 프로젝트에서는 그럴 필요가 없었고, 우선 파드가 노드에 배치돼 기동되는 것이 중요했기에 ScheduleAnyway로 설정했습니다. 

labelSelector는 기본적으로 디플로이먼트를 활용할 때 쿠버네티스를 통해 생성되는 레이블을 선택했습니다. 만약 파드를 더 다양하게 관리하고 있는 프로젝트라면 조금 더 세분화해 볼 수 있을 것 같습니다.

마지막으로 topologyKey로는 현재 저희 회사에서 사용하고 있는 사내 관리형 쿠버네티스 플랫폼인 VKS(Verda Kubernetes Service)의 노드풀(nodepool) 레이블로 설정했습니다.

노드풀은 VKS에서 사용하는 개념으로, 여러 노드를 관리하기 위해 묶어 놓은 것인데요. 노드풀에는 자동적으로 위와 같은 레이블(**********/nodepool)이 생성됩니다(상세한 URL은 가렸습니다). 저희 팀은 휴먼 에러를 줄이기 위해 이 레이블을 topologyKey를 설정했습니다. 만약 자동으로 생성되는 레이블을 활용하지 않는다면 신규 노드를 생성할 때마다 저희가 설정한 topologyKey값을 직접 적용해야 하는데요. 이 부분은 자동화하지 않으면 휴먼 에러가 발생할 수 있다고 판단해 시스템에서 자동으로 생성해 주는 키값을 활용했습니다.

마치며

이번 글에서는 파드를 분산 처리할 수 있는 세 가지 방법을 살펴보고, 그중에서 토폴로지 분산 제약 활용 방법을 자세히 설명드렸습니다. 향후 쿠버네티스를 통해 서버를 구현할 때 토폴로지 분산 제약 옵션을 활용해 쉽고 빠르게 파드 분산 배치를 구성하는 것을 고려해 보시면 좋을 것 같습니다. 또한 보다 구체적인 파드 배치가 필요할 때는 어피니티를 활용해 보는 것도 추천합니다. 다양한 환경에서 파드 분산 처리를 계획하고 계신 분들께 이 글이 도움이 되기를 바라며 이만 마치겠습니다.