[네트워크] gRPC vs JSON 성능을 측정해보자

Hocaron·2024년 10월 27일
2

트러블슈팅

목록 보기
14/14

gRPC 를 도입했을 때, JSON 과 성능상 유의미한 차이가 있는지 확인하기 위해 성능 테스트를 진행하였다.
과거에는 로컬에서 테스트를 진행해 외부 환경의 영향을 받을 가능성이 있었지만, 이번에는 회사에서 nGrinder와 실서버와 동일한 스펙의 인스턴스를 사용하여 정확도를 높여 테스트를 수행하였다.

gRPC란 무엇인가?

gRPC는 Google에서 개발한 원격 프로시저 호출(Remote Procedure Call, RPC) 시스템이다. 간단히 말해, gRPC는 서버와 클라이언트 간의 통신을 쉽게 할 수 있게 도와주는 프레임워크이다.

클라이언트가 서버에게 데이터를 요청할 때, 마치 서버 안에 있는 함수나 메서드를 직접 호출하는 것처럼 요청을 보낼 수 있다. 실제로는 네트워크를 통해 요청이 전달되지만, gRPC를 사용하면 서버에 메서드를 호출하는 것처럼 프로그래밍할 수 있다.

gRPC는 HTTP/2 프로토콜을 기반으로 하여 고성능, 저지연 네트워크 통신을 지원한다. HTTP/2는 기존의 HTTP/1.1과 비교하여 멀티플렉싱(Multiplexing), 헤더 압축(Header Compression) 등의 최적화 기능을 도입함으로써 대역폭 효율성을 극대화하고 전송 지연(Latency)을 최소화한다.

gRPC에서 지원하는 통신 방식

  • Unary RPC
    클라이언트가 하나의 요청을 보내고 서버는 하나의 응답을 반환하는 가장 일반적인 형태의 통신 방식이다.

  • Server Streaming RPC
    클라이언트가 하나의 요청을 보내면 서버는 여러 개의 응답을 스트리밍으로 보낸다.

  • Client Streaming RPC
    클라이언트가 여러 요청을 스트리밍 방식으로 보내고 서버는 하나의 응답을 반환한다.

  • Bidirectional Streaming RPC
    클라이언트와 서버가 양방향으로 여러 요청과 응답을 스트리밍 방식으로 주고받을 수 있다.

Protocol Buffers(Protobuf)란 무엇인가?

Protocol Buffers(Protobuf)는 gRPC에서 사용하는 데이터 직렬화 방식으로, 데이터를 작고 빠르게 전송하기 위해 바이너리 형식으로 압축하여 전달한다. JSON이나 XML처럼 사람이 읽기 쉬운 텍스트 형식과는 달리, Protobuf는 더 작은 파일 크기와 더 빠른 처리 속도를 제공하여 네트워크 효율성을 극대화한다.

Protobuf가 더 빠른 이유는 데이터를 바이너리 형식으로 직렬화하여 전송량을 줄이고, 파싱 오버헤드를 최소화하며, 작고 효율적인 데이터 구조 덕분에 더 빠른 처리 속도를 제공하기 때문이다. Protobuf는 텍스트 기반의 JSON이나 XML과 달리 태그나 공백 같은 불필요한 데이터를 포함하지 않으며, 숫자로 정의된 필드 번호를 사용해 데이터를 효율적으로 구분한다. 또한, 메타데이터 오버헤드를 줄여 데이터가 더 컴팩트해지며, 사전 정의된 데이터 구조로 인해 파싱 과정이 단순화되어 빠른 데이터 접근이 가능하다.

IDL(Interface Definition Language)이란 무엇인가?

IDL(인터페이스 정의 언어)는 클라이언트와 서버가 어떤 데이터 구조로 통신할지, 어떤 서비스를 제공할지 미리 정의해주는 언어이다. gRPC에서는 Protocol Buffers(proto 파일)를 IDL로 사용한다.

proto 파일에서 클라이언트와 서버가 통신하는 규칙을 정의하면, 이 파일을 바탕으로 코드를 생성할 수 있다. 즉, 서버와 클라이언트가 어떤 형식의 데이터를 주고받을지 사전에 약속을 정해두는 것이라고 이해할 수 있다.

Protocol Buffers (proto) 예시

Protocol Buffers 파일(.proto 파일)은 gRPC에서 서비스의 인터페이스와 데이터 구조를 정의하는 파일이다. 아래는 간단한 gRPC 서비스와 데이터 구조를 정의한 .proto 파일 예시이다.

syntax = "proto3";

service UserService {
  rpc GetUser (UserRequest) returns (UserResponse);
}

message UserRequest {
  int32 user_id = 1;
}

message UserResponse {
  string name = 1;
  string email = 2;
}

JSON과 gRPC 비교 성능 테스트

테스트 서버 및 데이터베이스 스펙

항목서비스스펙인스턴스 수
EC2php-servicec5.4xlarge13대
EC2java-servicec5.large12대
ElastiCacherediscache.r6g.large라이터 노드 1대 / 리더 노드 1대

테스트 플로우

  • nGrinder → java-service (JSON / gRPC)
  • nGrinder → php-service → java-service (JSON / gRPC)

성능 테스트

nGrinder → java-service (JSON / gRPC)`

프로토콜VusersTPS응답 시간(Time)JAVA CPU 사용률
JSON60029,116.219.94 ms80% 후반
JSON90030,800.028.46 ms80% 후반
gRPC60059,373.210.07 ms80% 중반
gRPC90062,308.714.40 ms80% 후반

gRPC는 동일한 CPU 사용률에서 JSON 대비 약 2배 이상의 TPS를 처리하고, 응답 시간은 절반 이하로 짧다.

nGrinder → php-service → java-service (JSON / gRPC)

프로토콜VusersTPS응답 시간(Time)PHP CPU 사용률JAVA CPU 사용률
JSON6004,761.5117.22 ms30% 초반20% 중반
JSON9005,928.3137.21 ms30% 초반20% 중반
gRPC60014,588.440.27 ms70% 초반30% 중반
gRPC90015,497.256.9 ms80% 초반30% 중반

gRPC는 JSON에 비해 더 높은 TPS를 기록하고 응답 시간도 훨씬 짧다. PHP와 JAVA CPU 사용률이 각각 상승하는 것을 볼 수 있다.

커넥션 재사용 테스트

nGrinder → java-service(gRPC)

테스트 환경활성 커넥션 비율 (ActiveConnection)새 연결 비율 (NewConnection)
gRPC82.94% ▲17.06% ▼

nGrinder → php-service -> java-service(gRPC)

테스트 환경활성 커넥션 비율 (ActiveConnection)새 연결 비율 (NewConnection)
gRPC78.82% ▲21.98% ▼
gRPC(force_new)0.74% ▲99.26% ▼
JSON0.31% ▲99.69% ▼

테스트 결과

  1. TPS와 CPU 사용률
  • gRPC는 동일한 CPU 사용률(80%대)에서 JSON 대비 약 2배 이상의 TPS를 처리할 수 있다.
    예: 600 Vusers 기준으로 gRPC는 59,373 TPS, JSON은 29,116 TPS를 기록했다.
  1. 응답 시간
  • gRPC의 응답 시간은 JSON보다 절반 이하로 짧다.
    예: 600 Vusers 기준으로 gRPC는 10.07ms, JSON은 19.94ms이다.
  1. 커넥션 재사용
  • gRPC는 활성 커넥션의 82.94%를 재사용하며, 새 연결 생성이 매우 적다.
  • JSON은 커넥션 재사용률이 거의 없고, 요청마다 새 연결을 99.69% 생성한다

테스트 결과 분석

PHP + JSON 병목 지점

  • 직렬화/역직렬화의 오버헤드
    JSON은 텍스트 기반으로, 직렬화 및 역직렬화에 상대적으로 많은 시간이 소요된다. 이는 JSON을 처리할 때 네트워크 오버헤드나 데이터 전송 지연을 발생시키며, TPS를 저하시킬 수 있다.

  • 네트워크 대역폭 병목
    JSON은 데이터가 커지고, 이를 전송하는 데 더 많은 대역폭이 필요하다. 특히 트래픽이 증가하면 네트워크 전송 속도가 저하되고 병목 현상이 발생할 수 있다. gRPC와 JSON의 요청당 크기를 비교했을 때, JSON 요청이 약 1278 bytes이고, gRPC 요청이 약 866 bytes로 약 47% 차이가 있다.

  • 커넥션 재생성으로 인한 병목
    HTTP/1.1 Keep-Alive가 활성화되어 있어도 요청이 순차적으로 처리되기 때문에 동시 요청을 처리하지 못한다. 이로 인해 동시 요청이 많은 경우, 대기 중인 요청을 처리하기 위해 커넥션이 자주 생성되며, 커넥션 재사용률이 매우 낮다. 실제로 JSON 서버와 통신할 때 커넥션 재사용률이 1% 이하로 나타났다. 커넥션을 매번 새로 생성하거나 직렬적으로 처리함에 따라 네트워크 레이턴시가 증가하고, 네트워크 연결 설정과 해제의 반복이 성능 병목을 일으킨다.

PHP에서의 커넥션 재사용 개념

PHP-FPM과 프로세스 재사용

  • PHP-FPM은 PHP의 요청 처리 방식을 최적화하기 위해 FastCGI 프로토콜을 사용한다.
  • 새로운 요청마다 프로세스를 생성하지 않고, 기존 프로세스를 재사용하여 효율성을 높인다.
  • 동일한 프로세스가 여러 번의 요청을 처리할 수 있으며, 이로 인해 높은 트래픽 환경에서 리소스 효율성을 높인다.

예시: PHP-FPM은 새로운 요청마다 웹 서버 프로세스를 생성 및 종료할 필요 없이 워커 프로세스를 반복해서 재사용하여 더 많은 트래픽을 처리할 수 있다.

PHP에서 gRPC 커넥션 재사용

  • gRPC 채널은 서버와 클라이언트 간의 통신 통로로, 클라이언트가 서버에 요청을 보낼 때 이 채널을 사용한다.
  • gRPC는 효율성을 위해 기존에 생성된 채널을 재사용할 수 있는 메커니즘을 갖추고 있다.

gRPC 커넥션 재사용 방법

  • Persistent List에 저장된 채널 재사용
    PHP는 처음 gRPC 채널을 생성할 때, 이 채널을 Persistent List에 저장한다.
    • 다음 요청에서 동일한 채널이 필요하면, 새로 생성하는 대신 Persistent List에 저장된 채널을 재사용하여 자원 낭비를 줄인다.

gRPC 채널 재사용 절차

  1. 채널이 이미 존재하는지 확인

    • 요청에서 사용할 채널이 Persistent List에 있는지 확인한다.
  2. 채널이 없으면 새로 생성

    • Persistent List에 채널이 없으면, 새로운 gRPC 채널을 생성하여 Persistent List에 저장한다.
  3. 채널이 있으면 재사용

    • 이미 같은 조건으로 만들어진 채널이 있으면, 새로운 채널을 만들지 않고 기존 채널을 재사용한다.

PHP 코드 예시

extern HashTable grpc_persistent_list;

if (!(PHP_GRPC_PERSISTENT_LIST_FIND(&grpc_persistent_list, key, key_len, rsrc))) {
    // 채널이 없으면 새로 생성해서 Persistent List에 저장
    create_and_add_channel_to_persistent_list(
        channel, target, args, creds, key, key_len, target_upper_bound TSRMLS_CC);
} else {
    // 채널이 존재하면 이를 재사용
    channel_persistent_le_t *le = (channel_persistent_le_t *)rsrc->ptr;
    if (strcmp(target, le->channel->target) != 0 ||
        strcmp(sha1str, le->channel->args_hashstr) != 0 ||
        (creds != NULL && creds->hashstr != NULL &&
         strcmp(creds->hashstr, le->channel->creds_hashstr) != 0)) {
      // 해시 충돌이 발생하거나 조건이 맞지 않으면 새로 생성
      create_and_add_channel_to_persistent_list(
          channel, target, args, creds, key, key_len, target_upper_bound TSRMLS_CC);
    } else {
      // 기존 채널 재사용
      efree(args.args);
      free_grpc_channel_wrapper(channel->wrapper, false);
      gpr_mu_destroy(&channel->wrapper->mu);
      free(channel->wrapper);
      channel->wrapper = NULL;
      channel->wrapper = le->channel;
      php_grpc_channel_ref(channel->wrapper);
      update_and_get_target_upper_bound(target, target_upper_bound);
    }
}

FYI; https://github.com/grpc/grpc/blob/df0b1dfed8f3b61ca625b2e4a43b29425ed2236b/src/php/ext/grpc/channel.c#L49

profile
기록을 통한 성장을

3개의 댓글

comment-user-thumbnail
2024년 11월 8일

안녕하세요 선생님
커넥션 재사용은 어떤식으로 파악하는건가요?

1개의 답글
comment-user-thumbnail
2024년 11월 15일

흠… 좋은 글 잘보고 갑니다 ^^

답글 달기