MSA 서비스간 통신 (MSA-2)

MSA IPC

앞서서 MSA 서비스를 어떻게 나눌것인가에대해 알아보았습니다.
그다음으로 정해야 할 것은 MSA의 핵심이라 할 수 있는 서비스간의 통신 IPC (Inter-Process Communication) 입니다.

MSA가 아니라도 서비스를 구축할때는 각 서비스의 환경에따른 다양한 방법으로 통신을 해야하는데, 아래와 같은 다양한 통신 방법들이 있습니다.

  • 1:1 통신
    • Request / Response
    • Request / None-block Response
    • Request / No Response (Notification)
  • 1:N 통신
    • Publish / Subscribe
    • Publish / Async Response

서비스 환경 및 조건에 따라 구현해야하는 통신의 방법이 결정되어야 할 것입니다.

IPC는 결국 서비스간 메세지 교환입니다.
이때 사용되는 메세지들은 아래와같은 포맷들 있습니다.

  • 텍스트 메세지 포맷
    • JSON
    • XML
  • 이진 메세지 포맷
    • protocol buffer
    • avro

텍스트 메시지의 경우 사람이 읽을 수 있고 텍스트 자체로 의미가 분명하게 전달된다는 장점을 갖지만 메시지가 길어지고 파싱하는 등의 오버헤드가 생긴다는 단점이 있습니다.

이와 다르게 이진 메세지 포맷은 사람이 읽을 수 없는 이진 데이터를 메시지로 통신을 하고 컴파일러가 메시지를 serialize/deserialize하는 코드를 생성하여 사용합니다. 텍스트 메시지에 비해 오버헤드 적어 많은 통신이 일어나는 환경에서 유리하지만 메시지 만으로는 사람이 이해할 수 없기 때문에 IDL(Interface Definition Language)을 정의해 사용해야 합니다.


API (Application Programming Interface)

MSA의 서비스는 각 서비스들이 하나의 서비스가 되기도하고 다른 서비스를 호출하는 클라이언트도 되기때문에 서비스와 클라이언트간의 약속이 매우 중요합니다.

어떤 통신 기술을 선택하든지, 서비스 APIIDL로 정확하게 정의해야하며 서비스 개발전에 API를 우선적으로 설계/정의해 주는것이 좋습니다.

물론 API는 늘 동일하게 정의 할 수 없으며, 어떤 IPC를 사용하냐에 따라 달라지게 될 것입니다.

  • HTTP 통신 : URL, HTTP Method, request 포맷, response 포맷 …
  • 메사지 통신 : 메사지 채널, 메시지 타입, 메시지 포맷

또 한가지 MSA에서의 중요한 고려사항은 ‘서비스들의 배포는 동일하게 일어나지 않는다’는 점 이기 때문에 서비스API의 업데이트는 이점을 고려하여 업데이트를 진행 할 수 있어야합니다.

  • 시맨틱 버저닝
    • Major.Minor.Patch (Version x.x.x)를 나누어 규칙에따른 버전관리
    • URL 혹은 메시지에 버전을 함께 명시하여 API 호출 할 수 있도록 함
  • 하위 호환 업데이트
  • Major 업데이트시 하위 버전 동시운영

일반적으로 API Gateway를 사용하여 서비스별 버전 관리를 수행하기도 합니다.


동기 RPI(Remote Procedure Invocation)

REST

현재통신의 ‘대세’인 RESTHTTP를 사용해 자원(RESOURCE) : URL, 행위(Verb) : HTTP Method, 표현(Representation) : Json, XML, RSS으로 구성된 IPC 입니다.

대부분 이미 잘 알고계실테니 자세한 설명은 생략하고 장/단점을 나열해보도록하겠습니다.

장점

  • 단순하고 익숙하게 사용 가능
  • 사람친화적 언어 사용 (Json)
  • curl등을 사용하여 간편한 테스트 가능
  • 중간 브로커가 필요하지않음

단점

  • request/response 스타일 통신만 지원
  • 한번요청으로 많은 리소스를 가져오기 어려움
  • 클라이언트/서비스 모두가 활성화된 상태에서만 사용 가능하여 가용성이 떨어짐

gRPC

이진 메시지 기반의 프로토콜인 gRPC는 REST의 단점들을 보완하여 대체가능한 새롭게 각광받고있는 IPC 입니다.

1

위에서 언급했듯이 이진 메시지를 사용하기때문에 API를 우선적으로 설계하면서 IDL을 정의하는게 중요합니다. 대표적으로 gRPCprotocol buffer를 사용하며 컴파일러를 통해 클라이언트 stub과 서버 skeleton을 손쉽게 구성해 줄 수 있습니다.

gRPC가 생소한 분들을 위해 아래에 예제 코드를 첨부하였습니다.

// Server

//HelloWorld.proto

// The greeting service definition.
service Greeter {
  rpc SayHello (HelloRequest) returns (HelloReply) {}
  rpc SayHelloAgain (HelloRequest) returns (HelloReply) {}
}

message HelloRequest {
  string name = 1;
}

message HelloReply {
  string message = 1;
}
//GreeterImpl.java

private class GreeterImpl extends GreeterGrpc.GreeterImplBase {

  @Override
  public void sayHello(HelloRequest req, StreamObserver<HelloReply> responseObserver) {
    HelloReply reply = HelloReply.newBuilder().setMessage("Hello " + req.getName()).build();
    responseObserver.onNext(reply);
    responseObserver.onCompleted();
  }

  @Override
  public void sayHelloAgain(HelloRequest req, StreamObserver<HelloReply> responseObserver) {
    HelloReply reply = HelloReply.newBuilder().setMessage("Hello again " + req.getName()).build();
    responseObserver.onNext(reply);
    responseObserver.onCompleted();
  }
}

// Client

public void greet(String name) {
  logger.info("Will try to greet " + name + " ...");
  HelloRequest request = HelloRequest.newBuilder().setName(name).build();
  HelloReply response;
  try {
    response = blockingStub.sayHello(request);
  } catch (StatusRuntimeException e) {
    logger.log(Level.WARNING, "RPC failed: {0}", e.getStatus());
    return;
  }
  logger.info("Greeting: " + response.getMessage());
  try {
    response = blockingStub.sayHelloAgain(request);
  } catch (StatusRuntimeException e) {
    logger.log(Level.WARNING, "RPC failed: {0}", e.getStatus());
    return;
  }
  logger.info("Greeting: " + response.getMessage());
}

장점

  • 다양한 업데이트 작업이 포함된 API를 설계하기 쉽다
  • 큰 메시지를 전달할때 효율적이다
  • 양방향 스트리밍이 가능하여 RPI뿐만 아니라, message 통신또한 가능하다

단점

  • JS Client에서 사용시 일반 REST 연결작업보다 많다
  • HTTP/2 를 사용해 구형 방화벽은 지원하지 않을 수 있다

RPI 통신 실패

동기 RPIrequest 이후에 response를 대기하기 때문에 통신의 실패를 처리해 주지 않으면 작업의 실패를 넘어 서비스의 중단을 초래 할 수 있습니다.

MSA에서는 수많은 통신이 이루어지기 때문에 이에 뒤따라 분명히 수많은 통신실패들이 발생하게 될 것입니다. 따라서 통신실패를 처리할수 있는 설계를 잘 만들어 두어야 합니다.

  • 네트워크 타임아웃 설정 : 무한정 블로킹 방어
  • 미처리요청 개수 제한
  • 회로차단기 패턴 : 통신 실패율 임계치를 넘으면 이후 일정시간동안 발생하는 request는 모두 차단

위 내용은 Netflix Hystirx 오픈소스 라이브러리에서 통신장애를 처리하기위한 패턴들입니다.

서비스 디스커버리

RPI 통신을 하기 위해서는 서비스를 찾아 갈 수 있는경로를 알아야합니다. IP가 늘 고정되어있다면 좋겠지만, 일반적으로는 서비스 인스턴스를 생성 할 때마다 네트워크 위치가 동적으로 변경되기 때문에 서비스의 위치를 동적으로 찾아 연결해주는 서비스 디스커버리 서비스 사용이 필요합니다.

Netflix Eureka 오픈소스 라이브러리가 Spring 진영에서는 기본적인 서비스 디스커버리역할을 수행하고있으며 도커나 쿠버네티스 배포 플랫폼에는 해당 기능이 탑재되어있습니다.


비동기 메시지 통신

프로세스간 비동기 통신을 위해 일반적인 방법으로 메시지를 사용합니다.

메시지는 일반적으로 헤더(header)와 본문(body) 으로 구성되며 일반적인 텍스트 메시지를 전달 할 수도있고, RPC 요청과같은 커맨드 역할을 할 수도 있고, 이벤트를 전달 할 수도 있습니다.

비동기 메시지 통신에도 서비스의 조건에 따라 다양한 통신 패턴으로 구성이 가능합니다.

  • 비동기 Request / response

2

  • 단방향 notification

3

  • 발행 (pub)/ 구독(sub)

4

  • 발행 / 비동기 응답

5

일반적으로 메시지기반 통신을 할때는 서비스간에 직접적인 통신을 하는것이 아닌 메시지 브로커를 두고 운영하는경우가 많습니다.

RabbitMQ, ActiveMQ, Kafka메시지 브로커로써 많이 사용되는 오픈소스드들인데, 브로커 기반 메시징의 장단점은 아래와 같습니다.

장점

  • 느슨한 결합 제공
  • 메시지 버퍼링
  • 유연한 통신

단점

  • 성능병목의 가능성
  • 단일 장애점의 가능성 (single point of failure)
  • 운영 복잡도의 증가

메시지 브로커를 사용할때 주의해야 할 점도 있습니다.

  • 스케일 아웃된 컨슈머의 환경에서, 메시지 발행 순서대로 처리가 안될수있음
  • 메세지가 중복발행되거나, 컨슈머에서 중복 처리를 일으킬수있음

위와 함께 한가지 더 주의해야 할 점은 DB작업과 함께 메시지를 발행한다면, 메시지 발행 실패에 유의해야합니다. DB 작업과 함께 메시지 브로커의 트랜잭션이 수행되면 좋겠지만 요즈음 메시지브로커의 경우 분산트랜잭션을 지원하지 않기때문에 약간의 트릭과같은 로직을 사용하여 메시지에 트랜잭션을 유지시켜줄 수도 있습니다.

DB에 임시 메시지큐처럼 사용할 수 있는 테이블을 구성하여 별도의 프로세스에서 메시지를 발행해주는 메시지 릴레이 서비스를 구축함으로써 트랜잭션이 보장되는 메시지발급을 수행해 줄 수도 있습니다.


카탈로그 서비스 IPC

그렇다면 위에서 알아본 MSA에서의 IPC를 통해 우리의 카탈로그 서비스 MSAIPC를 구성해보도록 하겠습니다.

6

카탈로그 매칭시 한가지 조건이있다면, 카탈로그 테이블 뿐만아니라 상품 테이블에서도 카탈로그 매칭 정보를 확인 하기 위해 동기화된 테이블이 필요합니다.

따라서 catalog service에서 매칭 작업이 일어날때마다 prod service의 DB에도 테이블 데이터를 반영시켜주기 위해 비동기적으로 처리를 진행 해 줄 수 있습니다.

7