본문 바로가기

로드 밸런서

연결의 주소와 포트로 나누는 L4와 HTTP의 내용을 보고 나누는 L7 로드 밸런서를 비교하고, TLS를 어디서 끝내는지와 클라이언트 주소를 뒤로 전하는 방법을 봅니다. 분산 알고리즘을 요청의 길이가 다른 상황에서 모의로 비교하고, 해시 분산의 재배치, liveness와 readiness, 상태 확인의 임계값, 세션을 유지하는 방법을 정리합니다.

사용자가 늘어 서버 한 대로 감당할 수 없게 되면 서버를 여러 대로 늘립니다. 그러면 들어오는 요청을 어느 서버에 보낼지 정하는 장치가 필요한데, 그것이 로드 밸런서(load balancer)입니다. 로드 밸런서는 요청을 나누는 일만 하지 않습니다. 죽은 서버를 빼고, 배포하는 동안 서버를 하나씩 비우고, TLS를 대신 처리하는 등 서비스의 가용성을 정하는 많은 일이 로드 밸런서에서 일어납니다.

L4와 L7

로드 밸런서는 어느 계층의 정보를 보고 나누느냐에 따라 두 종류로 나뉩니다. L4 로드 밸런서는 전송 계층에서 동작해, 연결의 IP 주소와 포트, 프로토콜만 보고 연결 단위로 서버를 고릅니다. HTTP의 내용은 보지 않으므로 HTTP가 아닌 TCP나 UDP 서비스에도 쓸 수 있고, 처리가 단순합니다. AWS의 NLB가 대표적입니다.

L7 로드 밸런서는 애플리케이션 계층에서 HTTP 요청을 해석해, 호스트와 경로, 헤더, 쿠키를 보고 요청 단위로 서버를 고릅니다. /api/로 시작하는 요청은 API 서버 묶음으로, /static/으로 시작하는 요청은 정적 파일 서버로 보내는 식입니다. AWS의 ALB가 대표적입니다. HTTPS 요청의 경로를 보려면 로드 밸런서가 TLS를 풀어야 하므로, L7 로드 밸런서는 보통 TLS를 끝내는(terminate) 지점이 됩니다.

HTTPS 경로를 고르려면, L7 진입점에서 TLS를 종료합니다

클라이언트가 L7 진입점으로 HTTPS 요청을 보냅니다. 해당 요청을 API 서버 그룹으로 보냅니다. 각 단계의 값과 전환 이유를 비교합니다.

로드 밸런서 · HTTPS 경로 라우팅

HTTPS 경로를 고르려면, L7 진입점에서 TLS를 종료합니다

이 예는 /api/*와 /static/*을 서로 다른 서버 그룹으로 보냅니다. L4 흐름 정보만으로는 HTTP 경로를 읽지 않으며, TLS 종단 뒤 백엔드 구간의 TLS는 별도로 설정합니다.

TLS 종단 뒤 HTTP 경로를 해석하는 L7 라우팅클라이언트 TLS는 로드 밸런서에서 끝나며 경로가 api이면 API 그룹, static이면 정적 그룹으로 보낸다. 백엔드 TLS는 별도 설정이다.클라이언트HTTPS 요청L7 로드 밸런서TLS 종단 · 경로 해석/api/*API 서버 그룹/static/*정적 서버 그룹TLS
TLS 종단 뒤 HTTP 경로를 해석하는 L7 라우팅클라이언트 TLS는 로드 밸런서에서 끝나며 경로가 api이면 API 그룹, static이면 정적 그룹으로 보낸다. 백엔드 TLS는 별도 설정이다.클라이언트HTTPS 요청L7 로드 밸런서TLS 종단경로 해석/api/*API 서버 그룹/static/*정적 서버 그룹TLS

청록색은 클라이언트→로드 밸런서의 TLS 구간입니다. 회색은 그룹을 고르는 라우팅 관계이며 평문 전송을 강제하지 않습니다. 각 그룹 안의 대상 선택·헬스 체크·drain과 응답 경로는 생략했습니다.

이 예의 조건

  • 본문과 전체 도표의 예를 따라 HTTPS 요청부터 그룹 전달까지 진행합니다.
예의 값과 변화
관찰·상황값·보충 정보변화·이유
HTTPS 요청

클라이언트가 L7 진입점으로 HTTPS 요청을 보냅니다.

  • TLS·요청 경로 1: client→L7 TLS
  • TLS·요청 경로 2: /api/*
  • 클라이언트와 진입점 사이 TLS 구간
종단과 해석

진입점이 TLS를 종료하고 HTTP 경로를 읽습니다.

  • TLS·요청 경로 1: L7 path=/api/*
  • 복호화한 /api/*로 경로 판단; L4 정보만으로는 읽을 수 없음
그룹 전달

해당 요청을 API 서버 그룹으로 보냅니다.

  • TLS·요청 경로 1: /api/*→API
  • TLS·요청 경로 2: /static/*→정적은 대안
  • 백엔드 TLS는 별도 선택; 회색 전달선이 평문을 강제하지 않음

HTTPS 경로를 고르려면, L7 진입점에서 TLS를 종료합니다

클라이언트가 L7 진입점으로 HTTPS 요청을 보냅니다. 해당 요청을 API 서버 그룹으로 보냅니다. 각 단계의 값과 전환 이유를 비교합니다.

로드 밸런서 · HTTPS 경로 라우팅

HTTPS 경로를 고르려면, L7 진입점에서 TLS를 종료합니다

이 예는 /api/*와 /static/*을 서로 다른 서버 그룹으로 보냅니다. L4 흐름 정보만으로는 HTTP 경로를 읽지 않으며, TLS 종단 뒤 백엔드 구간의 TLS는 별도로 설정합니다.

TLS 종단 뒤 HTTP 경로를 해석하는 L7 라우팅클라이언트 TLS는 로드 밸런서에서 끝나며 경로가 api이면 API 그룹, static이면 정적 그룹으로 보낸다. 백엔드 TLS는 별도 설정이다.클라이언트HTTPS 요청L7 로드 밸런서TLS 종단 · 경로 해석/api/*API 서버 그룹/static/*정적 서버 그룹TLS
TLS 종단 뒤 HTTP 경로를 해석하는 L7 라우팅클라이언트 TLS는 로드 밸런서에서 끝나며 경로가 api이면 API 그룹, static이면 정적 그룹으로 보낸다. 백엔드 TLS는 별도 설정이다.클라이언트HTTPS 요청L7 로드 밸런서TLS 종단경로 해석/api/*API 서버 그룹/static/*정적 서버 그룹TLS

청록색은 클라이언트→로드 밸런서의 TLS 구간입니다. 회색은 그룹을 고르는 라우팅 관계이며 평문 전송을 강제하지 않습니다. 각 그룹 안의 대상 선택·헬스 체크·drain과 응답 경로는 생략했습니다.

이 예의 조건

  • 본문과 전체 도표의 예를 따라 HTTPS 요청부터 그룹 전달까지 진행합니다.
예의 값과 변화
관찰·상황값·보충 정보변화·이유
HTTPS 요청

클라이언트가 L7 진입점으로 HTTPS 요청을 보냅니다.

  • TLS·요청 경로 1: client→L7 TLS
  • TLS·요청 경로 2: /api/*
  • 클라이언트와 진입점 사이 TLS 구간
종단과 해석

진입점이 TLS를 종료하고 HTTP 경로를 읽습니다.

  • TLS·요청 경로 1: L7 path=/api/*
  • 복호화한 /api/*로 경로 판단; L4 정보만으로는 읽을 수 없음
그룹 전달

해당 요청을 API 서버 그룹으로 보냅니다.

  • TLS·요청 경로 1: /api/*→API
  • TLS·요청 경로 2: /static/*→정적은 대안
  • 백엔드 TLS는 별도 선택; 회색 전달선이 평문을 강제하지 않음
구분보는 정보나누는 단위TLS주로 쓰는 곳
L4IP, 포트, 프로토콜연결그대로 통과시키거나 끝낼 수 있다TCP와 UDP 서비스 전반, 아주 많은 연결
L7호스트, 경로, 헤더, 쿠키요청경로를 보려면 끝내야 한다웹과 API, 경로별 라우팅

로드 밸런서에서 TLS를 끝낸 뒤 뒤쪽 서버까지의 구간은 평문 HTTP로 보낼 수도 있고, TLS로 다시 암호화할 수도 있습니다. 같은 데이터센터 안이라고 평문을 허용할지는 보안 정책으로 정해야 하며, 클라우드의 다른 사용자와 망을 함께 쓴다면 다시 암호화하는 편이 안전합니다.

뒤쪽 서버는 클라이언트를 직접 보지 못한다

로드 밸런서가 연결을 대신 맺으면, 뒤쪽 서버가 보는 출발지 주소는 클라이언트가 아니라 로드 밸런서의 주소입니다. 그래서 L7 로드 밸런서는 원래 클라이언트의 주소를 X-Forwarded-For 헤더나 표준인 Forwarded 헤더(RFC 7239)에 적어 전하고, HTTP를 해석하지 않는 L4 로드 밸런서는 연결의 맨 앞에 주소 정보를 붙이는 PROXY 프로토콜을 쓰기도 합니다.

이 헤더는 클라이언트도 마음대로 적어 보낼 수 있습니다. 뒤쪽 서버가 요청의 X-Forwarded-For를 그대로 믿으면, 공격자가 헤더에 아무 주소나 적어 IP로 막아 둔 관리 페이지에 들어오거나 10장 4편의 요청 수 제한을 피할 수 있습니다. 그래서 뒤쪽 서버는 로드 밸런서에서 온 연결의 헤더만 믿고, 로드 밸런서는 클라이언트가 보낸 같은 헤더를 지우거나 그 뒤에 자기가 본 주소를 덧붙이도록 설정합니다.

더 깊이: PROXY 프로토콜은 무엇을 어떻게 실어 보내나

PROXY 프로토콜은 HAProxy 프로젝트가 공개한 명세로 정의되며, TCP 연결의 맨 앞에 원래 연결의 주소와 포트를 담은 머리말을 한 번 붙입니다. 버전 1은 PROXY TCP4(IPv6라면 PROXY TCP6)로 시작해 출발지와 목적지의 주소, 포트를 공백으로 이은 사람이 읽을 수 있는 한 줄이고, 명세는 이 줄이 가장 길어도 107바이트라고 계산해 둡니다. 버전 2는 바이너리 형식으로, 다른 프로토콜의 첫 바이트로 오인되지 않도록 일부러 고른 12바이트의 서명으로 시작합니다.

명세는 받는 쪽의 규칙을 엄격하게 정합니다. 받는 쪽은 온전한 머리말을 받기 전에는 연결을 처리하지 말아야 하고, 머리말이 있는지 없는지를 짐작해서도 안 됩니다. 머리말이 있으면 읽고 없으면 그냥 넘어가는 서버라면, 누구든 직접 접속해 가짜 머리말로 출발지를 속일 수 있기 때문입니다. 그래서 PROXY 프로토콜을 받는 포트는 믿을 수 있는 로드 밸런서만 접속하게 막아 두고, 바깥에 직접 공개하는 포트와 함께 쓰지 않습니다. 또 여러 클라이언트의 요청을 한 연결로 묶어 보내는 프록시는 이 프로토콜을 쓰면 안 되는데, 머리말은 연결마다 한 번뿐이어서 받는 쪽이 모든 요청을 첫 클라이언트의 것으로 보기 때문입니다.

분산 알고리즘

알고리즘고르는 방법주의할 점
라운드 로빈차례대로 돌아가며 고른다요청의 수가 같아도 일의 양은 다를 수 있다
가중 라운드 로빈정해 둔 비율대로 돌아가며 고른다서버의 성능이 바뀌면 가중치도 고쳐야 한다
최소 연결지금 연결이 가장 적은 서버를 고른다연결마다 일의 양이 다르면 정확하지 않다
해시클라이언트 IP나 키를 해시해 고른다같은 클라이언트를 같은 서버로 보내지만, 서버 수가 바뀌면 크게 재배치된다
두 후보 중 선택(P2C)무작위로 두 서버를 골라 부하가 적은 쪽을 고른다부하 지표가 최신이어야 한다

라운드 로빈은 요청의 수를 고르게 나누지만, 요청마다 걸리는 시간이 다르면 부하는 고르지 않습니다. 다음 코드는 오래 걸리는 요청(10단위)과 짧은 요청(1단위)이 번갈아 오는 상황에서, 서버 두 대가 동시에 처리하게 되는 요청 수의 최댓값을 라운드 로빈과 최소 연결로 비교합니다.

def simulate(pick):
    servers = {"A": [], "B": []}                          # 서버마다 처리 중인 요청이 끝나는 시각
    peak = {"A": 0, "B": 0}
    for t, duration in enumerate([10, 1, 10, 1, 10, 1]):  # 매 시각 요청이 하나씩 온다
        for ends in servers.values():
            ends[:] = [e for e in ends if e > t]          # 끝난 요청은 뺀다
        name = pick(t, servers)
        servers[name].append(t + duration)
        peak[name] = max(peak[name], len(servers[name]))
    return peak

def round_robin(t, servers):
    return "AB"[t % 2]

def least_connections(t, servers):
    return min(servers, key=lambda name: len(servers[name]))   # 같으면 앞의 A

print("라운드 로빈:", simulate(round_robin))
print("최소 연결:", simulate(least_connections))
라운드 로빈: {'A': 3, 'B': 1}
최소 연결: {'A': 2, 'B': 2}

라운드 로빈은 순서가 공교롭게 맞아 오래 걸리는 요청 세 개를 모두 A에 보냈고, A는 한때 세 개를 동시에 처리했습니다. 최소 연결은 요청이 올 때마다 지금 처리 중인 수를 보고 골라 두 서버가 둘씩 나누어 맡았습니다. 다만 최소 연결도 연결의 수만 볼 뿐 그 연결이 얼마나 무거운지는 모르므로, 연결마다 일의 양이 크게 다르다면 응답 시간이나 CPU 사용률 같은 다른 지표를 함께 봐야 합니다.

해시 방식은 같은 클라이언트를 늘 같은 서버로 보내 캐시나 세션을 활용하기 좋지만, 해시 값을 서버 수로 나눈 나머지로 고른다면 서버 수가 바뀔 때 거의 모든 클라이언트가 다른 서버로 옮겨 갑니다.

keys = range(12)
moved = sum(1 for k in keys if k % 3 != k % 4)               # 서버 3대일 때와 4대일 때의 배정 비교
print(f"서버 3대 → 4대: {moved}/{len(keys)}개의 키가 다른 서버로 옮겨 감")
서버 3대 → 4대: 9/12개의 키가 다른 서버로 옮겨 감

서버를 한 대 늘렸을 뿐인데 열두 키 가운데 아홉이 다른 서버로 갔습니다. 서버마다 캐시를 두고 있었다면 거의 전부가 한꺼번에 미스가 나는 셈입니다. 이를 줄이려고 서버와 키를 같은 고리 위에 놓고 가장 가까운 서버를 고르는 일관된 해싱(consistent hashing)을 쓰면, 서버를 한 대 더할 때 대략 새 서버가 맡을 몫만큼만 옮겨 갑니다. 클라이언트 IP로 해시할 때는 회사나 모바일 통신사의 NAT 뒤에 있는 많은 사용자가 같은 IP를 쓰므로, 그 사용자들이 모두 한 서버에 몰릴 수 있다는 점도 기억해야 합니다.

liveness와 readiness

로드 밸런서는 정해진 간격으로 서버에 상태 확인(health check)을 보내 트래픽을 받을 수 있는 서버에만 요청을 보냅니다. 이때 무엇을 확인하느냐가 중요합니다. liveness는 프로세스가 살아서 응답할 수 있는지를 보는 얕은 확인이고, readiness는 지금 새 요청을 받아 처리할 준비가 되었는지를 보는 확인입니다. readiness에는 데이터베이스 연결처럼 요청을 처리하는 데 꼭 필요한 의존성을 넣습니다.

def live():
    return 200, {"status": "alive"}                              # 프로세스가 응답할 수 있는가

def ready(db_ping):
    try:
        db_ping()                                                # 요청을 처리하는 데 필요한 의존성
    except OSError:
        return 503, {"status": "unhealthy", "database": "fail"}  # 새 요청을 받을 준비가 안 됐다
    return 200, {"status": "ready", "database": "ok"}

def db_down():
    raise ConnectionRefusedError("db:5432")

print("live:", live())
print("ready:", ready(lambda: None))
print("ready(DB 장애):", ready(db_down))
live: (200, {'status': 'alive'})
ready: (200, {'status': 'ready', 'database': 'ok'})
ready(DB 장애): (503, {'status': 'unhealthy', 'database': 'fail'})

데이터베이스가 내려가면 readiness는 503을 돌려 로드 밸런서가 이 서버로 새 요청을 보내지 않게 하지만, liveness는 여전히 200입니다. 두 확인을 나누는 이유는 대응이 다르기 때문입니다. Kubernetes 같은 플랫폼은 liveness가 실패하면 프로세스를 다시 시작하는데, liveness에 데이터베이스 확인까지 넣으면 데이터베이스가 잠시 흔들릴 때 모든 서버가 동시에 재시작되어 장애가 더 커집니다. 다시 시작해도 데이터베이스는 살아나지 않으므로, 의존성의 문제는 readiness로 트래픽에서 빼는 것으로 충분합니다.

상태 확인의 임계값

로드 밸런서는 확인 한 번의 실패로 서버를 빼지 않습니다. 네트워크가 잠깐 흔들려도 서버가 빠졌다 들어오기를 반복하지 않도록, 연속으로 몇 번 실패해야 빼고 연속으로 몇 번 성공해야 다시 넣을지 임계값을 둡니다. 다음 코드는 세 번 연속 실패하면 빼고 두 번 연속 성공하면 다시 넣는 규칙을 확인 결과의 순서에 적용합니다.

def target_states(results, unhealthy_after=3, healthy_after=2):
    healthy, fails, oks, states = True, 0, 0, []
    for ok in results:
        fails, oks = (0, oks + 1) if ok else (fails + 1, 0)
        if healthy and fails >= unhealthy_after:
            healthy = False                                      # 연속 실패가 임계값에 이르면 뺀다
        elif not healthy and oks >= healthy_after:
            healthy = True                                       # 연속 성공이 임계값에 이르면 다시 넣는다
        states.append("정상" if healthy else "제외")
    return states

print(target_states([True, False, False, False, True, True, False, True]))
['정상', '정상', '정상', '제외', '제외', '정상', '정상', '정상']

두 번째 확인부터 실패했지만 세 번째 실패에서야 빠졌고, 다시 성공한 뒤에도 두 번째 성공에서야 들어왔습니다. 일곱 번째의 실패 한 번은 서버를 빼지 않았습니다. 이 규칙의 대가는 시간입니다. 확인 간격이 10초라면 서버가 죽고 빠지기까지 최대 30초가 걸리고, 그동안 그 서버로 간 요청은 실패합니다. 간격과 임계값을 줄이면 빨리 빠지지만, 잠깐의 흔들림에도 서버가 빠지기 쉬워집니다.

모든 서버가 비정상으로 판정되는 경우의 동작도 제품마다 다릅니다. 예를 들어 AWS ALB는 모든 대상이 비정상이면 오히려 모두에게 요청을 보내는 fail-open 동작을 합니다. 상태 확인 자체가 잘못되었을 때 서비스 전체가 멈추는 것을 막으려는 설계입니다.

배포할 때 서버를 빼는 일은 상태 확인의 실패와 다릅니다. 로드 밸런서에서 서버를 등록 해제하면 새 요청은 보내지 않지만, 이미 처리 중인 요청은 정해 둔 시간 동안 마치도록 기다리는데, 이를 드레이닝(draining)이라고 합니다. 드레이닝 시간이 가장 오래 걸리는 요청보다 짧으면 배포할 때마다 그 요청이 끊깁니다.

세션을 어떻게 유지할까

로그인 세션을 서버의 메모리에 둔다면, 다음 요청이 다른 서버로 가는 순간 로그인이 풀립니다. 이를 다루는 방법은 셋입니다.

방법하는 일생각할 점
스티키 세션(sticky session)로드 밸런서가 쿠키 등으로 같은 사용자를 같은 서버로 보낸다그 서버가 죽으면 세션이 사라지고, 서버마다 부하가 고르지 않아진다
공유 세션 저장소Redis 같은 저장소에 세션을 두고 모든 서버가 함께 읽는다저장소의 가용성과 복제를 운영해야 한다
토큰9장 3편의 서명된 토큰에 상태를 담아 클라이언트가 들고 다닌다만료 전에 회수하기 어렵고, 키를 바꾸는 운영이 필요하다

서버를 자유롭게 늘리고 줄이려면 서버의 메모리에 사용자의 상태를 두지 않는 편이 좋습니다. 그래도 스티키 세션이 틀린 선택은 아닙니다. 서버가 죽었을 때 세션을 잃어도 괜찮은 서비스라면 가장 단순한 방법일 수 있으므로, 장애가 났을 때 무엇을 잃고 부하가 어떻게 나뉘는지를 기준으로 고릅니다.

정리

  • L4 로드 밸런서는 주소와 포트로 연결 단위를, L7 로드 밸런서는 HTTP의 내용으로 요청 단위를 나누며, HTTPS의 경로를 보려면 L7이 TLS를 끝내야 합니다.
  • 뒤쪽 서버는 로드 밸런서의 주소를 보므로 클라이언트의 주소를 X-Forwarded-For나 Forwarded, PROXY 프로토콜로 전하고, 이 헤더는 로드 밸런서에서 온 것만 믿습니다.
  • 라운드 로빈은 요청의 수를, 최소 연결은 처리 중인 연결의 수를 고르게 하며, 나머지로 고르는 해시는 서버 수가 바뀔 때 크게 재배치되므로 일관된 해싱을 씁니다.
  • liveness는 살아 있는지, readiness는 요청을 받을 준비가 되었는지를 보며, 의존성의 장애는 readiness로만 반영해 재시작이 번지지 않게 합니다.
  • 상태 확인은 연속 실패와 연속 성공의 임계값으로 서버를 빼고 넣으므로 장애를 알아채는 데 간격 × 임계값만큼 걸리고, 배포 때의 등록 해제와 드레이닝은 따로 맞춥니다.
  • 세션은 스티키 세션, 공유 저장소, 토큰 가운데 장애 때 잃는 것과 부하의 분포를 기준으로 고릅니다.

연습 문제

1. 요청의 수와 일의 양

라운드 로빈으로 서버 A와 B에 요청을 두 개씩 보냈습니다. A가 받은 요청은 각각 CPU를 100ms씩 쓰고, B가 받은 요청은 5ms씩 씁니다. 두 서버의 부하는 고르게 나뉘었나요? 최소 연결 방식이라면 이 문제가 완전히 풀리나요?

해설 보기

요청의 수는 둘씩 같지만, 일의 양은 A가 200ms, B가 10ms로 스무 배 차이가 납니다. 라운드 로빈이 고르게 나누는 것은 요청의 수일 뿐입니다. 최소 연결은 A가 오래 걸리는 요청을 처리하는 동안 연결이 남아 있으므로 새 요청을 B로 보내는 경향이 있어 도움이 되지만, 연결 하나하나의 무게는 여전히 모릅니다. 짧고 무거운 요청이 섞여 있다면 응답 시간이나 CPU 사용률처럼 실제 부하에 가까운 지표로 고르는 방식을 검토합니다.

2. 살아 있지만 준비되지 않은 서버

본문의 코드에서 /live는 200을, /ready는 데이터베이스 연결의 실패로 503을 돌려주었습니다. 이 서버는 어떤 상태인가요? 이 결과만 보고 프로세스를 바로 다시 시작해야 하나요?

해설 보기

프로세스는 살아서 응답하고 있지만, 데이터베이스에 연결하지 못해 새 요청을 처리할 준비는 되지 않은 상태입니다. 로드 밸런서는 readiness의 실패로 이 서버에 새 요청을 보내지 않으면 됩니다. 원인이 데이터베이스에 있다면 프로세스를 다시 시작해도 나아지지 않고, 모든 서버가 같은 이유로 동시에 재시작되면 오히려 장애가 커집니다. 데이터베이스가 돌아오면 readiness가 다시 성공하고 서버는 임계값만큼의 성공 뒤에 자연스럽게 트래픽에 복귀합니다.

3. 빠지기 전까지 실패하는 요청

서버 네 대가 초당 100개의 요청을 고르게 나누어 받고 있습니다. 상태 확인은 10초마다 하고 세 번 연속 실패하면 서버를 뺍니다. 한 서버가 상태 확인에 성공한 직후 죽었다면, 그 서버가 빠지기까지 최대 몇 초가 걸리고 그동안 실패하는 요청은 대략 몇 개인가요?

해설 보기

죽은 직후부터 10초, 20초, 30초의 확인이 차례로 실패해야 하므로 빠지기까지 최대 30초가 걸립니다. 그동안 죽은 서버는 전체 요청의 4분의 1인 초당 25개를 받으므로, 대략 25 × 30 = 750개의 요청이 실패합니다. 확인 간격을 5초로 줄이면 15초, 약 375개로 줄지만, 짧은 흔들림에도 서버가 빠지기 쉬워집니다. 연결이 실패한 요청을 다른 서버로 한 번 더 보내는 로드 밸런서의 재시도 기능을 쓰면 이 사이의 실패를 더 줄일 수 있지만, 그 요청이 다시 보내도 괜찮은지는 따져야 합니다.

다음 글 프록시와 VPN에서는 요청을 대신 전하는 포워드 프록시와 리버스 프록시, 사설망을 인터넷 너머로 잇는 VPN의 구조와 보호 범위를 살펴봅니다.