Architecture

10 minute read


title: “[아키텍처] 대용량 처리를 위한 아키텍처 기본 개념 “ date: 2019-05-21 15:05:28 -0400 categories: jekyll update —

서버 아키텍처

  • failover(출처) 백업 시설로 프로덕션을 스위칭하는 프로세스
  • failback 재난이나 계획된 유지보수 기간 후에 프로덕션을 원래 위치로 되돌려 놓는 프로세스

  • 메시징모델
    • 큐(Queue) 모델 메시지가 쌓여있는 큐로부터 메시지를 가져와서 consumer pool 에 있는 consumer 중 하나에 메시지를 할당하는 방식
    • 발행-구독(pub-sub) 모델 topic 을 구독하는 모든 consumer 에게 메시지를 브로드캐스팅하는 방식이다.

    • 카프카
      • Consumer 와 Consumer Group (출처) 파티션은 consumer group 당 오로지 하나의 consumer 의 접근만을 허용하며, 해당 consumer 를 partition owner 라고 부른다. Consumer 가 추가/제거되면 해당 consumer 가 속한 consumer group 내에서 partition 재분배가 발생하고, broker 가 추가/제거되면 전체 consumer group 에서 partition 재분배가 발생한다. parition 개수와 consumer 수의 적절한 설정이 필요하다.

      • 분산 환경에서 용량뿐 아니라 복사본을 다른 노드에 저장함으로써 노드 장애에 대한 대응성을 가지고 있기 때문에 용량에는 확실하게 강점을 보인다.

      • 파일 시스템을 활용한 고성능 디자인 kafka 는 기존 메시징 시스템과는 다르게 메시지를 메모리 대신 파일 시스템에 쌓아두고 관리한다. 파일시스템을 쓰지만 기존 메시징 시스템보다 더 뛰어난 성능을 보여준다. Why? kafka 는 메모리에 별도의 캐시를 구현하지 않고, OS의 페이지 캐시에 이를 모두 위임한다. kafka 의 메시지는 하드디스크로 부터 순차적으로 읽혀지기 때문에, 하드 디스크의 랜덤 읽기 성능에 대한 단점을 보완함과 동시에 OS 페이지 캐시를 효과적으로 활용할 수 있다. 메시지를 파일 시스템에 저장하여 얻는 부수적인 효과도 있다. 메시지가 JVM 객체로 변환되며 크기가 커지는 것 방지 JVM의 GC로 인한 성능저하 회피 캐시 관리를 OS에 위임하기 때문에, 프로세스를 재시작 해도 OS의 페이지 캐시는 그대로 남아 있다. -> 프로세스 재시작 후 캐시 워밍업이 필요하지 않음 zero-copy 기법을 사용하여 데이터 전송 성능을 향상시켰다. -> 커널모드와 유저모드 간의 불필요한 데이터 복사를 피할 수 있다.

  • Object Storage AWS S3 왜 썼어요? Object Storage는 기존 파일 시스템과는 다르게 OS에서 디스크 형태로 마운트해서 사용하는 것이 아니라, HTTP/REST 와 같은 다른 파일 IO 인터페이스를 사용한다. 다른 인터페이스를 사용하기 때문에 파일 시스템으로의 접근은 OS 에 대한 제약을 받지 않는다. NFS와 같은 공유 파일 시스템은 중앙 컨트롤러를 통해서 접근하기 때문에 컨트롤러 병목에 의해서 동시 접속 클라이언트가 많을 경우 성능 저하가 올 수도 있다. 그러나, 이러한 Object Storage 같은 경우 처음부터 중앙 집중형 접속 포인트를 두지 않고, 아무 노드에나 접속을 가능하게 하는 형태이고, 파일 저장 구조 역시 전체 노드에 걸쳐서 분산해서 저장하기 때문에, 많은 사용자가 동시에 접속하는 대용량 파일 서비스 구조에 적합하다.

Business Layer

  • 스크립트 플랫폼 vs 자바 플랫폼 자바 플랫폼의 경우에는 여러 개의 데이터베이스를 묶어서 단일 트랜잭션으로 처리하는 분산 트랜잭션은 XA(eXtended Architecture) 라는 규격으로 지원해서 복잡한 트랜잭션 관리가 가능하고, 여러 프레임워크를 조합하여 복잡한 비즈니스 로직을 구현할 수 있다.

  • 스레드 풀 클라이언트로부터 요청이 오면 스레드를 미리 만들어 놓은 스레드 풀에서 스레드를 꺼내서 클라이언트의 요청을 처리하게 한다. 처리가 끝나면 스레드 풀로 돌려보낸 후 다른 요청이 오면 다시 꺼내서 요청을 처리하게 하는 구조이다. 동시에 처리할 수 있는 요청의 수는 스레드 풀의 스레드 개수와 같은데, 물리적으로 생성할 수 있는 스레드의 수는 한계가 있다. 즉, 동시에 처리할 수 있는 요청 수에 한계가 있다.

  • IO 효율 클라이언트 요청에 할당된 스레드는 IO 작업(DB, 네트워크, 파일)이 있을 경우 IO 호출을 해놓고 해당 스레드는 CPU를 사용하지 않는 대기(Wait) 상태로 빠져 버리게 된다. 즉, IO 응답 대기(Wait) 상태에서는 스레드를 점유하고 있으면서 아무 일을 하지 않는 비효율성이 발생하게 되는 것이다.

  • C10K 문제 현대의 대용량 서비스는 수만 건의 요청을 처리해야 하는 문제가 있기 때문에 대용량 시스템에서는 일반적인 스레드 풀 기반의 애플리케이션 서버로는 감당이 어렵다. 또는 위에서 살펴본 바와 같이 IO 블록킹 문제로 말미암아 IO 효율 상 성능 저하가 있을 수 있기 때문에, 근래에는 Node.js와 같은 싱글 스레드 기반의 비동기 서버를 사용하기도 한다.

  • Node.js 나 Nginx 는 하나의 스레드만을 사용해서 여러 클라이언트에게서 오는 요청을 처리한다. 단, IO 작업이 있을 경우, 앞에서 설명한 비동기 IO 방식으로 IO 요청을 던져놓고, 다시 돌아와서 다른 작업을 하다가, IO 작업이 끝나면, 이벤트를 받아서 처리하는 구조이다. 싱글 스레드 서버의 문제점은 CPU를 점유하고 무엇인가 작업을 하고 있을 때는 다른 요청을 처리하지 못한다는 것이다. 즉, 요청 처리에 걸리는 시간이 길다면 다른 요청을 처리하지 못하고 지연되기 때문에 CPU 사용량이 많은 로직에는 적절하지 않고, 빠른 처리가 가능한 간단한 로직에는 적절하다.

  • SN 아키텍처 확장성과 안정성을 보장하기 위한 SN(Shared Nothing: 무공유) 아키텍처라는 것이 있다. 분산 처리 시스템을 구성하는 여러 개의 노드가 서로 종속성을 가지지 않고 독립적으로 작동하는 아키텍처로, 종속성이 없기 때문에 이론적으로 노드를 무제한으로 늘릴 수 있으며, 노드 수에 비례하여 전체 시스템의 용량이 늘어나며, 특정 노드의 장애가 전체 시스템에 영향을 주지 않는 아키텍처를 일컫는다.

    노드 간의 종속성을 발생시키는 것이 노드 간의 공유 정보, 즉 상태 정보이다. HTTP 세션 정보가 대표적인 사례 이러한 상태 정보는 애플리케이션 서버의 클러스터링 기능을 이용하여 각 노드 간에 복제 되는데, 이 복제가 종속성을 유발한다. 이러한 공유 상태 정보를 제거해야 SN 아키텍처를 구현할 수 있다.

    상태 정보를 애플리케이션 서버에 저장하지 않으려면 어떻게 해야할까요? 다른 계층에 상태 정보를 저장하면 된다. 가장 쉽게 생각할 수 있는 건, 클라이언트에 상태 정보를 저장하는 방법으로, 쿠키를 사용하는 방법이 있다. 하지만, 쿠키는 저장할 수 있는 정보의 양에 한계가 있다. Memcached와 같은 캐시, 데이터 그리드 계층, NFS와 같은 공유 파일 시스템을 이용할 수 있다.

  • 데이터 그리드를 이용한 상태 정보 저장소 일종의 메모리 클러스터이다. 여러 개의 서버와 메모리를 연결하여 수십 기가의 메모리 저장소를 만들어놓고, 이를 애플리케이션 서버에서 접근해서 사용하는 방식이다. 주로 캐시, HTTP 세션, 사용자 로그인 정보와 같은 상태 정보를 여러 애플리케이션 서버에서 공유하는 등의 용도로 사용된다. 서버 간 클러스터링이 되어 있어서 서버 간에 데이터를 상호 복제하고 있고, 그래서 특정 서버가 장애가 나더라도 자동으로 fail over(장애가 나지 않은 서버가 그 역할을 내려 받음)하기 때문에 장애에 대한 HA를 가지고 있고, 서버를 추가함으로써 전체 용량을 늘릴 수 있는 확장성을 제공한다. 단점은 메모리 클러스터인 만큼 전체 서버가 다운되면 모든 데이터가 날아간다는 휘발성 특징을 갖는다. Redis 의 경우는 메모리에 저장된 내용을 주기적으로 디스크에 저장하고, 또한 클러스터링을 통한 확장을 지원하지 않기 때문에, 엄밀히 보면 데이터 그리드로 볼 수 없고, IMDB로 봐야 한다.

  • 메시지 큐를 이용한 비동기 요청 처리 비동기 방식은 요청을 보낸 후 비즈니스 로직이 처리가 완료되지 않은 상태에서 다음 로직을 진행한다. 비동기 방식은 요청만 보내놓고 응답이 오는 것과 상관없이 클라이언트가 대기 하지 않고 다음 로직을 수행한다. 콜백은 클라이언트에게 비동기로 요청된 작업이 끝났음을 알려주는 호출이다. 주로 시간이 오래 걸리는 대규모 작업에 유리하다. 동영상 인코딩과 같은 변환 작업이나, 시간이 오래 걸리는 계산 작업, 또는 하나의 메시지를 보냈지만 여러 개의 작업이 필요한 경우 이러한 비동기식 패턴 구현에는 일반적으로 메시지 큐라는 것을 사용한다.

    큐는 비동기 처리 솔루션이다. -> 즉, 응답 시간에 그렇게 민감하지 않다.

    • 에러 처리 메시지 큐에서 해당 메시지를 읽어서 처리하는 서버가 장애를 일으켜서 정지하였다고 가정해볼까요? 메시지는 유실되고 처리되지 않게 된다. 메시지 처리 중 에러가 났을 때 해당 메시지를 에러 큐라는 재처리용 쿠로 전달한다. 메시지의 에러 정책을 정해서 에러 처리를 어떻게 할지를 정할 수 있는데, 일반적으로 다음과 같이 크게 4가지 정책을 사용한다.
      1. 재처리(Retry) 다시 처리하도록 시도하는 방법이다. 비즈니스 컴포넌트가 일시적 장애(네트워크 문제, 리소스 부족 등)일 경우에는 효율적으로 사용할 수 있다. 재처리를 할 때, 에러 발생 후 바로 재처리를 시도하면 같은 원인으로 같은 에러가 날 수 있기 때문에 일정 시간을 기다렸다가 다시 처리하는 것이 좋다. 따라서 일정시간을 기다렸다가 재시도를 한다. 기다리는 시간은 재처리 횟수에 비례해서 늘려주는 게 좋은데, 예를 들어, 첫 번째 에러가 났을 경우 1초 후 재시도, 두 번째는 5초 후, 세 번째는 30초 후 식으로 늘려주면 비즈니스 컴포넌트가 장애에서 복구될 때까지 충분한 시간을 벌 수 있다.
      2. 무시(Ignore) 에러가 난 메시지는 무시하고 메시지를 없애 버리는 방식이다. 중요하지 않은 로그 정보를 저장할 때와 같이 메시지 유실이 허용되는 경우에만 사용한다.
      3. 알림(Notify) 메시지 처리 중 에러가 발생했을 때, 이메일, SNS 등을 이용하여 관리자에게 통보하여 관리자가 직접 에러에 대한 후처리를 할 수 있도록 해주는 방식이다.
      4. Human Interaction 에러 발생 시, 자동으로 처리하지 않고 관리자가 직접 처리할 수 있는 사용자 인터페이스를 제공한다. 에러 재처리 부분이 복잡하거나 규모가 큰 경우, 이런 메시지에 대한 에러 처리를 Error Hospital 이라 불리는 별도의 컴포넌트로 구현한다.
    • 비동기 메시지 패턴
    • Fire & Forget 패턴 클라이언트가 호출한 후 큐에 메시지가 제대로 들어갔으면 메시지의 처리 결과에 관계없이 응답을 기다리지 않고 바로 반환한다. 큐에 저장된 메시지는 비즈니스 컴포넌트에 의해 나중에 처리된다.
    • Publish & Subscribe 패턴 메시지 큐에 구독자를 등록하면 클라이언트에서 보낸 하나의 메시지가 등록된 모든 구독자에게 전달되어 처리된다. 1:N 관계의 비동기 처리를 구현하고자 할 때 사용된다.
    • Routing 패턴 큐에 저장된 메시지를 조건에 따라 특정 비즈니스 컴포넌트로 라우팅하는 기능이다. 특정 메시지는 조건에 따라서 특정 비즈니스 컴포넌트 한 개에만 전달된다.
    • Call Back 패턴 비즈니스 컴포넌트에서 처리가 끝나면 서버는 다시 클라이언트에 처리가 끝났다는 응답과 함께 처리 결과(response) 메시지를 콜백으로 보낸다. 서버가 응답 메시지를 보내면 이벤트 핸들러 방식에 의해서 응답 메시지를 처리하는 이벤트 핸들러를 호출하여 응답 메시지를 처리하도록 한다. 부가적인 정보가 필요하다. 1. 콜백 주소: 클라이언트 IP 주소와 포트 2. Correlation ID: 콜백 응답 메시지가 어느 요청에 대한 응답 메시지인지를 식별할 수 있게 해줌.

    • 메시지 큐 구성 시 고려해야 할 사항
    • 성능 및 fail over를 고려한 Persistence의 선택 메모리에 저장하는 모델이 빠르기는 하지만, 서버 장애가 났을 경우 처리되지 않은 메시지들이 유실될 수 있는 위험이 있다. 파일 저장 방식은 비교적 성능은 좋은 편이지만, 여러 개의 메시지 큐를 클러스터링 해서 사용할 경우 클러스터된 인스턴스 중 특정 인스턴스가 죽었을 경우 처리되지 않은 메시지를 살아있는 인스턴스로 넘기기 위해서는 이 파일 시스템이 인스턴스 간에 공유되어야 하는 경우가 있다. NFS와 같은 공유 파일 저장소를 사용해야 하는데 NFS 성능 저하 때문에 전체 메시징 시스템의 성능 저하가 올 수도 있다. RDBMS를 사용하면, 클러스터링 구성 시 Fail Over도 비교적 잘되고, 메시지에 대한 관리도 매우 쉽다는 장점을 가지지만, 3가지 구조 중에서 ` 가장 느리고 추가로 RDBMS 운영이 필요하다는 단점`을 갖는다.
    • 펜딩 메시지로 말미암은 Out of Memory 메시지 큐 미들웨어는 메모리 과다 사용으로 인한 OOM에러에 주의해야 한다. 이렇게 쌓여 있는 메시지들을 펜딩 메시지(Pending Message)라고 하는데, 이 펜딩 메시지에 의한 OOM에러는 메시지 저장소를 메모리로 사용하지 않더라도 발생하는 경우가 있다. 메시지 큐 미들웨어 특성상 성능 향상을 위해서 메시지를 파일이나 RDBMS에 저장하더라도 최소한의 메시지에 대한 메타 정보(메시지 ID와 같은 인덱스 정보)를 메모리에 저장할 때에 발생할 수 있다.
    • 트랜잭션 지원 기능 에러가 났을 경우의 메시지를 다시 큐에 복귀 시키거나, 에러 큐로 보내려면 트랜잭션 지원 기능이 필요하다. 분산 트랜잭션을 지원하는 메시지 큐 시스템은 데이터베이스와 큐의 트랜잭션을 하나로 묶어서 자동을 이러한 작업을 처리하는 반면에, 트랜잭션을 지원하지 않을 때에는 에러가 발생했을 때 개발자가 에러 큐로 메시지를 던지거나 다시 큐에 넣는 작업을 해줘야 한다.
    • 클러스터링 기능 메시지 큐에서 클러스터링이란 여러 개의 메시지 큐를 하나의 클러스터로 묶는 기능을 말한다. 두 가지 관점에서 장점을 생각해볼 수 있다.
      1. 클러스터링을 통해서 특정 인스턴스 장애 시 다른 인스턴스들이 장애가 난 인스턴스의 메시지를 받아서 처리하는 fail over 기능이 있을 수 있다.
      2. 한 대의 서버로 처리할 수 없는 양의 메시지를 여러 대의 서버에서 분산 처리하면서 올 수 있는 장점이 있다.
  • Map & Reduce 기반의 분석 시스템 하나의 큰 데이터를 여러 개의 조각으로 나눠서 처리하는 단계, 어떤 데이터의 집합을 받아들여 새로운 데이터를 생성하는 프로세스 -> Map 처리 결과를 모아서 하나로 합쳐서 결과를 내는 단계 -> Reduce

  • 검색 엔진의 구조
    • 검색 서버의 역할(검색을 하는 시스템) 만들어진 인덱스에서 결과를 뽑아 이용자에게 건내주는 것
    • 검색 백엔드의 역할(인덱스 생성하는 시스템)
      • 크롤링 다수의 크롤러로 웹페이지 수집 임시 저장소 영역에 보관
      • 인덱스 생성 임시 저장소에 저장된 웹 페이지를 꺼내 검색용 인덱스를 만들어 내는 과정
    • 인덱스의 역할 인덱스는 이용자가 앞으로 요청할 온갖 검색 결과들이 가능한 한 미리 계산되어 들어가 있는 것 검색을 위해 필요한 디스크 액세스는 최소한으로 억제해야 한다. -> 인덱스의 크기가 작아야 한다.
  • 마이크로 서비스 아키텍처
    • 장점
      • 배포 유연한 배포 모델이 장점이다. 각 서비스가 다른 서비스와 물리적으로 완벽하게 분리되기 때문에 변경이 있는 서비스 부분만 부분 배포가 가능하다. 변경이 있는 부분만 재배포 하면 되기 때문에, 전체 시스템의 영향을 최소화한 수준에서 빠르게 배포를 진행할 수 있다.
      • 확장성 부하가 많은 특정 서비스에 대해서만 확장할 수 있어서 조금 더 유연한 확장 모델을 가질 수 있다.
    • 문제점
      • 성능 서비스 간의 호출을 API 통신을 이용하기 때문에 값을 JSON 이나 XML 에서 프로그래밍에서 사용하는 데이터 모델로 변환하는 마샬링 오버헤드가 발생하고, 호출을 위해서 이 메시지들이 네트워크를 통해서 전송하기 때문에 그만큼 시간이 추가로 소요된다.
      • 메모리 현대 인프라 환경에서는 크게 문제가 되지 않는다.
      • 어려운 테스팅 테스트 환경 구축이나 문제 발생 시 분리된 여러 개의 시스템을 동시에 봐야 하기 때문에 테스팅의 복잡도가 올라간다.
      • 운영관점의 문제 운영해야 할 대상 시스템의 개수가 늘어나고 필요한 기술의 수도 늘어나게 된다.
      • 서비스 간 트랜잭션 처리 아예 애플리케이션 디자인 단계에서 여러 개의 API를 하나의 트랜잭션으로 묶어 분산 트랜잭션 시나리오 자체를 없애는 방안이다. 보상 트랜잭션? 출금하고 나서 다른 계좌로 이체하던 중 에러가 발생했을 때 명시적으로 돈을 원래 계좌로 돌려주는 에러 처리 로직을 구현해야 한다. 복합 서비스? 트랜잭션을 묶어야 하는 두 개의 시스템을 트랜잭션을 지원하는 네이티브 프로토콜을 이용해서 구현한 다음 이를 API로 노출하는 방법이다.
  • API Gateway 프락시 서버처럼 API들 앞에서 모든 API에 대한 엔드포인트를 통합하고 몇 가지 추가적인 기능을 제공하는 미들웨어이다.
    • 엔드포인트 통합 및 토폴로지 정리 마이크로 서비스 아키텍처의 문제점 중 하나는 각 서비스가 다른 서버에 분리/배포되기 때문에 API의 엔드포인트, 즉 서버의 URL이 각기 다르다는 것이다. API를 사용하는 클라이언트에서 서버 간의 통신이나 API 통신은 P2P 형태로 토폴로지가 복잡해지고 거미줄 모양의 서비스 컴포넌트 간 호출 구조는 향후 관리의 문제를 일으킬 수 있다. 하나의 엔드포인트를 변경하였을 때 제대로 관리가 되지 않을 경우가 있다. 이러한 토폴로지의 문제점을 해결하기 위해 중앙에 서비스 버스와 같은 역할을 하는 채널을 배치 시켜서 전체 토폴로지를 P2P에서 Hub&Spoke 방식으로 변환시켜서 서비스 간 호출을 단순화할 수 있다.
    • 오케스트레이션 여러 개의 서비스를 묶어서 하나의 새로운 서비스를 만든느 개념이다. 예를 들면, 포인트 적립 + 물품 구매 => 물품 구매 시 포인트 적립이라는 새로운 서비스를 만들어 낼 수 있다.
    • 공통 기능 처리 API에 대한 인증이나 로깅과 같은 공통 기능에 대해서 서비스 컴포넌트별로 중복 개발해야 하는 비효율성을 유발할 수 있다. API Gateway 에서 이러한 공통 기능을 처리하게 되면, API 자체는 비즈니스 로직에만 집중하여 개발 중에 발생할 수 있는 중복을 방지할 수 있다.
    • 중재

    • 배포
    • 확장성
    • 컨웨이의 법칙

Updated: