sonumb

Golang - gRPC 예제 본문

개발자 이야기/Go

Golang - gRPC 예제

sonumb 2021. 11. 12. 17:52

개요

 

gRPC 개념은 아래 글들에서 찾아보자.

 

 

✅ protobuf의 버전2와 3의 차이점이 여러 가지가 있는데 그중 하나가 "버전 3이 더 많은 언어를 지원"한다는 점이다. (마지막 글 참조))

 

예제

컴퓨팅 자원은 한정적이므로, 자원을 공유하는 아키텍처를 생각해 볼 수 있다.

특히 멀티프로세스 아키텍처 기반에서 서비스를 구현하는 경우, 파일, 공유 메모리, 심지어 서비스까지 공유 리소스로 정의할 수 있다.

특정 프로세스가 비정상 종료되는 경우,  다른 프로세스가 이 공유 리소스를 회수/복구하지 않으면 불가능 상태로 영원히 남을 것이다.

OS의 경우, 어느 프로세스가 죽으면 다른 프로세스가 커널모드로 진입한 후, 할당된 메모리/파일을 반환한다.

이와 비슷하게, MSA 구조에서도 이러한 리소스를 감시 및 복구하는 프로세스가 필요하다.

 

(도입이 길었지만, gRPC 예제란 걸 잊지 말자😅)

 

이때, 프로세스 감시 및 리소스 복구를 해주는 관리 프로세스는 2개로 나누어 구현해본다.

  • 메인데몬: 비정상 종료 감지
  • 서브데몬: 복구절차 수행
⚠️ 두 가지 프로세스로 나눈 이유
복구하는 중에 감시/감지는 계속되어야 한다.
나누지 않고 하나의 프로세스에서 처리하는 상황이라고 하자. 복구 도중 비정상 종료가 발생하면, 감시/감지 기능도 같이 종료된다.
따라서 두 가지 서비스를 각각의 프로세스로 구현한다.

(✅그런데, 두 기능을  분리하면, 복구 서비스를 하는 데몬이 죽은 경우가 가장 문제가 된다. 이런 경우, "1) 다른 리소스 감시/감지는 계속 실행,  2) "복구 데몬 재실행"을 우선적으로 실행, 3) 감시/복구 작업를 재진행" 의 절차로 이문제를 극복할 수 있다.)

 

그리고 비정상 종료를 감지한 이후, 이에 대한 "복구"는 gRPC를 이용하며, 다른 프로세스에게 요청될 것이다.

 

이 과정은 아래 그림의 1~5 과정으로 기술된다.

 

소스 코드

앞서 설명한 개념을 목업으로 구현해보자!

 

protobuf/subdaemon.proto

// 아래 내용으로 protobuf/subdaemon.proto 경로로 파일 작성
// $ protoc -I protobuf/ protobuf/subdaemon.proto --go_out=plugins=grpc:protobuf
// 명령으로 go 용 파일 생성
syntax = "proto3";

package subdaemonpb;

// SubDaemonGrpc 서비스 선언
service SubDaemonGrpc {
  // 연결 메시지
  rpc SayHello (HelloRequest) returns (HelloReply) {}
  // 복구 서비스
  rpc RecoveryProcess (RecoveryProcessRequest) returns (RecoveryProcessReply) {}
  // 서브데몬 종료
  rpc SubDaemonStop (SubDaemonStopRequest) returns (SubDaemonStopReply) {}
}

// todo: 모든 응답 메시지에  ErrorType 을 추가하고
//   - fatal, abort, debug, info으로 나누어서, 메인데몬 종료 여부등등으로 나누고,
//   - 작업을 재시도/abort 등도 할 수 있도록 해야 한다.

message HelloRequest {
  string name = 1;
}

message HelloReply {
  string message = 1;
}

message RecoveryProcessRequest {
  int32 pid = 1;
  // etc
}

message RecoveryProcessReply {
  bool   success = 1;
  string message = 2;
}

message SubDaemonStopRequest {
}

message SubDaemonStopReply {
  bool  success = 1 ;
  string message = 2;
}

subdaemon_mockup.go

개념적으로는 두개의 프로세스로 설명하였지만, 이 목업은 한 프로세스에서 여러 고루틴으로 구현될 것이다.  

 

main() 함수에서 2개의 고루틴을 실행하는데,

  • main() 함수 내 고루틴: gRPC 서버 고루틴: 서브데몬의 gRPC 호출을 처리
  • clientGoroutine(): 메인 데몬의 역활. 즉, 서브데몬 gRPC 호출

와 같다.

 

최종적으로 메인 데몬이 서브데몬의 종료(SubDaemonStop())를 호출하게 되는데 종료 채널에 시그널을 발생시한다.

메인 함수가 이 이벤트를 수신하였다면, gRPC 인스턴스의 GracefulStop()을 호출하여 종료한다. 

 

package main

import (
  "context"
  "log"
  "net"
  pb "github.com/foobar/go_exam/protobuf"
  "google.golang.org/grpc"
  "time"
  "fmt"
  "os"
  "errors"
)

const (
  port = "127.0.0.1:50051"
)

// SubDaemonGrpcImpl는 protobuf 실제 구현
type SubDaemonGrpcImpl struct{
  server *grpc.Server
  stopCh  chan struct{}
  //subd SubDaemonServer
}

func (s *SubDaemonGrpcImpl) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloReply, error) {
  log.Printf("Received: %v", in.Name)
  return &pb.HelloReply{Message: "Hello " + in.Name}, nil
}

func (s *SubDaemonGrpcImpl) SubDaemonStop(ctx context.Context, in *pb.SubDaemonStopRequest) (*pb.SubDaemonStopReply, error) {
  s.stopCh <- struct{}{}

  return &pb.SubDaemonStopReply{
    Success: true,
    Message: "SubDaemonStop Success",
  }, nil
}

func (s *SubDaemonGrpcImpl) RecoveryProcess(ctx context.Context, in *pb.RecoveryProcessRequest) (*pb.RecoveryProcessReply, error) {
  // err := s.subd.ReocveryMgr.RecoveryWithPid(in.GetPid())
  // if err != nil {
  //  return &pb.SubDaemonStopReply{
  //    Success: false;
  //      errorType: fatal, abort, info
  //    Message: err.Error(),
  //  }, nil
  // }
  //

  err := errors.New("not yet implemented")
  return &pb.RecoveryProcessReply{
    Success: false,
    Message: err.Error(),
  }, nil
}

func main() {
  lis, err := net.Listen("tcp", port)
  if err != nil {
    log.Fatalf("failed to listen: %v", err)
  }
  s := grpc.NewServer()
  srv := &SubDaemonGrpcImpl{s, make(chan struct{})}
  pb.RegisterSubDaemonGrpcServer(s, srv)

  go func () {
    if err := s.Serve(lis); err != nil {
      log.Fatalf("failed to serve: %v", err)
    }
    println("server stopped")
  } ()

  clientGoroutine := func () {
    conn , err := grpc.Dial(port, grpc.WithInsecure(), grpc.WithBlock())
    //conn , err := grpc.Dial(port)
    if err != nil {
      log.Fatalln(err.Error())
    }
    subdGrpc := pb.NewSubDaemonGrpcClient(conn)

    ctx, _ := context.WithCancel(context.Background())
    // ctx = metadata.AppendToOutgoingContext(ctx, "dapr-app-id", "server")


    // 1. 핸드쉐이크를 위한 Hello Msg
    rep, err := subdGrpc.SayHello(ctx, &pb.HelloRequest{Name:"Hi"})
    if err != nil {
      log.Fatalln(err.Error())
    }
    fmt.Println(rep)

    time.Sleep(3*time.Second)

    // 2. 프로세스 복구 요청
    recoveryReq := &pb.RecoveryProcessRequest{Pid: int32(os.Getpid())}
    recoveryRep, err := subdGrpc.RecoveryProcess(ctx, recoveryReq)
    if err != nil {
      log.Fatalln(err.Error())
    }
    if recoveryRep.GetSuccess() != true {
      fmt.Println("Warn:", recoveryRep.Message)
    } else {
      fmt.Println(recoveryRep)
    }
    time.Sleep(1*time.Second)

    // 3. 서브데몬 종료 요청
    stopRep, err := subdGrpc.SubDaemonStop(ctx, &pb.SubDaemonStopRequest{})
    if err != nil {
      log.Fatalln(err.Error())
    }
    fmt.Println(stopRep)
    time.Sleep(1*time.Second)
    fmt.Println("Stop Client")
    conn.Close()
  }
  go clientGoroutine()

  println("main: wait-for-stop")
  ticker := time.NewTicker(time.Second)

  for stopLoop := false ; stopLoop != true ; {
    select {
    case <-ticker.C:
      println("server-ticker")

    case <-srv.stopCh:
      fmt.Println("SubDaemonGrpcServer Received Stop Message -> Do Stop")
      s.GracefulStop()
      fmt.Println("SubDaemonGrpcServer Received Stop Message -> Do Stop -- done")
      stopLoop = true
    }
  }

  time.Sleep(3*time.Second)

  println("end-main")
}

go.mod

module github.com/foobar/go_exam
⛔️ go.mod 파일이 없으면 아래와 같은 메시지와 함께 빌드가 되지 않는다.
$ go build subdaemon_mockup.go 
go: finding module for package github.com/foobar/go_exam/protobuf
subdaemon_mockup.go:7:2: cannot find module providing package github.com/foobar/go_exam/protobuf: module github.com/foobar/go_exam/protobuf: git ls-remote -q origin in /Users/sonumb/go/pkg/mod/cache/vcs/420d97389aec40b542b69c679343083fd3c8679b219de4a1cae489c66c97ce40: exit status 128:
        fatal: could not read Username for 'https://github.com': terminal prompts disabled
Confirm the import path was entered correctly.

실행 및 결과

$ protoc -I protobuf/ protobuf/subdaemon.proto --go_out=plugins=grpc:protobuf
$ go build subdaemon_mockup.go ; ./subdaemon_mockup
main: wait-for-stop
2021/11/12 15:30:16 Received: Hi
message:"Hello Hi"
server-ticker
server-ticker
server-ticker
Warn: not yet implemented
server-ticker
SubDaemonGrpcServer Received Stop Message -> Do Stop
success:true message:"SubDaemonStop Success"
SubDaemonGrpcServer Received Stop Message -> Do Stop -- done
server stopped
Stop Client
end-main

$
반응형