Node.js 디자인 패턴 바이블 ch.12

Yi Kanghoon·2023년 2월 12일
1

12. 확장성과 아키텍처 패턴

12.1. 애플리케이션 확장 소개

스타트업을 만들었다. → 좀…..잘되네? → 사용자 증가

  1. 속도저하 및 충돌?
  2. 대량의 데이터와 I/O제어
  3. 효율적인 개발 팀 구성

12.1.1. Node.js 애플리케이션 확장

일반적인 Node.js 애플리케이션 - 단일 스레드 컨텍스트
단일 스레드에서의 용량은 제한됨
→ 고부하 애플리케이션에서는 여러 프로세스와 시스템에 걸친 확장 필요
➕ 고가용성, 장애 내성 같은 좋은 속성
시간이 지남에 따라 ‘확장 가능한’ 아키텍처도 중요하다.

12.1.2 확장성 3차원

확장성의 첫번째 기본원칙 - ‘부하 분산’

스케일 큐브

X - 복제
Y - 서비스/기능별 분해
Z - 데이터 파티션으로 분할

Starting Point - 모놀리식 애플리케이션

확장 방향

  • X - 복제
    가장 직관적인 진화, 간단하고 비용 적다
    그냥 애플리케이션을 복제하고 각각이 1/n을 처리하도록
  • Y - 서비스/기능 별 분해
    각각 고유한 코드베이스를 사용하는 독립된 애플리케이션
    ex) 관리자를 위한 기능과 사용자를 위한 기능을 분리
    애플리케이션 아키텍처 뿐만 아니라 개발과 운영 모두에서 큰 차이(MSA…!)
  • Z - 데이터 파티션으로 분할
    각 인스턴스가 데이터의 일부만 담당하는 방식
    A.K.A. 수평수직 분할
    DB에서 많이 활용하는 방식
    같은 애플리케이션의 여러 인스턴스가
    목록분할 ex) 국가에 따라
    범위분할 ex) 이름의 성을 기준으로
    해시분할 ex?) 해시 함수가 결정
    에 따라 나눈 각 파티션을 담당
    For 대규모 모놀리식 데이터 셋 처리 관련 문제 해결 → 데이터 스토리지 수준에서 적용
    애플리케이션에는? 특별한 활용 사례에서만 활용 가치가 있다….

12.2. 복제 및 로드 밸런싱

단일 스레드 → 전통적 웹 서버에 비해 빠른 확장 요구
단점인가? 확장의 강제 → 가용성과 내결함성에 유익한 효과
각 인스턴스의 공유할 수 없는 리소스(메모리, 디스크 등)에 저장하지 않고 공유DB를 사용해야 함

12.2.1. 클러스터 모듈

가장 간단한 패턴
새 인스턴스 분기를 단순화 하고 부하를 배분

마스터 프로세스

확장한 애플리케이션의 인스턴스를 나타내는 작업 프로세스 생성, 연결 분산
시스템에서 사용가능한 CPU의 수만큼 작업자 생성

클러스터 모듈 동작에 대한 참고사항

대부분 라운드로빈 로드 밸런싱 알고리즘(순환 방식) 사용
windows 제외한 모든 플랫폼에서 기본
cluster.schedulingPolicy , cluster.SCHED_RR, cluster.SCHED_NONE 설정해 전역적으로 수정가능
server.listen() 에 대한 모든 호출은 마스터 프로세스 담당
마스터 프로세스는 이를 작업자 풀에 배포

이것이 예상대로 작동하지 않는 경우?

  • server.listen({id}): 작업자가 특정 파일 설명자를 사용해 수신하는 경우
    파일 설명자는 프로세스 수준에서 매핑 → 마스터 프로세스의 동일한 파일과 일치하지 않음
    Solution. 마스터 프로세스에서 파일 설명자를 만들고 이를 작업자에 전달
  • server.listen(handle): 작업자가 명시적인 핸들 객체를 수신
    마스터 프로세스에 위임 X, 핸들을 직접 사용
  • server.listen(0): 서버가 임의의 포트에서 수신
    but 클러스터에서 각 작업자는 호출 때마다 같은 무작위 포트를 받음 → 처음에만 무작위
    Solution. 포트 번호를 직접 생성해야함

간단한 http 서버 만들기

pid가 포함된 메시지로 응답
프로세스가 하나로, pid 계속 동일
빈 루프는 CPU부하 시뮬레이션 위해

클러스터 모듈을 통한 확장

클러스터 모듈의 탄력성 및 가용성

작업자들은 별도의 프로세스로 필요에 따라 생성과 소멸
오작동, 충돌 발생해도 서비스 유지 가능 - 복원력
하나의 인스턴스 다운 → n-1개의 인스턴스가 처리하면 된다
직관적으로 보면 가용성이 100이 나올 것 같지만 이미 설정된 연결의 중단시 실패 → 그래도 높은 가용성

다운타임 제로 재시작

새 버전 릴리즈시 클러스터 없이는 작은 간격 동안 요청 처리가 불가
전문 애플리케이션이나 CI/CD에 의한 잦은 재배포시 문제가 될 수 있음
클러스터를 활용해 하나의 프로세스씩 재배포하는 것이 가능하다

상태 저장 통신 다루기

클러스터 모듈은 상태 저장이 필요한 통신에서는 정상작동 X
ex) 클러스터의 인스턴스 A에서 인증, 다음 요청은 B 인스턴스에 전달된다면? B는 인증 정보가 없는데?

  • 여러 인스턴스에 상태 공유
    DB를 사용하거나 Redis, Memcached 같은 메모리 저장소를 사용 가능
    단점 : 적용을 위해 코드의 상당량 리팩토링 필요
    ex) 공유 저장소를 사용하도록 라이브러리의 설정을 바꾸거나 심지어는 교체, 재구현 할 필요
  • 고정 로드 밸런싱
    세션과 관련된 모든 요청은 같은 인스턴스에 전달하는 것
    새 세션은 알고리즘에 따라가고, 그 다음부터는 해당 세션 ID에 연결된 인스턴스에 전달
    단점 : 복원력이 줄어든다

12.2.3. 역방향 프록시 확장

클러스터 사용의 대안
다른 포트 또는 물리머신에서 실행되는 여러 독립적 인스턴스를 시작
역방향 프록시(또는 게이트웨이)를 사용해 해당 인스턴스에 접근하도록
역방향 프록시는 로드 밸런서로도 사용

  • 사용의 근거?
    - 여러 프로세스 뿐만 아니라 여러 ‘시스템’에 부하 분산
    • 가장 널리 사용되는 역방향 프록시는 고정 로드 밸런싱 지원
    • 언어나 플랫폼에 상관 없음
    • 보다 강력한 로드 밸런싱 알고리즘의 선택
    • 많은 역방향 프록시가 완전한 웹 서버의 기능을 제공 - nginx!

클러스터와 역방향 프록시의 결합

단일 시스템에서는 클러스터로 수직 확장
그런 시스템 여러개를 이용하는 역방향 프록시의 수평 확장
Nginx, HAProxy, Node.js 기반 프록시, 클라우드 기반 프록시

Nginx를 이용한 로드밸런싱

클러스터 없이 쓸 수 없는 기능 - 충돌 시 자동 재시작
하지만 외부 프로세스 쓰면 해결!
forever, pm2 - node 기반
systemd, runit - OS 기반
monit, supervisord - 고급 모니터링 솔루션
Kubernetes, Nomad, DockerSwarm - 컨테이너 기반

  • forever 사용 예제
  1. daemon off - 권한이 없는 사용자를 이용해 Nginx를 독립 실행형 프로세스로 실행, 현재 터미널의 포그라운드에서 실행
  2. error_log - 흔한 로깅
  3. events - 네트워크 연결 관리, 여기서는 최대 연결을 2048개로
  4. http - 백엔드 목록 정의, 서버의 포트 설정, 위의 서버 그룹으로 요청 전달하도록 설정
원격에 배포시?
  1. n개의 백엔드 서버 프로비저닝
  2. 로드 밸런서와 라우팅 설정을 프로비저닝
  3. 로드 밸런서를 공개
  4. 브라우저로 로드 밸런서에 트래픽 전송

12.2.4. 동적 수평 확장

클라우드의 장점 - 현재/예측된 트래픽을 기반으로 ‘동적 확장’

서비스 레지스트리 사용

서비스 레지스트리 - 실행중인 서버와 서비스를 추적하는 저장소
![https://velog.velcdn.com/images%2Fwnwjq462%2Fpost%2F8ef7a4f2-8e7c-43a2-bbb0-ea70491b32a6%2Fimage.png%5D(https%3A%2F%2Fimages.velog.io%2Fimages%2Fwnwjq462%2Fpost%2F8ef7a4f2-8e7c-43a2-bbb0-ea70491b32a6%2Fimage.png)
현재 네트워크 토폴로지를 읽는다 → 자동화 위해서는 각각의 인스턴스가 온라인이 될 때 서비스레지스트리에 등록, 중지시 등록 취소해야 함
트래픽 분산에 유용, 실행중인 서버에서 서비스 인스턴스를 분리 할 수 있음

http-proxy와 Consul을 활용한 동적 로드 밸런서 구현

작업자
1. portfinder.getPortPromise() 사용, 사용 가능한 포트 검색
2. 레지스트리에 서비스 등록 위해 registerService() 선언
3. 서비스를 제거하는 unregisterService() 선언
4. 위의 함수를 이용하여 프로그램 종료시 Consul에서 등록해제
5. 발견된 포트와 현재 설정된 주소에서 서비스용 http서버 실행. registerService()호출 → 레지스트리에 등록

로드 밸런서
1. 로드 밸런서 경로 정의
2. consul 클라이언트, http-proxy 인스턴스화
3. 라우팅 테이블에서 url 검색
4. consul에서 서비스 구현 서버 목록 가져옴
5. 요청을 대상으로 라우팅(라운드 로빈 방식)
목록에서 다음서버를 가리키도록 route.index 갱신, 요청/응답 객체 proxy.web()에 전달

12.2.5. 피어 투 피어 로드 밸런싱

위의 방법은 내부 복잡성을 숨길 수 있어 공용 네트워크에 노출하기 좋지만,
내부용이라면 더 많은 유연성과 제어를 가질 수 있다
이전에는 서비스A → 로드밸런서 → 서비스B 였다면
이번에는 서비스A → 서비스B로, 서비스A가 로드 밸런싱을 담당한다

장점

인프라 복잡성 감소
더 빠른 통신
로드 밸런서에 의한 확장성 제한 없음

여러 서버에 요청을 분산할 수 있는 http 클라이언트 구현

라운드로빈 알고리즘으로 요청할 호스트 명과 포트를 재정의하도록 함

12.2.6. 컨테이너를 사용한 애플리케이션 확장

컨테이너란?

‘Docker’ - 코드와 모든 종속성을 패키지화 한 것
1. 컨테이너 이미지 빌드
2. 이미지에서 컨테이너 인스턴스 실행
컨테이너 이미지 생성 - Dockerfile

Kubernetes란?

최종 상태(바람직한 상태)를 정의하고 그 상태에 도달하는데 필요한 단계를 파악할 수 있도록 구성한 시스템

  • 컨테이너 오테스트레이션 도구의 책임
    • 여러 클라우드 서버를 하나의 논리적 클러스터로 결합, 노드의 동적 추가 및 제거
    • 다운타임 확인
    • 서비스 검색(=서비스 레지스트리) 및 로드 밸런싱
    • 스토리지에 대한 오케스트레이션 된 접근
    • 자동 롤아웃 및 롤백
    • 보안 저장 공간

12.3. 복잡한 애플리케이션 분해

지금까지는 X축 확장 위주, 이제는 Y축으로 확장해보자

12.3.1. 모놀리식 아키텍처

서비스가 분리되어 모듈화되어 있을 수도 있고 그것이 바람직하지만, 어쨌든 하나의 코드베이스
(Ourlim) 필터 때문에 전체 서비스가 죽었다….
모듈간의 낮은 결합을 유지하기 힘들다

12.3.2. 마이크로서비스 아키텍처

커다란 애플리케이션을 만들지 마십시오

서비스와 기능에 따라 별도의 독립적인 애플리케이션을 만드는 것
가능한 한 작게, 합리적인 한도 내에서

서비스 각각이 데이터 소유권을 지님
→ 시스템의 일관성을 위해 더 많은 통신이 필요

12.3.3. 마이크로서비스 아키텍처의 통합 패턴

모든 노드를 연결해야 하는데… 어떻게 연결하지?

API 프록시

앞에서 이야기 한 패턴, API프록시가 url경로에 따라 서비스에 연결됨 (단순한 분산 아키텍처?)

API 오케스트레이션

다양한 서비스를 명시적으로 통합하는 추상화 계층
서비스에 의해 만들어진 것과 다른 API를 노출 시키고, 추가적인 기능 분할을 가능케 함
서로 다른 서비스의 데이터를 단일 응답으로 결합 시킬 수 있음

메시지 브로커와의 통합

오케스트레이터는 너무 많은 것을 수행하는 God객체가 될 수 있다 → 높은 결합, 낮은 응집력, 높은 복잡성
서비스간의 직접적인 연결을 하되 모든 서비스는 분리된 상태 - 메시지 브로커와 발행/구독 패턴

profile
Full 'Snack' Developer

0개의 댓글