[Cloud] website 에서의 NLB?
웹사이트에서는 주로 ALB(Application Load Balancer)를 사용하는데, NLB(Network Load Balancer)를 사용하면 안될까?
이 글에서는, 웹사이트에서 NLB를 사용하는 구성과 Azure NLB 동작 알고리즘을 알아보고, 성능 테스트를 진행한다.
✔️ Azure NLB의 Load Balancing 일고리즘 & NLB 성능 테스트
✔️ Azure NLB 기준 동작 알고리즘?
5-tuple hash 에 의해 해시값 → 백엔드 풀의 특정 VM으로 매핑됨
5-tuple hash
-> (Source IP, Source Port, Destination IP, Destination Port, Protocol)
이 5개의 값들의 조합을 해시하여 백엔드 풀 중 하나를 결정.
목차
1. 현재 상태 (구성)
- Azure NLB 알고리즘을 알아보기 위한 리소스 구성
2. Curl 테스트
- Curl, tcpdump 를 통한 알고리즘 테스트
3. 성능 테스트
- Smoke, Load, Stress 테스트
4. 결론
1. 현재 상태
⬇️ 3개의 VM이 있으며,
⬇️ 1개의 NLB(Network Load Balancer)가 있고,
⬇️ 이 NLB에서 3개의 VM을 호스팅한다.
사용한 terraform 파일 (main.tf)
# provider "azurerm" {
# features {}
# }
provider "azurerm" {
features {}
subscription_id = xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
}
resource "azurerm_resource_group" "rg" {
name = "nlbtest-rg"
location = "Korea Central"
}
resource "azurerm_virtual_network" "vnet" {
name = "nlbtest-vnet"
address_space = ["10.0.0.0/16"]
location = azurerm_resource_group.rg.location
resource_group_name = azurerm_resource_group.rg.name
}
resource "azurerm_subnet" "subnet" {
name = "nlbtest-subnet"
resource_group_name = azurerm_resource_group.rg.name
virtual_network_name = azurerm_virtual_network.vnet.name
address_prefixes = ["10.0.1.0/24"]
}
resource "azurerm_public_ip" "lb_pip" {
name = "nlbtest-nlb-pip"
location = azurerm_resource_group.rg.location
resource_group_name = azurerm_resource_group.rg.name
allocation_method = "Static"
sku = "Standard"
}
resource "azurerm_lb" "nlb" {
name = "nlbtest-nlb"
location = azurerm_resource_group.rg.location
resource_group_name = azurerm_resource_group.rg.name
sku = "Standard"
frontend_ip_configuration {
name = "frontend"
public_ip_address_id = azurerm_public_ip.lb_pip.id
}
}
resource "azurerm_lb_backend_address_pool" "bepool" {
name = "nlbtest-backend-pool"
loadbalancer_id = azurerm_lb.nlb.id
}
resource "azurerm_network_security_group" "nsg" {
name = "nlbtest-nsg"
location = azurerm_resource_group.rg.location
resource_group_name = azurerm_resource_group.rg.name
security_rule {
name = "allow-http"
priority = 100
direction = "Inbound"
access = "Allow"
protocol = "Tcp"
source_port_range = "*"
destination_port_range = "80"
source_address_prefix = "*"
destination_address_prefix = "*"
}
security_rule {
name = "allow-ssh"
priority = 110
direction = "Inbound"
access = "Allow"
protocol = "Tcp"
source_port_range = "*"
destination_port_range = "22"
source_address_prefix = "*"
destination_address_prefix = "*"
}
}
resource "azurerm_network_interface" "nic" {
count = 3
name = "nlbtest-nic-${count.index}"
location = azurerm_resource_group.rg.location
resource_group_name = azurerm_resource_group.rg.name
ip_configuration {
name = "internal"
subnet_id = azurerm_subnet.subnet.id
private_ip_address_allocation = "Dynamic"
}
}
resource "azurerm_network_interface_backend_address_pool_association" "nic_lb_assoc" {
count = 3
network_interface_id = azurerm_network_interface.nic[count.index].id
ip_configuration_name = "internal"
backend_address_pool_id = azurerm_lb_backend_address_pool.bepool.id
}
resource "azurerm_network_interface_security_group_association" "nic_nsg" {
count = 3
network_interface_id = azurerm_network_interface.nic[count.index].id
network_security_group_id = azurerm_network_security_group.nsg.id
}
resource "azurerm_linux_virtual_machine" "vm" {
count = 3
name = "nlbtest-vm-${count.index}"
location = azurerm_resource_group.rg.location
resource_group_name = azurerm_resource_group.rg.name
size = "Standard_B1s"
admin_username = "azureuser"
network_interface_ids = [azurerm_network_interface.nic[count.index].id]
admin_ssh_key {
username = "azureuser"
public_key = file("~/.ssh/id_rsa.pub") # SSH 키 경로 확인 필요
}
os_disk {
caching = "ReadWrite"
storage_account_type = "Standard_LRS"
}
source_image_reference {
publisher = "Canonical"
offer = "0001-com-ubuntu-server-jammy"
sku = "22_04-lts"
version = "latest"
}
custom_data = base64encode(<<-EOF
#!/bin/bash
apt update
apt install -y nginx
echo "Hello from VM ${count.index}" > /var/www/html/index.html
systemctl enable nginx
systemctl restart nginx
EOF
)
}
resource "azurerm_lb_probe" "probe" {
name = "http-probe"
loadbalancer_id = azurerm_lb.nlb.id
protocol = "Tcp"
port = 80
}
resource "azurerm_lb_rule" "lbrule" {
name = "http-rule"
loadbalancer_id = azurerm_lb.nlb.id
protocol = "Tcp"
frontend_port = 80
backend_port = 80
frontend_ip_configuration_name = "frontend"
backend_address_pool_ids = [azurerm_lb_backend_address_pool.bepool.id]
probe_id = azurerm_lb_probe.probe.id
}
output "lb_public_ip" {
value = azurerm_public_ip.lb_pip.ip_address
}
2. Curl 테스트
public ip에 Curl로 요청을 보내본다.
VM2 -> VM1 -> VM0 -> VM2 ... 순서대로 작동하는데? seq 명령어로 다시 요청을 보내봤다.
100번씩 요청을 보내면, VM2 -> VM1 -> VM0 순서대로 34번씩 (33+33+34=100) 요청을 받는다.
그렇다면 Azure NLB는 RR(Round-Robin) 방식일까?
❌ Azure NLB는 Round-Robin 방식이 아니다.
아래 조건을 만족할 경우 Round-Robin처럼 보일 수 있다.
- ☑️ curl 요청의 Source Port가 매번 다르게 랜덤으로 바뀌는 경우
- curl을 여러 번 실행할 때 매번 새로운 TCP 커넥션을 생성하는 경우
- Destination은 동일 (4.218.19.35:80), Protocol은 TCP, Source IP는 동일
결국 5-튜플 중 source port만 계속 바뀌면, 해시 결과도 달라져서 VM2 → VM1 → VM0 처럼 Round-Robin 효과가 나는 것처럼 보이는 것이다.
📷 $ tcpdump 실행 후 Curl
VM 1 호출
VM 0 호출
VM 2 호출
지금 tcpdump 결과를 보면 curl 요청을 보낼 때마다 Source Port (64352, 64353, 64354) 가 계속 자동으로 증가하면서 바뀌고 있다는 걸 확인할 수 있다.
이 때문에 5-tuple Hash 결과가 계속 달라지고, Azure NLB가 다른 백엔드 VM으로 요청을 보내는 것이다.
3. NLB 성능테스트
K6 으로 부하테스트 실행
nlb-test.js (k6)
import http from 'k6/http';
import { check } from 'k6';
export const options = {
vus: 10000, // 동시 사용자 수
duration: '10s', // 테스트 시간
};
export default function () {
const res = http.get('http://4.218.19.35');
check(res, {
'status is 200': (r) => r.status === 200,
'body is VM 0/1/2': (r) =>
r.body.includes('Hello from VM 0') ||
r.body.includes('Hello from VM 1') ||
r.body.includes('Hello from VM 2'),
});
}
1. Smoke Test
10명 사용자, 10초 수행
xxng ~/desktop/terraform/nlb-website/az k6 run nlb-test.js
/\ Grafana /‾‾/
/\ / \ |\ __ / /
/ \/ \ | |/ / / ‾‾\
/ \ | ( | (‾) |
/ __________ \ |_|\_\ \_____/
execution: local
script: nlb-test.js
output: -
scenarios: (100.00%) 1 scenario, 10 max VUs, 40s max duration (incl. graceful stop):
* default: 10 looping VUs for 10s (gracefulStop: 30s)
█ TOTAL RESULTS
checks_total.......................: 16664 1665.01504/s
checks_succeeded...................: 100.00% 16664 out of 16664
checks_failed......................: 0.00% 0 out of 16664
✓ status is 200
✓ body is VM 0/1/2
HTTP
http_req_duration.......................................................: avg=11.76ms min=4.96ms med=9.18ms max=115.66ms p(90)=13.04ms p(95)=18.13ms
{ expected_response:true }............................................: avg=11.76ms min=4.96ms med=9.18ms max=115.66ms p(90)=13.04ms p(95)=18.13ms
http_req_failed.........................................................: 0.00% 0 out of 8332
http_reqs...............................................................: 8332 832.50752/s
EXECUTION
iteration_duration......................................................: avg=11.99ms min=4.99ms med=9.31ms max=115.82ms p(90)=14.36ms p(95)=19.19ms
iterations..............................................................: 8332 832.50752/s
vus.....................................................................: 10 min=10 max=10
vus_max.................................................................: 10 min=10 max=10
NETWORK
data_received...........................................................: 2.2 MB 217 kB/s
data_sent...............................................................: 558 kB 56 kB/s
running (10.0s), 00/10 VUs, 8332 complete and 0 interrupted iterations
default ✓ [======================================] 10 VUs 10s
x
2. Load Test
100명 사용자, 60초 수행
xxng ~/desktop/terraform/nlb-website/az k6 run nlb-test.js
/\ Grafana /‾‾/
/\ / \ |\ __ / /
/ \/ \ | |/ / / ‾‾\
/ \ | ( | (‾) |
/ __________ \ |_|\_\ \_____/
execution: local
script: nlb-test.js
output: -
scenarios: (100.00%) 1 scenario, 100 max VUs, 1m30s max duration (incl. graceful stop):
* default: 100 looping VUs for 1m0s (gracefulStop: 30s)
█ TOTAL RESULTS
checks_total.......................: 708564 11790.398986/s
checks_succeeded...................: 100.00% 708564 out of 708564
checks_failed......................: 0.00% 0 out of 708564
✓ status is 200
✓ body is VM 0/1/2
HTTP
http_req_duration.......................................................: avg=16.71ms min=4.65ms med=10.85ms max=377.83ms p(90)=25.17ms p(95)=35.28ms
{ expected_response:true }............................................: avg=16.71ms min=4.65ms med=10.85ms max=377.83ms p(90)=25.17ms p(95)=35.28ms
http_req_failed.........................................................: 0.00% 0 out of 354282
http_reqs...............................................................: 354282 5895.199493/s
EXECUTION
iteration_duration......................................................: avg=16.93ms min=4.71ms med=10.96ms max=385.62ms p(90)=25.51ms p(95)=35.93ms
iterations..............................................................: 354282 5895.199493/s
vus.....................................................................: 100 min=100 max=100
vus_max.................................................................: 100 min=100 max=100
NETWORK
data_received...........................................................: 93 MB 1.5 MB/s
data_sent...............................................................: 24 MB 395 kB/s
running (1m00.1s), 000/100 VUs, 354282 complete and 0 interrupted iterations
default ✓ [======================================] 100 VUs 1m0s
항목 | 10 VUs / 10s | 100 VUs / 60s |
---|---|---|
RPS | ~830 | ~5,900 |
평균 응답 시간 | ~11.7ms | ~16.7ms |
최대 응답 시간 | ~115ms | ~378ms |
성공률 | 100% | 100% |
🧠 해석:
- 사용자 10배 증가 → 처리량도 거의 10배 증가
- 응답 시간도 소폭 상승 (정상 범위)
- NLB 또는 백엔드 VM에 과부하 증거 없음 → 수평 확장 안정적으로 작동
✅ 웹사이트에 NLB를 사용했지만 꽤 괜찮은 성능.
3. Stress Test
10000명 사용자, 10초 수행
█ TOTAL RESULTS
checks_total.......................: 55964 1396.780297/s
checks_succeeded...................: 71.42% 39970 out of 55964
checks_failed......................: 28.57% 15994 out of 55964
✗ status is 200
↳ 71% — ✓ 19985 / ✗ 7997
✗ body is VM 0/1/2
↳ 71% — ✓ 19985 / ✗ 7997
HTTP
http_req_duration.......................................................: avg=3.61s min=0s med=858.28ms max=33.14s p(90)=8.99s p(95)=11.2s
{ expected_response:true }............................................: avg=4.46s min=6.83ms med=1.48s max=33.14s p(90)=9.4s p(95)=11.31s
http_req_failed.........................................................: 28.57% 7997 out of 27982
http_reqs...............................................................: 27982 698.390149/s
EXECUTION
iteration_duration......................................................: avg=3.99s min=1.72ms med=1.58s max=33.14s p(90)=9.22s p(95)=11.2s
iterations..............................................................: 27982 698.390149/s
vus.....................................................................: 454 min=454 max=10000
vus_max.................................................................: 10000 min=10000 max=10000
NETWORK
data_received...........................................................: 5.2 MB 130 kB/s
data_sent...............................................................: 2.1 MB 52 kB/s
running (40.1s), 00000/10000 VUs, 27982 complete and 454 interrupted iterations
default ✓ [======================================] 10000 VUs 10s
xxng ~/desktop/terraform/nlb-website/az
항목 | 10 VUs / 10s | 100 VUs / 60s | 10,000 VUs / 10s |
---|---|---|---|
RPS | ~830 | ~5,900 | ~698 |
평균 응답 시간 | ~11.7ms | ~16.7ms | ~3.6s |
최대 응답 시간 | ~115ms | ~378ms | ~33s |
성공률 | 100% | 100% | 71.4% (28.6% 실패) |
⬆️ 스트레스 테스트에는 버티지 못하는 모습
4. 결론
Azure NLB는 L4 기반의 로드 밸런서로 웹사이트 구축에는 제한적인 기능만 제공하지만,
단순한 정적 웹 페이지나 고성능이 필요한 서비스에서는 꽤 우수한 성능을 발휘한다. 실제 테스트에서도 평균 10ms 내외의 빠른 응답 속도와 100% 성공률을 기록하며 안정적인 트래픽 처리를 보였다.
하지만 URL 라우팅, SSL 종단 처리, 세션 유지와 같은 고급 기능이 필요한 경우에는 ALB 사용이 일반적으로 더 적절하다.
다만, 극단적으로 높은 동시 접속(예: 10,000 VUs)에서는 응답 지연과 실패가 발생하며, 이는 백엔드 VM 수의 증가, 커넥션 처리 설정, 그리고 인프라 확장이 필요하다는 점을 보여준다.