메뉴

문서정보

목차

gRPC

gRPC는 구글이 개발한 오픈소스 RPC(Remote procedure call) 시스템이다. HTTP/2와 Interface description language인 프로토콜 버퍼(Protocol Buffers)를 기반으로 하고 있다. 인증, 양방향 스트리밍, timeout, cancellation, 블럭킹, 넌블럭킹등의 기능을 제공한다.

gRPC를 이용하면, 원격에 있는 애플리케이션의 메서드를 로컬 메서드인 것 처럼 직접 호출 할 수 있다. 실행 함수를 원격 서버에 분산 할 수 있는 만큼, 분산 애플리케이션과 서비스를 보다 쉽게 만들 수 있다. gRPC라는 이름에서 처럼 RPC 모델을 거의 그대로 따르고 있다. 즉 서비스를 정의하고, 서비스를 위한 매개변수와 반환 값을 가지는 메서드를 만든다는 단순한 아이디어를 가지고 있다.

서버측은 정의한 서비스 규격에 따라서 인터페이스를 구현하고, gRPC 서버를 실행한다. 클라이언트는 동일한 서비스 규격에 따라서 gRPC 메서드를 호출한다. 함수를 정의하고 실행하는 일을 원격으로 수행한다고 보면 되겠다.

 grpc

gPRC 클라이언트와 서버는 클라우드 환경에서 데스크탑, 모바일까지 다양한 환경에서 실행 할 수 있다. 언어 또한 다양해서 Go, Python, Ruby, Java, C++을 지원한다.

gRPC Interperability

구글은 자신들의 데이터센터에서 실행되는 마이크로서비스들을 운용하기 위해서 범용 RPC인프라인 Stubby를 사용해 왔다. 2015년 구글은 Sutbby의 다음 버전을 오픈소스화 하기로 했는데, 이렇게 해서 gRPC가 만들어졌다. 클라우드 기반에서 운용되는 마이크로서비스들의 상호운용성을 목표로 한 만큼 뛰어난 상호운용성을 보장한다.

 gRPC 상호운용성

서비스 개발자는 프로토콜 버퍼를 이용해서 서비스를 정의 하고, 프로토콜 버퍼 컴파일러를 이용해서 추상 인터페이스를 만든다. 개발자는 이 인터페이스를 구현하면 된다. 컴파일러는 다양한 언어로 인터페이스를 만들기 때문에, 다양한 언어로 구성된 마이크로 서비스 시스템을 만들 수 있다.

Protocol Buffers

gRPC는 JSON과 같은 데이터 타입을 사용 할 수 있지만 보통 구글의 오픈소스 직렬화 프로토콜인 프로토콜버퍼(Protocol buffer)를 이용한다.

개발자는 proto 파일에 직렬화하려는 데이터의 구조를 정의하면 된다. 이 파일의 확장자는 .proto이다. 이 파일에는 Key, Value로 구성된 필드를 포함하는 논리적 레코드 정보를 가지고 있다. 아래는 간단한 예다.
message Person {
  string name = 1;
  int32 id = 2;
  bool has_ponycopter = 3;
}
이렇게 만들어진 proto 파일은 프로토콜버퍼 컴파일리언 protoc를 이용해서 원하는 언어의 데이터 엑세스 클래스 코드로 만든다. 선택한 언어가 C++이었다면, 컴파일러는 Person 이라는 클래스를 만들 것이다. 그 다음 응용 프로그램에서는 이 클래스를 이용하여, Person 프로토콜버퍼 메시지를 채우고 직렬화 할 수 있다.

Hello World

Hello World예제를 만들어보자.

패키지 구성

패키지 구성은 아래와 같다.
  bitbucket.org ---+--- dream_yun ---+--- helloworld
                                     |
                                     +--- gretter_server
                                     |
                                     +--- gretter_client

protocol 컴파일러 설치

github.com/protocolbuffers에서 protoc 컴파일러를 다운로드 할 수 있다. protoc-3.9.1-linux-x86_64.zip (2019년 8월 16일 최신버전)를 다운로드했다. /opt/protobuf디렉토리 밑에 압축을 풀었다. /opt/protobuf 의 디렉토리 구성은 아래와 같다.
.
├── bin
│   └── protoc
├── include
│   └── google
│       └── protobuf
│           ├── any.proto
│           ├── api.proto
│           ├── compiler
│           │   └── plugin.proto
│           ├── descriptor.proto
│           ├── duration.proto
│           ├── empty.proto
│           ├── field_mask.proto
│           ├── source_context.proto
│           ├── struct.proto
│           ├── timestamp.proto
│           ├── type.proto
│           └── wrappers.proto
└── readme.txt
protoc-get-go 를 설치한다.
# go get github.com/golang/protobuf/protoc-gen-go
# ls ~/go/bin/protoc-gen-go 
/home/yundream/go/bin/protoc-gen-go

protocol buffer 파일 만들고 컴파일하기

hellworld.proto파일을 만들었다. 이 파일을 컴파일 하면, gRPC 서버와 클라이언트가 사용 할 수 있는 패키지 파일이 만들어진다. 이 파일을 이용해서 hello world 서버와 클라이언트 프로그램을 만든다. 아래는 hello.proto파일이다.
syntax = "proto3";

package helloworld;

// Greeter 서비스를 만들었다.
// SayHello를 서비스한다. 
// 클라이언트가 HelloRequest 메시지를 보내면, 이를 매개변수로 받아서 처리하고
// HelloReply 메시지를 클라이언트에 반환한다.
service Greeter {
    rpc SayHello (HelloRequest) returns (HelloReply) {}
}

// 요청 메시지를 정의 한다.
// 클라이언트는 유저의 이름을 전송한다.
message HelloRequest {
    string name =1;
}

// 응답 메시지를 정의한다.
// 서버는 클라이언트에 Hello world 메시지를 전송한다.
message HelloReply {
    string message=1;
}
protoc를 이용해서 컴파일한다.
# protoc -I helloworld/ helloworld/helloworld.proto --go_out=plugins=grpc:helloworld
컴파일 하고나면 "helloworld.pb.go"파일이 만들어진다.

gretter_server

greeter server를 만들어보자.
package main

import (
    pb "bitbucket.org/dream_yun/helloworld"
    "golang.org/x/net/context" 
    "google.golang.org/grpc"   
    "google.golang.org/grpc/reflection"
    "net"
) 

type server struct {           
} 

func (s *server) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloReply, error) {
    return &pb.HelloReply{Message: "Hello " + in.Name}, nil
} 

func main() {                  
    lis, err := net.Listen("tcp", ":8888")
    if err != nil {            
        panic(err)
    }
    s := grpc.NewServer()
    pb.RegisterGreeterServer(s, &server{})
    reflection.Register(s)
    if err := s.Serve(lis); err != nil {
        panic(err)
    }
}
프로토콜버퍼 파일에서 service로 정의한 메서드 SayHello를 구현하면 된다.

gretter_client

클라이언트 프로그램이다. grep.Dial 로 연결한 다음 SayHello 서비스를 호출했다.
package main

import (
    pb "bitbucket.org/dream_yun/helloworld"
    "fmt"
    "golang.org/x/net/context" 
    "google.golang.org/grpc"   
) 

func main() {                  
    conn, err := grpc.Dial(":8888", grpc.WithInsecure())
    if err != nil {
        panic(err)             
    }
    defer conn.Close()         
    c := pb.NewGreeterClient(conn)

    r, err := c.SayHello(context.Background(), &pb.HelloRequest{Name: "yundream"})
    if err != nil {            
        panic(err)             
    }                          
    fmt.Printf("Greeting: %s\n", r.Message)
}

Redis Count 서비스 추가

Redis 기반의 count 서비스를 추가해 보자. 호출 하면, 현재의 count를 되돌려주고 +1을 하는 간단한 서비스다. count는 Redis의 INCR로 구현한다. proto 파일을 손 봤다.
syntax = "proto3";

package helloworld;

// Greeter 서비스를 만들었다.
// SayHello를 서비스한다. 
// 클라이언트가 HelloRequest 메시지를 보내면, 이를 매개변수로 받아서 처리하고
// HelloReply 메시지를 클라이언트에 반환한다.
service Greeter {
    rpc SayHello (HelloRequest) returns (HelloReply) {}
    rpc Counter(CountRequest) returns (CountReply){}
}   

// 요청 메시지를 정의 한다.    
// 클라이언트는 유저의 이름을 전송한다.
message HelloRequest {         
    string name =1;            
}   

// 응답 메시지를 정의한다.     
// 서버는 클라이언트에 Hello world 메시지를 전송한다.
message HelloReply {
    string message=1;
} 

message CountReply {
    int32 count=1; 
}

message CountRequest {
}

서버 프로그램이다.
package main

import (
    pb "bitbucket.org/dream_yun/helloworld"
    "github.com/go-redis/redis"
    "golang.org/x/net/context" 
    "google.golang.org/grpc"   
    "google.golang.org/grpc/reflection"
    "net"
) 

type server struct {
    R *redis.Client
}

func (h *server) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloReply, error) {
    return &pb.HelloReply{Message: "Hello " + in.Name}, nil
}   
    
func (h *server) Counter(ctx context.Context, in *pb.CountRequest) (*pb.CountReply, error) {
    cmd := h.R.Incr("count")
    return &pb.CountReply{Count: int32(cmd.Val())}, nil
} 

func Init() *server {
    r := redis.NewClient(&redis.Options{
        Addr:     "172.17.0.2:6379", 
        Password: "",
        DB:       0,
    })
    return &server{R: r}
}

func main() {
    serv := Init()
    lis, err := net.Listen("tcp", ":8888")
    if err != nil {
        panic(err)
    }
    s := grpc.NewServer()
    pb.RegisterGreeterServer(s, serv)
    reflection.Register(s)
    if err := s.Serve(lis); err != nil {
        panic(err)
    }
}

스트리밍

gRPC는 HTTP/2를 기반으로 한다. 기존에 사용했던 REST Over HTTP/1.1, HTTP/1.1에서 사용했던 Chunked Streaming Encoding, WebSocket(HTTP와는 다른 프로토콜이다.) 와의 성능 확장성의 차이는 프로토콜의 특징을 이해하는 것으로 어느정도 예상 할 수 있다. HTTP/1.1은 1999년에 발표된 (2018년 기준 거의 20년이 된)기술이다. 이 때는 빅데이터시대가 아니었다는 것을 기억할 필요가 있다. 노트북이 5GB! 하드 드라이브로 나오던 때다.

인터넷에서 서버가 클라이언트에 응답데이터를 보낼 때는 Content Length에 데이터의 크기를 명시해서 전송한다. 클라이언트는 Content Length를 이용해서 언제 응답이 완료되는지를 알 수 있다. 클라이언트는 데이터를 모두 읽기 전까지는 처리를 하지 않을 것이다.

스트리밍의 경우 전체크기를 알 수 없는데 따라서 HTTP/1.1은 메시지를 청크(chunk)로 분해해서 별도로 전송하는 방식을 사용하고 있다. 이때 각 청크의 데이터크기도 함께 전송한다. 아래 그림을 보자.

HTTP/1.1에서도 서버는 메시지 본문을 청크로 분해해서 별도로 전송 할수 있기는 하다. 하지만 이 방식은 파일 전송 외에는 널리 사용하지 않고 있다. 일단 JQuery가 지원을 하지 않고 있다. 자바스크립트의 경우 어느 브라우저에서나 사용 할 수 있는 공통 기능을 목표로 해야해서 XMLHttpRequest에 기능을 넣지 않았기 때문이다. Rails는 2011년에서야 지원을 추가했고, IE도 버전 10까지는 지원하지 않고 있다.

HTTP/1.1은 기술적으로는 스트리밍을 할 수 있지만 효율적인 방식은 아니다. HTTP/2는 헤더 압축, 바이너리 전송, 다중화 스트림과 같은 기능을 가지고 있다. 또한 서버는 클라이언트의 요청 없이 데이터를 전송 할 수 있다. 미래를 생각한다면 HTTP/2를 선택하는게 좋을 것이다. 스트리밍과 관련된 gRPC의 특징은 아래와 같다.

채팅 예제

시스템 구성

 chatting 시스템 구성

  1. Login : ID/Password 기반으로 로그인한다. 일단은 ID/Password를 비교하지는 않는다. 그냥 유저를 구분하기 위한 용도로만 사용한다.
  2. StreamOpen : 로그인에 성공하면, 스트림을 Open한다. 그리고 데이터를 읽기 위한 Recv 고루틴을 하나 만든다. 만약 Recv 루틴에 데이터가 입력되면, 모든 유저 채널의 Send 메서드를 호출한다.
  3. StreamRegist : Open한 스트림 정보를 map에 저장한다. map에는 "유저 id"와 채널이 들어간다.

테스트 환경

참고