gRPC
고친 사람 github-actions[bot]
gRPC 는 다른 컴퓨터에 있는 서버의 메서드를 내 코드의 메서드처럼 부르게 해 주는 원격 호출 규약입니다. 부를 메서드와 주고받을 데이터 모양을 파일 하나에 먼저 적어 둡니다. 그 파일에서 서버 코드와 클라이언트 코드가 함께 만들어집니다. 언어가 서로 다른 서비스끼리 잇는 데 씁니다.
쉽고 빠른 이해
gRPC 는 다른 서버의 메서드를 내 메서드처럼 부르게 해 줍니다. 자바로 짠 서버의 SayHello 를 파이썬 클라이언트가 객체 메서드 한 번으로 부르는 식입니다.
왜 이렇게 하나. 정의가 파일 하나에만 있으니 언어가 다른 서버와 클라이언트가 같은 정의를 나눠 씁니다. 네트워크를 건너는 일은 메서드 호출 한 번 뒤로 숨습니다. 언어가 갈리는 서비스끼리 잇는 데 씁니다.
어떻게 도나:
- 메서드 이름과 주고받을 데이터 모양을
.proto파일에 적습니다 - 도구가 그 파일에서 서버 뼈대와 클라이언트 호출 코드를 만들어 줍니다
- 호출 하나가 연결 안의 통로 하나를 씁니다. 메시지가 오간 뒤 성공인지 실패인지를 알리는 상태 코드가 맨 끝에 옵니다
대가도 있습니다. 기본으로 쓰는 직렬화가 바이너리 형식이라 사람이 눈으로 못 읽습니다. 2019년 1월 기준으로 브라우저는 이 규약을 그대로 구현할 수 없었습니다. 그래서 중간에 변환 프록시를 둡니다. 부하 분산은 프록시에 맡기든 클라이언트에 맡기든 내주는 것이 있습니다.
상세
gRPC 는 원격 프로시저 호출, 곧 RPC(Remote Procedure Call)를 하는 오픈소스 프레임워크입니다. 클라이언트 애플리케이션이 다른 컴퓨터에 있는 서버 애플리케이션의 메서드를 로컬 객체의 메서드처럼 바로 부릅니다. 예를 들어 자바로 짠 gRPC 서버를 고·파이썬·루비로 짠 클라이언트가 부를 수 있습니다.
네트워크를 건너는 호출을 메서드 호출 한 번 뒤로 숨기는 것이 목적입니다. 그러면 여러 컴퓨터에 나뉜 애플리케이션과 서비스를 짜기가 쉬워집니다.
gRPC 는 구글이 처음 만들었습니다. 구글은 데이터센터 안에서, 그리고 데이터센터 여럿에 걸쳐 도는 수많은 마이크로서비스를 Stubby 라는 범용 RPC 인프라 하나로 10년 넘게 이어 왔습니다. Stubby 는 어떤 표준에도 기대지 않았습니다. 구글 내부 인프라에 너무 단단히 묶여 있어 공개하기에도 맞지 않았습니다.
그사이 HTTP/2(HyperText Transfer Protocol version 2) 같은 공개 표준에 Stubby 와 같은 기능이 여럿 들어왔습니다. 구글은 2015년 3월에 Stubby 의 다음 판을 이 표준 위에 다시 지어 오픈소스로 내기로 했습니다. 그 결과가 gRPC 입니다.
이름의 g 에는 고정된 뜻이 없습니다. 판마다 바꿔 붙입니다. 1.0 판에서는 gRPC 자신, 1.1 판은 good, 1.2 판은 green 이었습니다.
.proto 파일과 IDL
gRPC 에서는 원격으로 부를 메서드와 그 인자·반환 타입을 먼저 서비스로 정의합니다. 이 정의를 적는 언어를 IDL(Interface Definition Language, 인터페이스 정의 언어)이라고 부릅니다. 특정 프로그래밍 언어에 매이지 않는 정의라서 서버와 클라이언트가 다른 언어여도 같은 파일을 나눠 씁니다.
gRPC 는 기본으로 프로토콜 버퍼(Protocol Buffers)를 IDL 로 씁니다. JSON(JavaScript Object Notation) 같은 다른 데이터 형식과도 쓸 수 있습니다. 프로토콜 버퍼는 구조화된 데이터를 바이트로 바꾸는 직렬화 방식이기도 합니다. 그래서 gRPC 에서는 프로토콜 버퍼 하나가 인터페이스를 적는 언어와 오가는 메시지 형식, 두 몫을 함께 맡습니다.
정의는 확장자가 .proto 인 평범한 텍스트 파일에 적습니다. 아래는 인사 서비스 하나를 정의한 파일입니다.
service HelloService {
rpc SayHello (HelloRequest) returns (HelloResponse);
}
message HelloRequest {
string greeting = 1;
}
message HelloResponse {
string reply = 1;
}
service 는 원격으로 부를 수 있는 메서드 묶음입니다. rpc 줄 하나가 메서드 하나입니다. message 는 주고받을 데이터의 모양입니다.
greeting = 1 의 1 은 값이 아니라 필드 번호입니다. 직렬화된 바이트에는 필드 이름 대신 이 번호가 들어갑니다. 아래 예시 절에서 그 바이트를 봅니다.
코드 생성과 스텁
.proto 파일을 다 적으면 프로토콜 버퍼 컴파일러 protoc 에 gRPC 전용 플러그인을 붙여 코드를 만듭니다. 결과물은 셋입니다.
flowchart TD
P[".proto 파일"] --> G["protoc + gRPC 플러그인"]
G --> C["클라이언트 스텁"]
G --> S["서버 쪽 코드"]
G --> M["메시지 데이터 접근 클래스"]
클라이언트 쪽에는 스텁이 생깁니다. 스텁은 서비스와 같은 메서드를 가진 로컬 객체입니다. 언어에 따라서는 그냥 클라이언트라고 부릅니다.
스텁의 메서드를 부르면 인자를 프로토콜 버퍼 메시지로 싸서 서버로 보냅니다. 서버가 돌려준 응답 메시지는 반환값이 됩니다.
서버 쪽에서는 서비스에 선언된 메서드를 개발자가 구현합니다. 그리고 gRPC 서버를 띄워 호출을 받습니다. 요청 풀기 · 메서드 실행 · 응답 싸기는 gRPC 런타임이 맡습니다.
셋째는 메시지 데이터 접근 클래스입니다. .proto 에 적은 메시지의 데이터를 다루는 클래스가 고른 언어로 생깁니다.
스텁 호출은 두 방식이 있습니다. 동기 호출은 서버의 응답이 올 때까지 멈춥니다. 원격 호출을 함수 호출처럼 보이게 한다는 목표에 가장 가깝습니다.
그런데 네트워크는 본래 비동기입니다. 그래서 지금 스레드를 멈추지 않고 호출을 시작하는 비동기 호출도 씁니다.
호출을 둘러싼 장치
메서드 호출 하나를 몇 가지 장치가 둘러쌉니다. 넷 다 이 편 뒤쪽에서 다시 나옵니다.
| 이름 | 무엇인가 |
|---|---|
| 채널 | 지정한 호스트와 포트의 gRPC 서버로 가는 연결입니다. 클라이언트 스텁을 만들 때 씁니다. 메시지 압축을 켜고 끄는 식으로 기본 동작을 바꾸는 인자를 받습니다. 연결됨·유휴 같은 상태를 가집니다 |
| 메타데이터 | 호출 하나에 딸린 정보를 키와 값의 목록으로 담습니다. 인증 정보가 대표적입니다. 키는 대소문자를 가리지 않습니다. grpc- 로 시작하는 키는 gRPC 몫이라 쓰면 안 됩니다. 값이 바이너리면 키 이름이 -bin 으로 끝납니다 |
| 데드라인 | 클라이언트가 이 호출을 얼마나 기다릴지 정한 기한입니다. 넘기면 호출이 DEADLINE_EXCEEDED 오류로 끝납니다 |
| 취소 | 클라이언트도 서버도 언제든 호출을 취소할 수 있습니다. 취소하면 호출이 바로 끝나 더 이상 일하지 않습니다. 취소 전에 바뀐 것은 되돌리지 않습니다 |
교환 순서
gRPC 호출 하나는 HTTP/2 연결 위에서 프레임 몇 개로 오갑니다. 요청 메시지 하나에 응답 메시지 하나가 오는 단항 호출로 그 차례를 봅니다.
요청은 「요청 헤더 → 길이 붙은 메시지 → 스트림 끝」입니다. 응답은 「응답 헤더 → 길이 붙은 메시지 → 트레일러」입니다. 트레일러는 본문 뒤에 한 번 더 붙는 헤더입니다. 호출이 성공했는지는 이 트레일러가 알려 줍니다.
HTTP/2 의 스트림과 프레임
HTTP/2 는 연결 하나에 요청 여럿을 함께 싣습니다. 이를 다중화라고 부릅니다. 연결 안에서 요청 하나와 그 응답이 오가는 따로 떨어진 통로를 스트림이라고 부릅니다.
gRPC 호출 하나가 스트림 하나를 씁니다. 호출을 가르는 번호로는 HTTP/2 의 스트림 번호를 씁니다.
flowchart TD
subgraph CONN["HTTP/2 연결 하나"]
subgraph SA["스트림 · 호출 A"]
A1["HEADERS · 요청 헤더"] --> A2["DATA · 요청 메시지"]
end
subgraph SB["스트림 · 호출 B"]
B1["HEADERS · 요청 헤더"] --> B2["DATA · 요청 메시지"]
end
end
스트림 번호는 열려 있는 HTTP/2 세션 안에서만 뜻이 있습니다. 한 프로세스가 HTTP/2 세션을 둘 이상 다루면 번호가 겹칠 수 있습니다. 전역 식별자로는 쓸 수 없습니다.
HTTP/2 에서 데이터는 프레임이라는 조각으로 오갑니다. gRPC 는 헤더를 싣는 HEADERS 프레임과 본문을 싣는 DATA 프레임을 씁니다. 헤더가 한 프레임에 다 안 들어가면 CONTINUATION 프레임이 뒤를 잇습니다. 마지막 프레임에는 END_STREAM 플래그를 세워 그 방향의 전송이 끝났다고 알립니다.
HTTP/2 에는 흐름 제어도 있습니다. 보내는 쪽이 한 번에 흘려보낼 양에 한도를 두는 장치입니다. 클라이언트와 서버는 계산 능력과 네트워크 용량이 서로 어긋나 있기 마련입니다. 흐름 제어는 버퍼를 관리하기 쉽게 해 줍니다. 지나치게 바쁜 상대가 일으키는 DOS(Denial of Service, 서비스 거부) 공격을 막는 몫도 합니다.
정상 왕복 한 번
단항 호출 하나는 프레임 다섯으로 오갑니다.
sequenceDiagram
participant 클라이언트
participant 서버
클라이언트->>서버: HEADERS · :method POST · :path /서비스/메서드
클라이언트->>서버: DATA · END_STREAM · 길이 붙은 요청 메시지
서버->>클라이언트: HEADERS · :status 200
서버->>클라이언트: DATA · 길이 붙은 응답 메시지
서버->>클라이언트: HEADERS · END_STREAM · grpc-status
- 클라이언트가
HEADERS프레임으로 요청 헤더를 보냅니다. 메서드는 언제나POST입니다 - 요청 메시지가
DATA프레임에 실려 갑니다. 마지막DATA프레임에END_STREAM플래그가 서면 요청 쪽이 끝납니다. 보낼 데이터가 더 없을 때 스트림을 닫으려면 빈DATA프레임에 이 플래그만 세워 보냅니다 - 서버가
HEADERS프레임으로 응답 헤더를 보냅니다. HTTP 상태는:status 200입니다 - 응답 메시지가
DATA프레임에 실려 옵니다 - 서버가
HEADERS프레임을 한 번 더 보냅니다. 본문 뒤에 붙는 이 헤더를 트레일러라고 부릅니다. 트레일러에END_STREAM플래그가 서면 응답 쪽이 끝납니다
트레일러는 호출 결과인 grpc-status 를 싣습니다. 0 이 성공(OK)입니다. 결과가 OK 여도 상태는 반드시 트레일러에 실어 보냅니다.
사람이 읽을 설명을 grpc-message 에 덧붙일 수 있습니다. 실패의 자세한 내용은 grpc-status-details-bin 에 담습니다. 이 헤더는 상태가 OK 가 아닐 때만 씁니다.
응답 헤더의 200 은 HTTP 전송이 됐다는 뜻일 뿐입니다. gRPC 오류 응답도 HTTP 상태는 200 을 씁니다. 그래서 호출 결과는 언제나 grpc-status 로 가립니다.
호출이 시작하자마자 오류가 나면 응답 헤더와 메시지 없이 트레일러 하나로 끝낼 수 있습니다. 이것을 Trailers-Only 라고 부릅니다. :status 200 · content-type · 트레일러가 HEADERS 프레임 하나에 함께 실립니다.
요청 헤더
요청 헤더에서 gRPC 가 정해 둔 주요한 것은 아래와 같습니다. 콜론으로 시작하는 HTTP/2 예약 헤더가 맨 앞에 옵니다. 구현은 grpc-timeout 을 그 바로 뒤에 보내기를 권합니다. 호출 정의 헤더는 사용자가 붙이는 메타데이터보다 먼저 보내기를 권합니다.
| 헤더 | 값 | 하는 일 |
|---|---|---|
:method |
POST |
언제나 같습니다 |
:scheme |
http 또는 https |
|
:path |
/서비스 이름/메서드 이름 |
서버는 이 경로로 부를 메서드를 고릅니다. 대소문자를 가립니다 |
:authority |
가상 호스트 이름 | 넣지 않아도 됩니다 |
grpc-timeout |
숫자 + 단위 | 남은 기한입니다. 넣지 않아도 됩니다. 아래 실패 절에서 봅니다 |
te |
trailers |
호환되지 않는 프록시를 가려내는 데 씁니다 |
content-type |
application/grpc 로 시작 |
뒤에 +proto · +json 을 붙일 수 있습니다 |
grpc-encoding |
gzip 등 |
메시지 압축 방식입니다 |
grpc-accept-encoding |
압축 방식 목록 | 받을 수 있는 압축 방식입니다 |
user-agent |
grpc-java/1.2.3 형식 |
부른 라이브러리·버전·플랫폼을 적습니다. 문제를 진단하라고 권하는 값입니다 |
서비스 이름은 .proto 의 패키지 이름과 서비스 이름을 점으로 이은 것입니다. 패키지가 greet 이고 서비스가 HelloService 면 서비스 이름은 greet.HelloService 입니다.
메타데이터 값이 바이너리면 HTTP/2 헤더에 바로 못 싣습니다. 그래서 Base64 로 바꿔 싣습니다.
길이 붙은 메시지
DATA 프레임에는 메시지가 그냥 실리지 않습니다. 메시지마다 앞에 5바이트 접두부가 붙습니다. 이 접두부와 메시지를 묶어 Length-Prefixed-Message 라고 부릅니다.
block-beta columns 3 a["Compressed-Flag · 1바이트"] b["Message-Length · 4바이트"] c["Message · 길이만큼"]
- Compressed-Flag 는 1바이트입니다. 1 이면 뒤의 메시지를
grpc-encoding헤더가 밝힌 방식으로 압축했다는 뜻입니다. 0 이면 압축하지 않았습니다.grpc-encoding헤더가 없으면 반드시 0 입니다 - Message-Length 는 4바이트 부호 없는 정수입니다. 큰 자리 바이트를 먼저 적는 빅 엔디언 순서로 씁니다
- Message 는 직렬화된 메시지 바이트입니다
압축은 메시지마다 새로 시작합니다. 앞 메시지의 압축 상태를 다음 메시지로 넘기지 않습니다.
이어진 바이트에서 메시지 하나의 경계를 가려내는 일을 프레이밍이라고 부릅니다. gRPC 에서 그 경계를 정하는 것이 이 접두부입니다. DATA 프레임의 경계와 메시지의 경계는 서로 아무 관계가 없습니다. 구현은 둘이 맞아떨어진다고 가정하지 말아야 합니다.
네 가지 호출 방식
요청 쪽 규칙은 「요청 헤더 → 길이 붙은 메시지 0개 이상 → 스트림 끝」입니다. 응답 쪽도 메시지를 0개 이상 싣습니다. 그래서 한 스트림에 메시지를 여러 개 이어 보낼 수 있습니다. 이것이 스트리밍입니다.
한 호출 안에서 몇 개를 보내느냐에 따라 메서드가 넷으로 갈립니다. .proto 에서는 여럿을 보내는 쪽 타입 앞에 stream 을 붙입니다.
| 방식 | 클라이언트 → 서버 | 서버 → 클라이언트 | .proto 표기 |
|---|---|---|---|
| 단항 | 하나 | 하나 | rpc SayHello(HelloRequest) returns (HelloResponse); |
| 서버 스트리밍 | 하나 | 여럿 | rpc LotsOfReplies(HelloRequest) returns (stream HelloResponse); |
| 클라이언트 스트리밍 | 여럿 | 하나 | rpc LotsOfGreetings(stream HelloRequest) returns (HelloResponse); |
| 양방향 스트리밍 | 여럿 | 여럿 | rpc BidiHello(stream HelloRequest) returns (stream HelloResponse); |
단항 호출은 영어로 unary 라고 부릅니다. 보통의 함수 호출과 모양이 같습니다.
클라이언트가 스텁 메서드를 부르면 서버는 이 호출의 메타데이터와 메서드 이름을 받습니다. 데드라인이 걸려 있으면 그것도 함께 받습니다.
서버는 응답 메시지보다 먼저 응답 헤더를 보낼 수 있습니다. 이 응답 헤더가 서버 자신의 초기 메타데이터입니다. 클라이언트의 요청 메시지를 기다렸다가 보낼 수도 있습니다. 어느 쪽이 먼저인지는 애플리케이션이 정합니다.
나머지 셋은 누가 언제 끝내는지가 다릅니다.
- 서버 스트리밍 — 서버가 메시지를 다 보낸 뒤 상태와 트레일러를 보냅니다. 거기서 서버 쪽 처리가 끝납니다. 클라이언트는 서버의 메시지를 다 받으면 끝납니다
- 클라이언트 스트리밍 — 서버는 응답 메시지 하나를 상태와 함께 보냅니다. 대개 클라이언트의 메시지를 다 받은 뒤에 보냅니다. 꼭 그렇지는 않습니다
- 양방향 스트리밍 — 두 방향의 스트림이 서로 독립입니다. 클라이언트와 서버가 원하는 차례로 읽고 씁니다. 서버가 메시지를 다 받은 뒤 답할 수도 있습니다. 요청 하나에 응답 하나씩 번갈아 주고받을 수도 있습니다
한 호출 안에서 메시지 순서는 보장됩니다. 양방향에서는 방향마다 순서가 지켜집니다.
스트리밍은 큰 데이터 묶음을 흘려보내는 저장 시스템에 씁니다. 음성을 글자로 바꾸는 서비스나 주식 시세처럼 시간 순서로 이어지는 메시지에도 씁니다.
예시
Pub/Sub 의 CreateTopic 단항 호출
구글 Pub/Sub 의 CreateTopic 메서드를 부르는 단항 호출 하나가 프레임으로는 이렇게 오갑니다.
요청입니다.
HEADERS (flags = END_HEADERS)
:method = POST
:scheme = http
:path = /google.pubsub.v2.PublisherService/CreateTopic
:authority = pubsub.googleapis.com
grpc-timeout = 1S
content-type = application/grpc+proto
grpc-encoding = gzip
authorization = Bearer y235.wef315yfh138vh31hv93hv8h3v
DATA (flags = END_STREAM)
<Length-Prefixed Message>
응답입니다.
HEADERS (flags = END_HEADERS)
:status = 200
grpc-encoding = gzip
content-type = application/grpc+proto
DATA
<Length-Prefixed Message>
HEADERS (flags = END_STREAM, END_HEADERS)
grpc-status = 0 # OK
trace-proto-bin = jher831yy13JHy3hc
값마다 하는 일은 이렇습니다.
END_HEADERS는 헤더 블록이 이 프레임에서 끝난다는 플래그입니다:path의google.pubsub.v2가 패키지 이름,PublisherService가 서비스 이름,CreateTopic이 메서드 이름입니다grpc-timeout = 1S는 기한이 1초라는 뜻입니다.S가 초 단위입니다content-type = application/grpc+proto는 메시지를 프로토콜 버퍼로 직렬화했다는 뜻입니다grpc-encoding = gzip은 메시지 압축에 쓸 방식이 gzip 이라는 뜻입니다. 메시지를 실제로 압축했는지는 그 메시지 접두부의 Compressed-Flag 가 정합니다authorization은 gRPC 가 정한 헤더가 아닙니다. 애플리케이션이 붙인 메타데이터입니다- 요청의
DATA프레임에END_STREAM이 서 있습니다. 요청 메시지가 하나뿐이라 이 프레임으로 요청 쪽이 끝납니다 - 응답의 마지막
HEADERS가 트레일러입니다.grpc-status = 0이 성공입니다. 그 프레임에END_STREAM이 서 있어 응답 쪽도 끝납니다 trace-proto-bin은 이름이-bin으로 끝나는 바이너리 메타데이터입니다. 값은 Base64 로 바꿔 실었습니다
문자열 필드 하나를 담은 메시지의 바이트
이번에는 DATA 프레임 안의 Length-Prefixed Message 를 바이트로 엽니다. 대상은 문자열 필드 하나만 가진 메시지 Test2 입니다.
message Test2 {
string b = 2;
}
b 에 "testing" 을 넣으면 프로토콜 버퍼는 9바이트 12 07 74 65 73 74 69 6e 67 을 만듭니다. 이 메시지를 압축 없이 gRPC 로 보내면 앞에 5바이트 접두부가 붙어 14바이트가 스트림에 실립니다.
00 Compressed-Flag · 압축 안 함
00 00 00 09 Message-Length · 뒤에 9바이트
12 태그 · 필드 번호 2 · 와이어 타입 LEN
07 값 길이 7
74 65 73 74 69 6e 67 "testing" 의 UTF-8 바이트
맨 앞 두 줄은 gRPC 의 접두부입니다. Compressed-Flag 는 0 입니다. 길이 9 는 빅 엔디언 4바이트로 적었습니다.
12 는 태그입니다. 태그는 필드 번호와 와이어 타입을 (field_number << 3) | wire_type 으로 합친 값입니다. 와이어 타입은 값을 어떤 형식으로 적었는지 알리는 번호입니다.
문자열은 길이가 뒤따르는 LEN 형식입니다. 그 번호는 2 입니다. 그래서 필드 번호 2 를 세 비트 밀고 2 를 더한 00010 010, 16진수로 12 가 됩니다.
바이트 어디에도 필드 이름 b 는 없습니다. 필드 번호 2 만 들어 있습니다. 앞의 .proto 파일에서 greeting = 1 의 1 이 맡는 일이 이것입니다.
실패
클라이언트가 시작한 gRPC 호출은 전부 상태 하나를 돌려받습니다. 어떤 조건에서 어떤 상태가 오는지를 봅니다. 대상은 트레일러에 실리는 grpc-status 값과, 라이브러리가 그 값을 스스로 만드는 조건입니다.
상태는 정수 코드와 문자열 메시지로 이루어집니다. 서버는 어느 상태를 돌려줄지 고를 수 있습니다.
상태 코드
정의된 코드는 17개입니다. 애플리케이션은 이 값만 써야 합니다. 범위 밖의 값을 받은 라이브러리는 그 값을 그대로 넘기거나 UNKNOWN 으로 바꿉니다.
| 이름 | 숫자 | 뜻 |
|---|---|---|
OK |
0 | 오류가 아닙니다. 성공했을 때 돌아옵니다 |
CANCELLED |
1 | 호출이 취소됐습니다. 대개 부른 쪽이 취소합니다 |
UNKNOWN |
2 | 알 수 없는 오류입니다. 오류 정보를 충분히 안 주는 API(Application Programming Interface)가 낸 오류도 이 값으로 바뀔 수 있습니다 |
INVALID_ARGUMENT |
3 | 클라이언트가 시스템 상태와 상관없이 잘못된 인자를 줬습니다. 형식이 틀린 파일 이름 같은 경우입니다 |
DEADLINE_EXCEEDED |
4 | 연산이 끝나기 전에 기한이 지났습니다. 시스템 상태를 바꾸는 연산이면 연산이 성공적으로 끝났는데도 이 오류가 돌아올 수 있습니다 |
NOT_FOUND |
5 | 요청한 대상이 없습니다 |
ALREADY_EXISTS |
6 | 만들려는 대상이 이미 있습니다 |
PERMISSION_DENIED |
7 | 부른 쪽에 권한이 없습니다. 자원이 바닥나서 거절할 때나 부른 쪽을 식별하지 못할 때는 쓰지 않습니다 |
RESOURCE_EXHAUSTED |
8 | 자원이 바닥났습니다. 사용자별 할당량일 수도 있고 파일 시스템 전체의 공간일 수도 있습니다 |
FAILED_PRECONDITION |
9 | 시스템이 연산을 실행할 상태가 아니라 거절했습니다. 비어 있지 않은 디렉터리를 지우려 할 때가 그렇습니다 |
ABORTED |
10 | 연산이 중단됐습니다. 대개 트랜잭션 중단 같은 동시성 문제 때문입니다 |
OUT_OF_RANGE |
11 | 유효 범위를 넘어서 연산하려 했습니다. 파일 끝을 넘어 읽으려 할 때가 그렇습니다 |
UNIMPLEMENTED |
12 | 이 서비스에서 구현하지 않았거나 지원·활성화하지 않은 연산입니다 |
INTERNAL |
13 | 내부 오류입니다. 하부 시스템이 지킬 것으로 기대한 불변식이 깨졌습니다. 심각한 오류에만 씁니다 |
UNAVAILABLE |
14 | 서비스가 지금 쓸 수 없습니다. 대개 일시적이라 간격을 두고 다시 시도하면 풀릴 가능성이 큽니다. 멱등이 아닌 연산, 곧 여러 번 하면 결과가 달라지는 연산은 다시 시도하는 것이 늘 안전하지는 않습니다 |
DATA_LOSS |
15 | 되돌릴 수 없는 데이터 손실이나 손상입니다 |
UNAUTHENTICATED |
16 | 요청에 이 연산에 맞는 유효한 인증 정보가 없습니다 |
트레일러의 grpc-status 값은 이 숫자를 10진 문자열로 적은 것입니다. 앞에 0 을 붙이지 않습니다.
라이브러리가 스스로 만드는 상태
오류가 나면 gRPC 클라이언트·서버 구현이 상태를 스스로 만들어 돌려주기도 합니다. 라이브러리가 만드는 코드는 정의된 코드 중 일부뿐입니다.
| 조건 | 코드 | 만드는 쪽 |
|---|---|---|
| 클라이언트 애플리케이션이 요청을 취소했다 | CANCELLED |
둘 다 |
| 서버가 상태를 돌려주기 전에 기한이 지났다 | DEADLINE_EXCEEDED |
둘 다 |
| 기한 안에 응답을 못 받았다. 클라이언트가 요청을 못 보냈을 수도 있고 서버가 제때 답을 못 했을 수도 있다 | DEADLINE_EXCEEDED |
둘 다 |
| 서버에 그 메서드가 없다 | UNIMPLEMENTED |
서버 |
| 서버가 종료하는 중이다 | UNAVAILABLE |
서버 |
| 서버 애플리케이션이 예외를 던졌다. 또는 상태 코드를 돌려주는 것 말고 다른 방법으로 호출을 끝냈다 | UNKNOWN |
서버 |
| 요청 메타데이터 같은 데이터를 일부 보낸 뒤 연결이 끊겼다 | UNAVAILABLE |
클라이언트 |
| 압축 방식은 지원하는데 압축을 못 풀었다 (클라이언트 → 서버) | INTERNAL |
서버 |
| 압축 방식은 지원하는데 압축을 못 풀었다 (서버 → 클라이언트) | INTERNAL |
클라이언트 |
| 클라이언트가 쓴 압축 방식을 서버가 지원하지 않는다 | UNIMPLEMENTED |
서버 |
| 흐름 제어 한도에 닿는 식으로 서버 자원이 잠시 모자라다 | RESOURCE_EXHAUSTED |
서버 |
| 서버 응답을 담을 메모리가 클라이언트에 모자라다 | RESOURCE_EXHAUSTED |
클라이언트 |
| 흐름 제어 규칙을 어겼다 | INTERNAL |
둘 다 |
| 돌아온 상태를 해석하지 못했다 | UNKNOWN |
클라이언트 |
인증 메타데이터가 틀렸다. 자격 증명이 메타데이터를 못 얻었거나 채널과 호출의 자격 증명이 서로 안 맞거나 :authority 의 호스트가 틀린 경우입니다 |
UNAUTHENTICATED |
둘 다 |
| 요청이 꼭 하나여야 하는 메서드에 클라이언트가 다른 개수를 보냈다 | UNIMPLEMENTED |
서버 |
| 응답이 꼭 하나여야 하는 메서드에 서버가 다른 개수를 보냈다 | UNIMPLEMENTED |
클라이언트 |
| 응답 메시지를 해석하지 못했다 | INTERNAL |
클라이언트 |
| 요청 메시지를 해석하지 못했다 | INTERNAL |
서버 |
| 보내거나 받은 메시지가 설정한 크기 한도보다 크다 | RESOURCE_EXHAUSTED |
둘 다 |
| keepalive 감시가 시간을 넘겼다 | UNAVAILABLE |
둘 다 |
keepalive 는 연결이 살아 있는지 주기적으로 지켜보는 장치를 가리킵니다. 그 감시가 무엇으로 도는지는 자료에 없습니다(미확인).
아래 일곱은 라이브러리가 절대 만들지 않습니다. 이 코드를 받았다면 애플리케이션이 돌려준 것입니다.
INVALID_ARGUMENT · NOT_FOUND · ALREADY_EXISTS · FAILED_PRECONDITION · ABORTED · OUT_OF_RANGE · DATA_LOSS
반대 방향은 성립하지 않습니다. 라이브러리가 만드는 코드를 서버 애플리케이션이 돌려줄 수도 있습니다.
재시도와 멱등
gRPC 는 따로 정하지 않는 한 호출이 멱등이라고 가정하지 않습니다. 멱등은 여러 번 해도 한 번 한 것과 결과가 같다는 뜻입니다. 멱등이 아닌 호출은 두 번 보내면 결과가 달라질 수 있습니다. 그래서 재시도 규칙이 셋입니다.
- 시작됐다고 증명할 수 없는 호출은 다시 보내지 않습니다
- 중복을 걸러 내는 장치는 없습니다. gRPC 는 그 장치가 필요 없다고 봅니다
- 멱등으로 표시한 호출은 여러 번 보낼 수 있습니다
어느 코드에서 다시 보낼지 정해 둔 목록은 없습니다. 라이브러리는 서로 다른 조건에서 같은 코드를 만듭니다. 서버 애플리케이션도 같은 코드를 돌려줄 수 있습니다. 그래서 어느 코드에서 재시도할지는 애플리케이션마다 정해야 합니다.
서버를 짜는 쪽에는 셋을 가르는 지침이 있습니다.
| 코드 | 언제 쓰나 |
|---|---|
UNAVAILABLE |
클라이언트가 실패한 호출 하나만 다시 보내면 될 때 |
ABORTED |
클라이언트가 더 높은 단계에서 다시 해야 할 때. 읽고 고쳐 쓰는 과정을 처음부터 다시 해야 하는 경우입니다 |
FAILED_PRECONDITION |
시스템 상태를 직접 고치기 전에는 다시 보내면 안 될 때 |
데드라인과 기한 초과
gRPC 는 기본으로 데드라인을 걸지 않습니다. 그래서 클라이언트가 응답을 사실상 영원히 기다릴 수 있습니다. 이를 피하려면 클라이언트에서 현실적인 데드라인을 늘 직접 걸어야 합니다.
데드라인은 요청 헤더 grpc-timeout 에 남은 시간으로 실려 갑니다. 값은 8자리 이하의 양의 정수와 단위 한 글자입니다. 단위는 H(시) · M(분) · S(초) · m(밀리초) · u(마이크로초) · n(나노초)입니다. 이 헤더가 없으면 서버는 기한이 무한하다고 봐야 합니다.
기한이 지나면 양쪽이 각자 호출을 끝냅니다.
- 클라이언트 — 서버가 기한을 넘기면 포기하고 호출을
DEADLINE_EXCEEDED로 끝냅니다 - 서버 — 클라이언트가 건 기한이 지나면 호출을 자동으로 취소합니다. 상태는
CANCELLED입니다. 그 호출 때문에 띄운 작업을 멈추는 것은 서버 애플리케이션의 몫입니다. 오래 도는 처리라면 호출이 취소됐는지 주기적으로 확인해야 합니다. 취소됐으면 처리를 멈춥니다
서버가 응답을 만들려고 다른 서버를 부를 때도 있습니다. 이때 서버는 클라이언트 노릇도 하므로 처음 클라이언트가 건 기한을 지키는 편이 맞습니다. 들어온 호출의 기한을 나가는 호출로 자동으로 넘기는 기능을 데드라인 전파라고 부릅니다.
sequenceDiagram
participant 클라이언트
participant 서버A
participant 서버B
클라이언트->>서버A: 호출 · grpc-timeout = 기한
Note over 서버A: 처리하는 동안 기한이 줄어든다
서버A->>서버B: 호출 · grpc-timeout = 남은 시간
서버B-->>서버A: 응답
서버A-->>클라이언트: 응답
- 일부 gRPC 구현만 지원합니다. C++ 는 직접 켜야 합니다. 자바와 고는 기본으로 켜져 있습니다
- 기한을 시각 그대로 넘기면 두 서버의 시계가 어긋나 있을 때 문제가 됩니다. 그래서 gRPC 는 이미 흐른 시간을 뺀 남은 시간으로 바꿔 넘깁니다. 시계 차이의 영향을 받지 않습니다
호출의 성공은 클라이언트와 서버가 따로 판정합니다. 둘의 결론이 다를 수 있습니다.
서버는 응답을 다 보냈으니 성공이라고 봅니다. 그런데 클라이언트는 응답이 기한 뒤에 왔으니 실패라고 볼 수 있습니다. 클라이언트가 요청을 다 보내기 전에 서버가 호출을 끝낼 수도 있습니다.
끊긴 스트림과 연결
메시지 프레이밍이 깨지면 gRPC 런타임은 RST_STREAM 프레임으로 상대에게 알릴 수 있습니다. 받은 쪽은 스트림이 그 즉시 완전히 닫혔다고 봐야 합니다. 그리고 오류를 애플리케이션까지 올려야 합니다. HTTP/2 의 오류 코드는 이렇게 gRPC 코드로 옮겨집니다.
HTTP/2 RST_STREAM 코드 |
gRPC 코드 |
|---|---|
NO_ERROR(0) |
INTERNAL. RST_STREAM 을 보낸 쪽이 OK 상태를 명시해 보냈어야 한다는 뜻입니다 |
PROTOCOL_ERROR(1) |
INTERNAL |
REFUSED_STREAM |
UNAVAILABLE. 아무 처리도 일어나지 않았다는 뜻이라 다시 보낼 수 있습니다. 다른 곳으로 보낼 수도 있습니다 |
CANCEL(8) |
클라이언트가 보내면 호출 취소, 서버가 보내면 CANCELLED |
ENHANCE_YOUR_CALM |
RESOURCE_EXHAUSTED. 바닥난 자원이 대역폭이라는 설명이 붙습니다 |
INADEQUATE_SECURITY |
PERMISSION_DENIED. 프로토콜의 보안 수준이 모자라 거절했다는 설명이 붙습니다 |
연결 단위의 사건도 상태 코드로 끝납니다. 두 프레임이 그 사건을 나릅니다.
GOAWAY 는 서버가 그 연결에서 새 스트림을 더 받지 않겠다고 알리는 프레임입니다. 마지막으로 받아들인 스트림 번호를 함께 싣습니다. 서버는 연결을 끊기 전에 이 프레임을 보내야 합니다. 그래야 클라이언트가 어느 호출이 서버에서 받아들여져 실행 중인지 알 수 있습니다.
PING 은 받은 쪽이 받은 내용을 그대로 되돌려 보내야 하는 프레임입니다. 연결이 살아 있는지 확인할 때 씁니다. 왕복 지연을 어림할 때도 씁니다.
| 사건 | 클라이언트 쪽 호출 | 서버 쪽 호출 |
|---|---|---|
서버가 GOAWAY 프레임을 보냈다 |
마지막으로 받아들여진 스트림 뒤에 시작한 호출은 UNAVAILABLE 로 보고 다른 곳으로 다시 보냅니다. 이미 받아들여진 호출은 계속합니다 |
|
서버가 보낸 PING 에 기한 안에 답이 없다 |
진행 중인 호출 전부가 CANCELLED |
|
클라이언트가 보낸 PING 이 기한을 넘겼다 |
호출 전부가 UNAVAILABLE |
|
| 연결이 끊긴 것을 알아챘다 | 호출 전부가 UNAVAILABLE |
열린 호출이 CANCELLED |
gRPC 가 아닌 상대
content-type 이 application/grpc 로 시작하지 않으면 gRPC 서버는 HTTP 상태 415(Unsupported Media Type)로 답하기를 권합니다(SHOULD). gRPC 오류 응답은 HTTP 상태 200 을 쓰기 때문입니다. 415 로 답하면 다른 HTTP/2 클라이언트가 gRPC 오류 응답을 성공으로 잘못 읽지 않습니다.
배포 구성이 망가져 있으면 응답에 200 이 아닌 HTTP 상태나 gRPC 가 아닌 content-type 이 올 수 있습니다. grpc-status 와 grpc-message 가 아예 빠질 수도 있습니다. 이때 구현은 상태와 메시지를 스스로 만들어 애플리케이션에 넘겨야 합니다.
대가
gRPC 를 고르면 내주는 것 셋을 봅니다. 부하 분산 방식의 맞바꿈, 브라우저가 바로 못 부르는 것, 사람이 메시지를 바로 못 읽는 것입니다.
부하 분산 방식의 맞바꿈
큰 규모의 gRPC 배포는 똑같은 백엔드 인스턴스 여럿과 클라이언트 여럿으로 이루어집니다. 서버마다 처리할 수 있는 양이 정해져 있습니다. 부하 분산은 클라이언트가 보내는 부하를 쓸 수 있는 서버들에 고르게 나누는 일입니다.
부하 분산을 프록시에 맡길지 클라이언트에 맡길지가 먼저 갈리는 설계 선택입니다. 어느 쪽을 골라도 내주는 것이 있습니다.
프록시 분산에서는 클라이언트가 분산 프록시로 호출을 보냅니다. 프록시는 백엔드 하나를 고릅니다. 그 백엔드로 호출을 넘깁니다.
클라이언트 분산에서는 클라이언트가 백엔드 여럿을 압니다. 호출마다 그중 하나를 고릅니다.
| 방식 | 얻는 것 | 내주는 것 |
|---|---|---|
| 프록시 분산 | 클라이언트가 단순합니다. 백엔드를 몰라도 됩니다. 믿을 수 없는 클라이언트와도 씁니다 | 프록시가 데이터 경로에 끼어 지연이 늘어납니다. 프록시 처리량이 확장의 한도가 될 수 있습니다 |
| 클라이언트 분산 | 거치는 단계가 하나 줄어 성능이 오릅니다 | 클라이언트가 복잡해집니다. 서버의 부하와 상태를 클라이언트가 추적하고 분산 알고리즘도 직접 구현합니다. 언어마다 구현하고 유지해야 합니다. 클라이언트를 믿을 수 있어야 합니다 |
프록시 분산은 다시 두 계층으로 갈립니다. 계층부터 짚습니다. HTTP/2 는 애플리케이션 계층(Application Layer, L7) 프로토콜입니다. 그 아래에서 전송 계층(4계층) 프로토콜인 TCP(Transmission Control Protocol)가 돕니다. TCP 는 네트워크 계층(3계층) 프로토콜인 IP(Internet Protocol) 위에서 돕니다.
| 방식 | 하는 일 | 내주는 것 |
|---|---|---|
| L3/L4 분산 (Network Layer · Transport Layer) | 클라이언트의 TCP 연결을 프록시에서 끝냅니다. 고른 백엔드로는 새 연결을 엽니다. HTTP/2·gRPC 프레임은 두 연결 사이에서 복사만 합니다 | 요청 내용을 보고 백엔드를 고르는 기능을 못 씁니다. 프록시 자원을 아끼는 것이 기능보다 중요할 때 권하는 쪽입니다 |
| L7 분산 | HTTP/2 를 받아 풀어 봅니다. 요청마다 내용을 보고 백엔드를 고릅니다. 한 클라이언트의 스트림을 여러 백엔드로 나눌 수 있습니다 | L3/L4 보다 지연이 더 붙고 자원을 더 씁니다 |
어느 쪽을 쓸지는 이렇게 권합니다.
| 상황 | 권하는 쪽 |
|---|---|
| 연결마다 실린 호출 부하가 크게 갈린다 | L7 분산 |
| 저장소나 계산이 특정 백엔드에 붙어야 한다 | L7 분산. 쿠키 같은 값으로 요청을 맞는 백엔드로 보냅니다 |
| 기능보다 프록시 자원을 아끼는 것이 중요하다 | L3/L4 분산 |
| 지연이 가장 중요하다 | L3/L4 분산 |
클라이언트 분산을 고르면서 클라이언트를 믿을 수 없으면 룩어사이드 방식을 씁니다. 분산 판단을 전용 서버 하나에 모읍니다. 클라이언트는 그 서버에 물어 쓸 백엔드를 받습니다. gRPC 는 클라이언트와 이 서버가 주고받는 프로토콜을 따로 정해 두었습니다.
브라우저용 별도 규격과 중계 프록시
2019년 1월 기준으로 브라우저에서는 HTTP/2 위의 gRPC 규격을 구현할 수 없었습니다. 요청을 그만큼 세밀하게 다루는 브라우저 API 가 없기 때문입니다. HTTP/2 를 쓰라고 강제할 방법이 없습니다. HTTP/2 를 쓰더라도 브라우저가 HTTP/2 프레임을 직접 다룰 수 없습니다.
그때까지 프런트엔드 개발자는 JSON 을 주고받는 REST(Representational State Transfer) 인터페이스에 기대 왔습니다.
그래서 gRPC-Web 이라는 별도 규격이 있습니다. HTTP/2 위의 gRPC 규격에서 출발해 다른 점만 정합니다.
- HTTP/1.1 과 HTTP/2 를 둘 다 받습니다
- gRPC 트레일러를 요청·응답 본문의 맨 끝에 싣습니다. 메시지 접두부의 새 비트로 트레일러라는 것을 표시합니다
- gRPC-Web 요청과 HTTP/2 위의 gRPC 응답을 서로 옮기는 프록시가 반드시 있어야 합니다
브라우저는 Fetch 같은 보통의 HTTP 요청을 보냅니다. gRPC 서버 앞에 둔 작은 프록시가 요청과 응답을 브라우저가 쓸 수 있는 형태로 옮깁니다. 대가는 배포 구성에 프록시가 하나 더 붙는다는 것입니다.
호출 방식도 줄어듭니다. HTTP/2 위의 gRPC 구현은 네 방식을 다 지원합니다. gRPC-Web 규격은 클라이언트 스트리밍과 양방향 스트리밍 지원을 요구하지 않습니다. 브라우저에 스트림 기능이 들어오면 구현한다고만 정해 두었습니다.
바이너리 메시지와 서버 리플렉션
gRPC 가 기본으로 쓰는 프로토콜 버퍼 직렬화는 사람이 읽을 수 없는 바이너리 형식입니다. 이 덕분에 호출이 크게 빨라집니다. 대신 사람이 서버와 손으로 주고받기는 어려워집니다.
curl 로 gRPC 요청을 손으로 보낸다고 해 봅시다. 먼저 셋을 알아야 합니다.
- 서버가 내놓은 RPC 서비스
- 요청 메시지의 프로토콜 버퍼 정의와 그 메시지가 가리키는 모든 타입
- 응답 메시지의 프로토콜 버퍼 정의와 그 메시지가 가리키는 모든 타입
그다음 요청 메시지를 손으로 바이너리로 짭니다. 돌아온 응답 메시지도 손으로 풀어야 합니다. 시간이 들고 틀리기 쉽습니다.
이 일을 대신하는 것이 서버 리플렉션입니다. gRPC 서버가 자기가 내놓은 API 정의를 표준 RPC 서비스로 알려 주는 프로토콜입니다. 요청·응답 메시지가 가리키는 타입까지 다 알려 줍니다.
클라이언트는 이 정보로 요청을 짭니다. 응답도 이 정보로 사람이 읽을 수 있게 풉니다. grpcurl 이나 Postman 같은 디버깅 도구가 이것을 씁니다. REST 쪽에 견주면 HTTP 서버가 OpenAPI 문서를 내주는 것과 비슷합니다.
리플렉션에도 대가가 있습니다.
- gRPC 서버에서 자동으로 켜지지 않습니다. 따로 켜야 합니다
- 외부 사용자가 쓰는 API 라면 리플렉션 서비스를 열어 두는 것이 보안 문제일 수 있습니다. 보안과 쓰기 편함 사이에서 직접 정해야 합니다
관련 항목
gRPC 가 한 갈래로 속하는 통신 방식
RPC · API 설계 · 프로토콜 · 마이크로서비스 · 서비스 간 통신 · 분산 시스템
gRPC 와 같은 역할을 두고 겨루는 API 방식
REST · GraphQL · SOAP · Thrift · JSON-RPC · WebSocket · OpenAPI
gRPC 가 비롯된 배경
Stubby · SPDY · QUIC · 오픈소스
gRPC 호출을 이루는 구성 요소
Protocol Buffers · IDL · protoc · 스텁 · 코드 생성 · 채널 · 메시지 · 필드 번호 · 메타데이터 · 데드라인 · 취소 · 인터셉터
gRPC 가 올라타는 전송 프로토콜
HTTP/2 · 스트림 · 프레임 · HEADERS 프레임 · DATA 프레임 · 트레일러 · 멀티플렉싱 · 흐름 제어 · TCP · TLS · HTTP
gRPC 메시지를 바이트로 바꾸는 직렬화 방식
직렬화 · 역직렬화 · 이진 인코딩 · JSON · 빅 엔디언 · varint · 와이어 타입 · 스키마 · 하위 호환 · gzip · Base64
gRPC 의 네 가지 호출 방식
단항 RPC · 서버 스트리밍 · 클라이언트 스트리밍 · 양방향 스트리밍 · 스트리밍
gRPC 호출이 끝날 때 돌려주는 상태 코드
상태 코드 · DEADLINE_EXCEEDED · UNAVAILABLE · CANCELLED · UNIMPLEMENTED · RESOURCE_EXHAUSTED · INTERNAL · UNKNOWN · UNAUTHENTICATED · PERMISSION_DENIED
gRPC 호출을 끊는 HTTP/2 제어 프레임
RST_STREAM · GOAWAY · PING 프레임 · keepalive · 그레이스풀 셧다운
gRPC 호출이 겪는 오류와 그 대비책
타임아웃 · 재시도 · 멱등성 · 백오프 · 취소 전파 · 데드라인 전파 · 클럭 스큐 · 서킷 브레이커 · 요청 헤징 · 헬스 체크
gRPC 트래픽을 나누고 중계하는 장치
로드 밸런서 · 부하 분산 · L4 로드 밸런싱 · L7 로드 밸런싱 · 클라이언트 측 로드 밸런싱 · 룩어사이드 로드 밸런싱 · 프록시 · 서비스 메시 · Envoy · gRPC-Web · gRPC-Gateway
gRPC 서버를 두드리고 재는 도구
grpcurl · Postman · 서버 리플렉션 · ghz · Gatling · k6 · 부하 테스트 · 벤치마크
다른 이름: gRPC Remote Procedure Calls · 지알피씨