유민정 버즈니 AI LAB 플랫폼 엔지니어

[컴퓨터월드]

▲ 유민정 버즈니 AI LAB 플랫폼 엔지니어

쿠버네티스 도입 배경

만약 도커 컨테이너를 올바르게 활용하고 있다면 애플리케이션의 런타임 환경 관리‘, ‘애플리케이션의 빌드와 실행 단계 분리’, ‘개발/프로덕션 환경 일치’ 등과 관련된 고민은 사라졌을 것이다.

그러나 애플리케이션을 배포하고 효율적으로 운영하기 위해서는 몇 가지 고민이 남는다. 버즈니 AI LAB에서는 이러한 고민을 해결하기 위해서 쿠버네티스를 이용한다. 먼저 그 고민과 쿠버네티스가 이를 어떻게 해결하는지에 대해서 이야기해보겠다. 독자들 또한 공감한다면 쿠버네티스 도입을 권한다.


리소스 관리

첫 번째는 리소스 관리이다. 여기서 리소스는 CPU, 메모리, 볼륨(디스크), GPU 등을 의미한다. 서버가 1대라면 문제가 되지 않는다. 그러나 서버가 여러 개일 때 컨테이너, 애플리케이션은 어느 서버에 배포되어야 할까? 현재 시점에 리소스가 남는 곳에 배포하면 되지만 시간대에 따라 필요한 리소스가 변화하는 태스크의 경우만 생각해보더라도 현실적인 방안은 아니다.

버즈니 AI LAB 또한 특정 시간대에 서버의 OOM이 자주 발생했었다. 이러한 문제를 해결하기 위해 단순히 서버를 늘리고 하나의 서버에 배포되는 컨테이너 자체를 줄인다면 서버의 리소스가 효율적으로 활용되지 않을 것이다. 쿠버네티스는 이러한 리소스 관리에 대한 고민을 해결해준다.

쿠버네티스는 여러 개의 서버를 클러스터로 묶어 구성한다. 이때 각각의 서버를 노드라고 한다. 쿠버네티스 클러스터에 컨테이너를 배포할 때 같은 리소스 의존성(네트워크, 디스크 등)이 필요한 컨테이너를 묶어서 파드라는 형태로 배포하며 이때 필요 리소스와 최대 리소스를 함께 정의한다. 쿠버네티스는 컨테이너 배포가 가능한 노드를 찾는 필터링과 필터링된 노드 중 알고리즘에 따라 가장 적합한 노드를 선택하는 스코어링을 거쳐 컨테이너가 배포될 노드를 선택하게 된다.

즉, 쿠버네티스는 서버들의 리소스를 추상화하여 개발자들에게 리소스 관리에 대한 고민을 해결해준다.


수평확장 및 오토스케일링

두 번째는 수평확장과 오토스케일링이다. 일반적으로 서비스를 배포할 때에는 하나의 프로세스가 아닌 여러 프로세스를 두어 트래픽을 분산시킨다. 이러한 방식으로 프로세스를 늘려 성능을 올리는 것을 수평확장이라고 하는데 쿠버네티스 또한 파드를 하나만 배치하는 것이 아닌 파드를 동시에 여러 개 배치하는 추상화를 제공한다.

이러한 추상화는 쿠버네티스에서 디플로이먼트, 스테이트풀셋 등으로 정의하는데, 레플리카 속성을 두어 파드의 개수를 제어할 수 있다. 실제 서비스 환경에서는 일반적으로 파드를 직접 배포하지 않고 이러한 추상화 리소스를 이용하여 배포한다.

디플로이먼트와 스테이트풀셋의 사용은 프로세스가 상태를 가지느냐 아니냐로 구분해 사용하여야 한다. 상태를 갖는다는 것은 레플리카로 배포되는 각각의 파드가 지속적인 스토리지(볼륨)가 필요하거나 고유한 네트워크 식별자가 필요함을 의미한다. 예시로 Rest API를 만든다면 디플로이먼트를, 데이터베이스를 배포할 때에는 스테이트풀셋을 이용한다. 물론 상태를 가지지 않는 프로세스의 장점을 취하기 위해 컴포넌트를 배포할 때에는 디플로이먼트로 배포하는 것을 지향하면 좋다. 무상태 컨테이너를 지향하자.

쿠버네티스는 또한 부하에 맞춰서 파드의 개수를 자동으로 조절해주는 오토스케일링을 제공한다. 일반적으로 위에서 언급한 리소스의 사용량에 따라 조건을 걸어 활용한다.


서비스 관리

세 번째는 서비스 관리이다. 여기서 서비스는 마이크로서비스 아키텍처에서의 서비스와 같은 맥락인 네트워크를 통해 이용되는 소프트웨어 컴포넌트를 의미한다.

우리는 일반적으로 서비스를 배포하고 이 앞에 로드밸런서, 리버스프록시를 둔다. 그 이유는 서비스의 수평확장과 무중단배포를 가능하게 하기 위함이다. 수평확장과 새로운 버전의 배포를 위해 새로운 프로세스가 생성되고 기존 프로세스가 내려가는 과정이 이루어진다고 하면, 앞단의 로드밸런서, 리버스프록시는 이를 감지(서비스 디스커버리)할 수 있어야 한다. 이러한 과정은 수동으로 해도 괜찮을 수 있으나 서비스가 무수히 많다고(마이크로서비스) 가정하면 꽤 큰 골칫거리가 된다.

쿠버네티스는 서비스라는 오브젝트를 정의할 수 있도록 하여 이러한 고민을 해결한다. 서비스는 앞서 언급한 파드(들)를 식별해 하나의 엔드포인트로 묶어준다. 이때 서비스는 엔드포인트의 활용에 따라 여러 가지 타입이 있으나 여기서는 따로 언급하지 않는다. 이와 마찬가지로 쿠버네티스는 서비스를 추상화하여 클라언트에 제공해 준다.


오브젝트 관리

네 번째는 오브젝트 관리이다. 오브젝트는 쿠버네티스에서 영속성을 가지는 개체를 의미한다. 앞서 언급한 파드, 서비스, 디플로이먼트, 스테이트풀 셋 등은 모두 쿠버네티스의 오브젝트의 형태로 만들어진다. 이때 타입은 리소스라고 부르고 이러한 리소스를 이용해 만들어진 개체를 오브젝트라 부른다. 완전히 유사하지는 않지만 객체지향 패러다임의 클래스와 객체의 관계를 떠올려도 좋다.

쿠버네티스는 이러한 오브젝트를 API를 통해 관리할 수 있게 해준다. 쿠버네티스의 컴포넌트 중 하나인 컨트롤 플레인은 API 서버의 역할을 하게 되고 이 API를 이용해 우리는 앞서 언급한 기능들을 사용할 수 있다. API는 기본적으로 HTTP API이나 편하게 사용할 수 있도록 커맨드라인 도구인 kubectl을 제공해주고 주류 언어마다 SDK를 제공해준다.

이러한 API가 있기 때문에 우리는 오브젝트를 쉽게 관리할 수 있는데, 잡(태스크)를 만들어 이벤트가 발생할 때마다 돌아가는 백그라운드 프로세스를 만든다든지 혹은 CI/CD를 구현하는데도 활용할 수 있다.


그 외
▪ 네임스페이스 : 클러스터 내에서 여러 네임스페이스를 두어 리소스를 격리할 수 있다.
▪ RBAC : RBAC를 제공하여 사용자 혹은 파드에 역할 기반으로 권한을 줄 수 있다.
▪ 구성, 시크릿 관리 : 구성, 시크릿 등을 클러스터에 미리 정의하고 컨테이너에서 쓸 수 있다.
▪ 서비스 추상화 : 파드, 디플로이먼트, 스테이트풀셋 뿐만 아니라 크론잡, 데몬셋 등을 제공해 기능을 확장한다. 그뿐만 아니라 커스텀 리소스를 정의할 수도 있다.


버즈니 AI Lab 추천 플랫폼 예시
추천 플랫폼 구현 예시를 통해 버즈니 AI Lab에서 쿠버네티스를 어떻게 활용했는지 알아본다. 이때 다른 도메인과 달리 중요하게 다뤄져야 했던 내용은 다음과 같다.

▪주기적인 추천 모델 학습
▪모델 프로세스를 적절한 노드에 배치
▪API의 속도를 위해 추천 결과는 미리 생성
▪사용자의 행동에 따라 실시간으로 추천 결과 갱신


쿠버네티스 클러스터 구성

버즈니 AI LAB에서는 쿠버네티스 클러스터를 GKE를 통해 구성했다. 온프레미스에서 쿠버네티스 클러스터를 구성해도 위에서 언급한 기능들을 전부 사용할 수 있으나 클라우드를 통해 구성하면 다음의 이점을 가진다.

▪클러스터 오토스케일링 - 노드 리소스 자체가 부족하면 자동으로 노드를 늘려준다. 이때 노드 풀을 선점형 인스턴스로 생성하면 비용을 획기적으로 줄일 수 있다.
▪볼륨(디스크) 관리 - 클라우드에서 제공하는 볼륨을 파드의 볼륨으로 활용할 수 있다.
▪기타 클라우드 서비스와의 통합 - 로드밸런서, 도메인 및 인증서를 편하게 구성할 수 있다.

클라우드를 사용하면서 드는 비용과 이점들을 잘 비교 분석해봐야 한다.


쿠버네티스 오브젝트 및 외부 리소스

추천 플랫폼을 위해 생성한 쿠버네티스 오브젝트와 외부 리소스들이다. Model Trainer와 Recommendation Worker 오브젝트는 추천 모델의 타입마다 존재한다. 편의를 위해 일부 컴포넌트는 생략했다. 단계에 따라 컴포넌트 간의 상호작용을 서술하며 쿠버네티스 오브젝트에 대한 이야기를 해볼까 한다. 위에서 설명하지 않은 키워드가 나올 수 있는데 활용에 중점을 맞춘 내용이니만큼 자세히 다루지는 않는다.

▲ <그림 1> 쿠버네티스 오브젝트와 외부 리소스

추천 모델 생성 단계

추천 모델 생성 단계의 시작은 Model Trainer라고 명명한 크론잡 오브젝트가 수행한다. 크론잡 리소스는 크론 형식의 일정에 따라 파드를 추상화한 잡을 실행할 수 있도록 해준다. 즉, 일정에 따라 컨테이너를 실행하고 싶을 때 사용한다.

Model Trainer는 로그를 가져와 추천 모델을 학습하고 이를 Model Storage에 저장한다. 저장이 완료되면 PubSub(Kafka) 컴포넌트에 신규 모델 토픽 메시지를 게시한다.

이때 모델을 학습하는 컨테이너는 많은 양의 CPU, 메모리 혹은 GPU가 필요할 수 있다. 작업을 정의할 때 필요 리소스와 최대 리소스를 조심스럽게 결정하고 노드 어피니티 혹은 테인트/톨러레이션을 활용해 파드가 특정 노드에 스케줄링 되도록 한다.

PubSub(Kafka)는 각각의 파드가 지속적인 볼륨을 가져야 하고 고유한 네트워크 식별자를 가져 여러 브로커를 노출해야 하므로 스테이트풀셋 리소스 타입으로 오브젝트를 생성해준다.

▲ <그림 2> 추천 모델 생성 단계

추천 모델 서빙 단계

추천 모델이 생성되었으니 이를 프로세스(워커)화 해줘야 한다. 이러한 책임은 Kubernetes Object Controller가 맡고 있는데 쿠버네티스 내부에서 쿠버네티스 API를 이용하여 오브젝트들을 관리한다.

Kubernetes Object Controller는 PubSub에서 신규 모델 토픽을 구독하고 신규 모델 발생 시 Recommendation Worker 오브젝트의 모델 경로를 업데이트하여 Worker가 롤링업데이트 되도록 한다. 이때 모델 경로는 런타임 시점에 결정돼야 하므로 env 혹은 args 필드에 넣어 환경변수나 인자로 넣어준다. 추가로 쿠버네티스 API를 사용하기 위한 역할을 가지고 있는 서비스 어카운트를 오브젝트에 함께 정의해야 한다.

▲ <그림 3> 추천 모델 서빙 단계

추천 생성 단계
Recommendation Worker는 디플로이먼트 오브젝트로 컨테이너가 시작되면 모델을 Model Stroage로 부터 가져와 메모리로 로드한다. 이 과정이 오래 걸릴 수 있으므로 readinessProbe를 정의하여 쿠버네티스에게 컨테이너가 요청을 처리할 준비가 되었음을 알려주어 쿠버네티스의 상태 관리를 도와야 한다. 그 후 PubSub에서 추천에 필요한 로그 토픽을 구독해 사용자의 신규 액션이 발생할 때마다 Key-Value Storage에 User-ItemList의 형태로 추천 결과를 저장한다.

▲ <그림 4> 추천 생성 단계

추천 API

Recommendation API 컴포넌트는 Key-Value Storage에 이미 저장된 추천 결과를 서빙하기 위한 웹 인터페이스의 책임만을 가진다.

API를 만들 때 주의해야 할 점은 Graceful Shutdown에 대한 고려이다. 쿠버네티스에서는 기본적으로 TERM 시그널을 보내 프로세스를 종료하는데 선택한 웹 서버가 TERM 시그널을 의도한대로 핸들링하는지 확인이 필요하다. 만약 웹 서버가 TERM 시그널을 의도한 바와 다르게 핸들링한다면 preStop이라는 훅을 고려하자. 이 훅을 리소스에 추가하여 서버의 종료 전에 해야 할 것들을 직접 정의할 수 있다.

마지막으로 이 디플로이먼트를 쿠버네티스 외부에 노출하기 위해 로드밸런서 타입의 서비스를 생성한다.


CI/CD

CI/CD 툴과 쿠버네티스 API을 이용해 각각의 컴포넌트들의 업데이트가 지속해서 일어나도록 한다.

저작권자 © 컴퓨터월드 무단전재 및 재배포 금지