스타트업을 만들었다. → 좀…..잘되네? → 사용자 증가
일반적인 Node.js 애플리케이션 - 단일 스레드 컨텍스트
단일 스레드에서의 용량은 제한됨
→ 고부하 애플리케이션에서는 여러 프로세스와 시스템에 걸친 확장 필요
➕ 고가용성, 장애 내성 같은 좋은 속성
시간이 지남에 따라 ‘확장 가능한’ 아키텍처도 중요하다.
확장성의 첫번째 기본원칙 - ‘부하 분산’
X - 복제
Y - 서비스/기능별 분해
Z - 데이터 파티션으로 분할
Starting Point - 모놀리식 애플리케이션
단일 스레드 → 전통적 웹 서버에 비해 빠른 확장 요구
단점인가? 확장의 강제 → 가용성과 내결함성에 유익한 효과
각 인스턴스의 공유할 수 없는 리소스(메모리, 디스크 등)에 저장하지 않고 공유DB를 사용해야 함
가장 간단한 패턴
새 인스턴스 분기를 단순화 하고 부하를 배분
확장한 애플리케이션의 인스턴스를 나타내는 작업 프로세스 생성, 연결 분산
시스템에서 사용가능한 CPU의 수만큼 작업자 생성
대부분 라운드로빈 로드 밸런싱 알고리즘(순환 방식) 사용
windows 제외한 모든 플랫폼에서 기본
cluster.schedulingPolicy
, cluster.SCHED_RR
, cluster.SCHED_NONE
설정해 전역적으로 수정가능
server.listen()
에 대한 모든 호출은 마스터 프로세스 담당
마스터 프로세스는 이를 작업자 풀에 배포
server.listen({id})
: 작업자가 특정 파일 설명자를 사용해 수신하는 경우server.listen(handle)
: 작업자가 명시적인 핸들 객체를 수신server.listen(0)
: 서버가 임의의 포트에서 수신pid가 포함된 메시지로 응답
프로세스가 하나로, pid 계속 동일
빈 루프는 CPU부하 시뮬레이션 위해
작업자들은 별도의 프로세스로 필요에 따라 생성과 소멸
오작동, 충돌 발생해도 서비스 유지 가능 - 복원력
하나의 인스턴스 다운 → n-1개의 인스턴스가 처리하면 된다
직관적으로 보면 가용성이 100이 나올 것 같지만 이미 설정된 연결의 중단시 실패 → 그래도 높은 가용성
새 버전 릴리즈시 클러스터 없이는 작은 간격 동안 요청 처리가 불가
전문 애플리케이션이나 CI/CD에 의한 잦은 재배포시 문제가 될 수 있음
클러스터를 활용해 하나의 프로세스씩 재배포하는 것이 가능하다
클러스터 모듈은 상태 저장이 필요한 통신에서는 정상작동 X
ex) 클러스터의 인스턴스 A에서 인증, 다음 요청은 B 인스턴스에 전달된다면? B는 인증 정보가 없는데?
클러스터 사용의 대안
다른 포트 또는 물리머신에서 실행되는 여러 독립적 인스턴스를 시작
역방향 프록시(또는 게이트웨이)를 사용해 해당 인스턴스에 접근하도록
역방향 프록시는 로드 밸런서로도 사용
단일 시스템에서는 클러스터로 수직 확장
그런 시스템 여러개를 이용하는 역방향 프록시의 수평 확장
Nginx, HAProxy, Node.js 기반 프록시, 클라우드 기반 프록시
클러스터 없이 쓸 수 없는 기능 - 충돌 시 자동 재시작
하지만 외부 프로세스 쓰면 해결!
forever, pm2 - node 기반
systemd, runit - OS 기반
monit, supervisord - 고급 모니터링 솔루션
Kubernetes, Nomad, DockerSwarm - 컨테이너 기반
클라우드의 장점 - 현재/예측된 트래픽을 기반으로 ‘동적 확장’
서비스 레지스트리 - 실행중인 서버와 서비스를 추적하는 저장소
![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)
현재 네트워크 토폴로지를 읽는다 → 자동화 위해서는 각각의 인스턴스가 온라인이 될 때 서비스레지스트리에 등록, 중지시 등록 취소해야 함
트래픽 분산에 유용, 실행중인 서버에서 서비스 인스턴스를 분리 할 수 있음
작업자
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()에 전달
위의 방법은 내부 복잡성을 숨길 수 있어 공용 네트워크에 노출하기 좋지만,
내부용이라면 더 많은 유연성과 제어를 가질 수 있다
이전에는 서비스A → 로드밸런서 → 서비스B 였다면
이번에는 서비스A → 서비스B로, 서비스A가 로드 밸런싱을 담당한다
인프라 복잡성 감소
더 빠른 통신
로드 밸런서에 의한 확장성 제한 없음
라운드로빈 알고리즘으로 요청할 호스트 명과 포트를 재정의하도록 함
‘Docker’ - 코드와 모든 종속성을 패키지화 한 것
1. 컨테이너 이미지 빌드
2. 이미지에서 컨테이너 인스턴스 실행
컨테이너 이미지 생성 - Dockerfile
최종 상태(바람직한 상태)를 정의하고 그 상태에 도달하는데 필요한 단계를 파악할 수 있도록 구성한 시스템
지금까지는 X축 확장 위주, 이제는 Y축으로 확장해보자
서비스가 분리되어 모듈화되어 있을 수도 있고 그것이 바람직하지만, 어쨌든 하나의 코드베이스
(Ourlim) 필터 때문에 전체 서비스가 죽었다….
모듈간의 낮은 결합을 유지하기 힘들다
커다란 애플리케이션을 만들지 마십시오
서비스와 기능에 따라 별도의 독립적인 애플리케이션을 만드는 것
가능한 한 작게, 합리적인 한도 내에서
서비스 각각이 데이터 소유권을 지님
→ 시스템의 일관성을 위해 더 많은 통신이 필요
모든 노드를 연결해야 하는데… 어떻게 연결하지?
앞에서 이야기 한 패턴, API프록시가 url경로에 따라 서비스에 연결됨 (단순한 분산 아키텍처?)
다양한 서비스를 명시적으로 통합하는 추상화 계층
서비스에 의해 만들어진 것과 다른 API를 노출 시키고, 추가적인 기능 분할을 가능케 함
서로 다른 서비스의 데이터를 단일 응답으로 결합 시킬 수 있음
오케스트레이터는 너무 많은 것을 수행하는 God객체가 될 수 있다 → 높은 결합, 낮은 응집력, 높은 복잡성
서비스간의 직접적인 연결을 하되 모든 서비스는 분리된 상태 - 메시지 브로커와 발행/구독 패턴