개발 학습 실습실

이 사이트의 실습은 자바스크립트를 켜야 움직입니다. 아래는 같은 내용을 글로 적은 것입니다.

기초개념 › 구조와 운영

로드 밸런서

서버를 여러 대 두면 요청은 어느 서버로 가는가

서버 한 대가 감당하지 못할 만큼 요청이 많으면 같은 서버 프로그램을 여러 대 띄웁니다. 그런데 클라이언트(요청을 보내는 쪽)가 서버 주소를 여러 개 알아야 한다면 불편합니다. 그래서 클라이언트는 주소 하나로만 접속하고, 그 주소에서 요청을 받는 로드 밸런서가 요청을 서버들에 나눠 보냅니다. 이 실습의 로드 밸런서는 서버 A, 서버 B에 번갈아 보내고, 연결이 거부된 서버는 건너뜁니다. 코드 칸은 자바 코드가 아니라, 어떤 프로그램이 어느 주소에서 무엇을 하는지 적은 구성입니다(#으로 시작하는 줄은 설명입니다). 서버는 자기가 받은 요청 수를 자기 메모리에 세어 "나는 A, 받은 요청 1번째"처럼 답합니다. 이 실습은 로드 밸런서를 빼 보고, 서버 A 프로그램을 요청 3번 뒤에 꺼 보면서 클라이언트가 받는 답이 어떻게 달라지는지 봅니다.

실행으로 대조 · Node.js 24.18.0의 http 서버 A(B)와 직접 만든 로드 밸런서, http 클라이언트로 네 가지 조합을 실행해 대조

이 실습에 나오는 개념

로드밸런서
들어오는 요청을 여러 서버에 나눠 보내는 장치다. 서버의 상태를 주기적으로 확인(헬스 체크)해 문제가 있는 서버로는 요청을 보내지 않는다. 한 서버에 부하가 몰리는 것을 막고, 일부 서버에 장애가 나도 서비스를 유지하게 한다.
라운드 로빈
라운드 로빈은 요청을 서버들에 차례대로 돌아가며 한 대씩 나눠 주는 방식이다. 서버가 A, B 두 대이면 요청이 A, B, A, B 순으로 간다. NGINX 문서는 서버들에 요청을 고르게 나누는 이 방식이 기본이며 따로 켜는 지시어가 없다고 적고, 서버마다 가중치를 줄 수 있다고 한다. 요청이 몇 번째로 어느 서버에 가는지가 정해진 순서를 따르므로, 서버가 늘 같은 상태일 때 요청은 서버들에 고르게 나뉜다.
실패한 서버 건너뛰기
로드 밸런서가 어느 서버에 연결하려는데 오류가 나면, 그 서버를 쓰지 않고 같은 요청을 다음 서버에 보내는 동작이다. NGINX 문서에서 proxy_next_upstream의 기본값은 error와 timeout이고, error에는 서버와 연결을 맺는 중 난 오류가 들어 있어서, 연결이 거부되면 요청이 다음 서버로 넘어간다. 또 max_fails(기본 1)번 실패하면 fail_timeout(기본 10초) 동안 그 서버를 쓸 수 없는 서버로 본다. 이 덕분에 서버 한 대가 꺼져도 남은 서버가 있으면 새로 오는 요청은 살아 있는 서버가 받는다(처리 중이던 요청은 실패할 수 있다).
연결 거부 오류
연결 거부 오류는 TCP로 연결을 요청했는데 그 포트를 열고 기다리는 프로그램이 없을 때 보내는 쪽이 받는 오류이다. Node.js에서는 이 경우 connect 이벤트 대신 error 이벤트가 오고 오류 코드가 ECONNREFUSED로 나타난다(net 문서는 연결에 문제가 있으면 connect 대신 error가 온다고 적고, ECONNREFUSED라는 이름은 문서에서 확인하지 못했으며 이 PC의 실행에서 관찰했다). 연결이 맺어지지 않았으므로 글은 하나도 보내지지 않는다. TCP 표준은 연결이 없는 상태로 온 요청에 거부 신호(RST)를 보내도록 정한다(RFC 9293 3.5.2절).

이렇게 바꿔 보세요

처음 설정으로 실행하면

  1. 서버가 뜹니다. 같은 서버 프로그램 두 개가 서버 A와 서버 B로 따로 뜹니다. 둘은 각자 받은 요청 수를 자기 메모리에 세고, 요청이 오면 "나는 A" 또는 "나는 B"와 그 수를 답합니다. 그 앞에 로드 밸런서가 뜹니다. 클라이언트는 서버가 몇 대인지 모르고 localhost:8080 주소 하나만 압니다. 이 주소에서 요청을 받는 것이 로드 밸런서입니다.
  2. 클라이언트가 GET / 요청을 보냅니다(1번째). 8080 주소에서 요청을 받는 것은 서버가 아니라 로드 밸런서입니다. 로드 밸런서는 서버 목록의 첫 서버인 서버 A에 이 요청을 전달합니다.
  3. 서버 A가 자기 메모리에 센 받은 요청 수를 하나 올리고 "나는 A, 받은 요청 1번째"라고 답합니다. 로드 밸런서가 이 답을 그대로 클라이언트에 돌려줍니다. 클라이언트가 받은 글에는 어느 서버가 답했는지가 적혀 있습니다.
  4. 클라이언트가 GET / 요청을 보냅니다(2번째). 로드 밸런서는 번갈아 보내므로 이번에는 서버 B 차례입니다. 서버 B가 자기 메모리에 센 수를 하나 올리고 "나는 B, 받은 요청 1번째"라고 답하고, 로드 밸런서가 이 답을 돌려줍니다. 서버 A가 이미 1번 답했지만 서버 B의 수는 "1번째"부터 시작합니다. 받은 요청 수는 서버마다 자기 메모리에 따로 세기 때문입니다.
  5. 클라이언트가 GET / 요청을 보냅니다(3번째). 로드 밸런서는 번갈아 보내므로 이번에는 서버 A 차례입니다. 서버 A가 자기 메모리에 센 수를 하나 올리고 "나는 A, 받은 요청 2번째"라고 답하고, 로드 밸런서가 이 답을 돌려줍니다.
  6. 클라이언트가 GET / 요청을 보냅니다(4번째). 로드 밸런서는 번갈아 보내므로 이번에는 서버 B 차례입니다. 서버 B가 자기 메모리에 센 수를 하나 올리고 "나는 B, 받은 요청 2번째"라고 답하고, 로드 밸런서가 이 답을 돌려줍니다.
  7. 클라이언트가 GET / 요청을 보냅니다(5번째). 로드 밸런서는 번갈아 보내므로 이번에는 서버 A 차례입니다. 서버 A가 자기 메모리에 센 수를 하나 올리고 "나는 A, 받은 요청 3번째"라고 답하고, 로드 밸런서가 이 답을 돌려줍니다.
  8. 클라이언트가 GET / 요청을 보냅니다(6번째). 로드 밸런서는 번갈아 보내므로 이번에는 서버 B 차례입니다. 서버 B가 자기 메모리에 센 수를 하나 올리고 "나는 B, 받은 요청 3번째"라고 답하고, 로드 밸런서가 이 답을 돌려줍니다.
  9. 요청 6번이 끝났습니다. 서버 A가 3번, 서버 B가 3번 답했고, 실패한 요청은 0번입니다. 서버 A가 센 받은 요청 수는 3번, 서버 B가 센 수는 3번입니다.

실행 결과

코드

# 클라이언트: http://localhost:8080/ 로 요청 6번
# 8080: 로드 밸런서 (서버 A, 서버 B에 번갈아 보낸다)
# 서버 A(9001), 서버 B(9002): "나는 A", "나는 B"라고 답한다
#   답에는 자기가 받은 요청 수도 적는다

이 모형의 전제

서버 A(와 서버 B)는 Node.js 24.18.0의 http 서버이고, 받은 요청 수를 서버마다 자기 변수에 세어 "나는 A, 받은 요청 1번째"처럼 답합니다. 로드 밸런서는 NGINX 같은 제품이 아니라 같은 Node.js의 http 서버로 직접 만든 것입니다. 요청이 올 때마다 서버 목록의 시작 자리를 하나씩 돌려(A, B, A, B...) 그 서버에 새 연결로 요청을 전달하고, 연결이 거부되면 그 서버를 건너뛰어 다음 서버에 같은 요청을 보내며, 한 번 거부당한 서버는 이후 다시 시도하지 않습니다. 클라이언트는 http 모듈로 GET / 요청을 요청마다 새 연결로 6번 차례로 보냈습니다. 서버 A를 끄는 것은 프로세스를 끝내는 대신 그 서버의 연결 받는 소켓과 열린 연결을 닫는 것으로 했고, 닫은 뒤 클라이언트나 로드 밸런서가 연결하면 ECONNREFUSED(연결 거부 오류)가 났습니다. 네 가지 조합을 모두 실행해, 서버 A와 서버 B가 답한 수, 실패한 요청 수, 요청 6번 각각을 어느 서버가 몇 번째로 답했는지, 서버가 메모리에 센 수, 로드 밸런서가 연결을 거부당한 요청과 서버, 서버가 받은 요청 글을 모형과 대조했고, 화면의 코드가 그 구성과 같은 내용인지도 조합마다 확인했습니다. 화면의 코드는 실행한 프로그램이 아니라 그 구성을 설명한 글이고, 화면의 8080, 9001, 9002는 예시 주소이며 실제 실행에서는 빈 포트를 썼습니다. 이것들은 이 구현에서 관찰한 동작입니다. 문서로 확인한 것은 RFC 9110 3.7절(게이트웨이, 곧 리버스 프록시는 받은 요청을 안쪽의 다른 서버나 여러 서버에 전달하는 중간 서버라는 정의)과 NGINX 문서(nginx.org의 ngx_http_upstream_module: 요청은 기본으로 가중치를 둔 라운드 로빈으로 서버들에 나뉘고, max_fails 기본 1과 fail_timeout 기본 10초가 실패한 서버를 그 시간 동안 쓰지 않을 서버로 본다는 설명, ngx_http_proxy_module: proxy_next_upstream 기본값이 error timeout이고 error에는 서버와 연결을 맺는 중 오류가 난 경우가 들어 있다는 설명)입니다. 이 PC에는 NGINX가 없어 NGINX를 실행해 보지는 않았고, 이 실습의 로드 밸런서가 NGINX와 같은 취지로 움직이게 짠 것일 뿐 NGINX의 실제 동작을 대조한 것은 아닙니다. 다루지 않은 것: 로드 밸런서 자신이 꺼지는 경우, 꺼진 서버가 다시 켜져 돌아오는 경우, 서버 상태를 주기적으로 확인하는 헬스 체크, 서버마다 처리 능력이나 가중치가 다른 경우, 같은 사용자의 요청을 같은 서버로 보내는 방식, 요청이 아주 많을 때 서버가 버티는 한계입니다. 서버를 늘려 처리량이 어디까지 느는지는 "세부 동작 실습: 로드밸런서와 스케일 아웃"에서, 서버를 하나씩 바꿔 배포하는 방법은 "배포 전략"에서, 죽은 서버를 자동으로 다시 띄우는 일은 "쿠버네티스 자가 치유"에서 다룹니다.

앞 실습: 다른 서버 부르기 · 다음 실습: 캐시

이 실습에 나오는 개념

이 실습에서 보는 것

설정

코드


      
    

그림

실행 비교

이렇게 바꿔 보세요