Loading
2022. 5. 28. 18:52 - lazykuna

MSA에서의 디자인 패턴

사실 이에 대해서 처음에 쓴 문서가 이미 있는데, 각각의 세분화된 패턴을 다루기보다 보다 포괄적인 측면에서의 내용을 다루고 싶었고, 그래서 이제 글을 다시 정리해 봅니다.

시작

Micro-Service Architecture(이하 MSA)에서 디자인 패턴을 논하기 전에, MSA를 왜 쓰는지부터 생각해 볼 필요가 있습니다. 사실 생각해보면 MSA가 만능은 절대 아닙니다. 오히려 반대되는 개념인 Monolithic 아키텍쳐에 비해서 단점도 많습니다.

  • 훨씬 더 복잡한 시스템 구조
    • 네트워크가 들어가기 때문에 기존 단일 시스템과 달리 Consistency, Availability, Partition(CAP)를 같이 고려해야 하게 됩니다.
    • 이를 위한 장치로 로드밸런서, 메시지큐 등 아주 많은 요소들이 추가되게 됩니다…
  • 복잡한 구조로 인한 유지보수의 비용 증가
    • 이를 줄이기 위한 terraform, ansible, docker 등이 나오고는 있지만 사실 monolithic에 비하면 근본적인 복잡함은 비교가 안 되는 수준이고, 그마저도 저 툴들을 다 숙지해야 하는 판입니다.
    • 단순 인프라 구조 측면에서의 유지보수 말고도 서비스 장애 발생시 확인해야 할 것도 그만큼 많아집니다. 디버깅도 훨씬 어려워지고요.

그럼에도 불구하고 MSA가 대두되는 것은 트래픽 크기의 증가, 그리고 이에 따른 분산 시스템 시장이 성장하고 있기 때문일 것입니다. 트래픽 크기가 기하급수적으로 커지면서 확장에 유리한 분산 시스템 시장이 성장하게 되고, 때마침 생긴 컨테이너 기반의 배포환경(및 서버리스)이 도래하면서 이에 더욱 박차를 가하게 되었습니다. 덤으로 작아진 컴포넌트 단위, replication을 통해서 더 높은 안정성(availability)도 장점이 되겠죠.

하지만 도메인/비즈니스 로직만 분산 처리된다고 문제가 해결되지는 않습니다. 다른 구성 요소들이 발맞춰서 빠르게 동작하지 않는다면 병목이 발생하게 되고, 그럼 다 소용없는 일입니다. 그래서, DB나 데이터 프로세싱 등 거의 모든 분야에서의 분산시스템 활용이 도래하게 되었습니다.

위에서 이야기한 신경써야 되는 항목들을 쉽게 파악하기 위해서 사람들은 디자인 패턴으로 정리하곤 합니다. 즉, 병목을 최대한 발생시키지 않고, 대규모 데이터에서 최대 성능을 뽑아내기 위해 신경써야 할 요소들을 “MSA의 디자인 패턴"으로 정의해 볼 수 있을 겁니다.

MSA에서의 디자인 패턴

그럼, MSA에서 신경써야 할 내용들이 무엇이 있을지 생각해 봅시다. 사실 모든 디자인 패턴이 그렇듯이 정답은 없으며, 특히나 요즈음 변화가 빠르고 내용 자체가 고착되지 않은 특성상 이러한 범주 자체가 누군가에게는 틀릴 수 있습니다! 그래도 주관을 살짝 섞어 중요 순서대로 내용을 적어 놓습니다. *이 내용은 언제든지 수정될 수 있습니다.

  • 데이터베이스 (DB)
  • 서비스 간 패턴: 이벤트 소싱 패턴, CQRS, SAGA
  • 데이터 파이프라인 및 DAG 워크플로우
  • API 게이트웨이
  • 서비스 등록 및 디스커버리
  • 분산 추적 (Distributed Tracing)

데이터베이스

아주 크게는 RDB와 NoSQL로 나뉘어지고, 더 세분화되어서는 NoSQL에 이젠 많은 종류의 데이터베이스가 존재합니다. 물론 다 이유가 있습니다.

RDB는 정합성에 굉장히 큰 강점을 가지고 있습니다. 동시에 성능과 확장성에 취약합니다. 그래서 트랜젝션이 비교적 많지는 않지만 중요한! 정보, 복잡한 정보를 다룰 때 사용하기 적합합니다. (e.g. 회원정보, 신용정보 등)

NoSQL은 정합성에서 약간 손해를 보는 대신 (정확히는 eventually consistent) 대규모 트랜잭션에 최적화되어 있기 때문에, QPS/TPS가 높은 경우에 적합합니다. (e.g. 블로그 포스팅, 결제 내역 등)

여기에서 한 가지 더 주목해야 할 점은 NoSQL에도 종류가 여러가지가 있다는 점입니다.

  • 스케일 확장에 유리하고 속도가 매우 빠른 키-값 DB
  • 여러 형태의 복잡한 도큐먼트를 삽입/조회하기 위한 도큐먼트 DB
  • 필요한 column 데이터만을 빠르게 가져올 수 있도록 하여 대규모 트랜젝션 수행에 유리한 컬럼 DB
  • 그래프 형태로 연관성을 가진 “노드"들에 대한 조회에 유리한 그래프 DB

등 여러 종류가 있는데, 자신이 다룰 데이터 형태에 적합한 DB를 사용하고, 필요하다면 데이터별로 여러 DB를 만들어서 관리하는 것이 최적의 성능을 보여줄 것입니다. 또한, 역으로 사용될 도메인 데이터를 데이터베이스에 알맞게 가공하는 설계 능력 또한 중요할 것입니다.

정리하면, 사용하고자 하는 자료를 파악하고, 이에 적합한 데이터베이스를 선정할 수 있는 능력이 굉장히 중요합니다. 따라서 각 DB가 어떤 자료에 대해서 사용되어야 하는지, 더 나아가서는 실제 사용예가 어떻게 되는지까지 알아두면 좋습니다.

서비스 간 패턴: 이벤트 소싱 패턴, CQRS, SAGA

이 부분은 도메인 서비스 개발에 관련한 내용일 거라고 생각합니다. 아무래도 어떤 task가 발생하게 되면 여러 서비스가 같이 합심(?)해서 작업이 진행되기 때문에, 서비스간의 통신 방법에 대해서 고민할 필요가 있고, 그 세부 내용으로 이벤트 소싱, CQRS, SAGA등이 있있습니다.

이벤트 소싱은 외부에 데이터를 전달하기보다는 자신의 이벤트를 전달하는 방식으로 task pipeline을 만드는 것이고, CQRS는 쿼리와 명령을 분리해서 작성하는 것, 그리고 SAGA는 task pipeline 실패에 대한 feedback을 줌으로서 atomicity을 보장하는 기술입니다.

  • 이벤트 소싱의 경우 자연스럽게 메시지큐를 사용하게 됩니다.
  • SAGA의 경우 two-phase commit 대비하여 성능이 좋습니다. Two-phase commit은 prepare과정에서 Lock을 걸기 때문에 경합이 생길 수 있으나, SAGA는 Lock을 걸지 않아 경합이 거의 없습니다. (실패가 많지 않을 경우 확실한 성능 향상 보장)
  • SAGA는 별도 오케스트라를 통해 중앙집중형으로 상태를 관리하거나, 간단한 경우라면 단순 P2P 메시징 만으로 구현할 수도 있습니다.

그리고 이렇게 패턴에 따라 서비스를 만들면, 특정 데이터에 접근하면서 부하가 많이 걸리는 로직이 존재할 것입니다. 그러면 해당 로직을 별개 서비스로 분리할 수 있을 것입니다.

보통 여기까지 아키텍쳐를 진행하면 적어도 구동 가능한 형태의 요소가 나오게 됩니다. 그래서 저는 이 두개를 가장 중요한 요소로 생각합니다.

데이터 파이프라인 및 DAG 워크플로우

빅데이터의 시대가 도래하면서 복잡한 데이터 가공을 위한 데이터 파이프라인 프레임워크들도 생겨나고 있습니다. 예로서 하둡, 아파치 에어플로우, 스파크, 스톰, 카프카 등이 존재합니다.

이들의 특징은 복잡한 형태의 대규모 데이터 가공(e.g. 텍스트 분석 및 가공, 머신러닝 등)을 수행한다는 것입니다. 그래서 이러한 구조에 최적화된 파이프라인을 프레임워크로 제공하고 있습니다. 예를 들면 Apache Spark의 경우 중간값을 저장하여 병목이 발생하던 기존 workflow와 달리 이를 메모리에 캐시해두어 최적화를 하고 있습니다.

또한 데이터들을 실시간성인 스트림 형태로 처리할 것인지, 혹은 데이터 레이크에 쌓인 데이터를 배치형태로 처리하느냐에 따라 또 아키텍쳐가 달라집니다.

  • 배치 형태라면 데이터레이크 + 하둡으로
  • 스트림(한번에 모든 데이터를 쓰는 것이 아닌 경우)이라면 스파크
  • 더 적은 레이턴시라면 카프카(데이터 큐)+스톰(데이터 프로세스)로 프레임워크를 만들 수가 있을 것입니다.

전반적인 workflow management(DAG)쪽에 관심이 있다면 직접 구현할 수도 있지만, Apache airflow나 temporal을 이용하여 작업 내역을 추적할 수도 있을 것입니다.

 

네이버 D2에서 Kafka + Storm 통해 데이터 파이프라인을 구성한 예제

그리고 당연하지만 목표하는 데이터 분석을 하기 위해서 어떤 알고리즘/모델, 저장소, 툴을 사용해야 할지도 고민해야 할 필요가 있습니다. 메타데이터는 DB에 저장하고, 영상과 같은 큰 데이터는 S3, 딥러닝을 위해서는 pytorch (세부 모델은 데이터 따라~), 파이프라인은 Spark 등 프레임워크를 선정하고 이를 통해서 세부 프레임워크를 짤 수 있겠네요. * 이 경우에는 머신러닝 모델과 이를 위한 아키텍쳐도 구성해야 할 수 있으나 굉장히 전문 지식이 필요한 부분이므로 여기서는 스킵 ...

또한 데이터 파이프라인으로 처리할 데이터셋들을 선정하고, 이를 데이터레이크에 넣고, 실제 서비스 할 데이터는 데이터 웨어하우스에 넣을 분류 작업도 고민해 볼 수 있습니다.

언급한 내용들 중 빅데이터 / 머신러닝 엔지니어 관련 도메인 지식도 있지만, 결국 이러한 작업의 일련으로 대규모 데이터에 적합한 형태의 NoSQL 및 워크플로우 구성을 해야 한다는 점에서는 큰 차이가 없기도 하여 여기 짤막하게 적어 둡니다.

API 게이트웨이

열심히 아키텍쳐를 구성했다고 끝은 아닙니다. 각 마이크로서비스 별로 엔드포인트는 어떻게 할지, 인증절차는 어떻게 할지 등의 현실적인 문제가 남아 있습니다. 이를 API 게이트웨이를 통해 쉽게 해결할 수 있습니다.

  • 보안 — API 게이트웨이에서 토큰 검사, 각 마이크로서비스가 이를 수행할 필요가 없도록 함
  • 캐시 — 상황에 따라 API 게이트웨이단에서 결과값을 캐시하여 부하를 줄일 수도 있음
  • 모니터링 — API 게이트웨이 자체에서 모니터링을 수행하여 가용성에 도움을 줄 수 있음

서비스 등록 및 디스커버리

MSA 간의 통신을 하기 위해서 또 고려해야 할 것은 바로 다른 서비스와 접근을 어떻게 할 것인지에 대한 내용입니다. 어째서 이것이 문제가 되느냐면, 특정 서비스를 직접 IP 주소로 접근하게 되면 이후 해당 서비스의 주소가 동적 할당(변경)되었을 때 곤란해지기 때문입니다.

예를 들어 운영중에 어떤 서비스를 업그레이드 해야 하는 상황인데 업그레이드 후 해당 서비스의 주소가 변경되는 경우가 생긴다면, 서비스 디스커버리 없이는 연관된 시스템을 불필요하게 재가동해야 하는 상황이 생길 수 있습니다. 이러면 가용성의 문제가 생기겠죠.

또 그 이외에도 서비스 스케일링 처리를 할 때, 여러 Pod들에 로드밸런싱을 해야 하는 경우에도 서비스 디스커버리가 이를 처리해 줄 수도 있습니다.

물론 서비스 디스커버리 없이 DNS + k8s 로드밸런서 조합으로도 충분히 가능합니다. 하지만 동적으로 IP가 자주 바뀌는 환경의 경우에서는 충분히 고려해 볼 수 있습니다.

분산 추적

문제가 발생하면 로그를 통해 원인을 분석하게 됩니다. 그리고 MSA는 시스템이 여러군데로 나뉘어져 있는 특성상 이 작업이 매우 어렵습니다. 그래서 이를 위한 툴이 있으면 상당히 좋습니다.

다양한 툴이 있는데, 이를테면 zipkin 등이 있습니다. 이를 통해 “작업 단위로 로그"를 볼 수 있도록 하여 문제 파악이 훨씬 용이해집니다. 이런 툴이 없다면, 연관된 서비스를 일일이 찾아서 해당 시간대에 발생한 로그들을 전수조사해야 해서 훨씬 힘들어지겠죠.

이외에도…

글이 길었지만, 결국 MSA는 거대한 데이터를 용이하게 처리하기 위함이 큰 목적이기 때문에, 최대한 효율적으로 작동할 수 있도록 하는 데에 설계의 주요 목적이 있습니다. 그렇게 하기 위해서는 결국 큰 그림에서의 병목이 되는 지점을 찾아나가는 것이 핵심이 될 것 같습니다. 알고리즘 최적화와 같은 지엽적인 최적화와는 다릅니다.

이를 위해서 계산적인 요소를 넣을 수 있도록 노력해야 합니다. 이를테면 Peak time에 어떤 DB/서비스에 과부하가 걸릴 것인지 찾아야 하고, 이를 위해 TPS / 트랜잭션당 데이터 사이즈 등을 계산해야 합니다. 그리고 이 점을 고려하여 해당 서비스에 최적화되도록 아키텍쳐를 구성할 수 있을 것입니다.

(물론 scalability가 만능은 아니고, trick을 사용하기도 합니다. 특히 SNS와 같이 빠른 응답을 요구하는 초거대 데이터를 다루는 곳일수록)

그리고 더 나아가서는 업그레이드까지 고려해서 구성요소(시스템/DB 등)을 나누는 것도 좋은 선택이 될 수 있을 것입니다.

참고