본문 바로가기
서버연구 블로그 서버연구 블로그

서버 한 대가 장애 나도 서비스가 유지되는 구조는 어떻게 만드는가?

읽는 시간 약 16분

웹 서비스를 운영하다 보면 반드시 생각해야 하는 문제가 있다.

“현재 서비스 중인 서버 한 대가 갑자기 멈추면 어떻게 될까?”

서버가 한 대뿐이라면 답은 단순하다. 해당 서버가 복구될 때까지 서비스도 함께 중단된다.

하지만 실제 서비스 환경에서는 서버 한 대의 장애가 곧 전체 서비스 장애로 이어지지 않도록 구조를 설계한다. Load Balancer를 두고 여러 대의 웹 서버를 배치하거나, Active-Standby 구조를 사용하고, 데이터베이스까지 복제하는 이유가 바로 여기에 있다.

중요한 것은 단순히 서버를 두 대 준비하는 것이 아니다.

장애가 발생했을 때 이를 감지하고, 문제가 발생한 서버를 서비스 경로에서 제외하며, 다른 서버가 요청을 이어받을 수 있어야 한다.

이번 글에서는 서버 한 대가 장애 나더라도 서비스가 계속 유지되는 고가용성(High Availability, HA) 구조​가 실제로 어떻게 동작하는지 살펴본다.

서버 한 대의 장애가 전체 서비스 장애로 이어지는 이유

가장 단순한 웹 서비스 구조는 다음과 같다.

사용자
   │
   ▼
Web Server
   │
   ▼
Database

사용자의 모든 요청이 하나의 웹 서버로 들어간다.

구조가 단순하기 때문에 관리하기 쉽지만 치명적인 문제가 있다.

웹 서버에 장애가 발생하면 사용자가 접속할 수 있는 다른 경로가 없다.

이처럼 하나의 구성 요소가 고장 났을 때 전체 서비스가 중단되는 지점을 SPOF(Single Point of Failure)​라고 한다.

SPOF는 웹 서버에만 존재하는 것이 아니다.

예를 들어 다음과 같은 구성 요소도 SPOF가 될 수 있다.

Load Balancer 1대
Database 1대
Network Switch 1대
Storage 1대
DNS 구조
전원 공급 장치

웹 서버를 여러 대 구축했더라도 앞단 Load Balancer가 한 대뿐이라면 Load Balancer 장애가 전체 서비스 장애로 이어질 수 있다.

따라서 고가용성 설계의 핵심은 단순히 서버 수를 늘리는 것이 아니라 서비스 경로에 존재하는 단일 장애 지점을 하나씩 제거하는 것이다.

고가용성 구조에서는 요청이 어떻게 이동할까?

웹 서버를 두 대로 늘렸다고 가정해보자.

                  ┌─ Web Server A
사용자 → Load Balancer
                  └─ Web Server B

정상 상태에서는 Load Balancer가 요청을 두 서버로 분산한다.

예를 들어 Round Robin 방식이라면 다음처럼 요청이 전달될 수 있다.

Request 1 → Server A
Request 2 → Server B
Request 3 → Server A
Request 4 → Server B

여기서 Server A에 장애가 발생한다.

단순히 서버 두 대만 운영한다면 Load Balancer는 계속 Server A로 요청을 보낼 수도 있다.

그렇게 되면 일부 사용자는 정상적으로 접속하지만 일부 사용자는 502, 503 또는 Connection Timeout 같은 오류를 경험하게 된다.

그래서 실제 Load Balancer에는 일반적으로 Health Check 기능이 사용된다.

Load Balancer
     │
     ├─ Health Check → Server A : FAIL
     │
     └─ Health Check → Server B : OK

Server A가 비정상으로 판단되면 Load Balancer는 해당 서버를 요청 대상에서 제외한다.

결과적으로 요청 흐름은 다음처럼 바뀐다.

사용자
   │
   ▼
Load Balancer
   │
   ▼
Server B

Server A가 장애 상태더라도 Server B가 요청을 계속 처리하기 때문에 전체 서비스는 유지될 수 있다.

이것이 고가용성 구조의 가장 기본적인 동작 원리다.

장애 서버는 어떻게 감지할까?

고가용성 구조에서 가장 중요한 부분 중 하나가 장애 판단이다.

단순히 서버에 Ping이 응답한다고 해서 해당 서버가 정상이라고 볼 수는 없다.

예를 들어 서버 운영체제는 정상인데 Nginx 프로세스가 중단됐을 수도 있다.

systemctl status nginx

이 명령은 Nginx 서비스 자체가 정상적으로 실행되고 있는지 확인할 때 사용한다.

하지만 Nginx가 실행 중이라고 해서 웹 애플리케이션까지 정상이라는 보장은 없다.

예를 들어 PHP-FPM이나 WAS가 장애 상태라면 Nginx 프로세스는 살아 있어도 실제 서비스는 502 오류를 반환할 수 있다.

그래서 Load Balancer에서는 보통 특정 URL로 요청을 보내 애플리케이션 상태를 확인한다.

예를 들면 다음과 같다.

GET /health

정상 서버라면 다음과 같은 응답을 반환하도록 구성할 수 있다.

HTTP/1.1 200 OK

Load Balancer가 일정 횟수 이상 정상 응답을 받지 못하면 해당 서버를 비정상 서버로 판단하고 트래픽 분배 대상에서 제거한다.

중요한 것은 Health Check 기준을 어디까지 잡느냐다.

웹 서버 프로세스만 검사할 수도 있고,

Nginx

애플리케이션까지 검사할 수도 있으며,

Nginx
  ↓
Application

DB 연결 상태까지 포함할 수도 있다.

Nginx
  ↓
Application
  ↓
Database

검사 범위가 깊어질수록 실제 서비스 상태를 정확하게 판단할 수 있지만 외부 시스템의 일시적인 문제 때문에 정상 서버까지 장애로 판정되는 상황이 발생할 수 있다.

따라서 Health Check는 단순히 많이 검사하는 것이 아니라 서비스가 정상 요청을 처리할 수 있는 최소 조건을 기준으로 설계하는 것이 중요하다.

실제 장애에서는 어떤 증상이 나타날까?

고가용성 구조라고 해서 장애가 발생할 때 사용자가 항상 아무것도 느끼지 못하는 것은 아니다.

Health Check에는 장애를 감지하는 시간이 필요하기 때문이다.

예를 들어 다음과 같이 설정되어 있다고 가정해보자.

Health Check Interval : 5초
Failure Threshold     : 3회

서버가 장애 난 직후 Load Balancer가 곧바로 이를 인식하는 것이 아니다.

Health Check가 세 번 실패해야 장애 서버로 판단한다면 그동안 일부 요청이 장애 서버로 전달될 가능성이 있다.

사용자 입장에서는 순간적으로 다음과 같은 증상이 나타날 수 있다.

502 Bad Gateway
503 Service Unavailable
Connection Timeout
Connection Reset
응답 지연

이후 Load Balancer가 장애 서버를 제외하면 다시 정상적으로 서비스가 동작한다.

따라서 운영자는 단순히

“서비스가 다시 정상인데?”

라고 판단해서는 안 된다.

실제로는 특정 서버가 장애 났고 Load Balancer가 해당 서버를 자동으로 제외해 서비스를 유지하고 있을 수 있기 때문이다.

이런 장애는 사용자 신고보다 모니터링 시스템에서 먼저 발견되는 경우가 많다.

서버 한 대가 빠졌을 때 남은 서버가 버틸 수 있는지도 중요하다

운영 환경에서 자주 놓치는 부분이다.

평소 웹 서버 두 대가 각각 50% 수준의 트래픽을 처리한다고 가정해보자.

Server A → 50%
Server B → 50%

Server A가 장애 나면 모든 요청이 Server B로 이동한다.

Server A → 장애

Server B → 100% 트래픽 처리

문제는 Server B가 평소 두 배에 가까운 요청을 감당해야 한다는 점이다.

CPU 사용률이나 Memory만 보는 것으로는 충분하지 않을 수 있다.

예를 들어 연결 수를 확인할 수 있다.

ss -s

이 명령은 현재 TCP 연결 상태를 전체적으로 파악할 때 유용하다.

장애 서버의 트래픽이 다른 서버로 이동한 뒤 estab 연결이나 전체 TCP 연결 수가 급격히 증가한다면 남은 서버가 추가 트래픽을 실제로 받고 있다는 의미다.

CPU 상태도 함께 확인한다.

top 또는 uptime을 통해 Load Average를 확인할 수 있다.

장애 이후 CPU 사용률과 Load Average, TCP Connection, 애플리케이션 응답 시간이 동시에 상승한다면 서버는 살아 있지만 장애 서버의 트래픽까지 처리하면서 한계에 가까워지고 있을 가능성이 있다.

따라서 고가용성 설계에서는 정상 상태가 아니라 한 대가 빠진 상태에서도 서비스가 유지되는지를 기준으로 용량을 계산해야 한다.

웹 서버 이중화만으로는 완전한 고가용성이 아니다

다음과 같이 웹 서버 두 대와 Load Balancer를 구성했다고 가정해보자.

                 ┌─ Web Server A ─┐
User → LB ───────┤                ├─ Database
                 └─ Web Server B ─┘

웹 서버 한 대가 장애 나도 서비스는 유지된다.

하지만 Database가 한 대라면 어떨까?

Database 장애
      ↓
Web A 사용 불가
Web B 사용 불가

웹 서버는 두 대 모두 정상인데 DB에 연결하지 못하기 때문에 서비스 전체가 장애 상태가 된다.

따라서 실제 고가용성 구조는 보통 여러 계층을 함께 고려한다.

              ┌─ LB 1
User ────────┤
              └─ LB 2
                  │
        ┌─────────┴─────────┐
     Web A                 Web B
        │                     │
        └─────────┬───────────┘
                  │
             Database
          Primary / Replica

환경에 따라 Load Balancer 자체도 이중화하고 Database 역시 복제 또는 Failover 구조를 구성한다.

즉 고가용성은 특정 서버 한 종류의 문제가 아니라 서비스를 구성하는 전체 경로의 문제다.

세션을 서버에 저장하면 장애 전환이 어려워질 수 있다

웹 서버 여러 대를 운영할 때 특히 주의해야 하는 부분이 Session이다.

사용자가 Server A에 로그인했다고 가정해보자.

User
 ↓
Server A
 ↓
Session 생성

만약 Session 데이터가 Server A 내부에만 존재한다면 Server A 장애 후 Server B로 요청이 이동했을 때 Server B에는 해당 사용자의 로그인 정보가 없다.

사용자는 서비스에는 접속할 수 있지만 갑자기 로그아웃된 것처럼 보일 수 있다.

Server A
Session ABC123 존재

Server B
Session ABC123 없음

이 문제 때문에 여러 대의 웹 서버를 사용하는 환경에서는 Session을 Redis와 같은 외부 저장소에서 공유하거나 애플리케이션을 가능한 한 Stateless 구조로 설계하는 경우가 많다.

        Server A
           │
           ├──── Redis Session
           │
        Server B

이렇게 구성하면 어느 웹 서버가 요청을 처리하더라도 동일한 Session 정보를 사용할 수 있다.

서버를 여러 대 배치하는 것만으로 고가용성이 완성되지 않는 이유가 바로 이런 애플리케이션 상태 관리 문제 때문이다.

고가용성 환경은 반드시 장애 테스트를 해봐야 한다

구성을 완료한 뒤 가장 중요한 작업 중 하나가 실제 장애 테스트다.

예를 들어 웹 서버 두 대가 있다면 한 대의 Nginx를 의도적으로 중지해볼 수 있다.

systemctl stop nginx

이때 확인해야 할 것은 단순히 다른 서버가 살아 있는지가 아니다.

다음 흐름을 확인해야 한다.

Server A Nginx 중지
        ↓
Health Check 실패
        ↓
Load Balancer가 Server A 제외
        ↓
트래픽이 Server B로 이동
        ↓
서비스 정상 유지

동시에 Server B의 상태도 확인해야 한다.

top
ss -s
free -m

CPU, TCP Connection, Memory 상태가 어떻게 변하는지 확인해야 한다.

애플리케이션 로그 역시 중요하다.

tail -f /var/log/nginx/access.log

장애 전환 이후 Server B의 요청량이 증가하는지 확인하면 실제로 트래픽이 정상적으로 이동했는지 판단하는 데 도움이 된다.

이런 테스트를 하지 않은 고가용성 구성은 문서상 이중화일 뿐 실제 장애 상황에서 정상적으로 작동한다는 보장이 없다.

운영자 관점에서 중요한 것은 ‘서버 수’가 아니라 ‘장애 경로’다

고가용성 환경을 처음 설계할 때 흔히 하는 접근이 있다.

“웹 서버를 두 대 만들면 안전하지 않을까?”

하지만 운영 관점에서는 질문을 조금 다르게 해야 한다.

Load Balancer가 죽으면?
Web Server가 죽으면?
Application이 죽으면?
Database가 죽으면?
Redis가 죽으면?
Network 장비가 죽으면?
특정 AZ나 데이터센터에 문제가 생기면?

각 질문에 대해 서비스가 계속 유지될 수 있는 경로가 존재하는지를 확인해야 한다.

예를 들어 다음 구조는 웹 서버는 이중화되어 있다.

        Web A
          │
LB ───────┤
          │
        Web B

하지만 Load Balancer가 한 대라면 여전히 LB가 SPOF다.

LB 장애
  ↓
Web A 접근 불가
Web B 접근 불가

따라서 서버를 몇 대 구축했느냐보다 장애가 발생했을 때 요청이 우회할 수 있는 경로가 존재하는가가 훨씬 중요하다.

이것이 단순한 서버 이중화와 고가용성 아키텍처의 가장 큰 차이다.

정리

서버 한 대가 장애 나도 서비스를 유지하려면 단순히 서버를 여러 대 운영하는 것만으로는 부족하다.

기본적으로 다음 과정이 자동으로 이루어져야 한다.

장애 발생
   ↓
Health Check로 장애 감지
   ↓
장애 서버를 서비스 경로에서 제외
   ↓
정상 서버로 트래픽 전환
   ↓
서비스 지속

하지만 실제 운영 환경에서는 여기서 한 단계 더 생각해야 한다.

장애 서버의 트래픽을 남은 서버가 감당할 수 있는지, Session과 같은 상태 정보가 특정 서버에 종속되어 있지 않은지, Database와 Load Balancer 자체가 또 다른 SPOF가 되고 있지는 않은지도 확인해야 한다.

결국 고가용성의 핵심은 “서버를 두 대 운영한다”가 아니라 “어떤 구성 요소 하나에 장애가 발생해도 다른 경로를 통해 서비스를 계속 제공할 수 있다”는 것​이다.

따라서 실제 운영자가 고가용성 구조를 검증할 때 가장 중요한 질문은 간단하다.

“지금 이 서버를 강제로 종료해도 서비스가 계속 동작하는가?”

이 질문에 확실하게 답할 수 있을 때 비로소 해당 서비스가 서버 한 대의 장애를 견딜 수 있는 구조라고 볼 수 있다.

instone
함께 보면 좋은 글

댓글 0

첫 댓글을 남겨보세요.

error: Content is protected !!

광고 차단 알림

광고 클릭 제한을 초과하여 광고가 차단되었습니다.

단시간에 반복적인 광고 클릭은 시스템에 의해 감지되며, IP가 수집되어 사이트 관리자가 확인 가능합니다.