[스터디] gRPC에 대해서...with Python

suhwani·2024년 7월 29일
0
post-thumbnail

gRPC


들어가며…

기존 프로젝트 1차 작업을 끝내고, 본격적으로 메인 프로젝트에 해당 기능을 붙이려고 하는데, 내가 맡았던 포지션이 애매해져서 메인 프로젝트의 일부 기능을 맡는 식으로 업무가 배정되었다.
기존에는 해당 기능이 쓸만한지 테스트하는 용도였기에 프론트엔드 개발자가 필요하지 않아서, 백엔드 인턴 중에서 프론트를 내가 맡아서 개발했는데,
이제 그 기능이 본격화 되면서 프론트엔드 개발자가 프론트를 맡기도 했다. 그래서 나는 팀장님께 말씀드려서 메인 프로젝트의 다른 부분을 맡기로 했다.

내가 맡은 부분이 gRPC를 이용한 소켓?? DB 임베딩?? 이쪽이여서 먼저 자료조사와 공부를 할 것이다.
메인 프로젝트를 담당하시던 담당자분 입장에선 갑자기 인턴이 배정된 것이라 나에게 넘겨줄 자료나 작업을 미리 준비하지 못해서 준비되면 보내주신다고 했다. 그럼 gRPC에 대해서 알아보자.

1. OSI 7 Layer 와 TCP/IP 4 Layer


‼️ 여기는 필요한 부분만 잡고 넘어가려고 합니다. gRPC를 공부하기 위한 배경지식에 필요한 정보를 얻기 위함입니다.

  • OSI 7계층은 데이터 통신에 필요한 계층과 역할을 정확하게 정의한 모델이다.
  • TCP/IP 4계층은 현재 인터넷에서 사용되는 프로토콜로, 실무적이고, 프로토콜 중심의 단순화된 모델이다.

OSI 7Layer


  • 3계층 - Network Layer
    • 데이터를 목적지까지 안전하고 빠르게 전달하는 라우팅 역할
    • 경로를 선택하고, 패킷을 전달한다.
    • IP 프로토콜
  • 4계층 - Transport Layer
    • End to End, Host to Host 를 연결하여 사용자들이 데이터를 주고 받는 역할
    • TCP 프로토콜 , UDP 프로토콜
  • 7계층 - Application Layer
    • Process to Process 를 연결하여 일반적인 응용 서비스를 수행한다.
      • HTTP, SMTP … 등

TCP/IP 4Layer


  • Network Access Layer
    • 프레임 송수신 역할
    • 주소: MAC, 데이터 단위: Frame
  • Internet Layer
    • 논리적 주소 및 경로 지정
    • 주소: IP, 데이터 단위: Packet
    • IP, ARP…
  • Transport Layer
    • 호스트끼리 송수신
    • 주소: Port, 데이터 단위: Segment
    • TCP, UDP
    • 통신 노드 간의 데이터 전송 및 흐름에 있어 신뢰성을 보장한다.
      TCP는 연결 지향형 프로토콜로, 패킷에 오류가 있다면 재전송을 위해 에러를 복구한다.
      UDP는 패킷을 중간에 잃거나 오류가 발생해도 대처하지 않고, 계속 데이터를 전송한다.
  • Application Layer
    • 응용프로그램끼리 데이터 송수신
    • 주소: - , 데이터 단위: Data/Message
    • HTTP, DNS

2. Socket 과 Web Socket


Socket

  • 일반적으로 TCP/IP 프로토콜을 이용한다.
  • 결국 응용계층 아래인 OSI 4Layer Transport Layer 위에 놓인다는 말이다.
  • 전송 계층의 프로토콜 제어를 위한 코드를 제공하는 부분이다.

Socket 하는 일

  • IP + Port 를 이용해서 Client 와 Server 를 연결한다.
  • Socket을 연결하고, 데이터를 송수신한다.
  • 통신 연결 요청을 받는 수신자 : Server Socket
  • 통신 연결 요청을 보내는 송신자 : Client Socket

Socket 데이터 송수신과 Port 역할

  • 먼저 Connect를 연결하고, Socket을 생성하는데 이 때 Connect는 IP + Port를 조합해서 연결한다.
  • Client는 수신자에게 요청(Connect Request)을 보내고
    Server는 송신자에게 응답(Connect Response)을 보낸다.
  • 이 때, Client 입장에서는 내가 언제 무엇을 보낼지 알 수 있지만,
    Server는 누가 언제 들어오는지 알수가 없기 때문에 수신하는 API는 별도의 Thread에서 진행된다.

  • 하지만, 수신하는 Server 입장에선 Port 번호를 식별해서 올바른 요청만 프로세스에게 넘겨줘야한다.
    따라서 프로세스 Port 번호와 서버 소켓이 고유하도록 결합해주는 작업이 필요하다.
  • 만약 위 그림처럼 Port 10000 에 3개의 Socket이 연결되어있다면, Server 입장에선 어떤 Socket에게
    데이터를 넘겨줘야하는지 알 수 없다. → 운영체제에서 같은 Port로 Socket 못 띄우는 이유
  • 참고! 하나의 Process 는 동일한 포트 번호를 가진 여러 개 Socket을 결합할 수 있다.
    → 단체 채팅방을 생각하자.
    특정 Port가 여러 Socket을 열고, 첫번째 소켓은 엄마, 두번째는 아빠 등등 한 채팅방에 모아서 같은 Port를 이용하는 것
  • 이후 연결이 되면 소켓이 생성된다.

Web Socket

  • 먼저 TCP/IP 에서 다룬 일반적인 Socket과는 다르다. 애초에 Layer가 다르다.
    Socket: TCP/IP 위인 Network Layer에서 동작
    WebSocket: HTTP 위인 Application Layer에서 동작
  • HTTP는 단방향적 구조이기 때문에 Connection을 할 수 없다. 즉, 실시간 통신이 안된다.
    • 이걸 해결하기 위해 나온 게 WebSocket이다.
    • TCP/IP 프로토콜을 사용하는 Socket처럼 Connection을 유지. 실시간 통신 가능.
  • WebSocket 이기에 당연히 HTTP 위에서 동작한다.
    • 핸드쉐이크 과정에서 헤더의 비중이 크지만, 한번 연결이 되면 간단한 메세지들만 오고 간다.
  • 다만, WebSocket은 HTML5이후 나온 기술이다. 이전에 나온 서비스는 Socket.io 나 등등 다른 방법 사용

3. RPC란 무엇인가


일반적으로 사용하는 통신 패턴

  • Client to Server 패턴
    • 보통 RESTful하게 사용. REST API를 이용한다.
    • REST API는 HTTP 위에서 작동하기 때문에 느리다.
    • 또한 HTTP는 stateless하기 때문에 연결이 필요한 경우 Socket을 사용하기도 한다.
    • 대신 Socket을 사용하면 Connection을 잡는만큼 서버에게 부담이 된다.

RPC가 필요한 이유

  • 최근 서비스가 커지면서, MSA 구조로 서비스를 만들게 되는데, 다양한 언어와 여러 프레임워크를 사용하여 개발한다.
  • 이럴 때 사용하는 게 RPC. 언어에 구애받지 않고, 원격에 있는 프로시저를 호출하여 개발할 수 있다.
  • 특히 HTTP 위가 아닌 아래 (Transport Layer, TCP/UDP)에서 작동하기 때문에 가볍고, 빠르다.

RPC의 개념

  • Remote Procedure Call 리모트 프로시저 콜
  • 별도의 원격 제어를 위한 코딩 없이 다른 주소 공간에서 함수나 프로시저를 실행할 수 있게하는 프로세스 간 통신 기술이다. 다시 말해, 원격 프로시저 호출을 이용하면 프로그래머는 함수가 실행 프로그램에 로컬 위치에 있든 원격 위치에 있든 동일한 코드를 이용할 수 있다.
  • 일반적인 프로세스는 자신의 주소공간 안에 있는 함수만 호출하고, 실행 가능하다. 하지만 RPC는 다른 주소공간에서 동작하는 프로세스의 함수를 실행할 수 있다. 즉 호출하는 프로시저와 호출되는 프로시저는 같은 주소공간에 존재하지 않고 네트워크가 그들을 연결하는 형태로 존재한다.

참고: RPC 는 제어 흐림이 호출자와 수신자 간에 교대로 이루어진다. 즉, 클라이언트와 서버를 동시에 실행하지 않고 실행 스레드가 호출자로부터 수신자에게 점프했다가 다시 돌아온다.

  • 함수: 인풋에 대비한 아웃풋의 발생을 목적으로 한다.
  • 프로시저: 결과 값에 집중하기 보단 명령 단위가 수행하는 절차를 목적으로 한다.

RPC의 궁극적인 목표

  • 클라이언트-서버 간 커뮤니케이션에 필요한 상세정보를 최대한 감춘다.
  • 클라이언트는 일반 메소드를 호출하는 것처럼 원격지의 프로시저를 호출할 수 있다.
  • 서버도 마찬가지로 일반 메소드를 다루는 것처럼 원격 메소드를 다룰 수 있다.

RPC 동작 방식

  • 동작 방식은 위 그림이 모든 걸 설명해줘서 짧게 요약만 하자면, Client에서 필요한 인자를 Stub에게 보내면 이걸 변환하는 과정을 거쳐서 Transport Layer에게 던져주면, 이걸 Server Stub이 받고 파라미터들을 모아서 서버측 루틴을 호출한다. 값이 나오면 이걸 다시 Client측에게 반환한다.
  • IDL(Interface Definition Language)를 사용해 서버의 호출 규약을 정의한다. IDL 파일을 이용해서 rpcgen 컴퍼일러를 이용하여 stub 코드를 자동으로 생성한다.
  • Stub은 원시소스코드(C코드 등)의 형태로 만들어지며, 클라이언트와 서버 프로그램에 포함하여 빌드한다.
  • 클라이언트 프로그램 입장에선 자신의 프로세스 주소공간의 함수를 호출하는 것처럼 동일하게 stub에 정의된 함수를 호출할 수 있게 된다.
  • Stub 코드는 데이터형을 XDR(External Data Representation) 형식으로 변환하여 RPC 호출을 실행하는데, XDR 변환 이유는 메모리 저장 방식, CPU 아키텍쳐별 바이트 처리, 네트워크 전송과정에서의 바이트 전송 순서 보장 등을 위함이다.
  • Server는 수신된 함수/프로시저 호출에 대한 처리 완료 후, 결과값을 XDR 변환하여 반환한다. 최종적으로 클라이언트 프로그램은 서버의 결과값을 반환받는다.

RPC의 대표적인 구현체는?

  • ProtocolBuffer by Google
  • Thrift by Facebook
  • Finalge by Twitter

RPC의 장점

  1. 고유 프로세스 개발 집중 가능 (하부 네트워크 프로토콜에 신경쓰지 않아도 되기 때문)
  2. 프로세스간 통신 기능을 비교적 쉽게 구현하고 정교한 제어가 가능

RPC의 단점

  1. 호출 실행과 반환 시간이 보장되지 않음 (네트워크 구간을 통하여 RPC 통신을 하는 경우, 네트워크가 끊겼을 때 치명적 문제 발생)
  2. 보안이 보장되지 않음

4. gRPC란 무엇인가


gRPC는 모든 환경에서 실행할 수 있는 최신 오픈 소스 고성능 RPC(원격 프로시저 호출) 프레임워크이다.

gRPC 특징

  • HTTP/2 기반 전송
  • 인증, 추적, 로드 밸런싱 및 상태 확인
  • 프로토콜 버퍼 사용한다. 구조화된 데이터를 직렬화하기 위한 언어 중립적이고 플랫폼 중립적인 확장 가능한 메커니즘이다. 프로토콜 버퍼는 정의 언어(.proto 파일로 생성됨), proto 컴파일러가 데이터와 인터페이스하기 위해 생성하는 코드, 언어별 런타임 라이브러리, 파일에 작성되거나 전송되는 데이터의 직렬화 형식의 조합이다

Protocol buffers(proto) 특징

  • 명세 정의 언어
  • 서버와 클라이언트가 정보를 주고 받는 규칙: 프로토콜 // 어떤 언어로 정보를 주고 받을지: IDL
  • Protocol buffers(proto)
    • XLM의 문제점을 개선한 IDL, XML보다 월등안 성능
    • 구조화(structured)된 데이터를 직렬화(serialization)하기 위한 프로토콜로 작고 빠르고 간단하다.
    • XML 스키마처럼 .proto 파일에 protocol buffer 메세지 타입을 정의한다.

HTTP 2.0 특징

gRPC 통신 패턴

  • Client - Server 구조이기에, Client / Server 각각이 어떤 통신을 하느냐에 따라서 4가지로 구분.

  1. Unary: 클라이언트에서 서버로 단일 요청 -> 서버에서 클라이언트로 단일 응답.

  2. Server Streaming: 클라이언트에서 서버로 단일 요청 -> 서버에서 클라이언트로 여러 응답.

  3. Client Streaming: 클라이언트에서 서버로 여러 요청 -> 서버에서 클라이언트로 단일 응답.

  4. Bi-directional Streaming: 클라이언트와 서버가 동시에 여러 요청과 응답을 스트리밍.

Streaming(스트리밍)만의 장점

  • Streaming
    • 연결 유지: 스트리밍 방식에서는 단일 연결을 통해 여러 메시지를 주고받으므로 연결 설정과 해제에 드는 오버헤드가 감소
    • 실시간 통신: 클라이언트와 서버가 동시에 메시지를 주고받을 수 있어 실시간 통신에 적합
    • 연결 오버헤드 감소: 지속적인 연결을 유지함으로써 연결 설정 및 해제에 따른 오버헤드를 감소
    • 복잡성 증가: 스트리밍을 구현하는 것은 단방향 RPC보다 복잡할 수 있습니다. 상태 관리, 오류 처리, 흐름 제어 등이 필요
    • 유연성 증가: 다양한 통신 패턴을 지원하므로, 더 복잡한 상호작용을 구현 가능
  • Unary 요청 여러번
    • 연결 설정 및 해제 오버헤드: 각 요청마다 새로운 연결을 설정하고 응답 후 해제하므로 오버헤드가 증가
    • 실시간성 부족: 각 요청과 응답이 독립적으로 처리되므로 실시간 통신에는 덜 적합
    • 단순성: 각 요청과 응답이 독립적이므로 구현이 단순
    • 제한된 상호작용: 각 요청과 응답이 독립적이므로 복잡한 상호작용을 구현힘듬
  • Unary 요청 리스트 형태로
    • 메모리 사용 증가: 대용량 데이터를 한 번에 전송하면, 서버와 클라이언트 모두에서 메모리 사용량이 급증할 수 있습니다. 이는 시스템 자원을 낭비하고, 경우에 따라 메모리 부족 문제를 발생
    • 실시간 처리 부족: 데이터 전송이 완료된 후에만 처리가 시작되므로, 실시간 데이터 처리에는 적합하지 않음
    • 네트워크 효율성 저하: 여러 개의 큰 요청을 한 번에 전송하면 네트워크 대역폭이 집중적으로 사용될 수 있어 네트워크 효율성이 떨어짐

5. gRPC 코드 예제 (with. python)

가상환경 띄우기

// 터미널 열고 가상환경 새로 생성 -> grpcvenv 라는 가상환경 생성
python3 -m venv grpcenv
source grpcvenv/bin/activate

gRPC 및 Protocol Buffers 설치

pip install grpcio
pip install grpcio-tools

helloworld.proto 작성

// helloworld.proto
syntax = "proto3";

package helloworld;

// 1번째 서비스
service Greeter {
		// Unary 패턴
	  rpc SayHello (HelloRequest) returns (HelloReply) {}

		// Streaming 패턴
	  rpc StreamHello (stream HelloRequest) returns (stream HelloReply) {}
}

// 2번째 서비스
service Farewell {
		// Unary 패턴
	  rpc SayGoodbye (GoodbyeRequest) returns (GoodbyeReply) {}
	
		// Streaming 패턴
	  rpc StreamGoodbye (stream GoodbyeRequest) returns (stream GoodbyeReply) {}
}

// 3번째 서비스: 일부러 Input 과 Output 에러 발생
service RaiseError {
		// Input Type 에러 발생 
    rpc WrongInput (WrongRequest) returns (RightReply) {}
		
		// Output Type 에러 발생
    rpc WrongOutput (RightRequest) returns (WrongReply) {} 
}

message HelloRequest {
	  string name = 1;
}

message HelloReply {
	  string name = 1;
}

message GoodbyeRequest {
	  string name = 1;
}

message GoodbyeReply {
	  string name = 1;
}
	
message WrongRequest {
    int32 name = 1;
}

message RightRequest {
    string name = 1;
}

message WrongReply {
    int32 name = 1;
}

message RightReply {
    string name = 1;
}

gRPC 컴파일러를 이용한 코드 생성

// 아래 명령어를 .proto 파일이 있는 디렉토리에서 입력
python -m grpc_tools.protoc -I. --python_out=. --grpc_python_out=. helloworld.proto

Server.py 코드

from concurrent import futures
import grpc
import helloworld_pb2
import helloworld_pb2_grpc

# 첫 번째 서비스 구현
class Greeter(helloworld_pb2_grpc.GreeterServicer):
    def SayHello(self, request, context):
        return helloworld_pb2.HelloReply(name=request.name)
    
    def StreamHello(self, request_iterator, context):
        for request in request_iterator:
            print(f"Greeter-StreamHello Server Received: "+ request.name)
            yield helloworld_pb2.HelloReply(name=request.name)

# 두 번째 서비스 구현
class Farewell(helloworld_pb2_grpc.FarewellServicer):
    def SayGoodbye(self, request, context):
        print(f"Farewell-StreamGoodbye Server Received: "+ request.name)
        return helloworld_pb2.GoodbyeReply(name=request.name)
    
    def StreamGoodbye(self, request_iterator, context):
        for request in request_iterator:
            print(f"Farewell-StreamGoodbye Server Received: "+ request.name)
            yield helloworld_pb2.GoodbyeReply(name=request.name)

# 세 번째 서비스 구현: 에러 발생 서비스 
class RaiseError(helloworld_pb2_grpc.RaiseErrorServicer):
    def WrongInput(self, request, context):
        return helloworld_pb2.RightReply(name='RightReply & client parameter: ' + request.name)
    
    def WrongOutput(self, request, context):
        return helloworld_pb2.WrongReply(name='WrongRepluy & client parameter: ' + request.name)

def serve():
    server = grpc.server(futures.ThreadPoolExecutor(max_workers=10))
    helloworld_pb2_grpc.add_GreeterServicer_to_server(Greeter(), server)
    helloworld_pb2_grpc.add_FarewellServicer_to_server(Farewell(), server)
    helloworld_pb2_grpc.add_RaiseErrorServicer_to_server(RaiseError(), server)
    server.add_insecure_port('[::]:50051')
    server.start()
    server.wait_for_termination()

if __name__ == '__main__':
    serve()

Client.py 코드

import grpc
import helloworld_pb2
import helloworld_pb2_grpc

channel = grpc.insecure_channel('localhost:50051')
greeterStub = helloworld_pb2_grpc.GreeterStub(channel=channel)
farewellStub = helloworld_pb2_grpc.FarewellStub(channel=channel)
raiseerrorStub = helloworld_pb2_grpc.RaiseErrorStub(channel=channel)

# 첫 번째 서비스 
def run_greeter():
    choice = input("Enter '1' to SayHello or '2' to StreamHello")
    if choice == '1':
        response = greeterStub.SayHello(helloworld_pb2.HelloRequest(name='정수환'))
        print(response)
        print("Greeter-SayHello received: " + response.name)
    else:
        responses = greeterStub.StreamHello(generate_requests(RequestModel=helloworld_pb2.HelloRequest))
        for response in responses:
            print("Greeter-StreamHello received: " + response.name)

# 두 번째 서비스
def run_farewell():
    choice = input("Enter '1' to SayGoodbye or '2' to StreamGoodbye")
    if choice == '1':
        response = farewellStub.SayGoodbye(helloworld_pb2.GoodbyeRequest(name='정수환'))
        print(response)
        print("Farewell-Goodbye received: " + response.name)
    else:
        responses = farewellStub.StreamGoodbye(generate_requests(RequestModel=helloworld_pb2.GoodbyeRequest))
        for response in responses:
            print("Farewell-StreamGoodbye received: " + response.name)
       
# 세 번째 서비스     
def run_raiseerror():
    choice = input("Enter '1' to WrongInput or '2' to WrongOutput")
    if choice == '1':
        response = raiseerrorStub.WrongInput(helloworld_pb2.WrongRequest(name="raise error"))
        print(response)
        print("RaiseError-WrongInput received: " + response.name)
    else:
        response = raiseerrorStub.WrongOutput(helloworld_pb2.RightRequest(name="raise error"))
        print(response)
        print("RaiseError-WrongOutput received: " + response.name)
    
# 스트리밍 서비스인 경우 사용
def generate_requests(RequestModel):
    names = ['정수환', '종수환', '장수환', '중수환']
    for name in names:
        yield RequestModel(name=name)
        
if __name__ == '__main__':
    while True:
        choice = input("Enter '1' to greet or '2' to say goodbye or '3' to raise error")
        if choice == '1':
            run_greeter()
        elif choice == '2':
            run_farewell()
        elif choice == '3':
            run_raiseerror()
        else:
            print("Invalid choice")
profile
Backend-Developer

0개의 댓글