자 일해라 토끼같은 서버들아

지난 분산 포스팅에 이어 오늘은 부하 테스트를 진행해볼 예정이다
https://ratatou2.tistory.com/285
홈서버 4대로 분산 시스템 구축 해보기 (feat. 생각보다 겁나 쉬움!)
분산 뉴비를 위한 가이드(가 되고 싶은 포스팅)공유기 포스팅 이후 지난 며칠간 정말 열심히 분산, 부하 시스템을 구축했습니다https://ratatou2.tistory.com/278 공유기 브릿지 모드 설정하기 (feat. ipTIME
ratatou2.tistory.com
여기까지 하고 나니까 분산 시스템을 구축했다는걸 드디어 체감했음
1. k6 설치

- k6는 서버에 부하를 줄 수 있는 툴이다
- 서버에 의도적으로 API를 동시 요청을 많이 발생시켜서, 성능/안정성/확장성 등을 체크하는 용도다
- 일단 서버에 k6를 설치해보자
curl -fsSL https://dl.k6.io/key.gpg | sudo gpg --dearmor -o /usr/share/keyrings/k6-archive-keyring.gpg
echo "deb [signed-by=/usr/share/keyrings/k6-archive-keyring.gpg] https://dl.k6.io/deb stable main" | sudo tee /etc/apt/sources.list.d/k6.list
sudo apt-get update && sudo apt-get install -y k6
1-1. 왜 k6를 골랐는가?
- 찾아보니 뭐 ab, wrk 같은 부하테스트 툴들도 있었지만, 결과적으로 k6의 아래 세가지 장점 때문에 골랐다
1) 스크립트 기반
- 스크립트 기반이라서 이것저것 테스트 해볼 수 있어서 좋았다
- 필요한 알림을 보낸다거나 기록을 남기거나 등등 스크립트여서 넣을 수 있는 플로우들이 있었는데 그게 좋았음
2) 결과 지표가 다양함
- 결과에서 볼 수 있는 것들이 다양했다 전체 요청 시간(e.g. p95)부터 테스트 성공 비율, 총 요청횟수 등등
3) 쉬움
- Grafana 연동하는 것도 자료도 많았고, JS라 접근하기 쉽기도 했고, 설치도 간단해서 좋았다
2. k6 부하 테스트
- 간단한 테스트 스크립트 생성
sudo vim k6_test.js
- 스크립트 내용은 아래와 같이 세팅
import http from 'k6/http';
import { sleep, check } from 'k6';
export const options = {
vus: 20, // 동시에 접속하는 가상 사용자 수 (Virtual Users)
duration: '10s', // 테스트 지속 시간
};
export default function () {
const res = http.get('http://localhost:52837/api/hello');
check(res, { 'status is 200': (r) => r.status === 200 });
sleep(0.1); // 각 요청 사이 간격
}
- 테스트 실행
k6 run k6_test.js
3. 결과
- 테스트가 끝나면 이런 식으로 결과가 나온다
- 아래와 같은 정보가 적혀있음

- 1개의 시나리오에서 k6_test.js라는 스크립트를 실행했음
- 20개의 가상 사용자 (=VUs)가 최대 40초 동안 지속됐음
- 190개 중에 190개가 체크 되어 100% 성공했음
- 평균 응답시간은 1초
- p(95)는 1.26초 -> p(95)는 95%의 사용자가 이 시간 내에 완료됐다는 것을 의미한다
4. 스트레스 테스트
- 잘되는 것을 확인했으니 이제부터 부하를 좀 세게 줘서 각 서버의 성능과 한계, 그리고 App 서버 세개를 분산 시스템으로 구축했을 때의 이점을 직접 확인할 예정이다
- 그전에 맥미니를 중계기로 세팅하고 서버 한대를 더 추가하여 아래와 같은 구조로 진행할 것이다
- 참고로 내 맥미니는 인텔 기반이라 리눅스를 설치해두었다

4-1. 서버 사양과 환경

4-2. 가장 성능 좋은 맥미니를 왜 중계기로 쓰는가?
- 오히려 성능이 좋으니까 App 서버로 둬서 더 많은 요청을 압도적인 속도로 쳐내야하는 것 아닌가?
- 맞긴한데 지난번 포스팅 때도 적어뒀지만 그 이유를 다시 요약하자면 아래 내용으로 정리할 수 있겠다
- 처음엔 맥미니가 보유하고 있는 서버 중 가장 성능이 좋았기에 App 서버로 쓰고자 했음
- N100을 중계기로 썼을 때 docker <-> 네트워크 I/O 구간에서 인풋렉이 발생하여 k6 테스트 자체가 불가능했음
- HTTP 처리 병목이 아니라, 순수한 하드웨어 성능 이슈
- 더군다나 N100을 중계기(nginx)로 쓰기엔 이미 여러 서비스가 구동 중이라 하드웨어 리소스를 나눠써야 하는 상황 (nginx & 서비스)
- 온전한 100%의 성능에서 튜닝값을 찾고 싶었고, 어중간한 성능의 서버는 중계기 역할조차 수행할 수 없다는 것을 깨달았기에 맥미니를 온전히 중계(nginx)로만 쓰기로 하였음
- 추가로 찾아보니 실제로 중계기들은 이러한 부하 때문에 성능이 좋은 기기들이 배치되어있다는 것을 알게 되었음
- N100 미니 PC가 docker 컨테이너 내부의 nginx로 인해 I/O 인풋렉이 발생하면서 터지는 모습

- 아마 로컬에 nginx를 설치해서 직접 물려줬어도 힘겨웠을판에 docker 컨테이너로 띄워진 nginx가 hosts 권한도 없다보니 통신량이 두배(로컬 - docker - nginx)가 되어버려 더 힘겨워 하지 않았나 싶다
4-3 맥미니 중계기 세팅
1) nginx 설치
sudo apt update
sudo apt install nginx -y
2) 정상동작 확인
sudo systemctl status nginx
3) nginx conf 파일 수정
sudo vim /etc/nginx/nginx.conf
- 초기 테스트 버전
- 일절의 튜닝 없이 기본적인 Round Robin으로만 동작하는 코드
user www-data;
worker_processes auto;
pid /run/nginx.pid;
events {
worker_connections 4096;
use epoll;
multi_accept on;
}
http {
include mime.types;
default_type application/octet-stream;
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
types_hash_max_size 2048;
server_tokens off;
# 로그 최소화
access_log off;
error_log /var/log/nginx/error.log warn;
# upstream 설정
upstream backend_pool {
least_conn;
server 192.168.0.201:5000
server 192.168.0.202:5000
server 192.168.0.203:5000
}
server {
listen 80;
server_name _;
location / {
proxy_pass http://backend_pool;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_connect_timeout 5s;
proxy_read_timeout 30s;
proxy_send_timeout 30s;
# 백엔드 장애 시 빠른 failover
proxy_next_upstream error timeout invalid_header http_500 http_502 http_503 http_504;
}
}
}
- 튜닝(가중치) 적용한 최종 버전
- 각 서버마다 k6 테스트를 하고, 성능에 따라 가중치를 부여하였다
- 성능 순은 아래와 같았다
LG-Desktop > SAMSUNG-Laptop >= SAMSUNG-500A2M
- 또한 연결 제한 시간 응답 등을 이리저리 조여가고 풀어가면서 서버 간의 적절한 옵션 값을 찾았다
- 연결이 안되는데 기다린다고 너무 오래 내버려두거나, 너무 빨리 교체해버리면 한쪽에만 부하가 몰리게 되었다
- 이는 결국 전체적인 시스템의 처리속도가 현저히 떨어지는 결과값이 나왔다
user www-data;
worker_processes auto;
pid /run/nginx.pid;
events {
worker_connections 4096;
use epoll;
multi_accept on;
}
http {
include mime.types;
default_type application/octet-stream;
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 0;
types_hash_max_size 2048;
server_tokens off;
# 로그 최소화
access_log /var/log/nginx/access.log;
error_log /var/log/nginx/error.log warn;
log_format upstreamlog '$remote_addr → $upstream_addr [$time_local] '
'"$request" $status $body_bytes_sent '
'in $request_time sec';
access_log /var/log/nginx/access.log upstreamlog;
upstream backend_pool {
server 192.168.0.202:5000 weight=40 max_fails=5 fail_timeout=8s; # LG_Desktop (주력)
server 192.168.0.203:5000 weight=5 max_fails=5 fail_timeout=8s; # Laptop
server 192.168.0.204:5000 weight=2 max_fails=5 fail_timeout=8s; # 500A2M
}
server {
listen 80;
server_name _;
location / {
proxy_pass http://backend_pool;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# 연결/응답 제한
proxy_connect_timeout 4s; # 연결 안 되면 빠르게 failover
proxy_read_timeout 12s; # 느린 응답 방지
proxy_send_timeout 10s;
proxy_next_upstream error timeout invalid_header http_500 http_502 http_503 http_504;
}
}
}
4) 설정 테스트 및 반영
sudo nginx -t
sudo systemctl reload nginx
4-4. 테스트 및 튜닝
stress_test.js
- 이전보단 좀 더 세고, 오래 지속되며, 변동성이 있는 스크립트를 준비했다
import http from 'k6/http';
import { sleep, check } from 'k6';
export const options = {
stages: [
{ duration: '10s', target: 100 },
{ duration: '10s', target: 300 },
{ duration: '10s', target: 500 },
{ duration: '10s', target: 800 },
{ duration: '10s', target: 1000 },
{ duration: '10s', target: 0 },
],
thresholds: {
http_req_duration: ['p(95)<30000'],
},
};
export default function () {
const res = http.get('http://192.168.0.201/api/hello', { timeout: '60s' });
check(res, { 'status is 200': (r) => r.status === 200 });
sleep(0.1);
}
- App 서버에서 바꾼 것은 없었다 (이전 포스팅의 Flask 코드와 동일)
- 기존의 Flask 서버가 DB도 없이 너무 단순한 구조인가 싶었지만, 당장은 서버간의 튜닝이 목적이었으므로 그 부분은 감안하였다
- Grafana로 실시간 조회하여 성능을 체크하고 비교하였음
0) 스크립트 간이 테스트

- 스크립트가 정상동작 하는지 체크하기 위해 간단히 비교해보았음
- 분산 구조의 성능이 최소 4배 이상 빠른 것을 볼 수 있었음
- 이로써 분산 구조는 유의미하며 스크립트 또한 정상이라는 것을 확인할 수 있었다
1) 개별 테스트

- 일단 보다시피 서버 각각의 성능을 확인할 수 있었다
- 같은 부하를 줘도 적은 CPU 리소스로 처리할 수 있다는 것은 그만큼 상대적 성능이 좋음을 의미한다
- 주황색은 맥미니이다 (맥북에서 발생시킨 k6 부하를 맥미니의 nginx로 App 서버들에게 나눠줌)
- 이로써 확인할 수 있는 것은 LG > Laptop >= 500A2M 순으로 성능이 좋다는 것이다




- 개별 테스트 결과를 보면 다들 성능이 처참하다 (아래표는 반올림 적용)
| PC | 성공률 | 실패율 | 평균 시간 | p(95) |
| LG-Desktop | 38% | 62% | 70ms | 115ms |
| SAMSUNG-Laptop | 21% | 79% | 61ms | 209ms |
| SAMSUNG-500A2M | 19% | 81% | 57ms | 228ms |
| All | 100% (99.9%) | 0% (0.01%) | 136ms | 231ms |
- LG 데스크탑의 p(95)는 115ms로 가장 훌륭하지만, 성공률이 38%밖에 안되니 쓸만하다고 할 수 없다
- 나머지 둘은 성공률이 20% 언저리이니까 말할 것도 없다
- 튜닝이 하나도 적용되지 않은 상태임에도 서버 3대를 모두 사용하면 100%의 성공률을 보여주고 있다
- 다만 지금 한 부하 테스트 강도가 '비교적' 약하기 때문에 사실상 100%가 '나와야하는 상황'인 셈이다 (처리할 친구를 둘이나 더 붙여줬으니)
- 다만 응답속도는 231ms로 가장 느린데, 이는 분산이 적용된만큼 병렬처리 오버헤드(네트워크 I/O, nginx 분배로 인한 지연)로 추측해볼 수 있겠다
2) 고강도 테스트 및 튜닝
- 여기서부터는 실질적으로 유의미한 테스트 결과를 낼 수 있는정도의 고강도 스크립트로 변경하였다
import http from 'k6/http';
import { sleep, check } from 'k6';
export const options = {
noConnectionReuse: true, // 핵심 옵션
stages: [
{ duration: '1m', target: 200 },
{ duration: '2m', target: 400 },
{ duration: '2m', target: 600 },
{ duration: '2m', target: 800 },
{ duration: '2m', target: 0 },
],
thresholds: {
http_req_duration: ['p(95)<30000'],
},
};
export default function () {
const res = http.get('http://192.168.0.201/api/hello', { timeout: '60s' });
check(res, { 'status is 200': (r) => r.status === 200 });
sleep(0.05);
}
- 10분 유지 하는 동안 2분마다 변주적으로 부하를 변경하였다
- 또한 noConnectionReuse 옵션을 사용함으로써 '실제 다수의 유저가 동시에 서버를 두드리는 상황'을 재연하고자 하였다
- 쉽게 말해 요청마다 새 TCP 연결을 생성하는 옵션이다
- 이후의 튜닝 과정은 전부 서버의 성능이 각각 달랐으므로 서버 성능에 따라 가중치를 다르게 주고자 하였다
3) 삽질 일대기
(0) 각 서버 성능 지수 계산
- 우선은 각 서버들의 성능을 수치화해서 weight를 나누고자 했다
- p95를 기준으로 성공률로 계산하고자 했고, 이 부분은 GPT 도움을 받아서 아래처럼 정의할 수 있었다
성능 점수 = (1 / p95) × (성공률 / 100)
| 서버명 | p95 | 성공률 | 계산식 | 성능점수 |
| LG_Desktop | 114.67 | 24.7 | (1/114.67)*0.247 | 0.00215 |
| SAMSUNG_Laptop | 212.04 | 13.9 | (1/212.04)*0.139 | 0.00066 |
| SAMSUNG_500A2M | 225.84 | 10.6 | (1/225.84)*0.106 | 0.00047 |
- 이 결과를 1.0으로 정규화해서 비율을 내면 7:2:1이기 때문에 초기 가중치는 7:2:1부터 시작했다
(1) least_conn 옵션
- 처음엔 nginx.conf에서 upstream에 'least_conn' 옵션을 주었다
- least_conn 옵션은 가장 한가한 서버를 찾아 선택하고 그 안에서 weight를 반영한다
- 문제는 응답이 느린 서버일수록 '연결이 오래 유지'되니까 늘 연결이 많아 남아있는 서버가 되어버린다
- 반면에 빠른 서버는 오히려 계속 더 많이 요청을 받게되고(일을 빨리 끝내니까), 그러면 실제 분배는 weight를 무시하게 되는 결과를 가져온다
- 쉽게 말하면 일잘러의 가중치는 1이라고 적혀있지만, 일을 워낙 잘하니까 그냥 무시하고 더 주게 된달까?
- 그래서 least conn 옵션을 제거하고 round robin + weight로만 진행했다
(2) weight 튜닝
- nginx에서 각 서버로 넘기는 비율을 체크했다

- 가중치 13:4:3
- %로 계산하면 31% / 43% / 25% (LG의 비율이 낮아서 폐기)
202 서버 : LG_Desktop (주력)
203 서버 : SAMSUNG_Laptop
204 서버 : SAMSUNG_500A2M
(3) weight + 옵션 미세 조정
- weight 튜닝을 반복하다가 40:5:2 비율이 가장 좋다는 것을 찾았다
- 이때부터는 옵션을 미세하게 조정하기 시작했다
- 예시로는 아래 옵션들이 있다
| 항목 | 현재 | 조절 | 이유 |
| proxy_read_timeout | 10s | 8s | 느린 응답 백엔드 빠른 failover |
| proxy_connect_timeout | 3s | 2s | 즉시 대체 시도 |
| max_fails | 5s | 4s | 느린 서버 조기 제외 (조금 공격적으로 진행) |
- 이때는 너무 쪼았는지 오히려 성공률 56%를 보이며 튜닝을 안했을 때보다도 못한 성능을 보였음

- 이후엔 다시 아래와 같이 조절해서 테스트 했다
| 항목 | 현재 | 조절 | 이유 |
| proxy_read_timeout | 8s | 12s | 좀 더 여유롭게 지연 허용 폭을 늘림 |
| proxy_connect_timeout | 2s | 4s | 좀 더 기다렸다가 대체 시도 |
| max_fails | 4s | 5s | 일시적 오류에 대해 너무 빨리 장애로 간주하지 않도록 조금 더 늘림 |
🫠 미쳤다 성공해버렸다 🫠

- 97%의 성공률 / 265ms의 준수한 응답시간
5. 최종 결과
- 튜닝 세팅값은 바로 위의 삽질 일대기에 있다
- 다만 환경마다 다르므로 내 세팅값이 정답은 아니니 참고정도만..!

- 유의미한 결과를 확인할 수 있었다
- 비록 p95는 5배 가까이 늘어나버렸지만 failover를 감안했을 땐 괜찮은 것도 같다... ㅠ (한 서버가 죽었을 떄 다른 서버로 전환하는 것)
- 그것보단 100% 수치에 가까워진 성공률과 재시도가 줄어들어 data_received가 줄어든 것이 유의미했다 (네트워크 효율 상승)

- 이전 테스트에서의 CPU 사용률을 보면 알 수 있지만 굉장히 튀거나, 서로 고르지 못한 모습을 볼 수 있는데 마지막 97%만큼은 제대로 안정적인 모습을 보이고 있다

초록색(202) : LG
노란색(203) : Laptop
파란색(204) : 500A2M
- 특정 서버의 CPU 사용률만 치솟지도 않고 서로 상호보완적인 그래프를 볼 수 있었다
- 특히, LG 서버가 성능이 가장 좋아 weight가 높은만큼 초반부터 많은 트래픽을 선점한다
- 이후에 점차 느려질 때 나머지 두 서버가 백업하는 식으로 동작되고 있음을 확인할 수 있다 (초록색 CPU 사용률이 감소하기 시작할 때부터 파란색과 노란색이 점차 상승하며 전체적으로 안정적인 상태를 유지한다)
- 그리고 그러한 구조 덕분에 전체적인 CPU 사용률이 50%를 크게 넘기지 않고 안정적으로 유지됨을 확인할 수 있다

- 성공한 요인은 타임아웃 완화 덕분인 것 같다
- failover가 너무 빨리 발생하지 않도록 하면서도 느린 서버는 적절히 제외되도록 하여 그 순간엔 빠른 서버가 처리할 수 있게 접근하였음

후기 & 번외
진짜 정-말 재밌었다
일단 그동안 분산 시스템이 어떻게 동작하고, 어떻게 알아서 찾아가고, 어떻게 효율적으로 쓰일 수 있는지 궁금했는데 직접 해보니까 정말 많은 것들을 알 수 있었다
또한 이렇게 하나씩 조율하고 타이밍 맞추고 쬐고 풀고 하는게 성향에 잘 맞나보다
그동안 남는 PC들 당근 하지도 않고 어떻게 써먹지 고민했었는데 정말 야무지게 써먹은 경험이었다
앞으로 K8S도 있고 사설 VPN 구축 계획도 있어서 각자의 용도가 또 생길지도 모르겠다
근데 24/7 PC는 어쩔 수 없이 저전력을 찾게 되는데 그럼 M1~M4 칩셋을 가진 맥미니가 짱이지 않나 싶다
물론 찾아보면 죄다 하지말라거나 systemd, cron 기초적인 것들도 없고 보안문제도 귀찮아서 맥미니로 서버 구축을 권장하지 않는 글을 굉장히 많이본다
애초에 맥미니 시리즈는 죄다 비싸니까 그 돈이면 N100 미니 PC를 6대쯤 살 수 있는데 굳이...? 라는 의견들이 압도적!!
고민이다 미니 PC 한대 더 살까.. 하늘에서 떨어지면 좋겠다
하나는 완성된, 구현 다된 서비스들만 돌리고, 한쪽은 심심하면 포멧하고 그러는 장난감으로 ㅎㅎ..
홈서버 5대의 전기세는 어때요?
나도 궁금해서 열심히 측정해봤다
근데 이번엔 엄청 긴 시간은 아니었고, 한 몇시간 정도?
대충 4일하고도 11시간 25분... 환산하면 얼추 4.47정도
이 시간동안 4.09KWh를 썼으니까 하루에 0.91498KWh
30일을 돌리면 달에 27.44KWh
한전 계산기 앱에 올림해서 계산하면 달에 5300원정도 나온다
생각보다 준수한듯!

이후 계획
이후에는 K8S를 구축하고 도전해볼 예정이다
K8S는 직접 분산 세팅을 해주는 것과 무엇이 다른지, 얼마나 편한지를 살펴볼 예정!
Grafana 1860 템플릿 좋더라구요
중계기인 맥미니에 띄우니까 연결된 서버 다 뜨고 한눈에 볼 수 있어서 좋았다

적용하는 방법은 대시보드 import 누르고 Load 클릭하면 위 템플릿이 적용된다

아래 사진처럼 각 CPU들의 사용량을 계산하는 쿼리는 밑에 작성해두었다

100 - (avg by (instance)(rate(node_cpu_seconds_total{mode="idle"}[1m])) * 100)
'Infra > DevOps' 카테고리의 다른 글
| SSH 22번 포트 자동 복구 시스템 구축하기 (feat. 서버 잃고 뇌 약간 고치기 ㅠ) (0) | 2025.10.24 |
|---|---|
| 나만의 VPN 서버 만들기 (feat. 홈서버 + WireGuard) (4) | 2025.10.23 |
| 홈서버 4대로 분산 시스템 구축 해보기 (feat. 생각보다 겁나 쉬움!) (0) | 2025.10.21 |
| 서버 CPU 과부하 시 알림 보내기 (feat. Grafana 알림 설정 방법) (0) | 2025.10.13 |
| 오라클 클라우드(OCI) 퍼블릭 IP 적용 및 SSH 접속하기 (0) | 2025.10.04 |
