티스토리 뷰

들어가며 

웹 서버에 요청이 오면 스레드를 할당받아 정해진 로직을 수행하고 응답을 보낸다. 하지만 이 단순해 보이는 과정 뒤에는 OS 내부에서 NIC, 커널, 유저 영역을 넘나드는 복잡한 여정이 숨어있다. 하나의 요청이 어떻게 네트워크를 타고 들어와 애플리케이션까지 전달되고, 다시 응답이 클라이언트에게 돌아가는지, 이 글에서는 TCP 연결이 맺어지는 순간부터 데이터가 오가는 과정까지를 커널 수준에서 파헤쳐본다.

TCP 연결, 3 way handshake 내부

알다시피 TCP는 3-way handshake 과정을 통해 연결을 맺는다. 그렇다면 이 과정에서 OS 내부에서는 어떤 일이 일어날까?

 

첫 번째로 클라이언트는 TCP 연결을 위해 SYN 패킷을 서버로 보낸다. 해당 패킷이 서버에 도착하면 가장 먼저 네트워크 인터페이스 카드(NIC)에 닿는다. NIC는 이를 DMA를 통해 RX Ring Buffer라는 공간에 저장한다.

 

여기서 DMA와 RX Ring Buffer라는 개념이 생소할 수 있다. 이에 대해 먼저 알아보자.

 

DMA란

DMA는 입출력 장치나 주변 기기가 CPU를 거치지 않고 시스템 메모리(RAM)에 직접 데이터를 전송하는 기술이다. NIC 내부에는 전용 칩이 있어서 패킷 수신/송신을 CPU없이 처리할 수 있다. 

만약 CPU가 직접 패킨 수신/송신을 처리한다면 아무래도 프로세스 실행하는데 바쁜 CPU가 더 많은 부담을 들여야 하므로 성능에 좋지않다. 그래서 NIC이 직접 메모리에 접근하여 패킷이 들어온다면 RAM에 접근해 데이터를 처리할 수 있도록 한것이다. 

 

RX Ring Buffer란 

RX Ring Buffer는 NIC이 패킷 수신을 하면 보관해두는 커널 메모리에 있는 영역이다. OS에 포함된 NIC 드라이버 코드가 부팅 시 NIC를 감지하고 내부적으로 메서드를 실행하여 커널 메모리에 RX Ring Buffer를 할당한다. 이후 그 물리 메모리 주소를 NIC 레지스터에 써줌으로써 NIC가 해당 주소를 알 수 있게 된다. 이후 패킷이 도착하면 NIC는 CPU 개입 없이 DMA를 통해 해당 주소에 직접 패킷을 기록한다. 

 

이렇게 NIC이 수신한 패킷을 RX Ring Buffer에 기록을한다면 NIC는 하드웨어 인터럽트를 보내 CPU에게 패킷이 도착했다고 알린다. 

CPU는 하던 일을 멈추고 인터럽트 핸들러를 실행하는데, 여기서 실제 패킷 처리를 하지않고 나중에 softirq를 실행하라고 표시만 하고 즉시 종료한다. 이후 softirq가 실행되면서 RX Ring Buffer에서 패킷을 꺼낸다. 

 

softirq는 이 글에서는 중요한 사항이 아니라 더 찾아보고 싶은 분들은 해당 글을 참고 바란다.

 

지금까지 한 일을 도식화 해보면 다음과 같다.

 

RX Ring Buffer에서 꺼낸 패킷을 커널 네트워크 스택이 확인한다. IP 레이어를 거쳐 TCP 레이어까지 올라오면 SYN 패킷임을 확인하고, 클라이언트 IP, Port, MSS 등 handshake에 필요한 정보를 request_sock 구조체에 담아 SYN Queue에 보관한다. 이후 커널이 자동으로 SYN+ACK 패킷을 클라이언트에게 전송한다.

 

클라이언트가 SYN+ACK 패킷을 수신하면 ACK를 서버로 보낸다. 서버에 도착하면 앞서 설명한 것과 동일하게 NIC가 RX Ring Buffer에 패킷을 저장하고 하드웨어 인터럽트를 발생시키며, softirq가 실행되어 패킷을 꺼내 ACK임을 확인한다. 이때 SYN Queue에 보관 중이던 request_sock을 꺼내 완성된 sock 구조체를 생성하고 ACCEPT Queue에 보관한다. 이로써 TCP 3-way handshake가 완료되고 연결이 수립된다.

 

ACCEPT Queue에 있는 연결은 서버 애플리케이션이 accept()를 호출할 때 꺼내간다. accept()는 시스템 콜로, 유저 영역에서 실행되던 코드가 커널 영역으로 전환되어 커널 메모리에 접근이 가능해진다. 이때 ACCEPT Queue에 있던 struct sock을 꺼내 struct socket을 커널 메모리에 생성한다. struct socket은 struct sock을 감싸는 래퍼 객체로, 유저 공간과의 인터페이스 역할을 한다. 실제 수신 버퍼와 송신 버퍼는 struct sock 내부에 존재하며 이를 통해 데이터를 송수신한다.

 

다만 struct socket은 커널 메모리에 존재하기 때문에 애플리케이션이 직접 접근할 수 없어, accept()의 반환값으로 파일 디스크립터 번호를 돌려준다. 애플리케이션은 이 번호를 이용해 read(), write() 등의 시스템 콜을 호출할 수 있고, 커널은 파일 디스크립터 번호를 보고 대응하는 struct socket을 찾아 struct sock의 버퍼에서 데이터를 읽거나 쓰는 작업을 수행한다. 즉 애플리케이션은 파일 디스크립터라는 티켓 번호만 들고 있으면 되고, 커널이 그 번호를 통해 실제 소켓 구조체를 찾아 데이터를 처리해주는 것이다.

 

그렇다면 tomcat을 사용한다고 했을때 accept queue에 있던 sock을 언제 꺼내고, 해당 소켓으로 데이터가 왔을때 어떻게 스레드를 할당 받을까 ? 

 

Tomcat에는 Acceptor 스레드가 있어 accept() 시스템 콜을 호출하며 ACCEPT Queue에 연결이 올 때까지 블로킹 대기한다. 연결이 오면 accept()가 반환되고 파일 디스크립터를 획득하여 해당 소켓을 Poller에 이벤트로 등록한다. 이벤트로 등록하는 이유는 이 시점이 TCP 3-way handshake만 완료된 상태로, 클라이언트가 아직 데이터를 보내기 전이기 때문이다. Poller 스레드는 등록된 소켓들을 epoll로 감시하다가 데이터가 도착한 소켓이 있으면 Selector에게 넘긴다. Selector는 해당 소켓을 Worker Queue에 넣고, Worker Thread Pool에서 유휴 스레드가 있으면 Worker Queue에서 소켓을 꺼내 실제 HTTP 요청을 처리하게 된다. 만약 유휴 스레드가 없다면 스레드가 반납될 때까지 Worker Queue에서 대기하게 된다.

데이터 송신, 수신시 OS 내부적으로 일어나는 과정 

데이터 송신 즉 애플리케이션에서 write 시스템 콜을 호출하면 OS 내부적으로 어떤일이 발생할까? 

 

애플리케이션이 write() 시스템 콜을 호출하면 파일 디스크립터 번호에 해당하는 struct socket을 찾고, 전달한 데이터를 struct sock의 송신 버퍼에 복사한다. 커널은 이 데이터를 MSS 크기에 맞게 잘라 TCP 세그먼트를 만들고, IP 헤더를 붙여 IP 데이터그램을 만든 뒤 최종적으로 패킷을 완성한다. 완성된 패킷은 qdisc에 들어가 우선순위와 순서가 결정된다. 이후 드라이버가 qdisc에서 패킷을 꺼내 TX Ring Buffer에 써주면, NIC가 DMA로 TX Ring Buffer에서 패킷을 읽어 물리 신호로 변환해 전송한다.

TX Ring Buffer는 RX Ring Buffer와 비슷하게 NIC이 패킷 송신을 위해 보관해두는 커널 메모리에 있는 영역이다.

 

반대로 데이터 수신 시에는 NIC가 패킷을 수신하고 DMA를 통해 RX Ring Buffer에 저장한다. 이후 하드웨어 인터럽트를 발생시키고 softirq가 실행되어 RX Ring Buffer에서 패킷을 꺼낸다. 커널 네트워크 스택을 거치며 IP 헤더를 제거하고 TCP 레이어까지 올라오면, 해당 패킷이 어느 연결의 것인지 파악해야 한다. 이때 커널은 내부적으로 해시 테이블을 이용하는데, 패킷의 출발지 IP, 목적지 IP, 출발지 Port, 목적지 Port 4가지를 키로 하여 대응하는 struct sock을 찾는다. 찾은 struct sock의 수신 버퍼에 데이터를 쌓는다.

 

애플리케이션이 read() 시스템 콜을 호출하면 파일 디스크립터 번호에 해당하는 struct socket을 찾고, struct sock의 수신 버퍼에서 데이터를 꺼내 애플리케이션 메모리로 복사해준다.

 

끝으로 

OS 내부적으로 이런 것들을 알아서 뭐에 쓸까 생각할 수도 있지만, 대규모 트래픽이 몰려올 때 이런 내부 구조를 깊이 이해하고 있어야 장애에 제대로 대응할 수 있다.

 

예를 들어 갑작스러운 트래픽 폭증 시 SYN Queue 크기를 적절히 튜닝해놓지 않았다면 TCP 3-way handshake조차 완료하지 못하는 클라이언트가 생길 수 있다. 마찬가지로 ACCEPT Queue가 너무 작다면 handshake는 완료됐지만 애플리케이션이 연결을 꺼내가기 전에 드롭되는 상황이 발생한다. RX Ring Buffer가 부족하다면 그보다 더 앞단에서 패킷 자체가 드롭된다. 결국 트래픽이 몰릴 때 어느 지점에서 드롭이 발생하는지 파악하지 못하면 원인도 모른 채 장애를 맞이하게 된다.

 

이처럼 데이터를 송수신하는 과정에 대한이해는 단순한 지식이 아니라 실제 운영 환경에서 장애를 진단하고 해결하는 데 도움이 될것이다. 

 

참고자료

 

네트워크 커널 튜닝

할 수 있을 듯 하기 어려운 리눅스 커널 튜닝 | 대량의 트래픽이 발생할 때를 대비해야 한다면..? 신상품 공개, 콘서트 티켓 세일 등과 같은 이벤트가 있는 경우, 웹사이트에 순간적으로 엄청난

brunch.co.kr

공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2026/04   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30
글 보관함