카테고리 없음

HTTP2, Protocol Buffer, gRPC

ㅈ현 2022. 9. 12. 21:16

이번 포스팅에서는 gRPC와 그와 관련된 HTTP2, Protocol Buffer 프로토콜 그리고 파이썬에서 gRPC의 사용에 대해 다룬다.

 

1. Terminology

gRPC는 google에서 개발한 RPC 프로토콜로써, 구글의 직렬화 기술인 프로토콜 버퍼를 사용하며 HTTP2 프로토콜 위에서 동작한다.

이를 위해 우선 프로토콜 버퍼와, http2에 대해 대략적으로 설명한다.

 

1.1 프로토콜 버퍼 

 

Protocol Buffers  |  Google Developers

Protocol buffers are a language-neutral, platform-neutral extensible mechanism for serializing structured data.

developers.google.com

프로토콜 버퍼는 구글에서 개발한 직렬화 기술로써 구조화된 데이터를 직렬화하여 더욱 작은 자원으로 데이터 전송을 가능하게 한다.

 

프로토콜 버퍼는 .proto 파일에 기술된 IDL을 기반으로 동작하게 되며, 이 IDL(.proto 파일)에는 호출할 프로시저, request, reply 메시지등이 기술되게 된다.

 

proto파일 예제

syntax = "proto3";

service GRPCExample {
    rpc HelloWorld (RequestMsg) returns (ReplyMsg) {}
}

message RequestMsg {
    optional string field1 = 1;
    int32 field2 = 2;
    double field3 = 3;
    repeated string field4 = 4;
}

message ReplyMsg {
    optional string helloworld = 1;
}

syntax : 버전을 명시한다.

service : 프로시저들을 사용할 객체를 명세한다. 

message : 주고받을 데이터를 명세한다.

각 필드에는 id값을 부여한다. (1,2,3,4) 이 id값은 1~15 까지는 1바이트로 인코딩, 16~2047까지는 2바이트로 인코딩 된다.

 

Language Guide (proto3)  |  Protocol Buffers  |  Google Developers

Language Guide (proto3) Notifications Save this page to your Developer Profile to get notifications on important updates. Stay organized with collections Save and categorize content based on your preferences. This guide describes how to use the protocol bu

developers.google.com

  optional : 해당 필드는 옵셔널(없어도 됨)임.

  repeated : 배열값을 전달한다.

 

 

1.2 RPC

RPC 에서 클라이언트와 서버는 각각 다른 주소공간을 사용하게 된다. 

이때 IDL을 통해 만들어진 Stub는 클라이언트와 서버 주소공간에 존재한다.

 

클라이언트는 Stub로 데이터를 보내게 되고 클라이언트 Stub는 서버로 데이터를 보내기 위해 마샬링(바이트 스트림으로 변환) 하게 되고 서버 Stub는 받은 바이트 스트림을 언마샬링(바이트 스트림을 객체로 변환) 하게 된다. (reply도 마찬가지)

 

Stub는 구현의 투명성(Transparency)을 제공해주게 된다. 즉 사용자는 Stub까지 데이터를 보내기만 하면 되며, 바이트 스트림으로 변환되어 전달되는 과정은 신경 쓰지 않아도 된다.

 

1.3 HTTP2

기존 http 프로토콜의 경우 비 연결성을 지향하다 보니, 데이터를 받아오고자 할때마다 커넥션 과정 (handshake)가 필요하게 되었다.

이 과정은 매우 큰 오버로드를 발생시키며 이 과정을 줄이고자 나온것이 http1.1의 keep-alive다.

 

즉 10개의 데이터를 가져오기 위해 10번의 악수를 하고, 10번의 요청을 하던것을 한번의 악수를 하고 10번의 요청을 하는것으로 개선된것이다.

 

하지만 이러한 keep-alive에도 단점이 있었다.

http프로토콜은 Sequential 하게 데이터를 받아오기 때문에 미리 요청한 데이터가 오지 않았다면 다음 데이터의 요청을 할 수 없던 문제가 있었다.

http HOL 블록킹

즉 첫번째 요청의 처리에 약 10초가 걸리고, 두번째 세번째 요청은 처리에 약 1초씩 걸리는 요청이 있다고 하자.

두번째, 세번째 요청은 빠르게 처리해줄 수 있지만 첫번째 요청때문에 처리가 block되어 같이 block되는 문제가 있었다. (HOL 블록킹)

 

 

이러한 문제를 해결한것이 http2 프로토콜이다.

 

여기선 http2의 다양한 특성중 위의 문제를 해결한 multiflexing과, binary frame에 대해 설명한다.

 

1.3.1 multiflexing

http2에서는 멀티플랙싱을 통해 커넥션 내에서 순서와 상관없이 데이터 전송이 가능하다.

즉 위의 그림처럼 첫번째 요청이 오래걸려 두번째 세번째 요청이 같이 블락되는 경우였지만, http2 에서는 멀티 플랙싱이 가능해 다음과 같은 그림으로 데이터 전송이 가능해진다.

http2 multiflexing

즉 두번째 세번째 요청이 오래 걸리는 첫번째 요청과 상관없이 reply 되게된다.

 

 

1.3.2 binary frame

기존 http의 경우 텍스트 기반 header-value로 데이터 전송이 되기 때문에 상당히 많은 자원이 필요로 한다.

http2에서는 이 데이터를 binary 인코딩을 통해 전송하기 때문에 훨씬 적은 자원으로 데이터 전송이 가능해지게 된다.

 

 

2. gRPC

이제 위의 기술들에 대해 대략적으로 설명이 되었으니, gRPC에 대해 알아보자.

 

위에서 설명한 기술들을 사용하는 gRPC는 http2를 사용하기 때문에, 더욱 적은 자원으로 데이터 전송이 가능한 특징을 가지고 있다.

또한 http2의 특성중 하나인 양방향 통신이 가능하다는 특징이 있다.

하지만, 아직까지의 대부분 웹서버에서는 gRPC 프로토콜에 대해 지원하지 않으며, http2를 지원하지 않는 프레임 워크또한 아직 간간히 있다.

 

하지만 기존의 IPC 방법인 REST에 비해 이진화를 통한 빠른 전송이라는 장점을 가지고 있어 많이 사용된다. (MSA)

 

2.1 파이썬에서의 gRPC

여기선 파이썬 gRPC서버, 클라이언트를 구성하는 방법에대해 다룬다.

 

gRPC는 거의 대부분 플랫폼(시스템, 언어등,...)에서 지원된다. 또한 구글에서 지원하는 많은 공식 자료등을 통해 쉽게 접근이 가능하다.

 

위에서 말했듯 gRPC는 프로토콜 버퍼를 사용한다.

즉 클라이언트와 서버의 통신에 사용될 IDL을 정의하고, 각각 시스템의 Stub를 빌드해야 한다.

 

아래는 통신에 사용될 IDL이다.

 

hello.proto

syntax = "proto3";

service GRPCExample {
    rpc HelloWorld (RequestMsg) returns (ReplyMsg) {}
}

message RequestMsg {
    optional string field1 = 1;
    int32 field2 = 2;
    double field3 = 3;
    repeated string field4 = 4;
}

message ReplyMsg {
    optional string helloworld = 1;
}

위에서 사용한 예제와 같은 예제다.

 

 

이제 파이썬에서 해당 IDL(proto파일)을 빌드해야한다.

해당 빌드를 위해 우선 필요한 패키지들을 받아준다.

pip install grpcio
pip install grpc_tools

 

 

이제 위의 proto파일을 빌드한다.

python3 -m grpc_tools.protoc -I . --python_out=. --grpc_python_out=. ./hello.proto

해당 hello.proto 파일을 빌드하게 되면 hello_pb2.py, hello_pb2_grpc.py라는 두개의 파일이 생성된다.

 

 

서버 코드 작성

server.py

import grpc
import hello_pb2
import hello_pb2_grpc
from concurrent.futures import ThreadPoolExecutor


class Hello(hello_pb2_grpc.GRPCExampleServicer):
    def HelloWorld(self, request, context):
        print(request)
        return hello_pb2.ReplyMsg(helloworld="hello world!")

def serve():
    server = grpc.server(ThreadPoolExecutor(5))
    hello_pb2_grpc.add_GRPCExampleServicer_to_server(Hello(), server)

    server.add_insecure_port('[::]:50000')
    server.start()
    server.wait_for_termination()


if __name__ == "__main__":
    serve()

 

 

import grpc, hello_pb2, hello_pb2_grpc

grpc 모듈과 위 proto를 통해 파이썬으로 빌드된 protocol buffer 산출물을 import한다.

 

Hello 클래스

이 객체는 클라이언트가 호출할 RPC 함수들을 가지고 있는 객체다. 

proto 파일에서 선언한 HelloWorld라는 프로시저를 가지고 있다. 

 

hello_pb2_grpc.GRPCExampleServicer를 상속한다

  proto 파일에는 GRPCExample 라는 이름의 서비스를 선언함

 

grpc.server()

ThreadPoolExecutor를 파라미터로 받아 grpc 서버를 생성한다.

 

add_GRPCExampleServicer_to_server

위에서 만든 Hello 인스턴스와, 캡슐화된 server를 받아 프로시저를 서버에 등록한다.

 

client.py

import grpc
import hello_pb2
import hello_pb2_grpc


def run():
    with grpc.insecure_channel('localhost:50000') as channel:
        stub = hello_pb2_grpc.GRPCExampleStub(channel)

        reply = stub.HelloWorld(hello_pb2.RequestMsg(field1='12', field2=3, field3=1.23, field4=['asdf', 'asd2f']))
        print(reply)

if __name__ == "__main__":
	run()

 

import grpc, hello_pb2, hello_pb2_grpc

마찬가지로 grpc와 관련된 모듈들을 import 한다.

 

grpc.insecure_channel('url')

캡슐화된 grpc 채널 객체를 리턴한다.

 

hello_pb2_grpc.GRPCExampleStub(channel)

위에서 만든 채널을 파라미터로 캡슐화된 클라이언트 stub를 리턴받는다.

 

stub.HelloWorld(hello_pb2.RequestMsg(msg))

 

IDL(.proto) 에 등록한 프로시저를 사용하기 위해 해당 프로시저의 request 파라미터인 RequestMsg에 필드를 채워 요청을 날린다.

(blocking으로 동작하며 response는 reply객체가 온다. )

 

결과

server.py 결과 : 클라이언트로부터 온 request

 

 

client.py 결과 : 서버로부터 온 reply

 

서버는 클라이언트의 request를 출력하고, 클라이언트는 서버로부터 받은 reply를 출력하게 된다.

각 request, reply의 원소는 .{fieldname} 로 접근 가능하다.

ex) request.field1, reply.helloworld

 

 

결론

gRPC는 구글에서 개발한 IPC 프로토콜중 하나이며, HTTP2기반 protocol buffer를 사용하여 빠르게 통신한다.

gRPC는 많은 플랫폼을 지원하며, 언어 독립적이다. (각 언어간 호출이 자유롭다)

 

gRPC를 사용하기 위해선 IDL(.proto file)을 작성하여 각 언어 및 시스템마다 필요한 파일(이게 결국 파라미터의 마샬링, 언마샬링을 하니 stub인가?) 을 만들어 사용한다.