Skip to main content Link Menu Expand (external link) Document Search Copy Copied

요즘 gRPC 를 외/내부망 통신에 많이 사용하는 추세이다. 듣기로는 소형 이진포멧으로 페이로드 크기를 줄일 수 있다고 한다. 성능 최적화에 기여할 수 있을 것 같아서 평소에 restAPI 만 쓰다가 이참에 gRPC 에 대해 알아보기로 했다.

protobuf 이진 내부구조 동작방식

gRPC 는 HTTP/2 + Protobuf 를 사용하는 RPC 이다. RPC 는 쉽게 말하면 서버의 특정함수를 원격으로 실행시키는 것임. 그리고 요즘 이 gRPC 가 많이 쓰이는 이유는 페이로드 크기를 줄일 수 있기 때문이다. 간단하게 아래의 json 을 protobuf 로 전송할 때 얼마나 소형으로 전송할 수 있는지 직접 계산해보자.

{
  "name": "A",
  "age": 30,
}

위 json 을 그대로 전송하면 key 랑 value 전부 바이트 변환했을 때 21 bytes 가 나온다. 이제 protobuf 스키마를 정의해보자.

message Person {
  string name = 1;
  int32 age = 2;
}

gRPC HEX 덤프보면 아래와 같이 찍힌다.

0A 01 41 | 10 1E

여기서 매우 재밌는 부분이 0A 와 10 부분이다. protobuf 직렬화 포맷을 보면 각 필드는 key-value 쌍으로 저장된다. key 는 타입이랑 필드 번호를 합쳐서 저장하는데 아래와 같이 계산된다.

key = (field_number << 3) | wire_type

wire_type 이 말 그대로 타입이고 matching table: https://developers.google.com/protocol-buffers/docs/encoding#structure 이 있다. 이거보고 타입에 맞춰 비트 연산해주면 된다. string 의 경우 2 (length-delimited), int32 의 경우 0 (varint) 임. 하위 3비트가 2면 길이도 나온다고 보면 된다. 그래서 name 필드는 (1 « 3) + 10(2) = 1010(2) = 0A(16) 으로 저장된다. 이진으로 읽으면 1 번쨰 필드가 010(2 = string)타입을 가지고 있다는 뜻이 된다. 만약에 필드번호가 8이다라고 하면 1000 000 이 되고, wire_type 2 를 합치면 1000 010 이 된다. 이걸 16진수로 바꾸면 42 가 된다.

그럼 필드번호가 매우 크다면 어떻게 될까?? key 가 8bit 로만 해결되지 않을 것 같은데, 이 점이 궁금해서 찾아봤더니 varint 인코딩 방식으로 key 2개로 쪼개져서 저장된다고 한다. 뭐 varint 는 7bit+ continuation_bit 1 로 저장하는 방식임. 그래서 아무리 큰 수라도 여러 byte 로 쪼개져서 저장될 수 있다. 최초 key 가 continuation_bit 1 로 설정된 다음 나오고, 2번째 key 는 continuation_bit 0 으로 세팅 및 3번째부터 실제 value 가 등장한다.

이제 0A 01 41 가 길이 1인 string ‘A’ 라는 것을 알았다. 이제 10 1E 부분을 보자. 10(16) 0001 0000(2) 으로 필드번호는 10(2) = 2, wire_typ = 0 으로 int 형이라는 것을 알 수 있다. 얘는 따로 길이가 없어서 바로 value 가 등장한다. 1E(16) = 30(10) 으로 age 가 30 이라는 것을 알 수 있다!

그래서 최종적으로 gRPC 로 전송되는 페이로드는 0A 01 41 10 1E 로 총 5byte 로도 충분히 그 뜻을 전달이 가능하다. 다만 중간에 사람이 읽기 좀 어렵다는 단점이 있다. 그래서 string ‘A’ 가 어떤 값인데? 를 알기 위해서는 protobuf 스키마를 미리 알고 있어야 한다는 단점이 있다. json txt 의 겨웅 name 이라는 key 가 같이 전송되니 몰라도 되서 편안하다.

즉, A 는 이름이구나…를 바로 알 수 있지만 gRPC 는 모른다. 어쨌든 내부망에서 서로 통신할 떄는 어떤 스키마인지 미리 알고 있기 때문에 이진포멧으로 주고받아도 괜찮다.

이제 protobuf 를 사용해서 전송하게된다면 기존 21 bytes(json txt) -> 5 bytes(protobuf) 로 페이로드 크기가 훨씬 작아지니 네트워크 전송량도 줄어들고, 응답속도도 빨라질 것이다. 이와 관련한 실험결과를 가져왔다. https://auth0.com/blog/beating-json-performance-with-protobuf

보통 대부분 non-compressed json 로 통신하고 있는데, protobuf 로 바꾸면 json 대비 934ms -> 720ms 로 꽤 큰 응답속도 향상이 있다고 한다!

reference from https://auth0.com/blog/beating-json-performance-with-protobuf

RPS + p90 딜레이 결과 확인

gRPC 는 좀 귀찮아진다. protobuffer 코드 생성단계를 거쳐야 하고, 디버깅도 어렵다. 클라이언트, 서버 둘 다 동일한 스키마를 공유해야하고 기존 웹 상에서 따로 지원해줘야한다. 그래서 서버간 통신시 성능적 이점은 확실하니 내부망에서 대용량 데이터를 주고받을 때는 아주 유리하다.

레퍼런스 들어가서 보면 진짜 인상깊다. RTS 를 천천히 늘려가면서 p90 딜레이와 cpu 사용량을 측정한 결과를 보는데 초반부에는 40% 미만인 낮은 부하조건에서 REST API 가 gRPC 보다 더 낮은 딜레이를 보여준다. 아마도 gRPC 가 내부적으로 HTTP/2 + protobuf 직렬화/역직렬화 오버헤드가 있어서 그런 것 같다.

여기보면 초당 핸들링 가능한 요청 수를 비교하는 표인데 gRPC 가 압도적으로 높다. 어느 순간부터 많은 요청을 주고받을 때는 gRPC + Protobuf 조합이 아주 유리하다. 작은 수의 요청을 서로 주고받을 떄는 그냥 REST API 쓰면 됨. 만약에 진짜 매우매우 많은 수 까지 핸들링해야한다면 거의 초당 9만건 요청을 처리하고있는(REST API 에 비해 약 1.7배 이상 핸들링) gRPC 를 쓰는게 맞다.