Microservice Design 3일차 - Event Driven Architecture, Message Delivery Guarantees

0

microservice

목록 보기
3/6

Event-Driven-Architecture

먼저 기존의 전통적인 요청-응답 구조를 보도록 하자.

client <--> API Gateway <--> Subscription service
                                    ^
                                    |
                                    v
                            Payment service
                                    ^
                                    |
                                    v
                            user service

우리는 구독 서비스 기반의 프로그램을 만든다고 하자. client가 구독 버튼을 누르면 API Gateway를 통해서 subscription service로 요청이 전달된다. subscription service는 payment service로 요청을 보내어 응답을 기다린다. payment serviceuser service를 통해 user`의 정보를 가져오거나, 결제가 완료되면 user의 정보를 업데이트한다.

이러한 문제를 해결하기 위해서 orchestration service를 만들어 병렬적으로 각 service를 동작시킬 수 있도록 만들 수 있다. 참고로 orchestration serviceAPI Gateway 내부의 일부 서비스로 만들 수도 있다.

                          Subscription service
                         /
                        /
client <--> API Gateway ------- Payment service
      (Orchestation service)
                        \
                         \
                          user service

client의 요청을 받으면 API Gateway의 Orchestation service가 각 service들에게 병렬적으로 요청을 보내는 것이다. 이는 가장 긴 transaction 처리 시간이 전체 시스템 응답 시간을 차지하겠지만, 여전히 복잡한 구조이고 API gateway와 service 간의 결합력이 더 강하게 유착된다는 문제가 남아있다.

이러한 요청 chain의 길이가 깊어질 수록 발생하는 가장 큰 문제는 각 service들끼리의 요청-응답을 기다려야하는 구조이다. 이는 사용자에게 너무 긴 시간동안 기다리게 만들어주는 문제를 발생시키고, 에러가 발생했을 때, 각 chain의 어떤 부분에서 에러가 발생했는 지 알기도 쉽지 않다.

event기반 아키텍처는 이러한 문제를 해결해주는 새로운 구조를 제사한다. 먼저 event기반 아키텍처에의 가장 핵심인 event는 다음의 정의를 가진다.

event: 어떠한 사실, action, 상태 변화로 항상 불변성을 가진다. 또한, 무기한으로 저장될 수 있으며, 다른 서비스들에 의해서 여러 번 소모될 수 있다.

event기반 아키텍처에서 event를 교환하는 참여자 3개 집단이 있다.


                                          | -> Consumer1
----------              ----------------  | -> Consumer2
|Producer|  --event---> |Message Broker|--| -> Consumer3
----------              ----------------  | -> Consumer4
                                          | -> Consumer5
  1. producer: event를 생성하는 생성자
  2. message broker: event를 관리하고 라우팅하는 broker
  3. consumer: event를 소모하는 사용자

여기서 message broker는 event를 전달받으면 안전하게 관리해주며, 특정 라우팅 룰에 따라 consumer에게 어떤 event들을 라우팅해줄 지 결정해준다.

요청-응답 기반의 방식에 비해 event-driven방식이 가지는 특징은 다음과 같다.
1. Asynchronous
2. Inversion of control(제어의 역전)
3. Loose coupling

--------                    --------
|      | ----request----->  |      |
|Client|                    |Server|
|      | <---response-----  |      |
--------                    --------

위와 같은 요청-응답의 구조는 synchronous하게 동작한다는 특징을 지닌다. 즉, clinet가 server에게 요청을 보낸다음 응답이 올 때까지 다른 일을 하지 못하고 기다려야한다는 문제가 있다는 것이다.

또한, client는 server의 API를 호출하기 위해서는 server의 API 특징과 매개변수와 응답에 대해서도 알아야한다. 이는 server와 client가 강하게 연결되었다는 것을 말한다.

반면에 event-driven 구조는 다음과 같다.


                                          | -> Consumer1
----------              ----------------  | -> Consumer2
|Producer|  --event---> |Message Broker|--| -> Consumer3
----------              ----------------  | -> Consumer4

producer는 consumer의 특징에 대해서 알 필요가 없다. 그저 event를 Message broker에게 전달하는 방법만 알면 된다. 이는 producer와 consumer 간의 관계가 약하게 결합되어있는 상태를 말한다. 또한, event를 전달하고 결과를 기다리지 않아도 되므로 asynchronous하게 동작한다는 것을 알 수 있다.

이는 client의 요청을 event로 만들어 요청을 처리하는 권한을 외부 엔티티로 전달하기 때문에 내부적으로 main process를 통해 처리하는 것이 아니다. 이는 IoC의 특징이며, 처리 권한을 외부의 엔티티로 전달하여 내부의 결합성을 제거하는 것이다. 덕분에 Event driven 구조에서는 component들 간에 서로를 인식할 필요가 없고, 분리되어있기 때문에 느슨한 결합 시스템을 구축하는데 적합한 선택이다.

그럼 언제 event driven architecture를 써야할까?? 다음의 대표적인 case들이 있다.
1. fire and forget: event가 발생시켰지만 거기에 대한 성공 여부가 사용자 경험에 영향을 주지 않는 경우이다. 가령 로그를 쓰는 것이 있다.
2. Reliable delivery: event가 반드시 실행되어야 하는 경우
3. infinite stream of events: iot data가 stream 형식으로 계속 들어온다면, 이를 event로 받아내어 계속 처리하도록 한다.
4. Anomaly detection/pattern recognition: 주기적으로 data를 받아내어 metric로 사용할 때
5. Broadcast: event를 받고자하는 모든 참여자에게 전송
6. Buffering: 대량의 traffic을 견딜 수 있도록 버퍼링을 제공해야할 때(Message broker의 특징)

반대로 요청-응답이 더 좋은 case들이 있다.
1. 빠른 요청-응답
2. 간단한 interaction

Event Delivery Patterns

여러가지 event전달 pattern들이 있는데, 첫번째는 event streaming이다.

event streaming은 message broker가 event의 임시 또는 영구 저장소로 사용된다. 이는, event가 다른 소비자에게 소모되어도 다른 소비자들도 똑같은 event를 사용할 수 있다. 또한, 특정 소비자가 나중에 도착해도 event streaming을 replay하도록 하여 이전의 event를 재생시켜 새로 온 소비자도 event 처리를 실행시킬 수 있도록 할 수 있다.

따라서, 새로운 소비자를 만들어내어, 이전에 발생했던 event를 기반으로 이상 감지 패턴을 확인할 수 있도록 활용할 수 있다.

message broker 덕분에 event streaming 기능은 신뢰성있는 전달이 보장된다.

정리하자면, event streaming은 event를 연속적으로 캡쳐, 저장, 실시간 처리, 분석, 전달하는 데이터 처리 패턴으로 주로 모니터링, 실시간 데이터 처리, 로그 메트릭 수집 등에 사용된다.

두 번째 패턴은 pub-sub pattern이다. 소비자들은 subscriber가 되어 특정 topic을 message broker에 구독한다음 event가 발생했을 때, 해당 정보를 subscriber에게 broadcast해줄 수 있다. 또한, 새로운 소비자들이 기존의 topic을 구독하는 것도 가능하여 확장성이 매우 휼륭하다.

이는 message borker가 broadcast용 임시 저장소로 쓰인다고 볼 수 있다. 또한, fire and forget 방식처럼 event를 전달만하고, 그에 대한 실행 결과는 필요없을 때 많이 사용한다. 주로 알림이나 채팅 시스템 같은 경우가 있다.

이벤트 중심 아키텍처에서의 메시지 전달 의미

3개의 집합인 producer, message broker, consumer 사이에서 network error나 지연이 발생하여 data 유실이 생기면 어떻게 해야할까?? 다시 event를 재전송 하면 된다고 생각하지만, 이 문제는 이전 event가 도착해버리면 중복 event가 발생할 수도 있다.


producer가 message Broker에 event를 발행할 때 ACK을 받아야한다. 만약 받지 못할 경우, 재전송을 하게 되는 경우가 있는데, 이 경우 이전에 전달했던 ACK이 늦게 온 경우라면 동일한 event가 message broker에 담겨있어 문제를 야기한다. 또한, consumer의 경우도 event를 받고 ACK을 전달해야하는데, ACK을 늦게보내면 event가 전달 도중에 누락된 줄 알고 다시 consumer에게 전달하는 경우가 있다.

이렇게 producer, message broker, consumer 사이의 event처리 정책들이 있는데, 이는 universal하게 쓰이는 기술적 용어로 생각하면 된다.

각 정책들은 producer와 consumer에 적용되며 동작은 약간 다르다.

At-Most-Once

At-Most-Once: 무슨 일이 있어도 최대 딱 한번만 전송한다. 이는 중복 전송을 피하고 싶어서 사용하는 것이다.

  • producer: producer가 생성한 event가 message broker로 전달되었을 때 ack이 producer에게 가지 않았다면 event를 또 생성해서 보내지 않는다는 것이다.
  • consumer: consumer의 경우도 event가 오면 바로 message broker에게 ack을 보내고, event 처리를 처리한다. 이는 event 처리 도중에 error가 발생해서 시스템이 다운되어 다시 실행되면, 이전의 event를 다시 받을 수 없게 된다.

결과적으로 At-Most-Once는 데이터의 일부 손실을 허용한다.

At-Most-Once를 쓰는 경우는 위치 정보를 업데이트하는 경우가 그렇다. 적은 오버헤드와 지연시간을 가지므로, 수시로 데이터를 업데이트하면 된다. 일부 데이터가 유실되어도 큰 문제는 없다.

At-Least-Once

At-Least-Once: 반드시 데이터의 전송을 보장한다.

  • producer: 이는 producer가 event를 생성하고 message broker에 event를 push했지만, ack이 오지 않는 경우 다시 event를 또 생성해서 전달한다는 것이다. 이 경우 event를 잃지 않도록 보장하지만, event가 중복으로 생성되는 문제가 있다.
  • consumer: consumer 역시도 event를 받아 모든 처리를 완료한 후에야 ack을 message broker에 전달한다. 이는 consumer가 event를 처리하는 도중 다운되어도, ack을 보내지 않았기 때문에 다시 실행되면 event를 받아 처리할 수 있도록 하는 것이다.

결과적으로 At-Least-Once는 이벤트를 절대로 일어버리지 않는다. 단, 이벤트 중복을 허용하는 문제가 있다.

알림을 보내는 경우를 생각해보자, 알림을 두 번 보냈다고 큰일이 생기지 않는다. 리뷰 역시도 마찬가지이다. 리뷰를 두 번 남기거나, 덮어쓰게해도 문제가 없다.

그러나, 이러한 방식은 지연 시간이 너무 긴 단점이 있다. 왜냐하면 producer가 event를 message broker에 전달하고 ack을 받을 때까지 blocking으로 동작해야하며, consumer 역시도 자신의 모든 로직을 마친 다음에 ack을 보내기 때문에 message broker는 message queue를 바로 비워내지 못한다. 또한, 손실이 발생하면 이를 처리하는 시간이 매우 오래걸린다.

Exactly-Once

Exactly-Once 재정이나 금융권에서 사용되는 것으로 가장 낮은 처리율과 높은 지연시간을 가진다. 즉, 오버헤드가 크지만 데이터의 안정성이 보장되고, 중복과 같은 에러가 발생하지 않는다.

  • producer: producer가 message broker에게 event를 전달 할 때 일시적인 event에대 한 id값을 발급받는다. 이는 message broker내에서 event를 식별하기 위해서 사용된다. 이 id값과 event를 함께 producer가 message broker에 전달하는 것이다. 만약 message broker로부터 producer가 ack을 받지 못하면, 다시 event와 id를 함께 message broker로 전달한다. 단, id값은 이전과 동일하다. 만약 해당 id를 가진 event가 이미 message broker의 log안에 있다면 event를 추가하는 연산을 무시한다. 이는 producer 측에서 단, 한 번만 event를 전달한다는 의미를 충족시킨다.
  • consumer: event를 받아서 event처리를 먼저 진행한 후에 ack을 messsage broker에 전달한다. 가령 계좌 이체 데이터를 받아서 database에 반영했다고 하자. 이제 ack을 보내면 되는데, 해당 consumer service가 다운되어버리는 경우가 있을 수 있다. 이때 새로운 인스턴스가 생겨나거나, 기존 인스턴스가 복구되면 message broker는 ack이 안왔으므로 해당 event를 인스턴스에 다시 전달하게 될 것이다. 문제는 이미 해당 event에 대한 계좌 이체가 DB에는 이전에 반영이 되어있었다는 것이다. 이는 중복 계좌 이체를 만들 수 있다. 따라서, 멱등성을 보장하기 위해서 consumer는 db에 저장할 때 최근에 실행된 event id도 같이 적는 것이다. 그렇게 적도록하여 event가 중복으로 들어와도 event id를 보고, 이미 db에 처리되었으니 무시해야겠다고 판단 할 수 있도록 하는 것이다. 이는 message broker에서 해주는 것이 아니라, 개발자가 직접해야한다.

consumer 측이 조금 복잡한데, 이는 message broker 이외의 외부 시스템인 DB에 같이 결합되어있어서 Exactly-once가 보장되지 않을 수 있기 때문이다. 그래서 DB에 event id를 적도록 하여 Exactly-once를 보장하는 것이다.

실제로 redis의 경우 pub-sub은 At-Most-Once이지만, stream data의 경우는 At-least-Once이다. kafka의 경우는 At-least-Once가 default이고 설정을 통해 바꿀 수 있다.

0개의 댓글