gRPC는 다양한 기업에서 사용하고 있고, 자주 사용했었던 기술인 Triton Inference Server에서도 사용되고 있다.$_{^{\ [1]}}$ 기존에는 REST API 기반 통신을 주로 사용했었는데 어떤 이유로 gRPC를 사용하기 시작한 것일까? 이 글을 통해 gRPC의 탄생 배경, 장단점, Python을 통한 구현 등을 알아보도록 하자!
Theoretical Background
gRPC의 탄생 배경을 알아보기 전 MSA와 RPC에 대해 이해해보자.
MSA
MSA(Micro Service Architecture)는 기존의 monolithic architecture와 달리, service 단위를 작게 나누어 독립적으로 구성함으로써 확장성, 생산성, 유지보수성, 유연성 등 다양한 장점을 제공한다. 다만, MSA를 구축하려면 Kubernetes와 같은 container orchestration 도구에 대한 이해가 필요하며, architecture의 복잡도가 증가하는 단점이 존재한다. 자세한 비교는 아래 표를 통해 알 수 있다.
Aspect
Monolithic Architecture
Micro Service Architecture
Deployment
전체 application을 한 번에 배포
각 service를 독립적으로 배포 가능
Scalability
특정 component 확장이 어려움
개별 service 단위로 확장 가능
Development
모든 팀이 동일한 codebase에서 작업
각 팀이 독립적인 service에서 병렬 작업 가능
Technology Stack
단일 기술 stack으로 제한됨
Service별로 다양한 기술 선택 가능
Fault Tolerance
한 부분의 장애가 전체 system에 영향을 미침
장애가 개별 service로 제한됨
Complexity
초기 설정 및 관리가 단순
Service 간 통신 및 orchestration 복잡성 증가
Performance
service 간 통신이 적어 overhead가 낮음
Network 통신으로 인해 추가 지연 발생 가능
Maintenance
Codebase가 커질수록 유지보수가 어려움
작은 단위로 나뉘어 유지보수가 용이함
Time-to-Market
높은 의존성으로 기능 개발 및 출시 속도가 느림
병렬 작업으로 기능 개발 및 출시 속도 향상
Operational Costs
단순한 환경에서 운영 비용이 낮음
Monitoring, 배포, orchestration으로 운영 비용 증가
Data Management
단일 database로 관리가 용이
분산 database로 데이터 일관성 관리가 어려움
Monolithic Architecture
Micro Service Architecture
graph TD;
A[Customer] --> B[Web Server]
B --> C[Order Management]
B --> D[Product Catalog]
B --> E[Payment Processing]
C --> F[(Order)]
D --> G[(Product)]
E --> H[(Payment)]
graph TD;
A[Customer] --> B[API Gateway]
B --> C[Order Service]
B --> D[Product Service]
B --> E[Payment Service]
C --> F[(Order)]
D --> G[(Product)]
E --> H[(Payment)]
C --> D[Product Service]
D --> E[Payment Service]
E --> C[Order Service]
RPC
RPC (Remote Procedure Call)는 network 상의 다른 computer에서 실행되는 함수를 호출하는 방식이다. Local function과 같이 보이지만 실제로는 network를 통해 다른 server나 process에서 실행된다. RPC를 통해 분산 system에서 서로 다른 system 간 통신을 저수준의 세부 사항까지 신경쓰지 않고 표준화된 방식으로 구축할 수 있다. 대표적인 RPC로 gRPC, Thrift, Dubbo 등이 있다.
gRPC
gRPC는 Google에서 개발한 대표적인 open source RPC로 HTTP/2를 기반으로 빠른 data 전송과 multiplexing을 지원하며 아래와 같이 binary protocol인 Protocol Buffers를 사용하여 data 직렬화가 효율적이다.$_{^{\ [2]}}$
1 2 3 4 5
message Person { string name = 1; int32 id = 2; bool has_ponycopter = 3; }
또한 다양한 언어 (C, C++, Java, Python, Go, Ruby, …) 지원으로 polyglot 구성이 가능하며 빠르고 효율적인 특징들로 MSA에서 각광받고 있다. 기존의 RESTful API는 주로 HTTP/1.1을 기반으로 사용되며, gRPC는 HTTP/2를 기반으로 하여 두 가지의 차이를 아래 표에서 확인할 수 있다.
Feature
HTTP/1.1
HTTP/2
Protocol
Text 기반 (ASCII)
Binary 기반
Header Handling
매 요청마다 header를 반복적으로 전송
Header 압축을 통해 중복을 제거, 효율적인 전송
Connection Management
요청마다 새로운 연결을 생성(혹은 Keep-Alive 사용)
하나의 연결에서 여러 요청/응답을 처리 (Multiplexing)
Performance
순차적 처리, 한 번에 하나의 요청만 처리
Multiplexing으로 여러 요청을 동시에 처리
Request/Response Handling
요청-응답 순차적으로 처리
요청-응답 비동기 처리, 서버 푸시 가능
Latency
높은 지연 시간 (Header 크기 및 연결 지연)
낮은 지연 시간 (헤더 압축 및 멀티플렉싱 덕분)
Performance Optimization
없음
Streaming, header 압축, multiplexing, server push 등
Overhead
높은 overhead (Header 크기, 연결 수)
낮은 overhead (효율적인 data 전송)
Multiplexing
없음
있음 (하나의 연결에서 여러 요청/응답을 병렬 처리)
Real-time Streaming
제한적, 순차적 요청 처리
양방향 streaming, 실시간 data 전송 최적화
Order Guarantee
응답 순서 보장
응답 순서 보장, 하지만 multiplexing으로 순서가 섞일 수 있음
Conncetion Persistence
하나의 연결로 여러 요청 가능 (Keep-Alive 필요)
하나의 연결로 다수의 요청 및 응답 처리 가능
Server Push
없음
있음 (Server가 client 요청 없이 resource를 미리 전송)
Experiments
RESTful API (HTTP/1.1) 기반의 backend와 gRPC 기반의 backend의 정량적 차이를 확인하기 위해 대표적인 HTTP/1.1 기반 backend framework인 FastAPI를 사용했다. 정량적인 비교를 위해 response time, RPS (requests per seconds)를 측정했다.
REST
아주 간단하게 POST를 통해 입력 받은 숫자의 factorial 값을 응답하는 API를 아래와 같이 만들었다.
$ python -m grpc_tools.protoc -I. --python_out=. --grpc_python_out=. item.proto $ head item_pb2.py # -*- coding: utf-8 -*- # Generated by the protocol buffer compiler. DO NOT EDIT! # NO CHECKED-IN PROTOBUF GENCODE # source: item.proto # Protobuf Python Version: 5.28.1 """Generated protocol buffer code.""" from google.protobuf import descriptor as _descriptor from google.protobuf import descriptor_pool as _descriptor_pool from google.protobuf import runtime_version as _runtime_version from google.protobuf import symbol_database as _symbol_database $ head item_pb2_grpc.py # Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT! """Client and server classes corresponding to protobuf-defined services.""" import grpc import warnings
여기서 average size가 5배 가량 차이나는 점을 확인할 수 있다. 이는 REST의 경우 ASCII 기반으로, gRPC의 경우 binary 기반으로 data를 주고 받기 때문으로 보인다.
각 요청에 따른 response time을 시각화하면 아래와 같은 결과가 도출된다.
확실히 gRPC가 REST에 비해 빠른 response time을 가지는 것을 보인다.
3분 간의 부하 test에 대한 RPS와 평균 response time은 아래와 같다.
RPS는 유사한 경향을 띄지만, response time에 대해서 약 2배 가량 빠른 것을 확인할 수 있다.
이 결과들이 엄밀하게 통제된 환경 속에서 실험된 것도 아니고, 각 backend들의 resource 제어를 한 상태에서 진행한 것도 아니기에 이를 통해 gRPC의 성능이 확실히 좋다고 단정하긴 어렵다. 경향 자체로는 확실히 HTTP/1.1 기반의 REST API에 비해 개선됨을 확인할 수 있었다. 또한 각 backend에 대해 복잡한 요청, 응답과 business logic을 적용하여 비교하면 더 명쾌한 차이를 확인할 수 있을 것으로 보인다.