AI 스타트업의 Cloud Cost Optimization

Choi Geonu
returnzero
Published in
14 min readJun 14, 2023

--

리턴제로는 음성인식 AI를 서비스하는 회사입니다. 음성인식 모델을 직접 만들어 유저들에게 유용한 서비스를 제공하고, 기업에게 API를 제공하고 있습니다.

높은 정확도의 음성인식 AI는 정말 많은 서버 리소스를 요구합니다. 리턴제로에서 운영하고 있는 서비스들의 트래픽만 고려하더라도 안일하게 운영했다간 감당할 수 없는 비용을 떠안게됩니다. 스타트업의 생존을 위해서 클라우드 비용 최적화는 필수 불가결의 요소인거죠.

이번 포스팅에서는 리턴제로에서 AI 서비스를 운영하기 위해 어떠한 비용 최적화를 진행했는지 이야기 해보려 합니다.

리턴제로의 인프라 구성

모든 이야기를 시작하기 전에 리턴제로의 인프라 구성에 대해 알아야할 것 입니다.

https://kubernetes.io/

리턴제로는 거의 모든 서비스를 EKS(Amazon Elastic Kubernetes Service) 위에서 운영하고 있습니다. 따라서 대부분의 비용 최적화는 모두 EKS 리소스 관리 차원에서 이루어지고 있습니다.

또한 노드는 모두 EC2 인스턴스를 이용하고 있으며 Fargate와 같은 Serverless 솔루션은 사용하고 있지 않습니다.

Karpenter를 이용한 스팟 인스턴스최적화

클라우드에서의 비용 최적화의 시작은 컴퓨팅 인스턴스 최적화라고 할 수 있습니다. 그중에 AWS에서 제공하는 가장 매력적인 옵션은 스팟 인스턴스 (Spot Instance)라고 할 수 있죠.

EC2 스팟 인스턴스

스팟 인스턴스는 AWS의 유휴 EC2 인스턴스를 저렴한 가격에 제공하는 옵션입니다. 온디맨드 인스턴스에 비해 비용이 최대 90%까지 저렴하지만 다음과 같은 유의사항이 있습니다.

  1. 유휴 인스턴스가 없다면 제공되지 않습니다. 이때는 온디맨드 인스턴스를 사용해야 합니다.
  2. 온디맨드 인스턴스 사용량이 늘어나면 AWS에서는 스팟 인스턴스를 회수합니다. 이 때 스팟 인스턴스 중단 인터럽트를 받은 후 2분 이내에 모든 리소스를 정리해야 합니다.

위와 같은 유의사항에만 잘 대응 되어 있다면 온디맨드에 비해 매우 저렴한 비용으로 서버 리소스를 이용할 수 있습니다.

Karpenter

https://karpenter.sh/

Karpenter는 AWS에서 개발한 클라우드 제공자 중립적인 Kubernetes 노드 스케일러입니다. 다른 기본적인 노드 스케일러에 비해 많은 장점이 있는데, 몇가지만 소개하면 다음과 같습니다.

  1. 노드 Scale-out이 필요한 상황에서 최적의 인스턴스 타입을 찾아서 프로비저닝 합니다.
    예를 들어, 현재 1 CPU / 2 GiB Memory 의 Pod 4개가 노드 부족으로 Pending 상태에 있다면, 4 CPU / 8 GiB Memory를 커버하는 최적의 인스턴스를 프로비저닝 해줍니다.
  2. Pod의 toleration, affinity, resource 요구사항 등을 이해합니다.
    만약 특정 Pod이 NVIDIA T4 GPU가 필요한 상황이라면 그 요구사항을 이해하고 g4dn 타입의 인스턴스를 프로비저닝 하도록 만들 수 있습니다.
    또한 node affinity를 이용해 스팟 인스턴스 관련 label이 있는 노드를 선호하도록 설정하면 프로비저닝시에 스팟 인스턴스를 선택하도록 할 수 있습니다.

ML 모델을 위한 Karpenter 와 스팟 인스턴스

리턴제로의 음성인식 모델을 스팟 인스턴스에 실행하기 위해서는 일반적인 서버와는 다르게 고려해야할 사항이 몇가지 있었습니다.

첫번째로는, GPU 인스턴스의 부족입니다. GPU 인스턴스, 특히 g4dn 타입의 인스턴스는 스팟 인스턴스 부족을 많이 격게됩니다. 그러므로 GPU를 사용하는 음성인식 모델을 위한 Pod는 온디맨드 인스턴스에서도 실행 가능하도록 설정해야합니다. 그러므로 nodeSelector 를 지정하는 대신 nodeAffinity 를 지정해 preference만 설정했습니다.

affinity:
nodeAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- preference:
matchExpressions:
- key: karpenter.sh/capacity-type
operator: In
values: [spot]
weight: 80
- preference:
matchExpressions:
- key: karpenter.sh/capacity-type
operator: In
values: [on-demand]
weight: 20

또한 일시적인 부족 현상으로 실행된 온디맨드 인스턴스가 너무 오래 유지되지 않도록 프로비저너에는 ttlSecondsUntilExpired 를 설정했습니다.

apiVersion: karpenter.sh/v1alpha5
kind: Provisioner
spec:
...
ttlSecondsUntilExpired: 86400 # 24h

두번째로는, 느린 Pod 프로비저닝 속도입니다. 스팟 인스턴스는 인터럽트를 받으면 2분 이내에 리소스를 정리해야합니다. 즉, 노드에 있는 Pod들이 2분 이내에 종료되어야 한다는 의미입니다. 만약 종료 대상이 되는 Deployment에 Pod이 하나밖에 남아있지 않다면 서비스는 장애 상황을 맞을 수 있습니다.

Service의 유일한 Pod이 Spot Interrupt를 받은 경우

물론 Pod Disruption Budgets를 이용하면 장애 상황을 피할 수 있겠지만 그 또한 새로운 Pod이 프로비저닝 되는데에 2분 미만의 시간이 필요할 때의 이야기입니다. 거대한 모델을 로드하고 GPU Warm up을 기다려야 하는 ML 서버에서는 다른 전략을 찾아봐야 합니다.

사실 해결 방법은 간단합니다. 최소 Pod 하나는 온디맨드 인스턴스에서 실행되도록 설정하는 것 입니다. 하지만 Kubernetes에서는 “최소 하나의 Pod은 온디맨드 인스턴스에 할당해줘” 와 같은 설정이 불가능합니다.

이 문제를 해결하기 위해서 온디맨드용 Deployment와 스팟용 Deployment를 분리해서 운영하는 선택을 했습니다. 온디맨드용 Deployment는 운영 안정성을 위한 최소 리소스를 항상 보장해주게 됩니다. 물론 템플릿으로 관리하기에 Deployment관리 리소스가 두배로 들지는 않습니다.

KEDA를 이용한 자린고비 Pod 오토 스케일링

노드 스케일링을 최적화 했으니 다음은 Pod 스케일링을 최적화 해야할 차례입니다.

Kubernetes에서 기본적으로 제공하는 스케일링 메트릭은 만족스럽지 않습니다. CPU보다 GPU를 압도적으로 많이 사용하는 모델의 경우 CPU 메트릭으론 스케일링을 판단하기 어렵고, 보다 복잡한 조건을 표현하기 위해선 다른 Pod Scaler가 필요했습니다.

KEDA

https://keda.sh/

KEDA는 Microsoft에서 Azure Function을 팔기 위해 만든 오픈소스 Pod Autoscaler입니다. 많은 기능들이 있지만, 우리가 주목한 기능은 Prometheus 연동이었습니다.

리턴제로의 음성인식 모델은 NVIDIA Triton Inference Server를 사용해 서빙는데, Triton에서는 많은 유용한 메트릭을 Prometheus를 통해 제공하고 있습니다. 특히 추론 실행 횟수를 의미하는 nv_inference_count 메트릭은 스케일링 메트릭으로 안성맞춤입니다. Dynamic Batching등 여러가지 추론 최적화를 하는 Triton에서는 실제 추론 실행 횟수가 가장 유의미한 메트릭이었습니다.

KEDA와 Prometheus의 합작으로 다음과 같은 복잡한 쿼리도 스케일링 메트릭으로 활용할 수 있었습니다.

1 * sum(increase(nv_inference_count{
namespace="vitospeech",
pod=~"^vitospeech-triton-sd-\\w+-\\w+$",
model="sd_1"
}[1m])) +
10 * sum(increase(nv_inference_count{
namespace="vitospeech",
pod=~"^vitospeech-triton-sd-\\w+-\\w+$",
model="sd_10"
}[1m])) +
100 * sum(increase(nv_inference_count{
namespace="vitospeech",
pod=~"^vitospeech-triton-sd-\\w+-\\w+$",
model="sd_100"
}[1m]))

이마저도 이번 포스팅을 위해 간소화 한 쿼리로, 실제로는 더 복잡한 쿼리를 스케일링 메트릭으로 사용하고 있습니다.

이처럼 리턴제로에서는 KEDA를 통해 ML모델을 안정적이고 비용 효율적으로 서비스할 수 있도록 스케일링 하고 있습니다.

Istio를 이용한 Locality Load Balancing

Cost Optimization이라는 주제에 Istio는 조금 뜬금없어 보일 수 있습니다. 하지만 ML 서비스에서는 네트워킹에 대한 통제도 비용 절감에 큰 도움이 된다는 점을 이야기 해보려 합니다.

모든 인프라 이슈의 근원이자 만병통치약, 애증의 Istio https://istio.io/

Availability Zone간 네트워크 전송 비용

AWS에서 운영되는 대부분의 서비스에서 놓치기 쉬운 비용 발생 요인은 Availability Zone간 네트워크 전송 비용입니다.

리턴제로 음성인식 서비스 내의 하나의 컴포넌트에서 발생하는 네트워크 I/O

대부분의 서비스는 네트워크 전송량이 비용에 큰 부분을 차지할 정도로 많지 않을 것 입니다. 저 또한 리턴제로에 합류하기전 까지는 생각해본적 없는 비용이었지요. 하지만 음성 Raw 데이터를 이용한 추론 요청을 비용으로 맞아보니 결코 무시할 수없는 비용이라고 생각이 들었습니다.

AWS에서는 같은 리전(Region)일지라도 서로 다른 Availability Zone이라면 네트워크 전송 비용을 부과합니다. 이러한 비용을 피하기 위해서는 추론과 같은 내부 컴포넌트간의 통신은 같은 Availability Zone을 선호하도록 설정해야합니다.

Istio Locality Load Balancing

Istio는 이를 위한 아주 간단한 해결책을 제공합니다. Locality Load Balancing은 Istio를 통한 로드밸런싱에서 같은 Availability Zone, 같은 리전을 선호하도록 설정해줍니다. 물론 같은 Zone에 해당 컴포넌트가 없다면 Zone을 넘어간 네트워킹으로 넘어가도록 허용하죠.

apiVersion: networking.istio.io/v1alpha3
kind: DestinationRule
metadata:
name: vitospeech-triton-sd
spec:
host: vitospeech-triton-sd
trafficPolicy:
outlierDetection: {}
loadBalancer:
localityLbSetting:
enabled: true

설정법도 매우 간단합니다. Istio의 Destination Rule에서 Outlier Detection과 Locality LB Setting을 활성화 하기만 하면 됩니다.

물론 효율적인 Pod 배치를 위해서 topologySpreadConstraints 를 통해 Zone별로 Pod을 균형있게 배치하는것도 중요합니다.

topologySpreadConstraints:
- labelSelector:
matchLabels:
service: vitospeech-triton-sd
topologyKey: topology.kubernetes.io/zone
maxSkew: 1
whenUnsatisfiable: ScheduleAnyway

Descheduler를 이용한 리소스 Bin-Packing

아무리 Pod 오토스케일링을 잘하고 노드를 Fit 하게 프로비저닝을 해도, 놀고있는 노드가 있다면 비용은 펑펑 날아가게 될 것입니다.

Bin-Packing을 통한 노드 최적화

위 그림과 같이 Bin-Packing을 통해 클러스터 내의 노드 사용량을 최적화 한다면 비용 절감에 큰 도움이 되죠

Descheduler

https://sigs.k8s.io/descheduler

이러한 요구사항을 해결해주는 도구가 Descheduler입니다. Kubernetes Scheduler가 노드에 Pod를 할당해 주는 컴포넌트라면, Descheduler는 노드에 할단된 Pod을 제거하는 여러가지 전략을 제공합니다. Affinity를 해치는 Pod, 너무 오래 실행되고 있는 Pod 등 여러가지 전략이 있습니다.

우리는 그중 사용률이 너무 낮은 노드의 Pod HighNodeUtilization 전략을 사용했습니다. 사용법은 간단합니다. Descheduler 설정을 다음과 같이 하면 적용할 수 있습니다.

deschedulerPolicy:
HighNodeUtilization:
enabled: true
params:
nodeResourceUtilizationThresholds:
thresholds:
cpu: 50
memory: 50

위와 같이 설정한 경우 CPU 혹은 메모리 사용률이 50% 미만인 노드는 비워지게 됩니다.

해결해야할 문제

HighNodeUtilization 전략의 경우, Kubernetes Scheduler의 기본 스케쥴링 전략이 MostAllocated 인 상황을 가정해서 동작합니다. 하지만 EKS에서는 기본 스케쥴링 설정을 LeastAllocated변경할 수 없게 되어있어 완벽한 Bin-Packing으로 동작하지는 못합니다. 그렇지만 LeastAllocated + HighNodeUtilization 의 조합도 그럭저럭 유효하게 동작하기에 비용 절감에는 유의미한 효과를 보여줍니다.

그 외에 도움이 되는 요소들

리턴제로에서 중요하게 생각하는 비용 최적화 요소들은 모두 소개하였고 그외에 도움이 되는 요소들을 간단하게 소개하고 마무리하려 합니다.

Savings Plan

스팟 인스턴스가 아무리 저렴하다 해도 여전히 많은 노드가 온디맨드로 운영되어야 합니다. Savings Plan은 온디맨드 인스턴스에 대한 비용 절감 옵션을 제공합니다.

비용 대시보드

비용을 절감하기 위해서는 현재 얼마를 사용하고 있는지 파악하는것이 첫번째 입니다. 팀에서 사용하는 비용이 어디에 집중되어있는지 파악할 수 있는 대시보드를 만들고 자주 확인하면 큰 도움이 될 것 입니다.

리턴제로에서는 Metabase를 통해 비용 대시보드를 구축하고 있습니다.

그래서 얼마나 절약했나요?

위와 같은 노력들 끝에, 현재는 작년 비용 피크 대비 절반 이하의 AWS 비용으로 서비스를 운영하고 있습니다. 리턴제로 팀도 이정도의 비용을 절약할 수 있을거라고 생각하지 못했지만 Cost Optimization의 영향력이 크다는 것을 알게되어 매우 인상적이었습니다.

대부분의 비용 절감이 자동화된 컴포넌트를 통해 이루어졌습니다. 이 글을 읽는 독자분들도 한번 Cost Optimization에 도전해보시면 어떨까요?

--

--