본문으로 건너뛰기

[ray] [Ray] gRPC 메트릭 객체 재사용을 통한 GCS 확장성 개선 (13% 성능 향상)

PR 링크: ray-project/ray#64732 상태: Merged | 변경: +121 / -64

들어가며

Ray는 대규모 분산 컴퓨팅을 위한 프레임워크로, 최근 RL(강화학습)이나 모델 Post-training/Pre-training 워크로드에서 그 중요성이 더욱 커지고 있습니다. 특히 GB300과 같은 최신 GPU 노드(노드당 4개 GPU) 환경에서는 수천 개의 액터(Actor)를 빠르게 스케줄링하고 관리하는 능력이 시스템의 전체 처리량(Throughput)을 결정짓는 핵심 요소가 됩니다.

기존 Ray 아키텍처에서는 액터의 등록(Register), 생성(Create), 리스(Lease) 처리 등이 모두 GCS(Global Control Service)를 거치게 됩니다. 하지만 액터 수가 수만 개로 늘어남에 따라 GCS의 메인 스레드에 과도한 부하가 걸리는 병목 현상이 발견되었습니다.

이번 포스팅에서는 gRPC 요청마다 반복적으로 수행되던 불필요한 객체 생성 로직을 제거하여, E2E(End-to-End) 실행 시간을 최대 13% 단축시킨 Ray의 최적화 사례를 살펴보겠습니다.


문제의 핵심: 매 요청마다 생성되는 메트릭 객체

Ray의 내부 통신은 gRPC를 기반으로 합니다. 기존 코드에서는 gRPC 서버가 요청을 처리할 때마다(ServerCallImpl) 또는 클라이언트가 요청을 보낼 때마다(ClientCallImpl) 메트릭을 기록하기 위해 새로운 메트릭 객체를 매번 생성하고 있었습니다.

액터 하나를 생성하기 위해서는 RegisterActor, CreateActor 등 여러 번의 RPC가 발생합니다. 수만 개의 액터가 동시에 스케줄링되는 상황에서, 이러한 작은 객체 생성(Allocation)과 소멸(Deallocation) 작업은 GCS 메인 스레드의 CPU 사이클을 야금야금 갉아먹는 주범이 되었습니다.

메트릭 객체의 구조는 프로세스 생명주기 동안 변하지 않으므로, 이를 매번 새로 만들 이유가 전혀 없었습니다.


코드 분석: 객체 생성에서 참조 전달로

이번 PR의 핵심 변경 사항은 GrpcServerMetrics 객체를 프로세스당 한 번만 생성하고, 이를 각 서비스 핸들러에 참조(Reference) 형태로 전달하도록 구조를 변경한 것입니다.

1. 서비스 초기화 인터페이스 변경

먼저 GrpcService의 초기화 함수인 InitServerCallFactories의 시그니처가 변경되었습니다.

Before:

// src/ray/core_worker/grpc_service.h
virtual void InitServerCallFactories(
    const std::unique_ptr<grpc::ServerCompletionQueue> &cq,
    std::vector<std::unique_ptr<ServerCallFactory>> *server_call_factories,
    const ClusterID &cluster_id,
    std::shared_ptr<const AuthenticationToken> auth_token) override;

After:

// src/ray/core_worker/grpc_service.h
virtual void InitServerCallFactories(
    const std::unique_ptr<grpc::ServerCompletionQueue> &cq,
    std::vector<std::unique_ptr<ServerCallFactory>> *server_call_factories,
    const ClusterID &cluster_id,
    std::shared_ptr<const AuthenticationToken> auth_token,
    GrpcServerMetrics &server_metrics) override; // 메트릭 참조 추가

2. GCS 서비스 레이어의 전파

GCS 내의 수많은 하위 서비스(ActorInfo, NodeInfo, JobInfo 등)들도 이제 외부에서 생성된 server_metrics를 주입받습니다.

Before/After (src/ray/gcs/grpc_services.cc):

 void ActorInfoGrpcService::InitServerCallFactories(
     const std::unique_ptr<grpc::ServerCompletionQueue> &cq,
     std::vector<std::unique_ptr<ServerCallFactory>> *server_call_factories,
     const ClusterID &cluster_id,
-    std::shared_ptr<const AuthenticationToken> auth_token) {
+    std::shared_ptr<const AuthenticationToken> auth_token,
+    GrpcServerMetrics &server_metrics) {
   RPC_SERVICE_HANDLER(ActorInfoGcsService, RegisterActor, -1)

이 변경을 통해 RPC_SERVICE_HANDLER 매크로 내부에서 매번 메트릭 객체를 new 하던 로직이 사라지고, 주입받은 server_metrics 참조를 사용하게 됩니다.


리뷰어들의 기술적 통찰

PR 리뷰 과정에서 두 가지 중요한 논의가 있었습니다.

  1. 테스트 용이성 (Testability): edoakes는 메트릭을 정적(static) 구조체로 두지 않고 참조로 전달하는 방식이 다른 곳에서 사용하는 테스트 패턴과 일치하며, 모킹(Mocking)이나 단위 테스트 작성이 훨씬 수월해진다고 평가했습니다.
  2. 스레드 안전성 및 성능: Yicheng-Lu-llll은 메트릭 기록 시점을 OnReplyReceived가 아닌 폴링 스레드(Polling thread)에서 수행하도록 설계하여, 메인 이벤트 루프의 블로킹을 방지했다는 점을 강조했습니다. 이는 메트릭 수집 자체가 서비스 성능에 영향을 주지 않도록 하는 세심한 설계입니다.

왜 이게 좋은 최적화인가?

1. 실질적인 성능 향상

벤치마크 결과는 놀랍습니다. 10,000개의 액터를 스케줄링하는 워크로드에서:

  • 기존: 94.2초
  • 최적화 후: 82.0초 (-13.0% 감소)

단순히 객체 생성을 줄인 것만으로도 초당 수천 번 발생하는 RPC 환경에서는 유의미한 CPU 오버헤드 감소를 가져왔습니다.

2. Hot Path에서의 무거운 작업 제거

소프트웨어 엔지니어링의 고전적인 원칙 중 하나는 "Hot Path(자주 실행되는 경로)에서 할당을 최소화하라"는 것입니다. gRPC의 ServerCall 생성은 Ray에서 가장 빈번하게 호출되는 Hot Path 중 하나입니다. 여기서 발생하는 malloc/free 오버헤드와 캐시 미스를 제거함으로써 시스템의 확장성(Scalability) 한계치를 높였습니다.

3. 일반적인 교훈

우리는 흔히 성능 최적화라고 하면 복잡한 알고리즘 개선을 떠올립니다. 하지만 실제 대규모 분산 시스템에서는 "당연히 고정되어 있어야 할 데이터가 매번 생성되고 있지는 않은가?"를 점검하는 것만으로도 큰 이득을 얻을 수 있습니다. 의존성 주입(Dependency Injection) 패턴을 통해 공통 자원을 관리하는 것이 성능과 코드 품질(테스트 용이성) 모두를 잡는 방법임을 이 PR은 잘 보여줍니다.


마치며

이번 최적화는 Ray GCS의 병목을 해결하여 더 많은 액터를 더 빠르게 띄울 수 있게 했습니다. 이는 특히 대규모 GPU 클러스터를 사용하는 최신 AI 학습 환경에서 Ray의 경쟁력을 한층 강화하는 변경입니다. 여러분의 프로젝트에서도 매 요청마다 불필요하게 생성되는 '상태가 없는 객체'가 없는지 한 번 점검해보시는 건 어떨까요?

참고 자료

⚠️ 알림: 이 분석은 AI가 실제 코드 diff를 기반으로 작성했습니다.

댓글

관련 포스트

PR Analysis 의 다른글