High Availability
고가용성(High Availability) 은 시스템 내의 어떤 구성 요소에 장애가 발생하더라도 실패나 다운타임 없이 시스템을 지속적으로 운영할 수 있는 능력을 의미한다.
보통 고가용성을 99%, 99.9% 와 같은 비율로 표현하는데 99.999%의 매우 높은 가용성을 의미하는 fine-nines availability 라는 가용성 표준이 있으며, 이 가용성을 유지하려면 1년에 약 5분 15초 이하의 다운타임을 유지해야 한다. 고가용성 시스템을 잘 구축하려면 시스템을 잘 설계해야 하며 철저한 테스트를 거쳐야 한다. 시스템의 구성 요소가 서비스 중단 발생 시에도 시스템이 지정된 가용성 표준을 유지할 수 있도록 설계해야 한다.
고가용성은 어떻게 작동하나?
시스템을 항상 100%로 가동하는 것은 불가능하다. 애플리케이션 버전 업데이트나 OS 업그레이드 등으로 인해 서버가 내려갈 수도 있고, 작업의 실수로 인해 특정 서버집단이 장애가 발생할 수도 있다. 또는 네트워크나 하이퍼바이저 문제로 예상치 못한 장애가 발생할 가능성도 있다. 특정 부분에 대한 장애가 발생할 가능성은 충분히 있지만 시스템을 잘 설계한다면 특정 부분에 대한 장애가 실 서비스에 영향이 없을 수도 있다.
고가용성 시스템을 설계할 때 보통은 다음 원칙을 준수한다.
- 단일 실패 지점 (Single points of failure) 제거: 단일 실패 지점은 해당 구성 요소에 장애가 발생하면 전체 시스템에 장애를 일으킬 수 있는 구성 요소이다. 예를 들어 애플리케이션을 하나의 서버에만 가동한다면 서버 자체가 단일 실패 지점이 된다.
- 안정적인 장애 조치(Failover) 구축: 시스템 구성요소가 장애가 발생한다면 해당 구성요소가 복구될 때 까지 유사한 구성요소가 즉시 사용 가능해야 한다.따라서 시스템에는 이중화가 되어있어야 하며 데이터 손실이나 성능 및 운영에 영향을 주지 않고 한 구성 요소에서 다른 구성 요소로 전환하는 안정적인 장애 조치를 보장해야 한다.
- 자동 장애 감지 구현: 장애나 결함을 즉시 감지하고 조치해야 한다. 이상적으로는 시스템 자체적으로 장애를 처리할 수 있는 자동화 기능이 내정되어야 한다.
- 데이터 손실이 없도록 보장하기: 구성요소가 실패할 때, 데이터 보호 조치를 취하지 않으면 데이터가 손실될 수 있다. 고가용성 시스템은 시스템 결함이 있을 때 데이터 손실을 피하거나 최소화하는 메커니즘이 필수로 있어야 한다.
- 부하 분산 (로드밸런싱) : 부하 분산은 들어오는 여러 요청을 서로 다른 서버로 보내는 등 워크로드를 자동으로 분산한다. 로드 밸런서를 사용하면 단일 리소스에 과부하가 걸리지 않으므로 단일 실패 지점을 제거할 수 있다.
Kubernetes in HA
쿠버네티스를 고가용성 시스템을 구축할 때 사용한다면 여러가지 이점을 얻을 수 있다.
- 단일 실패 지점 제거: 쿠버네티스는 여러가지 워커노드를 묶어서 하나의 클러스터로 관리하며, Deployment나 StatefulSet을 사용하여 애플리케이션을 레플리카 단위로 배포를 한다. 그렇다면 한 한 종류의 애플리케이션이 여러개의 파드로 배포가 되며 여러개의 워커노드로 분산될 수 있다. 그러면 워커노드 단위의 단일 실패 지점이 제거가 된다. 물론 워커노드 단위의 단일 실패지점을 완전히 없애려면 스케줄링 전략을 잘 짜야 한다.
- 자동 장애 감지 및 조치 : 쿠버네티스는 파드라는 단위로 애플리케이션을 띄우는데, Self-healing이 쿠버네티스에 내장되어 있다. 쿠버네티스 각 워커노드에 띄워져있는 kubelet 에이전트는 워커노드에 띄워져있는 모든 파드들의 상태를 주기적으로 체크하며, 상태가 이상하다고 판단을 할 경우 kubelet은 파드를 재구동 시킨다. 물론 파드 수준에서 상태를 확인하는 기준을 정해야 하는데, 이는 liveness probe에서 정할 수 있다. 그리고 파드는 보통 레플리카 단위로 관리가 되며 하나의 파드가 죽으면 레플리카에 있는 다른 파드가 그 기능을 대신할 수 있기 때문에 Deployment나 StatefulSet을 사용하여 애플리케이션을 배포한다면 이중화가 가능하다.
- 데이터 손실이 없도록 보장: 상태가 있는 애플리케이션의 경우에는 StatefulSet을 사용하며, 데이터를 보장하기 위해서 PersistentVolume을 붙여서 StatefulSet을 운영한다. 물론 상태가 있는 애플리케이션 자체적으로 데이터 손실이 없거나 최소화 하는 방식으로 설계가 되어야 한다.
- 부하 분산 : 보통 Deployment와 같은 레플리카 단위의 워크로드를 통해 애플리케이션을 배포하며, 쿠버네티스에서는 Service를 통해 로드밸런싱과 서비스 디스커버리 기능을 쉽게 사용할 수 있다. 하지만 완전한 부하 분산을 보장받기 위해서는 파드(애플리케이션) 수준에서 readiness probe와 같이 준비 상태를 알리는 방법을 구성해야 하며 Graceful shutdown이 가능하도록 애플리케이션을 구성해야 한다.
쿠버네티스를 사용하면 웬만한 고가용성 원칙은 준수하지만 쿠버네티스를 사용만 한다고 해서 완전한 고가용성 원칙은 준수하지 못할 수도 있다. 최대한 완전한 고가용성을 준수하기 위해서는 여러가지 노력이 동반되어야 하는데, 크게는 두 가지 노력이 동반되어야 한다고 생각한다.
- 단일 실패 지점 제거를 위한 노력 : 노드 수준 뿐 아니라 가용성 존 및 클러스터 수준의 단일 지점을 제거하기 위한 노력이 필요하다.
- 사용자 친화적인 장애 감지: 쿠버네티스가 파드 수준의 장애를 자동으로 감지하여 자가 복구를 하지만, 사용자는 이를 알아차리지 못할 수도 있다. 그래서 사용자도 알아차릴 수 있도록 모니터링 및 알람 시스템을 구축해야 하며, 마찬가지로 파드 수준부터 노드, 가용성 존 및 클러스터 수준의 모니터링도 필요하다.
일단 그 중 쿠버네티스에서 단일 실패 지점을 제거하는 방법을 알아보자.
Pod 수준 단일 실패 지점
쿠버네티스에서 애플리케이션은 파드라는 단위로 띄워진다. 만약 하나의 애플리케이션이 하나의 파드로만 배포된다면 파드가 죽는 경우 애플리케이션 기능이 모두 중단된다. 여기서는 하나의 파드가 단일 실패 지점이 된다.
그래서 보통은 Deployment (Stateless application) 또는 StatefulSet (Stateful application) 을 사용하여 여러개의 레플리카로 애플리케이션을 배포하게 된다. 그리고 이 레플리카를 로드밸런싱하는 서비스를 하나 만들어 요청이 분산되도록 한다. 이 때 하나의 파드가 죽어도 나머지 파드들이 요청을 받을 수 있어서 파드가 단일 실패 지점이 되지는 않는다.
하지만 이것이 100% 장애를 막지는 못한다. 만약 하나의 파드에서 장애가 발생했지만 Readiness probe를 구성하지 못한다면 서비스는 장애가 발생한 파드를 비정상으로 판단하지 않아서 장애가 발생한 파드로 트래픽을 전달할 수 있다. 또한 Liveness probe를 구성하지 못한다면 장애가 발생한 파드를 재구동 시켜주지도 않는다. 따라서 파드 수준의 단일 실패 지점을 없애려면 Readiness probe와 Liveness probe를 잘 구성해야 한다.
또한 배포를 진행할 때는 파드가 종료되었다가 다시 실행되는데 파드가 종료되는 시점에 이미 진입한 트래픽이 처리가 된 후 종료가 되지 않으면 장애가 발생할 수 있다. 진입한 트래픽이 처리된 후 종료시키기 위해서 애플리케이션 수준에서 Graceful shutdown을 구현하거나 구현이 힘든 경우에는 파드 수준에서 preStop hook을 통해 몇 초 정도 쉬어주도록 구성해야 한다.
요약
파드 수준에서 단일 실패 지점을 해결하려면
- Deployment나 StatefulSet으로 배포한다.
- Service를 사용하여 부하분산을 적용한다.
- Readiness probe를 구성하여 배포되거나 트래픽을 못받는 경우에 트래픽을 전달해주지 않도록 한다.
- Liveness probe를 구성하여 파드가 장애났을 때 자가복구가 되도록 한다. (재구동)
- Graceful shutdown을 적용하여 파드를 종료시키는 시점에 이미 진입한 트래픽을 처리하도록 한다.
Node 수준 단일 실패 지점
쿠버네티스가 파드를 자동으로 스케줄링하지만 아주 운이 나쁘게도 특정 애플리케이션의 모든 파드가 하나의 워커노드에 배포되었다고 가정해보자. 그런데 그 워커노드에서 OOM이 발생한다거나 하는 워커노드 수준의 장애가 발생하여 워커노드가 제대로 동작하지 못한다면 해당 노드에 있는 모든 파드들이 장애가 발생할 것이다. 이와 같은 상황이 발생한다면 해당 애플리케이션의 레플리카가 모두 장애가 발생하기 때문에 노드가 단일 실패 지점이 될 수도 있다.
노드가 단일 실패 지점이 되지 않으려면 어떻게 해야할까? 같은 종류의 파드를 같은 노드에 띄우지 않도록 스케줄링 전략을 구성해야 한다. 쿠버네티스는 PodAntiAffinity를 통해 같은 종류의 파드를 같은 노드에 띄우지 않도록 제한시킬 수 있다.
위와 같이 같은 종류의 파드를 같은 노드에 띄우지 않도록 하면 하나의 노드에서 장애가 발생해도 나머지 노드에 있는 파드가 살아있기 떄문에 정상적으로 트래픽을 받을 수 있다.
가용성 존 수준의 단일 실패지점
클라우드 환경에서 쿠버네티스를 사용하는 경우, 클라우드 인프라에서는 리전과 가용성 존이라는 것이 있다. 리전은 말그대로 물리적인 위치라고 보면 되고, 가용성 존은 하나 이상의 데이터센터를 묶어놓은 영역이며 하나의 가용성존에는 전원과 냉각 시스템 등이 독립적으로 이루어져있기 때문에 일반적으로는 하나의 가용성 존이 장애가 나더라도 다른 가용성 존은 장애가 발생하지 않는다.
당연히 클라우드 인스턴스는 특정 리전의 특정 가용성 존에 존재하므로, 워커노드도 마찬가지로 특정 리전의 특정 가용성 존에 존재한다. 클라우드 제품에 따라 다를 수 있겠지만 보통은 하나의 VPC (Virtual Private Cloud) 내의 네트워크에서 쿠버네티스를 구축하는데 하나의 VPC에서 여러 가용성 존을 사용할 수 있다. 따라서 쿠버네티스 클러스터에는 여러 워커노드가 있고 이 워커노드는 각각 다른 가용성 존에 있다.
그런데 만약 아주 운이 안좋게도 특정 애플리케이션의 모든 레플리카가 하나의 가용성존에 있는 노드로만 스케줄링되어 있고 하필 그 가용성존 전체가 장애가 났다고 하자. 그러면 해당 애플리케이션의 레플리카가 모두 장애가 발생할 것이고 가용성 존이 단일 실패 지점이 될 수도 있다.
가용성 존에 대해 단일 실패 지점을 피하려면 레플리카를 가용성 존 별로 골고루 분배를 해야 한다. 쿠버네티스에서 존재하는 가용성 존 별로 파드를 골고루 분산시키려면 Pod Topology Spread Constraints를 사용하면 된다. 파드를 가용성 존 별로 골고루 분산시키면 하나의 가용성 존이 장애가 나더라도 다른 가용성 존에 있는 파드가 살아있기 때문에 전체 장애를 면할 수 있다.
리전 수준의 단일 실패 지점
운이 나쁘게도, 하나의 리전에서 전체 장애가 발생하는 경우에는 해당 리전에서 돌아가는 쿠버네티스 클러스터는 전체 장애가 발생한다. 그렇다면 리전이 단일 실패 지점이 될 수도 있다.
리전 단위의 단일 실패 지점을 피하려면 리전별로 클러스터를 구축하면 된다. 두 개 이상의 클러스터를 사용하고 관리하는 것을 멀티 클러스터라고 하는데, 멀티 클러스터 아키텍쳐의 종류로는 크게 Replicated 와 Split-by-service 가 있다. Replicated 는 한 클러스터의 상태를 완전 복사하여 새로운 클러스터에 적용하는 클러스터 단위 레플리케이션이라고 보면 되고, Split-by-service 는 서비스별로 클러스터를 나누는 방식이라고 보면된다.
리전 수준의 단일 실패 지점을 해결하려면 리전별로 클러스터를 복제하면 된다. (Replicated architecture) 이렇게 구성하면 하나의 리전이 장애가 발생해도 다른 리전에 있는 클러스터가 트래픽을 받아주기 때문에 장애를 면할 수 있다. 대신에, 이 방식을 채택한다면 두개 이상의 클러스터의 상태 (쿠버네티스 리소스)가 동일하게 맞춰줘야 하기 때문에 관리에 대한 러닝커브가 있다.
클러스터의 상태를 맞춰주기 위해서는 ArgoCD와 같은 GitOps 도구를 사용하여 쿠버네티스에 적용해야 할 매니페스트를 모두 GitHub에 저장한 후 GitHub에 있는 매니페스트 파일을 ArgoCD가 주기적으로 두 개 이상의 복제 클러스터에 적용하게끔 하면 클러스터의 상태가 맞춰질 것이다.
그 외에도 복제 클러스터로의 트래픽 분산에 대해서도 신경을 써야 한다. 리전별로 로드밸런싱이 가능한 상품이라면 크게 어렵지 않지만 리전별로 로드밸런서를 따로 둬야 하는 경우라면 DNS를 Round-Robin으로 한다거나 GSLB를 고려해보는 것도 좋은 방법이 될 것이다.
참고 자료
https://kubernetes.io/docs/concepts/scheduling-eviction/topology-spread-constraints/
Pod Topology Spread Constraints
You can use topology spread constraints to control how Pods are spread across your cluster among failure-domains such as regions, zones, nodes, and other user-defined topology domains. This can help to achieve high availability as well as efficient resourc
kubernetes.io
https://www.tigera.io/learn/guides/kubernetes-networking/kubernetes-multi-cluster/
Kubernetes Multi Cluster
Multi-cluster Kubernetes refers to the management of multiple Kubernetes clusters, which are a group of nodes working together to orchestrate and run containerized applications.
www.tigera.io
'DevOps > Kubernetes' 카테고리의 다른 글
Kubernetes HPA (파드 오토스케일링) [2] - 동작 제어 (0) | 2025.01.05 |
---|---|
Kubernetes HPA (파드 오토스케일링) [1] - 기초 (0) | 2025.01.05 |
Kubernetes Scheduling - Node Affinity (0) | 2024.05.02 |
Kubernetes Scheduling 소개 및 NodeSelector (1) | 2024.05.01 |
[1] 쿠버네티스 확장 - kubectl plugin (0) | 2023.10.29 |
댓글