기본적으로 SSL Termination과 3-Tier Architecture 구조로 구성하였다. DB는 성능과 안정성을 위해 호스트 프로세스로 직접 운영하는 하이브리드 구성을 택했다.

 

서버 이중화 시 고려한 점

1. Sticky Session과 Session Clustering

Sticky Session은 LB가 특정 사용자의 요청을 처음 접속했던 서버로 보내주는 것으로 사용자의 브라우저 쿠키나 IP 주소를 확인한다. 서버가 죽었을 때 해당 서버에 있는 사용자 세션이 전부 날라가 버린다. 그래서 WAS 서버에서 로직으로 failover가 가능한 Session Clustering도 같이 고려해야한다. 보통 속도를 위해서 redis에 세션을 저장해서 클러스터링한다. Redis Sentinel 구조로 고가용성을 확보해야하고 굉장히 인프라적으로 복잡하다. 그런데 JWT를 사용하면 Stateless 방식이라서 둘 다 필요없다.

 

Sticky Session을 구성하는 방식

1. ip_hash

 

nginx가 HTTP 레벨(L7)에서 동작하지만, 클라이언트의 IP(L3 정보)를 기준으로 해싱(hash 알고리즘)해 항상 같은 서버로 보낸다.

L4의 알고리즘 방식과 관계없이 L7의 알고리즘 방식을 hash를 사용해 Client IP가 같으면 항상 Target WAS를 같게 해주는 것이다. 특정 서버로 트래픽이 몰릴 수 있다.

upstream backend_servers {
    ip_hash;
    server was1.example.com;
    server was2.example.com;
}

 

2. Cookie Insertion - Nginx Plus(유료) 버전만 가능

Nginx에서 들어오는 요청의 쿠키값을 본다. 서버에 고르게 트래픽이 분산되지만 유료 버전만 가능하여 인프라 종속성이 생긴다.

upstream backend_servers {
    server was1.example.com;
    server was2.example.com;

    # 로드밸런서가 'route'라는 이름의 쿠키를 삽입함
    sticky cookie route expires=1h domain=.example.com path=/;
}

 

결론은 JWT를 사용해서 인프라 관리 복잡성을 낮추는게 좋다!!!!!

 

메커니즘 역할
proxy_next_upstream 매 요청마다 실행됨. 현재 서버가 실패하면 그 요청 안에서 즉시 다음 서버로 재시도
max_fails / fail_timeout 실패 횟수가 누적되면, 이후 새로운 요청들이 아예 그 죽은 서버를 시도조차 안 하게 미리 건너뛰게 함 (일종의 캐시)

트러블 슈팅

서버 shutdown 시, 재시도를 하면서 무한로딩 발생한다. Docker 다운이나 HTTP 오류일때는 무한로딩이 발생하지 않고, 서버 다운일 경우에만 발생했다. 해당 문제는 타임아웃일 경우에만 발생하는 것으로 error나 http일때는 상관없다.

-> timeout 발생한 서버를 헬스체크 한번 발생했다면 DB의 Active-Stanby 형식처럼 down된 서버에 접속이 안되는지 찾아봤는데 이건 Nginx Plus(유료)에서 지원하는 Active Health Check 기능이다.

 

특히 이건 내가 proxy_connect_timeout + max_fails  + fail_timeout 의 정말 알맞은 값을 찾기 위해서 여러 시나리오를 생각하고 작성한 것이다. 무료버전에서 서버 shutdown으로 타임아웃 발생 시 웹 페이지에서 크롬으로 따지면 맨 위에서 빙글빙글 돌면서 무한 로딩이 발생하는 이유는 타임아웃 시간만큼 기다렸다가 upstream을 하기 때문이다. 그래서 fail_timeout 시간 내에 max_fails 만큼 오류가 나야되는데 proxy_connect_timeout을 잘 조절해야한다. 즉 30초 안에 3번 오류가 날때 타임아웃 오류일 경우엔 바로 다음 서버로 넘기는게 아니라 proxy_connect_timeout 시간만큼 기다렸다가 넘긴다. 그 모든게 fail_timeout 시간 안에 이루어져야한다. 30초 안에 사용자 3명이 동시에 접속할 경우 3명의 사용자(병렬처리)가 모두 proxy_connect_timeout 만큼 무한로딩을 겪고 다음 4번째 사용자부터 죽은 서버를 제외하고 바로 upstream된다.

최악의 경우는 1명의 사용자가 홀로 접속해서 30초 내에 3번의 클릭을 했는데 proxy_connect_timeout 시간을 너무 길게 잡아서 fail_timeout 시간을 넘어 max_fails 가 초기화되는 경우이다. 이러면 정상 서버로 넘어갈 때 계속 비정상 서버를 체크하면서 proxy_connect_timeout 만큼 기다리기에 계속 로딩이 오래걸린다고 느끼게 된다.

또 다른 최악의 상황은 3명의 사용자가 순차적으로 접속하는 경우이다. 이런 경우엔 upstream되면서 fail_timeout이 초기화되지 않고 첫번째 요청을 기준으로 한다. 즉 fail_timeout - proxy_connect_timeout 시간 만큼만 죽은 서버를 제외해서 fail_timeout보다 훨씬 빠르게 죽은 서버를 다시 찔러본다.

 

1. 동시 접속 3명 (병렬)

t=0: 3명 동시 요청 시작
t=30: 3명 다 거의 동시에 타임아웃 → 실패 카운트 1,2,3이 "거의 동시"에 쌓임 (창은 t=30에 막 시작)
      → 이 순간 마킹됨. 창이 "막 시작"했으니 fail_timeout(30초)이 거의 통째로 남아있음
t=30~60: 이 구간(약 30초) 동안 죽은 서버 스킵됨 ✅

 

2. 1명 순차 클릭 (= 3. 3명 순차 접속과 동일)

t=0:  요청1 → 죽은 서버 시도 → 30초 대기
t=30: 실패 카운트 1 (창 시작: t=30~60) → 정상 서버로 재시도 → 응답
      (요청2가 바로 이어서 들어옴, 1명이 다시 클릭하든 다른 사용자든 동일)
t=30: 요청2 → 죽은 서버 시도 → 30초 대기
t=60: 실패 카운트 시도... 근데 창(t=30~60)이 "지금 막" 끝나는 시점!
      → fail_timeout(30초)이 이미 다 지나버려서 카운터가 리셋됨
      → 이 실패가 "카운트 2"가 아니라 새로운 창의 "카운트 1"이 됨 (창 재시작: t=60~90)
t=60: 요청3 → 죽은 서버 시도 → 30초 대기
t=90: 또다시 창(t=60~90)이 끝나는 시점에 걸림 → 다시 리셋, 카운트 1로 재시작

결과: max_fails=3에 영원히 도달하지 못함 ❌
      → 서버는 절대 unavailable로 마킹되지 않음
      → 모든 요청이 각각 30초씩 무한 로딩을 반복

 

결론은 max_fails × proxy_connect_timeout이 fail_timeout보다 충분히 작아야 한다. 이상적으로는 fail_timeout ≥ max_fails × proxy_connect_timeout × 2 정도의 여유를 두면, 어떤 시나리오(동시/순차)에서도 안정적인 스킵 구간이 확보된다.


proxy_next_upstream + proxy_connect_timeout

Nginx가 WAS 1번으로 보냈는데 타임아웃이나 에러가 났을 때, 사용자에게 에러를 주지 않고 즉시 WAS 2번으로 재시도하게 만드는 설정이다. 오픈소스에서 사용할 수 있는 옵션이다. 둘 다 정상적으로 서버가 살아있다면 해당 옵션은 무시된다.

upstream backend_servers {
    server was1:8080 max_fails=3 fail_timeout=30s;
    server was2:8080 max_fails=3 fail_timeout=30s;
}

server {
    location / {
        proxy_pass http://backend_servers;
        
        # 타임아웃, 에러, HTTP 500번대 발생 시 다음 서버로 즉시 넘김
        # error timeout은 기본값이라서 http_500 http_502 http_503 http_504를 적어야지 의미가 있다.
        # 그렇다고 error timeout를 빼고 http만 적는다면 error timeout일때 upstream이 안되니 주의필요
        proxy_next_upstream error timeout http_500 http_502 http_503 http_504;
        
        # 타임아웃 발생 시 해당 시간만큼 빙글빙글이 발생하게 된다.
        proxy_connect_timeout 15s;
        
        # 다음 서버로 넘길 때의 타임아웃 제한 (너무 오래 걸리면 안 되니까)
        proxy_next_upstream_timeout 10s;
        proxy_next_upstream_tries 2;
    }
}

 

결론적으로는 무료 nginx에서 proxy_next_upstream과 proxy_connect_timeout의 값을 조절하여 정상 서버로 넘기는 것을 보장할 수 있다.

 

Nginx Plus(유료)에서 지원하는 Active Health Check 기능을 구현할 수 있는 무료 nginx에서의 2가지 방법

운영중인 서비스에 적용하기에는 리스크가 커서 적용하지는 못했다. 시간상 여유가 있다면 개발 혹은 스테이징 서버에서 충분한 테스트 후에 적용해야한다.

 

1. nginx_upstream_check_module로 Nginx Plus(유료)에서 지원하는 Active Health Check 기능을 구현할 수 있는데, nginx 코어의 upstream 라운드로빈 로직 자체를 수정해야해서 nginx를 처음부터 통째로 다시 빌드해야하고 2014년 이후로 사실상 유지보수가 멈춘 프로젝트라서 추천하지 않는다. 기존 yum으로 설치된 nginx는 삭제하고 소스 재빌드해야한다.

 

2. OpenResty(Nginx+Lua 통합 배포판)를 쓰면 순수 Lua 스크립트로 active health check를 구현할 수 있다. 그러나 config 파일 경로 이동도 필수적이고 Nginx 서비스 다운타임도 필수적이라 영향도 파악에 너무 많은 시간이 소요될거같아서 따로 더 이상 찾아보지는 않았다.

 

+ Recent posts