모든 AI 파이프라인을 개별 프로세스에서 실행하는 이유
요약
AI 파이프라인의 안정성을 위해 각 실행을 독립된 프로세스로 격리하는 아키텍처 설계 방식을 설명합니다. 프로세스 격리를 통해 네이티브 라이브러리 충돌이나 메모리 오염이 전체 시스템으로 확산되는 것을 방지하고 장애 복구력을 높입니다.
핵심 포인트
- 각 파이프라인 실행을 독립된 자식 프로세스로 격리하여 폭발 반경 최소화
- C 확장 및 네이티브 라이브러리 충돌 시 전체 시스템 장애 방지
- 부모 런타임이 자식 프로세스의 수명 주기와 작업 레지스트리 관리
- 프로세스 생성 비용보다 시스템 안정성과 디버깅 용이성에 우선순위 부여
RocketRide의 충돌 격리(crash isolation), 작업 수명 주기(task lifecycle) 및 클라우드 운영(Cloud operations) 뒤에 숨겨진 런타임 경계.
Krish Garg 및 Mithilesh Gaurihar 작성
오전 9시, 만 명의 사용자가 세션 중간에 있을 때, AI 파이프라인의 한 노드가 잘못된 포인터(bad pointer)를 역참조(dereferences)합니다. 이를 실행 중인 프로세스는 Python이 유용한 예외(exception)를 발생시키기도 전에 사라져 버립니다. 이는 불쾌한 실패이지만, 우리가 가장 중요하게 생각하는 문제는 아닙니다.
문제는 그다음에 어떤 일이 일어나는가입니다. 그 충돌이 관련 없는 다른 파이프라인들까지 함께 끌고 내려가나요? 서버를 재시작해야 하나요? 당직 엔지니어(on-call engineer)가 시스템 전체의 장애 상황에 직면하게 되나요, 아니면 단 하나의 실패한 작업과 왜 실패했는지에 대한 유용한 기록을 마주하게 되나요?
그러한 동작은 우리가 초기에 내린 결정에서 비롯되었습니다. 바로 모든 파이프라인 실행(pipeline run)이 자신만의 격리된 프로세스(isolated process)를 갖도록 하는 것입니다.
이것이 가장 저렴하거나 가장 빠른 아키텍처는 아닙니다. 프로세스를 시작하는 데 비용이 들고, 이를 유지하는 데도 비용이 듭니다. 하지만 Python 코드, 네이티브 라이브러리(native libraries), 모델 런타임(model runtimes), 그리고 사용자 정의 노드(user-defined nodes)가 모두 동일한 서비스 내에서 실행될 때, 실패 원인을 파악하는 것이 훨씬 더 어려워진다는 점을 고려하여 우리는 그 비용을 수용했습니다.
하나의 프로세스, 하나의 폭발 반경 (One Process, One Blast Radius)
AI 파이프라인은 일반적인 요청 처리기(request handler)처럼 실패하지 않습니다. 일반적인 예외(exception)는 별개의 문제입니다. C 확장(C extension)에서의 세그멘테이션 폴트(segfault), 미디어 디코더(media decoder)의 충돌, 또는 손상된 네이티브 추론 라이브러리(native inference library)는 또 다른 문제입니다. 일단 프로세스의 메모리가 오염되면, 애플리케이션 수준의 에러 핸들링(error handling)은 더 이상 신뢰할 수 있는 방어선이 되지 못합니다.
따라서 각 RocketRide 작업은 자체적인 임베디드 Python 인터프리터(embedded Python interpreter)를 가진 새로운 자식 프로세스(child process)로 시작됩니다. 이 프로세스는 하나의 파이프라인을 로드하고, 해당 파이프라인의 노드들을 초기화하며, 해당 실행에 대한 작업을 소유합니다. 부모 런타임(parent runtime)은 작업 레지스트리(task registry)를 유지하고, 작업의 로컬 포트(local ports)를 할당하며, 자식 프로세스를 모니터링하고, 그 수명 주기(lifecycle)를 관리합니다.
여기서는 전송(transport) 세부 사항이 중요합니다. 부모 프로세스는 표준 입력(standard input) 및 출력(standard output) 스트림을 통해 자식 프로세스를 감독하는 반면, 파이프라인 데이터는 해당 작업에 할당된 로컬 WebSocket 엔드포인트를 통해 처리됩니다. 이는 반대편에 응답 파이프(response pipe)가 있는 단순한 요청 파이프(request pipe)가 아닙니다. 중요한 속성은 경계(boundary)입니다. 즉, 작업은 감독자(supervisor), 알려진 프로세스(known process), 그리고 종료 상태(exit status)를 가집니다.
이와 동일한 경계는 역방향으로도 작동합니다. 자식 프로세스는 자동 종료(auto-terminate) 설정과 함께 실행되므로, 서버 종료 후에도 남아있는 관리되지 않는 작업(unsupervised work)이 되는 대신 부모 프로세스가 사라지면 함께 종료됩니다.
이는 결함 격리(fault isolation)이지, 보안 샌드박스(security sandbox)가 아닙니다. 별도의 프로세스는 고장 난 워크로드의 폭발 반경(blast radius)을 제한합니다. 프로세스 분리 그 자체만으로는 신뢰할 수 없는 코드를 안전한 멀티테넌트(multi-tenant) 보안 경계로 바꿔주지는 않습니다.
양호한 프로세스 유지하기
프로세스 격리의 명백한 단점은 시작(startup) 비용입니다. 새로운 작업은 Python을 시작하고, 패키지를 임포트(import)하고, .pipe 파일을 읽고, 그래프를 구성하고, 서비스에 연결하며, 노드에 필요한 모든 상태(state)를 준비해야 할 수도 있습니다. 채팅의 모든 메시지나 문서 작업의 모든 페이지마다 이 비용을 지불하는 것은 낭비일 것입니다.
현재 파이프라인은 실행 중이거나 실행 중이 아니거나 둘 중 하나입니다. 아직 미리 가열된(pre-warmed) 중간 상태는 없습니다. 런타임(runtime)에 의해 인지되어 요청이 도착하는 즉시 실행되는 작업들은 계획되어 있지만, 우리는 무엇이 배포(ships)되는지를 설명하는 편을 택하겠습니다. 배포되는 것은 실행 중인 파이프라인이 언제 유지되고 언제 사라질지를 결정하는 일련의 제어 장치들입니다.
운영(production) 환경에서는 애플리케이션이 파이프라인의 수명 주기(lifetime)를 소유합니다. 즉, 필요한 파이프라인을 시작하고 작업이 완료되면 중지합니다. 개발(development) 환경은 런타임이 개입하는 곳이며, 세 가지 메커니즘을 사용합니다:
- 변경 시 재시작 (Restart on change). 런타임이 파이프라인이 저장되는 것을 감지하고 이를 재시작합니다. 이는 자동으로 발생하게 하거나, 프롬프트(prompt)를 거친 후 실행하거나, 혹은 아예 실행되지 않도록 설정할 수 있습니다.
- 생존 시간 (Time-to-live, TTL). 애플리케이션보다 오래 지속되는 개발용 파이프라인이 조용히 비용을 발생시켜서는 안 됩니다. 기본 TTL은 15분입니다. 해당 시간 동안 작업에 아무런 접근이 없으면 스스로 종료되며, 호출자가 원하는 파이프라인으로 다시 시작하는 것은 호출자의 몫입니다.
useExisting. 파이프라인만 필요로 하는 애플리케이션은useExisting을 설정하고 TTL은 그대로 둘 수 있습니다. 파이프라인이 이미 실행 중이라면 앱은 이를 사용하고, 실행 중이 아니라면 런타임이 이를 시작합니다. 다음 실행 시, 중지된 파이프라인은 자동으로 다시 시작되며, 유휴 시간(idle window) 내에 있는 파이프라인은 단순히 재사용됩니다.
작업이 유지되는 동안에는 상태(state)가 유지됩니다. 즉, 동일한 프로세스, 동일하게 초기화된 파이프라인을 유지합니다. 첫 번째 요청이 초기화 비용을 지불하며, 이후의 요청들은 이미 준비된 프로세스를 사용합니다. 그렇다고 해서 공유 워커 풀(shared worker pool)인 것은 아닙니다. 실행 중인 작업은 하나의 파이프라인 프로세스로 남으며, 하나의 파이프라인 상태를 가집니다.
비용은 실재합니다
우리는 실행당 프로세스(process-per-run) 방식이 모든 시스템에 정답이라고 생각하지 않습니다. 프로세스 내(in-process) 런타임은 더 빠르게 시작되고, 메모리를 적게 사용하며, 프로세스 경계를 넘나드는 일을 피할 수 있습니다. 로컬 실험, 신뢰할 수 있는 스크립트, 또는 수명이 짧은 내부 도구의 경우에는 그것이 올바른 선택일 수 있습니다.
우리의 우려는 런타임이 공유 인프라(shared infrastructure)가 되는 시점에 있었습니다. 그 시점에서 플랫폼은 어떤 프로세스가 파이프라인을 소유하고 있는지, 어떤 프로세스가 포트(port)를 소유하고 있는지, 작업이 어떻게 종료되는지, 종료되었을 때 어떤 일이 발생했는지, 그리고 고아 작업(orphaned work)을 어떻게 피할지를 알아야 합니다. 팀은 워커 프레임워크(worker frameworks)와 프로세스 매니저(process managers)를 사용하여 이러한 요소들을 조립할 수 있습니다. 우리는 이러한 요소들이 파이프라인 작성자마다 매번 다시 만들어야 하는 운영 프로젝트가 아니라, 런타임의 기본 동작이 되기를 원했습니다.
C++ 레이어는 CPU 집약적인 (CPU-bound) Python 코드를 마법처럼 빠르게 만들기 위해 존재하는 것이 아닙니다. Python은 개발자들이 모델 라이브러리, 데이터 생태계, 그리고 현대적인 AI를 실용적으로 만들어주는 노트북 친화적인 도구들을 사용하는 위치에 그대로 남아 있습니다. 런타임의 역할은 해당 Python 코드에 신뢰할 수 있는 기반을 제공하는 것입니다. 즉, 네이티브 오케스트레이션 (native orchestration), 태스크 생명주기 (task lifecycles), 명시적인 정리 (explicit cleanup), 그리고 무언가 잘못되었을 때의 프로세스 경계 (process boundary)를 제공하는 것입니다.
클라우드가 시작되는 지점
오픈 소스 런타임은 자체 인프라 위에서 팀에게 이러한 실행 모델을 제공합니다. 이를 서비스로 실행하는 것은 또 다른 차원의 작업이 추가됨을 의미합니다. 실행 중인 태스크는 상태를 가지며 (stateful), 접근을 위해서는 신원 (identity)이 필요하고, 태스크 사용량은 조직, 팀, 그리고 사용자에게 귀속되어야 합니다. 이러한 고려 사항들이 한 명의 개발자 노트북 범위를 벗어나게 되면, 이는 부수적인 배포 세부 사항이 아닙니다. 그것들은 런타임을 둘러싼 운영 체제 (operating system)입니다.
RocketRide Cloud는 파이프라인 형식을 휴대 가능하게 (portable) 유지하면서 이러한 작업을 대신 수행합니다. Cloud는 API 키 접근 방식을 사용하고, 관련 조직, 팀 및 사용자에 대한 태스크 수준의 사용량을 기록하며, 태스크를 감독하고 라우팅하는 서비스들에 대해 상태 확인 (health checks)을 수행합니다. 세션 수준의 라우팅 (Session-level routing)은 실행 중인 태스크가 특정 런타임 인스턴스에 속하기 때문에 Cloud 아키텍처의 일부가 됩니다. 이것은 운영 작업이지, 배포 다이어그램을 무제한 확장에 대한 약속으로 바꾸기 위한 이유가 아닙니다.
비용 추적 (Cost tracking)은 이러한 귀속 (attribution) 작업이 가치를 발휘하는 지점이며, 파이프라인 수명의 양쪽 측면을 모두 다룹니다. 개발과 배포는 별도의 비용으로 추적되므로, 팀은 파이프라인이 실제 운영 트래픽을 처리하기 전의 구축 비용과, 처리하기 시작한 후의 실행 비용을 각각 확인할 수 있습니다.
개발 단계에서는 5명의 팀원으로 구성된 프로젝트 팀이 각 개인의 실행 비용이 정확히 얼마인지 확인한 다음, 그 수치들을 프로젝트 전체 합계로 합산할 수 있습니다. 배포 시에는 파이프라인이 프로덕션 팀에 배포되며, 해당 팀의 비용 또한 개별 사용자 단위까지 동일한 방식으로 확인할 수 있습니다. 모든 태스크 (task)의 사용량은 이를 소비한 조직 (organization), 팀 (team), 그리고 사용자 (user)에게 귀속되므로, 파이프라인 비용이 얼마인지에 대한 질문은 누군가 실제로 질문하는 모든 수준—개인별, 팀별, 조직별—에서 답변을 얻을 수 있습니다.
이것이 바로 셀프 호스팅 (self-hosting)이 마지못해 언급되는 각주가 아니라, 여전히 실질적인 선택지로 남아 있는 이유이기도 합니다. VPC 요구 사항, 에어갭 (air-gapped) 배포, 엄격한 데이터 레지던시 (data residency), 또는 기존 플랫폼 그룹을 보유한 팀은 모든 레이어를 직접 소유하기를 원할 수 있습니다. 해당 경로에서도 동일한 .pipe 파일과 런타임 (runtime)을 사용할 수 있습니다. 클라우드 (Cloud)는 운영에 따른 일상적인 책임을 맡지 않으면서 실행 모델만을 원하는 팀을 위한 것입니다.
결정 규칙은 간단합니다. 인프라 (infrastructure)를 소유하는 것이 업무의 일부라면 셀프 호스팅을 하십시오. 파이프라인 자체가 업무이고 파이프라인 플랫폼을 운영하는 것이 업무가 아니라면 클라우드를 사용하십시오. 어떤 방식이든, 여러분이 구축한 아티팩트 (artifact)는 여전히 여러분의 것입니다.
RocketRide Cloud에서 파이프라인을 구축하고 실행하거나, RocketRide Server로 셀프 호스팅을 하거나, RocketRide VS Code extension에서 시작해 보세요. 이와 관련하여 질문이 있다면 RocketRide Discord에서 저희를 찾아주세요.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기