Loading
2022. 2. 14. 23:24 - lazykuna

MSA에서의 디자인 패턴 - 세부요소 및 Terminology

다양한 아키텍쳐들을 단순히 소개하고 정리하는 것을 넘어, 중요 순서대로 요소들을 추려서 글을 다시 썼습니다.

최근 분산 시스템을 기반으로 한 MSA(Microsoft Architecture) 기반 서비스의 수요가 많이 증가했다. 반대 개념의 Monolithic 프로그램만 개발해오던 나도 시대의 흐름을 피해갈 수 없었고, 조만간 관련 일을 해야 될 것 같다. 그래서 나도 공부하려고 내용들을 좀 정리해 보았다.

일단 MSA를 왜 쓰는지는 자명한 이야기라 짧게만 정리하면. 기존 Monolithic 시스템의 특성으로는 기하급수적인 트래픽을 처리하기 어려웠기 때문이다. Scale-out 방식의 도래, 안정적인 시스템의 수요, 그리고 클라우드/IaaS로 점차 트렌드가 바뀜에 따라서 자연스럽게 주류가 되었다.

그리고 MSA는 loosely coupled 될 수 밖에 없기 때문에, 최초 아키텍처를 구상할 때 결합성을 더 신경써야 한다는 특징이 있는데 간단하게 짚고만 넘어가는 걸로... (monolithic은 결합성이 엉망이어도 만들수야 있지, MSA는 만들수도 없으니...)

나는 큰 틀에서 MSA를 어떻게 써야 하는지(설계해야 하는지), 아키텍처 중심의 설계로 살펴보았다. 더 구체적으로는, MSA 패턴 관련하여 다룬 책도 있고, MSA 서비스 아키텍처도 어느정도 정형화된 상황이기 때문에 이들을 중심으로 공부해 보았다.

Shared library

MSA에서의 Shared library는 바뀌지 않는 정적인 코드들이 위치해 있어야만 한다. 일례로 Shared library에서 Order 이라는 객체를 제공하는데, 만약 이 Order 객체의 명세가 바뀌게 되면 모든 시스템을 업데이트 시켜야 한다. 이는 약한 결합으로 구성되어 있어 필요한 모듈만 업데이트 하면 MSA 구성에서의 장점을 없애버리는 큰 문제점이 된다. 유동적으로 변하는 객체나 서비스의 경우에는 별도의 시스템으로 분리하거나, Database로 분리하는 방식을 사용하는게 차라리 나은 설계가 된다. (GOD Classes Prevent Decomposition 참조)

실제로 모놀리식으로 회귀한 사례에 대한 아티클이 있다.

IPC

MSA는 기본적으로 시스템 간 통신이라는 점이 Monolithic과 제일 큰 차이점 중 하나일 것이다. 이를 위해서 IPC를 사용하게 되고, 아주 핵심적인 요소로서 작용하게 되므로 API를 결정하는 것은 중요하다.

IPC를 사용하는 형태는 보통 아래의 분류들로 나눌 수 있다. 잘 보면 동기/비동기 여부, 일대일/일대다가 고려해야 할 핵심 요소로서 많이 등장한다.

일대일/일대다

클라이언트의 요청을 하나의 서비스가 처리하는지, 아니면 여러 서비스가 협동하여 처리하는지의 여부.

후자의 경우, 스케줄링 툴이긴 하지만 Apache Airflow에서의 내부 구현이 해당될 듯?

동기/비동기

어떤 요청에 대해서 동기/블로킹으로 처리할 것인지, 비동기/논블로킹으로 처리할 것인지 판단이 필요할 수 있다.

보통 오래 걸리는 작업들이 비동기 설계의 대상이 된다.

또 비동기 방식의 세부 구현으로 높은 요청을 처리하기 위한 메시지 큐(MQ) 같은 것을 이용하기도 한다. 이에 대해서도 고려하면 좋을 듯?

일대일 상호작용의 종류

1:1 상호작용의 경우 위에서의 동기/비동기 응답과, 단방향 알림 정도가 있다.

마지막 단방향 알림과 같은 경우는 성능이 중요하고, 응답이 필요없는(확인이 필요없는) 정도의 소소한 작업을 수행할 때 쓰지 않나 싶은데, 글쎄... 일대일 상호작용에서 그럴 경우가 많은지는 잘 모르겠다.

일대다 상호작용의 종류

1:N에서는 Pub/Sub(발행/구독), 발행/비동기 응답 정도가 있다.

Pub/Sub은 유명한 메시지 큐(MQ) 모델이 있고, 발행/비동기는 발행 후 특정 시간동안 응답을 기다리는 구조인데, 이것 또한 MQ의 일부 구현 방식으로 알고 있다. (Message Timeout)

API Versioning

그리고 API는 바뀔 수 있는 요소이다. 그래서 이에 따른 디자인을 해야 하는데, 보통 이를 위해 MAJOR/MINOR/PATCH semantic versioning을 취하게 된다. 그리고 backward compatilibty 또한 고려해야 할 것이다.

이를 API에 녹여내면, URL이나 JSON/XML에 정보를 같이 전달하는 식으로 디자인할 수 있다. api/v1/… 식으로...

Service discovery 패턴

API의 도래에 따라 자연스럽게 서비스 디스커버리가 생겨나게 되었다. REST이든 gRPC든 서비스를 호출하는 코드를 개발하면 해당 서비스를 호출하는 HTTP/코드가 어디 있는지를 파악해야 하는데, 이를 유연하게 할 수 있도록 하기 위해서는 서비스 디스커버리의 존재가 필수적이다.

서비스 인스턴스의 위치가 동적으로 바뀌거나 여러 사유로 클라이언트 코드가 달라질 때, 실행중인 서비스 재기동 필요없이 Service Discovery를 이용하여 해결해줄 수 있는 것이다.

Consul

"Discovery Service"를 구현해준다. 다른말로, 서비스간 통신을 위한 메커니즘 구현이다. Load Balancing / Service Discovery / Dynamic Request / Circuit Breaking 등등... 해준다.

크게 Server과 Client로 구성되어 있는데, Server 경우 4~5개 이상의 cluster로 구성하기를 권장, Client는 health check / server query 전송 등의 역할을 수행하게 된다.

각 Service는 name과 tag를 가지고, 이를 통해서 추후 필요한 service를 조회할 수 있다. DNS / HTTP API를 통해서 조회가 가능하다.

# bash
> dig @127.0.0.1 -p 8600 web.service.consul

# http
> curl http://localhost:8500/v1/catalog/service/web

분산 시스템에서의 효율적이고 정합성을 보장하기 위해서 아래와 같은 아키텍쳐를 사용하고 있다.

  • Failover: health 관련: failed node에게 reconnect request가 전달됨 (graceful left에는 시도하지 않음)

특징적인 것이, Gossip 프로토콜을 적극적으로 활용하고 있다는 것이다. peer to peer 프로토콜을 사용함으로서 아래와 같은 장점을 취한다.

  • 편리함: 전체에 propagate 하지 않아도 정보를 결국 다 전달받을 수 있는 구조
  • 강력한 내구성: 특정 경로가 끊어져도 다른 경로를 통해 전달받을 수 있음
    단점으로는 거짓 정보에 취약해질 수 있고, 중복전달에 대한 처리가 필요할 수 있다.

참조: https://yenoss.github.io/2019/04/04/Consul-tutorial-01.html
아키텍쳐: https://www.consul.io/docs/architecture

메시지 큐

동기 방식으로는 처리할 수 없는 수준의 많은 작업이 들어올 때 사용되는 디자인.

동기 방식으로 블로킹이 이루어지면, 서비스들 사이에서 그만큼의 대기시간이 발생하게 되고, 이는 전체 시스템의 효용성을 크게 저하시키게 된다.

메시지큐에서의 핵심은 확장성과 전체 시스템의 고가용성을 보장하는 것이라고 생각하는데, 이를 구현하기 위한 세부 아키텍처를 파악하면 도움이 많이 된다고 생각한다.

  • 메시지 브로커: 메시지를 보관하고 전달하고 이를 보장(정합성 보장)해주는 역할을 해주는 시스템.
    • 정합성을 보장해주는 DB 자체를 메시지 브로커로 쓸 수도 있겠으나, 아무래도 Pub/Sub에 최적화된 메시지 큐 시스템보다 더 무거울 수밖에 없다.
    • 아래의 플랫폼들이 현재 주류로 쓰인다.

Kafka

  • Pub/Sub 모델을 기반으로 하고, 가운데 broker가 위치하는 구조
  • 메시지의 대상은 특정 Client가 아니라 Topic이 된다.
    • 별도의 고급진(?) Routing 기능은 없음
  • Producer 중심적인 구조
    • 토픽에 대해서 다수의 파티션을 만들고, 이를 Producer이 마구 뿌림
  • Consumer이 Broker로부터 메시지를 가지고 가는 pull 방식
    • 컨슈머의 자원을 최대한도로 활용하는 구조
    • 성능을 위해 batch 형태로 가져갈 수도 있도록 함
  • broker들이 Cluster로 작동
    • Cluster 기반이 장애 대응에도 유연한 모습 (replication)
    • Cluster 시스템 기반으로 _Zookeeper_을 이용하고 있음.
  • 이를 통해 확장성(Scale-out)과 고가용성(High-availability) 및 안정성을 구현

RabbitMQ

  • 유연한 라우팅 가능
    • Exchanger이 라우팅을 수행 - 복잡한 형태의 라우팅도 자체적으로 지원
    • 관리도 쉽다
  • Broker 중심적인 구조
  • 마찬가지로 cluster 시스템 지원 - 안정성 및 고가용성의 이점을 취함
  • 메시지 전달 보장에 보다 집중한 모습
    • 아무래도 이러한 설계 때문에 broker 이 message push를 하는 것 같은데, 이로 인해 고가용성은 다소 떨어지는 모습을 보여주게 된다. 그래도 batch 처리하면 차이가 많이 줄지 않을까 싶긴 한데...

트랜잭션 관리

Monolithiic한 DB 시스템에서도 ACID 트랜잭션을 보장하는 건 그렇게 간단한 일만은 아니다. 그런데 분산 시스템에서는 더더욱 골치아프다. 시스템에서는 일반적으로 일관성/가용성/분할 허용성 중 두 가지만 취할 수 있다고 알려져 있고, 최근에는 성능을 위해 가용성을 집중적으로 취하는 모습이다.

이렇게 복잡한 분산 시스템에서 사용되는 트랜잭션 패턴에 대해서 정리해 보았다.

SAGA 패턴

일관성을 분산 트랜잭션 없이 보장하는 방법으로, 각 서비스를 트랜잭션 별로 수행하다가, 실패할 경우 역순으로 보상 트랜잭션을 수행하여 atomic을 보장하는 방법이다.

이를 구현하기 위해서는 호출에 참여하는 SAGA 흐름을 제어하는 역할을 수행하는 곳이 있어야 하는데, 해당 방식에 따라서 또 구현이 나뉜다.

  • 코레오그래피 : SAGA 참여자들이 흐름을 제어함 (분산 구조)
  • 오케스트레이션 : 오케스트레이터가 SAGA의 흐름을 제어함 (집중화 구조)

SAGA를 통해 트랜잭션의 결과적 일관성은 챙기겠지만, ACID의 격리(Isolation)가 되지 않아, 동시에 여러 SAGA가 수행되다 보면 잘못된 값을 보거나/덮어쓰는 문제가 발생할 수 있다. 이를 해결하기 위해 versioning, semantic lock 등의 방법을 제공할 수 있다.

비즈니스 로직 설계

서비스나 제품을 개발하다 보면 프레임워크와 별도로 핵심 가치를 수행하는 로직이 따로 있을 텐데, 이를 도메인이라고 부른다.

그런데 도메인 모델은 서로 얽혀있는 경우가 많다 보니 종속성도 복잡해지고 설계도 어려운 경우가 많다. 도메인 로직을 항상 말끔하게 분리할 수는 없지만, 이를 분리할 수 있는 방안에 대해 고민해볼 필요가 있다.

이 부분은 관련 서적으로 아예 책이 있을 정도이니 필요하다면 찾아읽는 게 좋을 듯. (DDD Start! 같은 것...)

DDD(Domain Driven Design)

도메인 위주로 시스템을 설계하는 것, 아래와 같은 요소들이 있을 것이다.

  • Entity: ID를 가진 객체
  • Value Object: 값들을 모아놓은 객체
  • Factory: 객체 생성 로직
  • Repository: entity들을 저장하는 DB 로직
  • Service: 여타 비즈니스 로직들

애그리거트(Aggregate)

이러한 도메인 로직을 자르는 단위로서, 아래를 보통 기준으로 삼아 디자인하게 된다.

  • 다른 Aggregate의 Root Entity를 참조할 때는 ID를 참조
  • 한 트랜잭션에서는 하나의 Aggregate만 변경
  • 작은 단위로 Aggregate를 구성 - 최소한일 필요는 없다, 과도하게 작으면 괜히 시스템의 복잡성만 증가...

이런 내용은 언제나 그렇듯 글로 쓴다고 되는 건 아니고 보다 심도깊은 책을 읽거나 실무를 해봐야 한다. 글은 참고만...

도메인 이벤트

한 애그리거트 단위에서 발생한 사건을, 다른 애그리거트에 공유하기 위해 이벤트를 발행한다. 세부적인 구현으로서는, Pub/Sub 인프라를 쓰거나, REST API 방식으로 이벤트 전달을 하여 최대한 종속성을 줄이는 설계를 해볼 수 있을 것이다. 크게 다룰 내용은 없고, 상태 공유를 이벤트로 할 수 있다 정도...

이벤트 소싱

이벤트의 발행, 저장 및 관리를 수행하는 것이다.

반대 개념으로 흔히 사용하는 ORM(Object Relational Mapping; 영속화 모델)이 있다. 그러니까, 대략 프로그램의 상태를 그대로 저장하는 것. 이벤트 소싱과 비교하여 ORM은 아래와 같은 단점들이 있다.

  • 지나간 작업 목록에 대해 기록을 찾기가 어려움
  • 로깅 구현의 어려움
  • 이벤트 발행 로직이 비즈니스 로직에 추가됨

이벤트 소싱을 이용하여, 특정 애그리거트 상태를 재연할 수 있다는 장점이 있다.

이벤트 소싱은 모든 이벤트가 기록되기 때문에, 최종 상태를 알기 위해서는 모든 이벤트를 처음부터 수행해야 하여 시간이 오래 걸릴 수 있고, 용량도 불필요할 정도로 많이 소요될 수 있다. 그래서, 이를 해결하고자 중간중간 상태를 저장하는 스냅샷을 사용할 수도 있고, CQRS 구조를 도입하기도 한다.

CQRS

이벤트 소싱에서의 읽기 성능을 높이기 위한 구조로서, 명령(Command)과 조회(Read)를 분리한다.

  • 명령으로 이벤트를 쌓고 조회(Read) 모델에서 조회
  • Event Exchange가 적절한 시기에 Read 모델을 갱신시켜 주는 역할을 수행한다.

단점?

  • 이벤트 삭제 불가능. 이벤트를 쌓는 것이 목적이다 보니...
  • 이벤트가 중복으로 올 수 있는 점을 고려하여 멱등하게 처리하는 것을 고려해야 함.

SAGA 패턴과 연동

위의 도메인 이벤트에서처럼, 다른 애그리거트와 연동해서 작업이 이루어져야 하는 경우를 단일 트랜잭션이라고 상정할 수 있다. 만약 작업이 취소되고 트랜잭션이 취소되어야 하는 경우라면, SAGA에서처럼 보상 트랜잭션을 수행할 수 있을 것이다.

  • 관련 제품: axon

분산(Cluster) 코디네이터

분산 시스템의 기반이 되는 프레임워크이다. 분산 시스템간 정보를 공유하고, 매개해주는 역할을 수행한다.

분산 코디네이터는 아래 특징이 있다.

  • 분산 시스템 자체를 밑받침해야 하기 때문에 빨라야 하고
  • 장애상황에 대해서 대응할 수 있어야 함 (데이터 유실 X, Failover 여부)

대표적인 Zookeeper에 대해서 확인해 보았다.

Apache Zookeeper

자체 아키텍쳐는 Follower과 Leader로 이루어져 있고, 여러 client들이 그들의 집합인 앙상블(Ensemble)에 붙어 있는 형태이다. Follower에 write 를 수행하면, Leader에 전달 후 다시 follower들에게 propagate 된다. 이로서 Server들이 데이터를 함께 가지고 있는 클러스터링 형태의 특징을 가진다.

내부적으로 파일 시스템을 사용하고 있는데, 단순하지만 용도에 알맞는 파워풀한 znode라는 분산 파일 시스템을 사용한다. 특징은...

  • 글로벌 락
  • 리더 선출
  • 글로벌 상태 갱신

Watcher이 있어서, 특정 파일 시스템에 변화가 발생하면 이벤트가 발생한다. 파일 시스템 뿐만 아니라 리더가 죽었거나 할 때도 이벤트가 발생한다. (이는 리더 재선출을 수행하게 만든다)

리더 선출의 경우, 위의 Watcher의 이벤트 기능을 이용하여, Leader 노드가 죽었을 때 다음 ID를 기반으로 다음 Leader을 자동으로 선출한다. 이로서 분산 시스템의 안정성을 보장한다.

정합성을 보장하는 방식은 최종 일관성(Eventually Consistent) 형태이다. 클라이언트의 요청은 분산 시스템 사이로 퍼져나가는 방식으로 업데이트가 수행된다. 이러한 방식을 가십 프로토콜(Gossip Protocol) 이라고 하는데, 모든 서버에게 요청을 보내는 Multicast와 다르다. 자세한 내용은 링크 참조.

아무튼, 들어온 요청은 time-based로 순차적으로 처리되도록 하고, 또한, failover로 다른 서버가 동기화를 진행할 때는 시간이 오래 걸리더라도 동기화 전까지는 response 하지 않는다. 이러한 구조로 인해 Write를 수행하는 데는 다소 비용이 들지만, 읽기 자체는 빠르다.

Zookeeper는 독립적인 서비스를 제공하기보다는 다른 플랫폼의 기반 요소로 쓰이는 경우가 많다. (Kafka, Storm, ...)

사용 예

스케쥴러

Task를 수행하기 위한 자원을 요청하고 사용하도록 하는 플랫폼들이다. 시스템 자원 특성상 이미 소유한 자원에 대한 선점(preemption) 등 신경써야 할 요소들을 자동으로 감안해서 최적의 throughput을 제공하는 역할을 하게 된다.

Hadoop YARN

MapReduce 기반의

여러 프레임워크의 분산 시스템들을 조율(orchestration)해주는 솔루션이다. 일종의 Job 기반의 Worker 모델 같은 느낌..?

먼저 Hadoop 플랫폼에 대해서 간단하게 짚고 넘어가면

  • 자원관리: YARN
  • 처리: MapReducer, Spark, Storm...
  • 저장: HDFS

본래 Job scheduler은 Hadoop의 MapReducer 안에 위치해 있었는데, Spark/Storm와 같은 다른 플랫폼의 도래로 이들을 묶어 스케줄링 할 수 있는 시스템의 수요가 생기게 되어 (확장성 문제), Hadoop YARN이 탄생했다.

YARN의 아키텍쳐는 다음과 같다.

  • 각 노드별로 Node Manager이 있음
  • Node Manager: Resource Manager에게 cluster의 리소스 정보(CPU, I/O, memory 등)를 전달함
  • Resource Manager에서는 스케쥴러, 어플리케이션 매니저, 리소스 트랙커로 구성되어 있음.

실제로 Client가 작업을 수행하는 경우에서의 이야기를 하면 -
Client가 Application을 제출하면, Application Master이 생성됨. 그리고 App Mstr은 YARN에서 Container을 요청함, 그러면 해당 container들에서 작업이 수행되고, 작업 내역은 Application Master에게 전달받아져 client로 전달됨.

그렇다면 YARN에서는 어떤 방식으로 자원 스케줄링을 하는가?

  • FIFO
  • Capacity - 특정 작업이 항상 일정한 리소스를 점유하도록 함. 예전에는 안됐지만 이젠 선점(preemption)도 지원한다.
  • Fair - "Queue"를 사용하여 계층 구조로 자원을 "동적" 할당해주는 구조. Fair이 보다 시스템 자원을 더 효율적으로 쓰게 하지만, 컨테이너 확보에 시간이 걸리기 때문에 선점이 조금 딜레이가 생길 거라는 것 빼곤 큰 차이를 잘 모르겠다...

Kubernetes

Container들을 관리하는 orchestration 툴로 널리 알려져 있다. 특이사항은 리눅스의 컨테이너 기능을 이용한 (namespace, cgroups) 환경 그 자체를 제공해주기 때문에, dependency 문제 같은 것들에서 아주 자유롭다는 장점이 있다. 또한 분리된 환경이라 각종 permission 관리에도 이점이 크다.

Kubernetes의 작업 단위는 Pod이다. 다시 말하면, 익히 알려진 컨테이너 설정을 하는 Dockerfiles에 추가로 네트워크, 스토리지 세팅이 포함된 yaml 단위로 배포된다.

이용하기에 따라 YARN 대신 쓰일 수도 있다. (Spark의 경우 kubernetes와 직접 연동 지원)

링크

Workflow 모델

Workflow는 어떤 Task를 수행하기 위해서 DAG(비순환 그래프) 형태의 작업들을 수행하는 것이다. 보통 Batch processing과 Asynchronous processing이 수반된다.

Batch Process란 예약된 시간에 프로세스를 수행하는 작업이고, 비동기 프로세스는 오래 걸리는 프로세스에 대해서 블로킹 없이 수행이 필요할 때 쓰이는 것이다. 보통 이러한 작업을 수행하기 위해서 cron 및 Worker 방식으로 구현이 많이 되곤 했다. 기존에는 DAG 형태의 dependency는 어떻게 했는지는 잘 모르겠다. 직접 도메인 로직으로 짰나? 그리고 이를 분산 시스템에 적용한 시스템이 나타나게 되었다.

대표적인 분산 시스템에서의 Workflow 플랫폼 몇 가지만 살펴 보았다.

Apache Airflow

Workflow를 구축하고 실행할 수 있는 플랫폼.

Workflow는 위에서 말한 것처럼 DAG(Directed Acyclic Graph)로 구성되어 있다. Task를 그래프 순차적으로 처리하겠다는 이야기이다.

아키텍처를 보면 눈에 띄는 점이 Scheduler 가 내장되어 있는 점이다. DAG의 스케줄링을 관리하고, 실행 경과 및 결과를 저장하기도 하는 등의 작업을 수행한다.

Temporal

Temporal 플랫폼. Cadence에서 fork 되었다고 이야기하고 있다.

사이트를 들어가보면 아래 문구로 간단 명료하게 소개하고 있다.

Temporal is the open source microservice orchestration platform for writing durable workflows as code.

철저하게 workflow를 Code로서 작성할 수 있도록 하여 IaC(Infrastructure as Code)에 충실한 게 특징이다. _Golang 기반인 것도 재미있는 특징?_-

내부 아키텍쳐를 살펴보면, 먼저 Temporal Cluster이라는 개념을 사용하고 있다. Temporal.io의 실제 서비스 로직이라고 할 수 있을 텐데, 프론트엔드 서비스 및 task 수행 등을 위한 상태 저장소를 요구한다. 또 다른 부분은 Application인데, worker가 붙어 해당 application을 수행하게 된다. 정확히는, temporal service가 요구하는 일을 수행하게 되는 것이다.

이러한 과정에 있어 핵심적인 역할을 수행하고 있는 것은 Task Queue이다. Task를 수행하기 위한 컨텍스트가 담겨져 있고, 정합성과 성능에 결정적인 역할을 끼치기 때문에 스케쥴링 및 failover 등 처리가 핵심적인 요소가 된다.

  • Scheduling
    • Worker이 polling하는 구조 : overloading 되는 것을 방지함
    • Server-side throttling 지원: task queue 자체에서 Task dispatching rate를 조절할 수 있고, spike 발생시 자동으로 dispatching rate를 증가시키는 구조.
  • Routing
    • Worker 및 process specific하게 Task routing을 결정할 수 있음
  • Failover
    • Worker이 내려가도 메시지는 Task queue에 있어, worker이 recover 되면 다시 수행할 수 있음.

이외 근본적인 기능은 Airflow와 둘 다 같을 것이라고 생각해서 더 쓸 내용은 없다.

실제 사용례...

아무래도 역시 실제 어떤 식으로 사용하는지 직접 보는 쪽이 더 좋다.

데이터 분석 디자인

위에서 잠깐 살펴보았던 MapReduce 디자인은 Hadoop 플랫폼에서 처음 도입된 형태이다. 어떠한 작업을 cluster에 분배해주는 Map 과정과, 각각의 결과를 모아 합치는 Reduce 과정은 분산 시스템에 최적화된 디자인 형태이다.

최초에는 Hadoop의 MapReduce 밖에 없었지만 이후에 Spark, Storm 등 여러 디자인이 나왔는데 이에 대해서 간략하게 찾아봤다.

MapReduce

  • 맵리듀스 모델을 충실히 따르는 가장 원초적인 형태
  • Job으로 구성되며, 각각의 Job은 Map / Reduce로 구성되어 있음.
  • Disk I/O 를 전제로 작업이 수행된다
  • 따라서 성능이 훌륭하지는 않으나, 큰 데이터에 대한 배치 처리를 할때 적합한 구조

Spark

  • Hadoop의 map-reduce를 위해서는 이에 대한 모든 과정을 직접 구현해야 하는데, 이는 Ad-hoc(임의) 데이터에는 적합하지 않은 형태 - 유저 친화적으로 쓸 수 있도록 변경
  • 분산 인메모리 프로세싱 엔진 으로서 하둡에 비해 빠르게 작업될 수 있는 여지가 있으나, 항상 메모리만 쓰는 것은 아니고, HDFS와 호환되기도 함.
  • Stream Processing을 도입: 실시간 데이터처리/batch가 가능하도록 함
  • 스파크에도 Job이 있는데, DAG(Directed Acyclic Graph)인 Stage로 구성됨. Stage는 분할되어서 분산 시스템에서 병렬로 실행됨.
    • 이러한 구조를 통해, 결과를 바로 기록하는 방식인 hdfs와는 다르게 바로 연속으로 다음 Stage에 데이터를 건네줄 수 있음
  • 실질적으로 DAG 그래프는 physical plan에 해당된다. 논리적인 작업형태인 RDD로 Job을 만들 수 있다.
  • 종합적으로, 전통적인 MapReduce 디자인에 추가적인 편의요소를 더 얹은 형태

출처
하둡과 맵리듀스 스파크의 관계

Storm

Hadoop과 Spark가 Job 단위로 돌아가는 방식이라면 Storm은 실시간 스트리밍 방식의 작업을 추구한다는 것이 차이점이다.

참고: https://d2.naver.com/helloworld/484148

로그 분석 중앙화

필요성

결국 독립되기 쉬운 구조인 MSA 구조에서 로그는 통합해서 관리할 필요성이 있기 때문이다.

  1. 전체 로그의 검색, 저장 관리를 위해 필요하다.
  2. 문제가 발생한 인스턴스 파악을 위해 필요하다. 마이크로서비스는 어플리케이션이 분리된 컨테이너에서 돌아가는 특성상 로그도 각 컨테이너별로 떨어지게 된다. 하지만 어떠한 사건(장애라던가...)이 발생하였을 때의 원인파악은 어플리케이션이 아닌 서비스 전체의 관점에서 볼 수 있어야 가능하다.
  3. MSA의 orchestration 서버가 역으로 특정 어플리케이션에 대해서 로그를 남기고 싶을 수도 있을 것이다.

복잡한 분석을 요한다면 이를 위한 별도의 Workflow가 추가될 수도 있다.

관련 툴

  • Elasticsearch
  • Fluentd
  • kibana

Service mesh 패턴

Microservice Architecture를 적용한 내부 시스템이 "Mesh 네트워크"처럼 복잡한 형태를 띄게 되면서, 이를 제어하고/추적할 수 있는 커다란 시스템을 원하게 되었습니다.

그냥 Service Discovery, Load Balancing, Circuit Breaking, Districuted Tracing ... 등을 모조리 합해놓은 패턴이라고 보면 된다. 위에서 설명해 놓은 패턴들의 총집합.

구현 자체는 사이드카 패턴과 연관성이 깊다. MSA로 들어가는 데이터 전후에 붙어서 통신을 제어하게 된다.

사이드카 패턴

사이드카 패턴은 어플리케이션 컨테이너와 독립적으로 동작하는 별도의 컨테이너를 붙이는 패턴을 이야기한다. 아무래도 DSP plugin 같은 거랑 유사한 느낌...

쉽게 붙이고 뗄 수 있어, 상호 의존성이 적은 구조를 보이는 이점이 있는 반면, 과다하게 의존할 경우 복잡성 등이 크게 증가할 수 있는 단점도 있다. 쉽게 붙이고 뗄 수 있는 구조가 필요한 경우 생각해 볼 만 하다. 아무래도 Service Discovery 패턴이랑 필히 같이 쓰여야 할 것 같은 느낌...

이상 감지도구의 예

  • 스프링 : Resilience4j
  • Hystrix

서킷 브레이커 - 장애 대처

기존 Monolithic 시스템에서 예외가 발생하면 try - catch로 받아서 처리하면 땡이었다. 하지만 Microservice Architecture에서는 그럴 수가 없다. 그리고, 서비스 상태에서 장애가 발생하면 치명적인 것은 둘 다 매한가지이다. 하지만 Microservice Architecture과 같은 경우에는 장애가 발생하면 다른 서비스까지 전파된다는 점에서 그 복잡성이 더 커진다.

이런 문제를 해결하기 위해 나온 서킷 브레이커는 오류 발생시 리소스를 잡아두지 못하게 하는 방법의 역할을 수행한다.

서킷 브레이커에서 각 노드들은 아래 3가지의 상태를 가진다.

  • Close
  • Open
  • Half-open

전기의 회로 차단기에 비유할 수 있는데, 문제가 발생하면 Open circuit로 만들어서 기능 자체의 동작을 막아버린다. 이후 정상화가 되었다고 판단되면, Half-open state로 기능이 정상적으로 동작하는지 간을 보게 된다. 최종적으로 정상이라는 판단이 되었다면, 그때서야 다시 Open state로 가게 된다.

Circuit breaker이 들어간 최종적인 아키텍처는 아래와 같이 된다. Server A와 B 사이에서 지속적으로 상태를 체크하고, 문제 발생시 traffic을 차단하게 된다.

사이드카 패턴과 유사하게 생겼는데, 실제로 사이드카처럼 붙여서 쓰는 보조격 패턴이라고 볼 수 있다.

샤딩 방식

DB에서의 Partitioning 과 유사한데, 그 중에서도 수평적 분할 (horizontal parition)이다.

다만, MSA, 특히 빅데이터에서의 샤딩은 빠른 속도가 절대적으로 요구된다. 그래서 보통 hashing 방식을 이용한다.

hashing에 따른 샤딩 방식은 보통 아래의 선택지가 있다.

  • Modular
    • 장점: 비교적 균일
    • 단점: DB 추가 증설 때 적재된 데이터 재정렬 필요 --> 특정 DB로 몰릴 수 있음
  • Range
    • 장점: 증설에 재정렬 비용이 들지 않음
    • 단점: 데이터 몰릴 수 있음
  • Consistent hashing (hash ring 사용)
    • 데이터 이전이 분산화되어서 비용도 비교적 적고 데이터 분배도 비교적 균일하게 됨.

샤딩을 하면서도 성능 및 안정성을 위해서 replicate를 동시에 사용할 수도 있다.

실제 사용 예

풀링 패턴 (Pooling Pattern)

자주 사용되는 리소스에 대해 Provider에서 사용할 때마다 생성, 반환시 성능상의 문제가 있다.
최초 정해진 갯수의 리소스를 초기화해서 Pool에 미리 만들어놓고 필요할때마다 사용하여 성능을 높일 수 있다.
생성 비용이 비싸고 재사용이 가능한 객체에 사용한다. 주로 네트워크 커넥션, 오브젝트 인스턴스, 스레드(Worker) 등에 대해서 많이 사용됨.

단독으로 알아놓을 필요는 없지만, 세부 구현을 하거나 아주 드물게 low-level 구현을 해야 한다면 알아둘 필요가 있어 짤막하게 정리.

세부 기술들

마이크로아키텍처와 직접적인 상관은 없을 수 있으나, 세부 기술로서 자주 사용되는 것들 일부만 조금 따로 빼서 다뤄 보았다.

로드 밸런싱

트래픽을 분산시키는 도구이다. 속도 향상뿐만 아니라 안정성도 책임지는 역할을 한다. B2C 같이 발생하는 트래픽 빈도수가 높을수록 그 역할이 중대하다.

Uber 아키텍처를 가져왔는데, Uber 아키텍처에서는 방화벽(WAF) 뒤에 로드밸런서를 두어

여기서 로드밸런스는 1.빠르고 / 2.균등하게 트래픽을 분산할 수 있어야 한다. Uber와 같이 큰 트래픽을 감당하는 경우에는 레이어3, 4, 7에 대해서 로드 밸런싱을 수행한다. Uber architecture 번역 글을 빌려오면,

레이어 3, 레이어 4 및 레이어 7과 같은 다른 로드 밸런서 레이어를 사용할 수 있습니다. 레이어 3은 IP 기반 로드 밸런서를 기반으로 작동합니다 (트래픽의 모든 IP는 레이어 3 로드 밸런서로 이동합니다. 레이어 4에서는 DNS 기반의 로드 발랜싱을 사용할 수 있습니다. Layer7에서는 어플리케이션 레벨의 로드 발랜싱 기반으로 작동합니다.)

각 레이어에서의 트래픽 분기 방식은 Round Robin, Least Connection, Hashing 등이 있다. 해싱 방식은 위에 나온 샤딩 방식과 같은 기술을 사용한다.

정도의 차이는 있을 수 있으나, 많이들 이러한 방식으로 아키텍처 및 로드밸런싱을 구성한다.

CDN / 스토리지 서버

데이터 파일을 서빙할 전용 서버를 두는 것 또한 좋은 설계 기술이다. static 파일들을 서빙하는 서버를 따로 둠으로서, 웹 서버는 오롯이 콘텐츠를 제공하는 데 집중할 수 있고, static 서버는 빠르게 데이터를 제공할 수 있어 최적의 환경을 구현해주는 데 도움을 준다.

CDN 자체에서도 최적화를 위한 다양한 기술을 사용하는데, CDN 서버 자체를 국지적으로 구성하고 적절한 라우팅 시스템을 갖추는 것과 같은 것들이 그러한 예다. 이외에도 사용자가 세팅할 수 있는 부분도 있는데, 자세한 내용은 CDN과 로드밸런서를 활용한 부하 분산 포스팅 참조.

멀티플렉싱

사실 이 부분만 봐서는 Microservice와는 전혀 관련 없고 웹 기술과 밀접한 관련이 있어 별도로 빼야 적합하다. 하지만 HTTP/2.0 프로토콜에서도 채택되었고, 최신 테크놀로지에서 많이 채용되는 모습인데 이러다 보니 MSA와 밀접한 연관이 있어 짧막하게 써 놓는다 (...)

multiplexing은 하나의 채널로 2개 이상의 데이터를 보내는 기술이다. 왜 쓰게 되었는지 간단하게 이야기하자면, 기존의 pipelined 경우에는 앞 데이터가 준비되어야 뒤 데이터가 보내지기 때문에 high-throughput에 제약사항이 생기는 경우가 있었다. multiplexing으로 Server push를 해버리면 아무래도 이런 제약사항이 사라진다.

성능을 극한까지 쥐어짜내는 걸 추구하는 분산시스템 환경에서 선호할 수밖에 없는 요소이다.

마무리지으며...

며칠간 꾸준히 조사했던 내용들을 하나로 우겨넣어 작성한 글이라서 이야기가 오락가락 한다... ㅠㅠ

분산시스템 및 마이크로서비스 아키텍처에서 근래 많이 사용하는 패턴들을 다루어 보았다. 꼭 분산시스템에서만 쓰일 것이 아닐 것들도 집어넣었다. 책에서 다룬 전통적인 분산시스템 내용 뿐만 아니라 근래 도래한 패턴들에 대해서도 마구잡이로 집어넣어서 신뢰성이 떨어지고 복잡성이 올라가지만... 적어도 이러한 내용들을 알고 있으면 architecture from 1000 feet 느낌으로 설계할 수 있지 않을까?

대략적으로 최근 MSA에서 많이 사용되는 구조나 패턴들을 최대한으로 모두 다 다뤄 보았는데, 놓친 부분이 있을지 모르겠다. 포스트를 올리는 이전 시점에 혹시 놓친 부류가 있다면 나중에 업데이트 해 보려고 한다 ㅎㅎ...

사실 아직 못다룬 내용들이 많다. MSA에서의 테스트, Deploy, 리팩토링 관련 내용들도 책에서 다루고 있었는데 시간과 내용 길이의 문제상 일단 뺐다. 기회가 된다면 나중에 별도 포스트로 다룰 수 있으면 좋겠다.

추가) 그리고 아키텍처 관련 큰그림은 이 블로그를 매우 추천한다! 정말 좋은 번역글이 많다.

출처 및 참조