[네트워크] gRPC, Rest API 에 비해 얼마나 빠를까

Hocaron·2024년 7월 16일
2

네트워크

목록 보기
6/6

트래픽이 많은 내부 트래픽에 gRPC 를 적용하면 Rest API 에 비해 얼마나 빠를까?

gRPC, 얼마나 빠른걸까?!

테스트 환경은 다음과 같아요.

K6가 10명 / 100명의 가상 사용자를 생성하여 60초 동안 지속적으로 서버에 요청을 보내도록 한다.

K6 성능 테스트 결과 비교

10명 테스트(60초)

항목gRPC 결과HTTP 결과
수신된 데이터141 kB134 kB
전송된 데이터239 kB147 kB
평균 응답시간3.19ms6.31ms
최소 응답시간455.45µs328µs
중앙값 응답시간1.95ms4.95ms
최대 응답시간56.57ms84.35ms
p(90) 응답시간3.75ms8.13m
p(95) 응답시간5.6ms12.28ms

100명 테스트(60초)

항목gRPC 결과HTTP 결과
수신된 데이터2.8 MB2.7 MB
전송된 데이터4.7 MB2.9 MB
평균 응답시간3.25ms6.61ms
최소 응답시간165.45µs175µs
중앙값 응답시간2.36ms2.26ms
최대 응답시간23.96ms143.18ms
p(90) 응답시간5.86ms8.68ms
p(95) 응답시간8.64ms16.22ms

gRPC, 왜 빠른걸까?!

통신 방식

gRPC는 HTTP/2를 사용하고, REST API는 주로 HTTP/1.1을 사용한다. HTTP/2는 멀티플렉싱, 헤더 압축, 서버 푸시 등의 기능을 지원해서 더 빠르게 데이터를 주고받을 수 있다.

HTTP/1.1이 비교적으로 느린 이유

  1. 요청/응답의 순차 처리

HTTP/1.1에서는 동시에 여러 요청을 보낼 수 있지만, 이 요청들은 하나씩 처리된다. 이를 헤드 오브 라인 블로킹 문제라고 한다.

  1. 연결 관리

HTTP/1.1에서는 지속 연결(Persistent Connection)을 사용할 수 있지만, 기본적으로 요청/응답마다 새로운 TCP 연결을 설정하는 방식이다. 이로 인해 연결 설정과 해제 과정에서 시간이 소요된다.

HTTP/2의 개선점

  1. 멀티플렉싱
    HTTP/2는 멀티플렉싱을 통해 하나의 TCP 연결에서 여러 요청과 응답을 동시에 주고받을 수 있다. 이는 헤드 오브 라인 블로킹 문제를 해결해준다.

  2. 연결 설정 및 유지
    HTTP/2는 하나의 연결을 지속적으로 사용하며, 이 연결에서 여러 스트림으로 데이터를 주고받을 수 있다.

  3. 헤더 압축
    HTTP/2는 HPACK이라는 헤더 압축 방식을 사용한다. 이는 헤더를 효율적으로 압축하고, 중복된 헤더 정보를 줄여 데이터 전송량을 줄여준다.

데이터 직렬화 형식

gRPC는 Protobuf를 사용해서 데이터를 직렬화한다. Protobuf는 바이너리 형식이라 데이터 크기가 작고, 처리 속도가 빠르다. 반면에 REST API는 JSON을 사용한다. JSON은 사람이 읽기 쉽지만 텍스트 형식이라 데이터 크기가 크고, 파싱 속도가 느리다.

전송 프로토콜

gRPC는 상태 유지 방식이고, 양방향 스트리밍을 지원한다. 그래서 클라이언트와 서버가 지속적으로 연결된 상태에서 데이터를 주고받을 수 있다. 반면에 REST API는 상태 비유지 방식이라, 각 요청마다 새로운 연결을 설정하고 해제해야 하는데, 이 과정에서 오버헤드가 발생한다.

grpc-ecosystem 을 이용해서 빠르게 gRPC 환경 구축하기

버전 호환

grpc-spring-boot-starter의 최신버전은 3.1.0.RELEASE이고, spring-boot 3.2.4와 spring-cloud 2023.0.0 과 호환이 된다.
fyi; https://github.com/grpc-ecosystem/grpc-spring?tab=readme-ov-file

다른 버전과의 호환성이 궁금하다면 해당 문서를 참고하자.

Spring Boot 2.x 에서 gRPC 서버 설정하기

build.gradle 설정

개별 프로젝트 설정

  • grpc-interface: gRPC 인터페이스를 정의하는 모듈이다. generateProto 작업을 통해 프로토콜 버퍼 코드를 생성하고, 이를 compileJava 작업에 포함시킨다.
  • grpc-client: Spring Boot를 사용해 gRPC 클라이언트를 구현하는 모듈이다.
  • grpc-server: Spring Boot를 사용해 gRPC 서버를 구현하는 모듈이야. grpc-interface 모듈에 의존하고, gRPC 서버 스타터를 사용한다.

project(':grpc-interface') {
    apply plugin: 'java'
    apply plugin: 'com.google.protobuf'

    dependencies {
        implementation "io.grpc:grpc-netty-shaded:$grpcVersion"
        implementation "io.grpc:grpc-protobuf:$grpcVersion"
        implementation "io.grpc:grpc-stub:$grpcVersion"
        implementation "com.google.protobuf:protobuf-java:$protobufVersion"
        compileOnly 'jakarta.annotation:jakarta.annotation-api:1.3.5'
    }

    protobuf {
        protoc {
            artifact = "com.google.protobuf:protoc:${protobufVersion}"
        }
        plugins {
            grpc {
                artifact = "io.grpc:protoc-gen-grpc-java:${grpcVersion}"
            }
        }
        generatedFilesBaseDir = "$projectDir/src/generated"

        generateProtoTasks {
            all().each { task ->
                task.plugins {
                    grpc {}
                    php {}
                }
            }
        }
    }

    tasks.clean {
        delete "$projectDir/src/generated"
    }
}

project(':grpc-client') {
    apply plugin: 'org.springframework.boot'

    dependencies {
        implementation project(':grpc-interface')
        implementation 'org.springframework.boot:spring-boot-starter-web'
        implementation 'net.devh:grpc-client-spring-boot-starter:2.15.0.RELEASE'
        implementation 'org.springframework.boot:spring-boot-starter'
    }

    bootJar.enabled = true
    jar.enabled = false
}

project(':grpc-server') {
    apply plugin: 'org.springframework.boot'

    dependencies {
        implementation project(':grpc-interface')
        implementation 'net.devh:grpc-server-spring-boot-starter:2.15.0.RELEASE'
        implementation 'org.springframework.boot:spring-boot-starter'
    }

    bootJar.enabled = true
    jar.enabled = false
}

.proto 파일 설정

프로토콜 버퍼(Protocol Buffers)라는 직렬화 포맷을 정의한 스키마 파일로, .proto 파일이 컴파일되어 gRPC 및 프로토콜 버퍼 관련 코드를 생성하는 과정을 살펴보자.

1. .proto 파일 작성

먼저, 서비스와 메시지 구조를 정의한 .proto 파일을 작성한다.

syntax = "proto3";

option java_package = "com.example.grpc";
option java_outer_classname = "ExampleProto";

message Request {
    string name = 1;
}

message Response {
    string message = 1;
}

service ExampleService {
    rpc SayHello (Request) returns (Response);
}

2. .proto 파일 컴파일

프로젝트의 build.gradle 파일에서 protobuf 플러그인을 설정한다. 이 설정은 프로토콜 버퍼 컴파일러(protoc)와 gRPC 플러그인을 사용해 Java 코드를 생성한다.

protobuf {
    protoc {
        artifact = "com.google.protobuf:protoc:${protobufVersion}"
    }
    plugins {
        grpc {
            artifact = "io.grpc:protoc-gen-grpc-java:${grpcVersion}"
        }
    }
    generateProtoTasks {
        all().each { task ->
            task.builtins {
                java {}
            }
            task.plugins {
                grpc {}
            }
        }
    }
}

3. 코드 생성

generateProto 작업을 실행하면, .proto 파일을 찾아서 protoc 컴파일러를 사용해 Java 소스 파일을 생성하고 gRPC 서비스 인터페이스 및 메시지 클래스가 포함된 Java 파일이 생성된다.

@GrpcService 코드 생성

생성된 Java 코드를 프로젝트에서 사용해보자.

    @Override
    public void sayHello(HelloRequest request, StreamObserver<HelloReply> responseObserver) {
        var reply = HelloReply.newBuilder().setMessage("Hello ==> " + request.getName()).build();
        responseObserver.onNext(reply);
        responseObserver.onCompleted();
    }

🔬 gRPC 서버 설정 및 k6 를 이용한 Rest API 와의 성능을 테스트할 수 있는 레포

https://github.com/hocaron/spring-study/tree/main/spring-grpc)

profile
기록을 통한 성장을

0개의 댓글