사전 부하 분산
개념

부하 분산

gabury1고친 사람 github-actions[bot]

부하 분산은 들어오는 일을 여러 대의 서버에 나눠 맡기는 일입니다. 한 대가 모든 요청을 떠안지 않게 합니다. 그래서 서버를 더 붙이는 만큼 더 많은 요청을 받을 수 있습니다. 한 대가 멈춰도 남은 서버가 요청을 이어받게 하는 바탕이 되기도 합니다.

쉽고 빠른 이해

부하 분산은 요청을 여러 서버에 나눠 보내는 일입니다. 같은 애플리케이션을 세 대에 띄워 둡니다. 앞에 선 장치가 요청마다 셋 중 하나를 골라 넘깁니다.

이게 없으면 서버 한 대가 모든 요청을 받습니다. 그 한 대가 버틸 수 있는 양이 곧 서비스 전체의 한계가 됩니다. 그 한 대가 멈추면 서비스도 같이 멈춥니다.

어떻게 도나:

  1. 사용자는 주소 하나로 요청을 보냅니다
  2. 앞에 선 장치가 정해 둔 규칙으로 서버 하나를 고릅니다. 차례대로 돌리거나, 일이 가장 적은 서버를 고릅니다
  3. 장치는 서버들이 살아 있는지 주기적으로 물어봅니다. 대답이 없는 서버에는 요청을 안 보냅니다

언제 쓰나: 요청이 한 대로 감당이 안 되거나, 서버 한 대가 멈춰도 서비스가 계속 돌아야 할 때 씁니다. 한 대로 요청을 넉넉히 받고 잠깐 멈춰도 괜찮은 서비스라면 필요 없습니다.

대가도 있습니다. 요청 사이에 기억해야 할 것이 서버 메모리에 있으면 다음 요청이 다른 서버로 가서 그 기억을 잃습니다. 앞에 선 장치 자체가 멈추면 뒤의 서버가 멀쩡해도 요청이 못 들어옵니다.

상세

은행에 가면 번호표를 뽑습니다. 창구 여럿 중 먼저 비는 곳이 다음 번호를 부릅니다. 같은 손님도 올 때마다 새 번호표를 뽑으니 매번 다른 창구에 설 수 있습니다.

부하 분산(load balancing)은 요청이나 작업을 같은 일을 하는 여러 서버에 나눠 보내는 일입니다. 여기서 부하는 서버가 떠안은 일의 양입니다.

부하를 잴 때는 요청과 연결을 구분합니다. 연결은 클라이언트와 서버 사이에 한 번 열어 두는 통로입니다. 요청은 그 통로로 보내는 한 번의 부탁입니다. 연결 하나에 요청 여럿이 차례로 실릴 수 있습니다.

그래서 부하를 재는 잣대도 여럿입니다. 동시에 처리 중인 요청 수, 열린 연결 수, CPU(Central Processing Unit, 중앙 처리 장치) 사용률이 흔한 잣대입니다.

이 절은 요청 하나가 나뉘어 가는 길을 따라가며, 나누는 방법과 그 대가를 차례로 봅니다.

한 대로는 모자라는 까닭

서버 한 대가 처리할 수 있는 요청 수에는 끝이 있습니다. CPU 코어 수와 메모리 크기가 그 끝을 정합니다. 요청이 그 끝을 넘으면 응답이 느려집니다. 더 넘으면 요청이 쌓이다가 실패합니다.

한 대를 코어와 메모리가 더 많은 기계로 바꾸는 방법이 있습니다. 이것을 수직 확장이라고 부릅니다. 하지만 기계 한 대를 키우는 데는 한계가 있습니다. 기계가 클수록 값도 가파르게 오릅니다.

다른 방법은 같은 서버를 여러 대 두는 것입니다. 이 방법을 수평 확장이라 합니다. 서버를 늘리려면 들어온 요청을 그 서버들에 나눠 줄 누군가가 필요합니다. 부하 분산이 그 일을 맡습니다.

한 대만 두면 그 한 대가 멈출 때 서비스 전체가 멈춥니다. 멈추면 전체가 멈추는 구성 요소를 단일 장애점이라고 부릅니다. 서버를 여러 대로 나눠 두면 서버 쪽에서는 이 약점이 사라집니다.

요청이 지나가는 길

가장 흔한 구성은 서버들 앞에 요청을 받아 넘기는 장치를 하나 세우는 것입니다. 로드 밸런서는 바로 이 장치입니다. 사용자는 로드 밸런서의 주소 하나만 압니다. 뒤에 서버가 몇 대인지는 모릅니다.

flowchart TD
    U["사용자 요청"] --> LB["로드 밸런서 · 주소 하나"]
    subgraph 서버들["같은 애플리케이션을 띄운 서버들"]
        S1["서버 1"]
        S2["서버 2"]
        S3["서버 3"]
    end
    LB --> S1
    LB --> S2
    LB --> S3

로드 밸런서는 요청마다 어느 서버로 보낼지 새로 고릅니다. 서버를 한 대 더 붙이면 로드 밸런서의 목록에 한 줄만 더하면 됩니다.

나누는 일을 로드 밸런서만 하는 것은 아닙니다. 나누는 주체에 따라 크게 셋으로 갈립니다. 아래 표 첫 줄의 DNS(Domain Name System, 도메인 네임 시스템)는 도메인 이름을 서버 주소로 바꿔 주는 체계입니다. 한 번 조회한 결과는 곳곳에 캐시됩니다. 캐시는 다음 조회를 빨리 하려고 결과를 잠시 적어 두는 것입니다.

나누는 주체 어떻게 나누나 약점
DNS 이름 하나에 서버 주소를 여럿 적어 두고 조회마다 다르게 돌려줍니다 조회 결과가 캐시되어, 죽은 서버를 빼도 한동안 그 주소로 요청이 갑니다
로드 밸런서 서버들 앞에 선 장치가 요청을 받아 골라 넘깁니다 요청이 한 곳을 더 거칩니다. 그 장치가 멈추면 전부 멈춥니다
클라이언트 부르는 쪽이 서버 목록을 들고 직접 하나를 고릅니다 부르는 쪽마다 목록과 고르는 규칙을 들고 있어야 합니다

셋은 누가 서버 목록을 쥐느냐가 다릅니다. 백엔드 개발자가 가장 자주 만나는 것은 가운데 줄의 로드 밸런서입니다.

어느 서버로 보낼지 고르는 규칙

나누는 쪽은 요청마다 서버 하나를 고릅니다. 고르는 규칙을 분산 알고리즘이라고 부릅니다. 규칙마다 무엇을 보고 고르는지가 다릅니다.

가장 단순한 규칙은 차례대로 돌리는 것입니다. 라운드로빈이 그 규칙입니다. 서버 상태를 전혀 보지 않고 순서만 기억합니다.

Python
servers = ["A", "B", "C"]
i = 0

def pick():
    global i
    s = servers[i % len(servers)]
    i += 1
    return s

pick()  # "A"
pick()  # "B"
pick()  # "C"
pick()  # "A"

네 번째 호출에서 다시 A 로 돌아옵니다. 요청마다 걸리는 시간이 비슷하면 이것만으로 고르게 나뉩니다. 어떤 요청은 1초, 어떤 요청은 1분이 걸리면 한 서버에 긴 요청이 몰릴 수 있습니다.

그래서 서버 상태를 보는 규칙들이 있습니다. 아래 표는 흔히 쓰는 넷입니다.

규칙 무엇을 보고 고르나 잘 맞는 경우
라운드로빈 순서만 봅니다 서버 성능이 같고 요청이 고르게 짧을 때
가중 라운드로빈 서버마다 매긴 비율대로 돌립니다 서버 성능이 서로 다를 때
최소 연결 지금 열린 연결이 가장 적은 서버를 고릅니다 요청마다 걸리는 시간이 크게 다를 때
IP(Internet Protocol) 해시 보낸 쪽 주소로 계산한 값으로 고릅니다 같은 사용자를 늘 같은 서버로 보내야 할 때

표의 마지막 줄인 IP 해시는 보낸 쪽 IP 주소를 해시 함수에 넣는 규칙입니다. 나온 해시 값을 서버 수로 나눈 나머지가 고를 서버의 번호가 됩니다. 해시 함수는 같은 입력에 늘 같은 값을 돌려주므로, 같은 주소는 늘 같은 서버로 갑니다.

문제는 서버 수가 바뀔 때입니다. 나누는 수가 달라지므로 서버가 한 대만 늘거나 줄어도 대부분의 사용자가 다른 서버로 옮겨 갑니다. 일관성 해싱은 서버 수가 바뀌어도 일부 사용자만 옮겨 가게 만드는 해싱 방식이라, 이 흔들림을 줄이려고 씁니다.

죽은 서버를 빼는 헬스체크

어떤 규칙으로 고르든 멈춘 서버를 고르면 요청이 실패합니다. 그래서 로드 밸런서는 서버가 살아 있는지 계속 확인합니다. 이 확인을 헬스 체크라고 부르며, 로드 밸런서가 기본으로 하는 일입니다.

헬스체크는 정해 둔 간격마다 서버에 작은 요청을 보내는 식으로 돕니다. 연결만 열어 보는 방식이 있습니다. /health 처럼 상태를 알려 주는 주소를 불러 응답 코드가 200 인지 보는 방식도 있습니다. 정해 둔 횟수만큼 연달아 실패하면 그 서버를 후보에서 뺍니다.

stateDiagram-v2
    [*] --> 후보
    후보 --> 제외: 연달아 실패
    제외 --> 후보: 연달아 성공

그림처럼 서버는 후보와 제외 사이를 오갑니다. 빠진 서버에도 확인 요청은 계속 갑니다. 다시 정해 둔 횟수만큼 성공하면 후보로 돌아옵니다. 한 번의 실패로 바로 빼지 않는 까닭은 잠깐 느려진 서버를 죽은 것으로 오판하지 않기 위해서입니다.

서버 메모리에 둔 세션

로그인 정보처럼 요청 사이에 이어지는 정보를 세션이라고 부릅니다. 세션을 서버 메모리에 두면 문제가 생깁니다. 사용자가 서버 1 에서 로그인했다고 해 봅시다. 다음 요청이 서버 2 로 가면 서버 2 는 그 사용자를 모릅니다.

해결은 두 갈래입니다. 하나는 같은 사용자를 늘 같은 서버로 보내는 세션 어피니티입니다. 흔히 스티키 세션이라고도 합니다. 쿠키나 보낸 쪽 주소로 사용자를 알아봅니다.

다른 하나는 서버가 세션을 들고 있지 않게 만드는 것입니다. 세션을 Redis 같은 공유 저장소에 두면 어느 서버가 요청을 받아도 같은 결과를 냅니다.

이렇게 요청 사이의 정보를 자기 메모리에 들고 있지 않는 서버를 무상태 서버라고 부릅니다. 어느 서버로 보내도 결과가 같으니, 부하 분산은 무상태 서버와 가장 잘 맞습니다.

세션 어피니티는 고치기 쉽지만 대가가 있습니다. 한 서버에 요청을 많이 보내는 사용자가 몰려도 다른 서버로 옮길 수 없습니다. 그 서버가 멈추면 거기 묶인 사용자의 세션도 함께 사라집니다.

L4 와 L7

나누는 쪽이 요청의 어디까지 들여다보는지에 따라 둘로 갈립니다. 이름은 네트워크를 일곱 계층으로 나눈 OSI 모델(Open Systems Interconnection, 개방형 시스템 상호 연결)의 계층 번호에서 왔습니다.

L4 로드 밸런싱은 주소와 포트 번호만 보고 고릅니다. 4계층인 전송 계층까지만 보기 때문입니다. 고른 뒤에는 TCP 연결을 전부 서버 하나에 넘깁니다. TCP 는 Transmission Control Protocol 의 줄임말이고, 연결을 열어 데이터를 순서대로 주고받는 방식입니다. 내용을 안 읽으므로 빠르지만, 요청이 어느 경로를 부르는지는 모릅니다.

L7 로드 밸런싱은 요청 내용까지 보고 고릅니다. 7계층인 응용 계층까지 읽기 때문입니다. HTTP(HyperText Transfer Protocol) 요청이면 경로, 헤더, 쿠키를 봅니다. /images 로 시작하는 요청은 이미지 서버로, 나머지는 애플리케이션 서버로 보내는 식입니다. 암호화된 연결은 이 장치에서 풀어야 내용을 읽을 수 있어서 일이 더 많습니다.

부하 분산과 고가용성

고가용성은 한 대가 멈춰도 서비스가 계속 도는 성질입니다. 부하 분산은 헬스체크로 죽은 서버를 빼므로, 서버 쪽의 멈춤은 비껴 갈 수 있습니다. 그렇다고 부하 분산만으로 고가용성이 되지는 않습니다.

로드 밸런서 자신도 한 대면 그것이 새 단일 장애점이 됩니다. 그래서 로드 밸런서를 두 대 두고, 한 대가 멈추면 다른 대가 넘겨받게 하는 장애 조치를 따로 붙입니다.

나누면 생기는 대가

요청이 로드 밸런서를 한 번 더 거치므로 지연이 조금 늡니다. 관리할 장치도 하나 늘어납니다. 로그도 여러 서버에 흩어지므로 요청 하나를 따라가려면 로그를 한곳에 모아야 합니다.

L4 처럼 연결 단위로 서버를 고르면, 연결 하나에 실린 요청은 모두 같은 서버로 갑니다. 그래서 오래 열려 있는 연결은 고르게 안 나뉩니다. WebSocket 처럼 한 번 연결해서 오래 쓰는 방식은 처음 고른 서버에 계속 붙어 있습니다. 서버를 새로 붙여도 이미 열린 연결은 옮겨 가지 않습니다.

서버가 공유하는 뒤쪽 데이터베이스는 나눠지지 않습니다. 애플리케이션 서버를 열 대로 늘려도 모두 같은 데이터베이스 한 대를 부르면 병목이 그쪽으로 옮겨 갈 뿐입니다. 데이터베이스를 나누는 일은 복제나 샤딩이 따로 맡습니다.

이 대가 때문에 모든 서비스에 부하 분산이 필요한 것은 아닙니다. 서버 한 대로 요청을 넉넉히 받고, 잠깐 멈춰도 괜찮은 서비스라면 한 대로 두는 편이 단순합니다. 요청이 한 대의 끝을 넘거나, 한 대가 멈춰도 서비스가 계속 돌아야 할 때 부하 분산을 붙입니다.

관련 항목

부하 분산을 맡는 장치와 소프트웨어

로드 밸런서 · 리버스 프록시 · nginx · HAProxy · Envoy · API 게이트웨이 · 서비스 메시

부하 분산이 서버를 고르는 알고리즘

라운드로빈 · 가중 라운드로빈 · 최소 연결 · IP 해시 · 일관성 해싱 · 무작위 선택

부하 분산이 서버 상태를 다루는 방법

헬스 체크 · 세션 어피니티 · 세션 · 무상태 · 연결 드레이닝

부하 분산이 나누는 계층과 경로

L4 로드 밸런싱 · L7 로드 밸런싱 · OSI 모델 · DNS · DNS 라운드로빈 · 애니캐스트 · CDN

부하 분산으로 얻으려는 성질

수평 확장 · 수직 확장 · 고가용성 · 가용성 · 처리량 · 단일 장애점 · 장애 조치

부하 분산이 못 나눠 따로 풀어야 하는 병목

데이터베이스 · 복제 · 샤딩 · 캐싱 · 오토스케일링

부하 분산을 재고 시험하는 방법

부하 테스트 · 지연 시간 · 백프레셔 · 레이트 리미팅

다른 이름: load balancing · 로드 밸런싱