Skip to content

실습 목표#

  • Thread Pool의 빈칸을 채워 Queue와 Worker가 맞물려 돌게 만든다.
  • Pool 크기를 바꿔 가며 재고 포화되는 지점을 찾는다.
  • Socket Buffer를 바꿔 가며 대용량 전송의 처리량이 포화되는 지점을 찾는다.


과제 1. Queue와 Worker#


accept()로 수락한 연결을 Queue에 넣은 뒤 worker를 활성화해야 한다. 라즈베리파이 ecu_pool_blank.cppmain() 함수 내 [BLANK1]을 채워 구현해보자.

ecu_pool_blank.cpp
int conn = accept(srv, nullptr, nullptr);
if (conn < 0) continue;
{ std::lock_guard<std::mutex> lk(m); q.push(conn); }
// 방금 Queue에 넣었으니 자고 있는 Worker 하나를 깨움
cv./* [[BLANK1]] */;


예시답안 : Queue와 Worker#

Queue에 넣은 직후 Worker 하나를 활성화한다.

ecu_pool_blank.cpp
{ std::lock_guard<std::mutex> lk(m); q.push(conn); }
cv.notify_one();
라즈베리파이·터미널
make


과제 2. Worker의 대기 조건#


Worker는 Queue에 요청이 왔을 때만 활성화되어아 한다. Condition Variable는 이유 없이 활성화되기도 하므로 조건을 반드시 확인해야 한다.

라즈베리파이·터미널
std::unique_lock<std::mutex> lk(m);
// Queue가 비어 있지 않을 때까지 잠들어 대기
cv.wait(lk, []{ return /* [[BLANK2]] */; });
conn = q.front();
q.pop();


예시 답안 : Worker의 대기 조건#

Queue가 비어 있지 않은 경우에만 활성화되어 다음으로 처리로 진행한다. 조건이 충족되지 않으면 대기 상태로 복귀하므로 가짜 기상이 발생해도 안전하게 처리된다.

ecu_pool_blank.cpp
cv.wait(lk, []{ return !q.empty(); });
라즈베리파이 · 빌드와 실행
make
./ecu_pool_blank 1000 1024 0 4

관찰 포인트

인자는 순서대로 처리 지연, App Buffer, Socket Buffer, Pool 크기.


실습 1. Pool 크기 스윕#


Worker 수만 변경하며 동일한 부하를 적용하여 측정한다. 동시 클라이언트 수는 10으로 고정하고 Pool 크기만 1에서 16까지 증가시킨다.

라즈베리파이·터미널 / Worker 1
./ecu_pool_blank 1000 1024 0 1
라즈베리파이·터미널 / Worker 4
./ecu_pool_blank 1000 1024 0 4
라즈베리파이·터미널 / Worker 16
./ecu_pool_blank 1000 1024 0 16
노트북·WSL
./load_client 192.168.10.2 10 100

Pool 크기 처리량 (req/s) 지연 p99 (μs)
1
4
16
직접 정한 값

풀 크기 1#

alt text

풀 크기 4#

alt text

풀 크기 16#

alt text


결과 : Pool 크기와 처리량#

Pool 크기 증가에 비례하여 처리량이 향상되며, Worker 수에 비례하여 동시 처리 능력이 확대된다. 다만 무한정 증가하지는 않으며, 동시 연결 수 근처에서 포화 상태에 도달한다.

처리량 · req/s Pool 크기 857 1,729 2,886 4,294 8,100 1 2 4 8 16 참고 실측 · 컨테이너 환경 · 동시 클라이언트 10 · 처리 지연 1,000 μs

확인할 것

위 수치는 참고값이다. 실제 측정 결과는 장비 성능과 실행 환경에 따라 달라질 수 있다.


분석: 최적 Pool 크기#

Worker 수가 동시 연결 수보다 많으면 남는 Worker는 처리할 작업 없이 대기한다. 반면, Worker가 동시 연결 수보다 적으면 일부 연결이 Queue에서 차례를 기다린다.

Pool 크기 발생하는 상황 결과
동시 연결보다 적음 일부 연결이 Queue에서 차례를 기다림 처리량 감소
동시 연결과 비슷함 모든 연결이 곧바로 Worker를 얻음 가장 좋은 지점
동시 연결보다 많음 남는 Worker가 작업 없이 대기함 처리량이 더 증가하지 않음
Core 수보다 훨씬 많음 문맥 전환 비용이 증가함 오히려 성능이 낮아질 수 있음

추가 실험

동시 클라이언트를 10으로 고정하고 Pool 크기를 8, 10, 12, 16으로 바꿔 측정하면 처리량이 포화되는 지점을 확인할 수 있다.


실습 2. Socket Buffer 스윕#


1 MB 응답을 수신하는 과정에서 Socket 수신 버퍼의 크기만 변경하며 처리량을 측정한다. App Buffer는 4 KB로 고정하고 Socket Buffer만 변경한다.

라즈베리파이·터미널
make
./bulk_server 1024
노트북·WSL
make
./bulk_client 192.168.10.2 1024 4096 4096   30
./bulk_client 192.168.10.2 1024 4096 65536  30
./bulk_client 192.168.10.2 1024 4096 262144 30

명령어 인자

bulk_client의 인자는 서버 주소, 응답 크기, App Buffer, Socket Buffer, 반복 횟수 순서이다.

요청한 Socket Buffer 커널이 적용한 값 처리량 (MB/s)
4 KB
64 KB
256 KB
직접 정한 값

SO_RCVBUF 4KB#

alt text

SO_RCVBUF 64KB#

alt text

SO_RCVBUF 256KB#

alt text

관찰 포인트

요청한 버퍼 크기와 커널이 실제로 적용한 버퍼 크기가 같은지 확인한다. 운영체제에 따라 커널이 요청값보다 크게 설정할 수 있다.

측정이 끝나면 라즈베리파이 터미널에서 Ctrl+C를 눌러 서버를 종료한다.


실습 2 결과: Buffer와 처리량#

Buffer크기가 작을수록 처리량이 크게 저하되고, 크기를 증가시킬수록 처리량이 빠르게 향상된다. 다만 버퍼 크기가 일정 수준을 초과하면 처리량이 더 이상 증가하지 않는다.

처리량 · MB/s 요청한 Socket Buffer 85 270 1,586 1,467 4 KB 16 KB 64 KB 256 KB 참고 실측 · 1 MB 응답 기준 · 64 KB 부근에서 포화

확인할 것

위 수치는 참고값이다. 실제 처리량은 장비 성능과 네트워크 환경에 따라 달라질 수 있다. 측정값이 더 증가하지 않는 첫 지점을 실용적인 버퍼 크기로 본다.


분석: 지연과 Buffer 크기#

필요한 Buffer 크기는 고정된 값이 아니며 네트워크 속도와 왕복 시간(RTT)에 따라 달라진다. 직결 환경은 왕복 시간이 짧아 소규모 Buffer로도 처리량이 빠르게 포화 상태에 도달한다. 반면 지연이 큰 환경에서는 동일한 데이터를 전송하기 위해 더 큰 Buffer가 필요하다.

환경 필요한 Buffer 캡처에서 확인할 내용
직결 환경 · 지연이 거의 없음 64 KB 정도에서 포화될 수 있음 Window 값이 충분하고 전송이 막히지 않음
지연이 큰 환경 더 큰 Buffer가 필요함 작은 Window에서는 TCP Window Full이 반복될 수 있음

Wireshark 관찰 포인트

Socket Buffer가 작을 때와 클 때의 Win 값과 TCP Window Full 발생 여부를 비교한다.


실습 정리#


튜닝 대상 적용 원칙 실습에서 확인한 근거
Thread 방식 연결마다 Thread를 만들지 않고 Thread Pool을 사용함 생성되는 Thread 수를 제한할 수 있음
Pool 크기 예상 동시 연결 수에 맞추고 Core 수도 고려함 일정 크기 이후 처리량이 포화됨
Socket Buffer 네트워크 속도와 왕복 시간에 맞춰 조정함 작은 버퍼에서는 처리량이 낮고 일정 크기 이후 포화됨
제어기 메모리 버퍼와 Pool을 무조건 크게 설정하지 않음 통신에 사용하는 메모리가 늘면 다른 기능에 사용할 메모리가 줄어듦

핵심

가장 큰 값을 선택하는 것이 아니라, 처리량이 더 이상 크게 증가하지 않는 첫 지점을 찾는다.