Tech

CLOps - 플랫폼의 핵심! 코어 컴포넌트

이번 글은 CLOps 시리즈의 두 번째 편으로 ML 서빙 플랫폼 CLOps의 핵심을 담당하는 코어 컴포넌트에 대해 이야기하고자 합니다.

앞서 모델을 서빙하는 데 필요한 요소를 크게 다섯 가지로 나눠 말씀드렸습니다. 코어 컴포넌트와 컴퓨팅 리소스 관리, 모델 부가 기능 확장, 모델 운영 관리, 인터페이스인데요. 이 중에서 코어 컴포넌트는 이름에서 알 수 있듯이 나머지 네 가지 요소를 긴밀하게 연결하고 유연하게 확장할 수 있도록 만드는 데 중요한 기반이 되는 컴포넌트입니다. 이번 글에서는 이 코어 컴포넌트에 대해 자세히 알아보겠습니다.

코어 컴포넌트의 역할

코어 컴포넌트는 ‘코드를 변경하지 않고 다양한 기능을 자동으로 확장하고 배포하며 제어하기 위한 중요한 역할’을 담당하는 컴포넌트입니다. 이 역할의 중요 포인트를 다음과 같이 나눠 이야기해 볼 수 있을 것 같습니다.

  • 코드를 변경하지 않는다.
  • 다양한 기능을 별도의 제어 없이 자동으로 확장한다.
  • 자동으로 쉽게 애플리케이션을 배포 및 제어한다.

첫 번째, ‘코드를 변경하지 않는다’는 배포하고자 하는 모델 애플리케이션의 구현부를 변경하지 않아도 배포가 가능해야 한다는 것을 의미합니다. 즉, 모델러가 별도의 추가 작업 없이 모델 애플리케이션 로직에만 집중할 수 있어야 한다는 뜻입니다.

두 번째, ‘다양한 기능을 별도의 제어 없이 자동으로 확장한다’는 모델 운영 및 부가 기능을 쉽게 확장할 수 있어야 한다는 것을 의미합니다. 이는 동기 혹은 비동기와 같이 모델 서빙 형태를 변형하는 모델 부가 기능 추가나, 운영에 필요한 로그 수집이나 지표 시각화, 헬스 체크를 별도의 작업 없이 자동으로 확장할 수 있어야 한다는 뜻입니다.

세 번째, ‘자동으로 쉽게 애플리케이션을 배포 및 제어한다’는 앞서 설명한 두 가지 형태를 배포 명세 파일을 통해 쉽게 배포하고 제어할 수 있어야 한다는 것을 의미합니다. 이는 복잡한 배포 및 제어 방식을 단순화해서 선언적 명세를 통해 쉽게 재현할 수 있어야 한다는 뜻입니다.

위 세 가지 목표를 달성하기 위해 쿠버네티스의 컨트롤러를 확장하는 오퍼레이터(operator) 패턴과, 애플리케이션 추가 기능을 확장 및 제어하기 위한 사이드카(sidecar) 패턴을 이용한 익스큐터(executor)를 활용했습니다. 먼저 오퍼레이터 패턴을 이용하면 쿠버네티스 사용법과 똑같이 YAML 명세와 같은 선언적인 형태로 애플리케이션을 배포 및 제어할 수 있습니다. 또한 사이드카 패턴을 이용한 익스큐터를 활용하면 배포 전 사이드카로 익스큐터를 주입해 배포할 수 있기 때문에 모델 트래픽을 우선적으로 받아 미들웨어 로직을 쉽게 추가할 수 있고 확장성 있는 형태로 구현할 수 있습니다.

오퍼레이터(operator)

오퍼레이터는 CLOps로 배포되는 모델 애플리케이션의 배포부터 기능 확장 자동화에 이르기까지 가장 핵심적인 역할을 담당합니다.

오퍼레이터 조정 프로세스
그림1. 오퍼레이터 조정

오퍼레이터 패턴은 쿠버네티스를 작동시키는 기본 메커니즘으로 이 패턴을 이용하면 누구나 쿠버네티스 기능을 확장할 수 있습니다. 이는 쿠버네티스가 선언적 오브젝트로 명세하기 때문인데요. 선언적 접근법은, 원하는 애플리케이션의 배포 상태 즉, 애플리케이션 요구(desired) 상태를 정의된 명세에 맞게 제출하면, 오퍼레이터가 현재 상태와 요구 상태를 비교해 멱등성(idempotent)을 지키며 요구 상태에 맞게 로직을 수행할 수 있도록 해줍니다.

오퍼레이터는 일반적으로 요구 명세를 새로 정의하고 이를 이용해 로직을 수행합니다. 여기서 명세서 정의를 'Custom Resource Definition(이하 CRD)'이라고 부르고, 정의된 명세서에 맞게 사용자가 제출한 명세를 'Custom Resource(이하 CR)'라고 부릅니다.

오퍼레이터 기본값/변형 및 검증 프로세스
그림2. 오퍼레이터 프로세스

오퍼레이터는 크게 네 가지 단계로 나눠 로직을 실행합니다. [그림 2]에서 볼 수 있듯 1) CR의 기본값을 채워 넣거나 변경하는 단계(쿠버네티스 변경 관리자) 와, 2) OpenAPI 스펙 기반으로 CR을 검증하는 단계(Open API 검증자), 3) CR의 추가 정보를 종합해 필드를 검증하는 단계(쿠버네티스 검증 관리자) 를 먼저 실행합니다. 이 세 단계를 모두 완료하면 [그림 1]과 같이 오퍼레이터로 전달돼 마지막으로 4) 검증된 CR을 반복적으로 조정(reconcile)하는 단계(차이가 있다면 조정) 를 통해 로직을 수행합니다.

CLOps는 이와 같은 구조를 활용해 모델러의 모델 애플리케이션 CR을 변형 혹은 검증 후 배포를 수행하도록 구성했습니다.

CR 계층구조
그림3. CR 계층구조

모델러가 모델 애플리케이션를 배포하려면 CLOpsDeployment라는 CRD를 스펙에 맞게 사용해야 합니다. 이 단일한 CR 하나로 모델 레지스트리 정보부터 모델 배포 정보, 확장 기능 설정과 같은 수많은 기능을 설정하고 배포할 수 있도록 구성했습니다.

수많은 설정과 기능을 단순하고 확장성 있게 사용하기 위해서 최상위 CR인 CLOpsDeployment 아래에 ModelDeployment, AsyncAPI와 같은 여러 CR을 계층 형태로 구성했습니다. 그 결과, 최상위 CR은 단순하고 풍부하게 요구 명세를 정의할 수 있고, 내부적으로는 CR을 유연하게 확장 또는 축소할 수 있으며, 기능별 조정 로직을 단순하게 구성할 수 있습니다.

오퍼레이터 기능 확장
그림4. 오퍼레이터 기능 확장

이렇게 모델러가 CLOpsDeployment CR을 사용해 모델 레지스트리, 모델 코드와 같은 요구 사항을 제출하면, 앞서 설명한 프로세스대로 각 필드에 대한 변형 및 검증을 거쳐 조정 단계를 실행합니다. 이 과정에서 오퍼레이터는 모델 레지스트리로부터 모델 파라미터를 자동으로 내려받는 초기화 컨테이너를 주입합니다. 초기화 컨테이너의 초기화 작업이 완료되면 사이드카로 익스큐터를 주입하고 모든 트래픽이 먼저 익스큐터로 향하도록 서비스 오브젝트를 생성하는 작업을 합니다.

익스큐터(executor)

앞서 익스큐터는 모델 애플리케이션이 배포될 때 사이드카로 자동으로 함께 배포돼 모델 트래픽을 우선적으로 받는다고 말씀드렸습니다. 이와 같은 구조는 모델러의 소스 코드를 변경하지 않고도 앞단에서 미들웨어를 확장할 수 있게 해줍니다. 오퍼레이터에 이어 두 번째로 핵심적인 역할이라고 할 수 있습니다.

익스큐터
그림5. 익스큐터 구조

오퍼레이터가 익스큐터를 주입하면 모델 CR에 대해 필요한 정보를 ConfigMap을 통해 주입받습니다. 이 ConfigMap에는 모델 애플리케이션에 확장해야 할 기능 정보와 프록시해야 할 포트 정보 등 자세한 정보가 담겨 있습니다. 익스큐터는 이 정보를 파싱해서 설정해야 할 정보들을 초기화하고 기능을 확장합니다. 예를 들어 모델에 대한 지표를 제공하기 위해 미들웨어를 추가해 모델의 요청과 응답 지연시간(latency)를 제공한다거나, 모델 서빙 방식을 변경하기 위해 큐를 주고받을 수 있는 미들웨어를 추가해 비동기 방식으로 변경할 수도 있습니다.

이와 같이 미들웨어를 추가해도 트래픽은 모델 애플리케이션으로 그대로 전달되기 때문에, 모델러는 모델 애플리케이션에 어떤 기능이 추가되는지 고려할 필요 없이 모델 로직에만 집중할 수 있는 구조를 만들 수 있었습니다.


하위 호환성과 서빙 안정성 고려

모델 애플리케이션을 서빙하기 위한 프로세스를 CR 하나로 자동화하고 익스큐터를 통해 기능을 확장하는 구조는 마치 마법과도 같았습니다. 하지만 이 구조를 구현하면서 가장 많이 고민하고 고려한 부분을 뽑는다면 단연 하위 호환성과 서빙 안정성이라고 생각합니다.

하위 호환성

Hub and Spoke
그림6. Hub and Spoke

오퍼레이터는 CRD 변화에 대한 사이드 이펙트가 매우 크게 작용합니다. 따라서 이미 사용하고 있는 CRD에 대한 명세와 조정 로직이 달라질 때는 API 버전을 변경해 제공해야 합니다. 예를 들어 API 스펙을 변경했을 때 호환성을 보장하기 위해 엔드포인트를 api/v1api/v2로 분리해 이중으로 엔드포인트를 제공하는 것과 같습니다.

쿠버네티스는 CRD에 대한 API 그룹마다 API 버전을 분리해서 제공할 수 있는 기능을 제공합니다. 하지만, 버전을 분리하면 관리 포인트가 늘어나는 문제가 발생하는데요. 쿠버네티스는 이를 유연하게 해결할 수 있도록 허브 앤 스포크(hub and spoke) 방식을 제공하고 있습니다. 이 방식은 CRD에 대해서 여러 버전을 제공하되, 어떤 버전 하나를 허브 버전으로 지정하고 다른 버전의 CR을 허브 버전의 CR 명세로 변환해 단일한 조정 로직으로 처리하는 방법입니다.

CLOps는 지금도 새로운 명세가 추가되거나 삭제되고 있습니다. 이와 같이 CRD 변화가 잦기 때문에 하위 호환성 고려를 중요하게 여기고 있습니다.

서빙 안정성

앞서 오퍼레이터는 요구 상태와 현재 상태가 다르면 요구 상태를 유지하기 위해 로직을 수행한다고 말씀드렸습니다. 이와 같은 조정 로직은 대부분 모델러가 요구 상태를 새로 변경하면서 발생하는 변화 때문에 발생하지만, 오퍼레이터의 버전 업그레이드 때문에 발생할 수도 있습니다. 오퍼레이터 로직 오류로 패치가 실행되거나 추가 로직을 반영하기 위한 배포로 인해 트리거되는 경우인데요. 이때 새 버전의 오퍼레이터 배포가 이미 배포된 모델 애플리케이션의 요구 상태와 현재 상태의 차이를 발생시키면서 모델 애플리케이션의 원치 않는 재배포를 야기할 수 있습니다.

이에 모델 애플리케이션의 원치 않는 재배포를 발생시킬 수 있는 변경과 그렇지 않은 변경을 구분했습니다. 변경이 불가피한 경우에는 이에 대한 사이드 이펙트를 사전에 고지해 서비스에 미치는 영향을 최소화할 수 있도록 고려했습니다. 이와 같이 서빙 안정성을 확보하는 것 또한 코어 컴포넌트에서 중요한 부분 중 하나입니다.


코어 컴포넌트를 기반으로 이제 어떻게 확장해 볼까요? 궁금하지 않으신가요?

이번 글에서는 ML 서빙 플랫폼 CLOps의 코어 컴포넌트를 소개하고, 가장 중요하게 생각했던 부분을 살펴봤습니다. 코어 컴포넌트는 간단한 구동 방식으로 작동하지만, 이를 확장해 수많은 기능을 추가할 수 있는 가장 중요한 컴포넌트이자 메커니즘입니다. 이 기본 메커니즘을 활용해 어떻게 CLOps를 확장해 나갔는지 궁금하지 않으신가요? 이어지는 글에서는 컴퓨팅 리소스 관리를 효율적으로 하기 위해 어떤 방식으로 접근했는지 살펴보며 그 고군분투했던 과정을 공유하겠습니다.