본문으로 건너뛰기

[Ray] Ray gRPC 토큰 인증 최적화 -- shared_ptr 캐싱

PR 링크: ray-project/ray#59500 상태: Merged | 변경: +192 / -152

들어가며

Ray 클러스터는 gRPC를 통해 노드 간 통신하며, 인증 토큰으로 요청을 검증한다. 기존에는 std::optional<AuthenticationToken>으로 토큰을 관리했는데, 이 방식은 GetToken() 호출 때마다 토큰 객체를 값으로 복사하고, ValidateToken() 시에도 매번 문자열 파싱(FromMetadata)이 발생한다. 수천 개의 RPC가 초당 처리되는 환경에서 이 오버헤드가 누적된다. 이 PR은 shared_ptr<const AuthenticationToken>으로 전환해 복사를 제거한다.

핵심 코드 분석

GetToken 반환 타입 변경

Before:

// authentication_token_loader.h
optional[CAuthenticationToken] GetToken(c_bool ignore_auth_mode)

After:

shared_ptr[const CAuthenticationToken] GetToken(c_bool ignore_auth_mode)

GetToken()optional<AuthenticationToken> 대신 shared_ptr<const AuthenticationToken>을 반환한다. 토큰 객체는 한 번 생성되면 변하지 않으므로 const로 공유한다. 호출자가 몇이든 포인터만 복사되고 실제 토큰 데이터는 한 곳에만 존재한다.

ValidateToken: 파싱 제거

Before:

// authentication_token_validator.h
c_bool ValidateToken(
    const optional[CAuthenticationToken]& expected_token,
    const CAuthenticationToken& provided_token)

After:

c_bool ValidateToken(
    const shared_ptr[const CAuthenticationToken]& expected_token,
    const string& provided_metadata)

기존에는 검증 전에 메타데이터 문자열에서 AuthenticationToken 객체를 생성(FromMetadata)해야 했다. 변경 후에는 raw 메타데이터 문자열을 직접 받아 내부에서 비교한다. 불필요한 객체 생성이 제거된다.

Python Cython 바인딩 업데이트

Before:

# rpc_token_authentication.pxi
cdef optional[CAuthenticationToken] expected_opt

expected_opt = CAuthenticationTokenLoader.instance().GetToken(False)
if not expected_opt.has_value():
    return False

provided = CAuthenticationToken.FromMetadata(provided_token.encode())
if provided.empty():
    return False

return CAuthenticationTokenValidator.instance().ValidateToken(expected_opt, provided)

After:

cdef shared_ptr[const CAuthenticationToken] expected_ptr

expected_ptr = CAuthenticationTokenLoader.instance().GetToken(False)
if not expected_ptr:
    return False

return CAuthenticationTokenValidator.instance().ValidateToken(
    expected_ptr, provided_metadata.encode())

Python 쪽 코드도 간결해졌다. FromMetadata 파싱 단계가 사라지고, 토큰 존재 여부 확인이 has_value() 대신 shared_ptr의 boolean 변환으로 단순해졌다.

서비스 레이어 전파

Before:

// grpc_services.h (모든 GrpcService 하위 클래스)
void InitServerCallFactories(
    const std::unique_ptr<grpc::ServerCompletionQueue> &cq,
    std::vector<std::unique_ptr<ServerCallFactory>> *server_call_factories,
    const ClusterID &cluster_id,
    const std::optional<AuthenticationToken> &auth_token) override;

After:

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;

10개 이상의 gRPC 서비스 클래스(ActorInfoGrpcService, NodeInfoGrpcService, JobInfoGrpcService 등) 모두에서 optional -> shared_ptr 전환이 일관되게 적용되었다.

왜 이게 좋은가

  1. 복사 비용 제거: optional 값 복사 대신 shared_ptr 포인터 복사만 발생한다. 토큰 문자열이 길수록 효과가 크다.
  2. 파싱 비용 제거: 매 검증마다 FromMetadata 호출로 문자열을 파싱해 토큰 객체를 만들던 비용이 사라진다. CompareWithMetadata로 직접 비교한다.
  3. const 보장: shared_ptr<const T>이므로 토큰이 불변임이 타입 시스템으로 보장된다.
  4. 일관성: optional -> shared_ptr 전환이 loader, validator, 모든 gRPC 서비스, Python 바인딩까지 빠짐없이 적용되어 인터페이스가 통일되었다.

정리

optional<T> -> shared_ptr<const T> 전환은 단순해 보이지만, gRPC 인증처럼 매 요청마다 반복 호출되는 핫패스에서는 의미 있는 최적화다. 값 복사 제거와 불필요한 파싱 제거라는 두 가지 개선을 동시에 달성하면서, const 불변 보장까지 얻었다. C++ 스마트 포인터의 교과서적인 활용 사례다.

참고 자료

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

댓글

관련 포스트

PR Analysis 의 다른글