32
IOCP Advanced NHN NEXT 남현욱

Iocp advanced

Embed Size (px)

Citation preview

IOCP Advanced

NHN NEXT남현욱

01

Proactor VS Reactor

01 Proactor VS Reactor

•ReactorReactor는 ‘결과에 대한 반응’이다. 지금 당장 작업해야할 내용이 있는 지 아닌지 확인하고 있다

면 그에 따른 명령을 수행한다. 대표적인 예시로 select, epoll같은 것들이 있다. select, epoll

을 이용할 경우 작업할 거리가 있는지 확인한 후(demultiplexer) 있다면 어떤 작업을 해야하는

지 분석하고(dispatcher), 적절한 방법을 통해 이를 처리한다(request handler).

직관적이고 구현이 어렵지 않다는 장점이 있으나 많은 요청이 몰리면 부하가 발생해서 성능적으

로 뒤떨어질 수 있다(게임 서버로 생각하면, 처음 게임 서버 오픈했을 때 100만명,200만명씩 한

번에 접속 시도가 일어나는데 이 때 Reactor 방식으로 동작하면 정말 답도 없을 것이다).

01 Proactor VS Reactor

•ProactorProactor는 Reactor와는 반대로 ‘미리 대비하는 것’이라고 할 수 있다. Proactor는 매번 작업

이 완료됐는지 아닌지 확인하며 동작하지 않고, 작업을 시켜놓은 후 그 작업이 완료되면 그때 그

완료에 대한 처리를 수행한다. IOCP와 같이 비동기적으로 IO의 완료를 기반으로 동작하는 것들

이 Proactor라고 할 수 있다.

Reactor에서 어떤 이벤트의 발생이란 IO 작업을 할 데이터가 있다 라면, Proactor에서 어떤 이

벤트의 발생이란 IO 작업이 완료되었다 이다. 여기서 OS의 지원이 필요한데, OS가 내부적으로

비동기적인 IO 작업을 수행한 뒤 그게 완료되면 어플리케이션에게 IO 작업이 완료되었음을 알려

준다. 이런 비동기 IO 작업에 대한 OS의 지원이 있어야 Proactor 방식으로 동작할 수 있는 것이

다.

* JAVA의 NIO는 OS에 상관없이 비동기적인 콜백 함수 기반의 IO 처리를 지원하지만, 이건 결

국 OS 단에서의 처리가 아니라 어플리케이션 단에서(JVM도 결국 어플리케이션이다) 비동기적

인 IO를 흉내낸 것이다. 즉 인터페이스는 Proactor 방식을 흉내낼 수 있겠지만 제대로

Proactor 방식을 구현하려면 OS 레벨에서의 비동기 IO가 꼭 필요하다.

01 Proactor VS Reactor

•AcceptEx..지금까지 우리는 accept를 이용해서 클라이언트 소켓의 연결을 수락했다. 하지만 IOCP에서

accept를 쓰는 건 뭔가 이상하다. accept는 누가 봐도 명백히 reactor 방식이다. accept 함수

에서 클라이언트의 연결 요청이 올 때까지 기다렸다가, 연결 요청이 오면 그 때서야 그걸 확인하

고 그 연결을 수락하는 등의 처리를 했다. proactor 방식의 IOCP와는 별로 어울리지 않는다.

그래서 IOCP에 어울리는 proactor 방식의 accept, AcceptEx가 필요하다.

BOOL AcceptEx(SOCKET sListenSocket, SOCKET sAcceptSocket,

PVOID lpOutputBuffer, DWORD dwReceiveDataLength,

DWORD dwLocalAddressLength, DWORD dwRemoteAddressLength,

LPDWORD lpdwBytesReceived, LPOVERLAPPED lpOverlapped)

원형이 굉장히 복잡하다. AcceptEx는 어떤 식으로 이용하는 지 차근차근 살펴보자.

01 Proactor VS Reactor

•AcceptEx..인자설명sListenSocket : listen socket. 서버에서 사용하는 listen socket을 인자로 넘겨준다.

sAcceptSocket: 클라이언트의 연결을 수용할 소켓. 보통 미리 소켓 풀을 만들어 둔다.

lpOutputBuffer : 새로운 연결로부터 전송된 데이터를 바로 송신받고, 그에 덧붙여 서버의 로

컬 어드레스와 클라이언트의 리모트 어드레스를 저장받기 위한 버퍼이다.

dwReceiveDataLength : 실제로 전송받을 데이터를 저장할 크기이다. 주소 부분을 제외하고

전송된 데이터에 대해 얼마나 버퍼에 저장할지 정한다. 0으로 설정할 경우 어떤 쓰기 작업도 하

지 않고 최대한 빨리 연결을 완료한다.

dwLocalAddressLength : 로컬 어드레스 정보를 저장하기 위해 예약될 크기이다. 반드시 16

바이트 이상이어야한다.

dwRemoteAddressLength: 리모트 어드레스 정보를 저장하기 위해 예약될 크기이다. 반드

시 16바이트 이상이어야한다. 0은 안 됨

lpdwBytesReceived : 받은 데이터의 양이다. 동기적인 IO일 때만 의미 있음

lpOverlapped: 요청을 수행하기 위한 OVERLAPPED 구조체의 포인터. 당연히 NULL은 안

된다.

01 Proactor VS Reactor

•AcceptEx..WSAIoctl

최신 Window SDK에서는 AcceptEx 함수를 그냥 지원해주지만 이전 버전에서는 그렇지 않다.

WSAIoctl이라는 함수를 이용해 AcceptEx 함수의 포인터를 받아와야 한다. 아래 코드를 수행

하면 lpfnAcceptEx에 AcceptEx 함수의 포인터가 저장된다.

LPFN_ACCEPTEX lpfnAcceptEx = nullptr;GUID GuidAcceptEx = WSAID_ACCEPTEX;

DWORD dwBytes; WSAIoctl(mListenSocket, SIO_GET_EXTENSION_FUNCTION_POINTER, &GuidAcceptEx, sizeof(GuidAcceptEx), lpfnAcceptEx, sizeof(lpfnAcceptEx), &dwBytes, nullptr, nullptr);

01 Proactor VS Reactor

•AcceptEx..예시코드(에러처리생략)

//accept socket 생성. 풀 형태로 필요한 만큼 만들어서 쓰면 된다.SOCKET acceptSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);

//overlapped 구조체 초기화memset(&overlap, 0, sizeof(overlap));

//WSAIoctl을 통해 얻은 AcceptEx 함수 호출lpfnAcceptEx(listenSocket, acceptSocket, lpOutputBuf, outBufLen - ((sizeof(sockaddr_in) + 16) * 2), sizeof(sockaddr_in) + 16, sizeof(sockaddr_in) + 16, &dwBytes, &overlap);

//accept 완료되면 IOCP를 통해 통지 받도록 IOCP에 등록hCompPort = CreateIoCompletionPort((HANDLE) acceptSocket, hCompPort, 0, 0);

01 Proactor VS Reactor

•ConnectEx..소켓과의 연결을 Proactor 방식으로 수행하기 위한 함수다. (역시 사용하려면 WSAIoctl을 통

해 불러와야한다.

GUID 값으로 WSAID_CONNECTEX값을 넘기면 됨.

함수원형

BOOL ConnectEx(SOCKET s, const sockaddr* name, int namelen, PVOID lpSendBuffer, DWORD dwSendDataLength, LPDWORD lpdwBytesSent, LPOVERLAPPED lpOverlapped);

01 Proactor VS Reactor

•ConnectEx..인자설명

s : 아직 연결되지 않은 bind()만 된 상태의 소켓.

name : 연결할 클라이언트의 주소를 담고 있는 구조체의 포인터.

namelen : 2번째 인자로 넘긴 name 구조체의 크기

lpSendBuffer : 옵션. 연결이 되자마자 전송할 데이터를 담고 있는 버퍼. 연결하기 위한 데이터

를 가리키는 것은 아님.

dwSendDataLength : lpSendBuffer에 담긴 데이터의 길이. lpSendBuffer가 nullptr이면

무시된다.

lpdwBytesSent: 연결 완료된 후 전송된 데이터의 크기. 역시 lpSendBuffer가 nullptr이면

무시.

lpOverlapped : 요청을 수행하기 위한 OVERLAPPED 구조체의 포인터. 당연한 거지만

nullptr면 안된다.

01 Proactor VS Reactor

•ConnectEx..ConnectEx가 TRUE를 리턴했다면 소켓 s는 연결된 소켓의 기본적인 상태를 가진다. 이 소켓 s

의 속성이나 옵션같은 걸 변경하고 싶다면 아래와 같이 SO_UPDATE_CONNECT_CONTEXT

속성을 해당 소켓에 대해 지정해주어야한다(이건 AcceptEx도 마찬가지).

setsockopt(s, SOL_SOCKET, SO_UPDATE_CONNECT_CONTEXT, NULL, 0);

01 Proactor VS Reactor

•DisconnectEx..DisconnectEx는 소켓의 연결을 닫고 해당 소켓 핸들을 재활용할 수 있게 해준다.

함수원형BOOL DisconnectEx(SOCKET hSocket, LPOVERLAPPED lpOverlapped,

DWORD dwFlags, DWORD reserved)

인자설명hSocket : 연결된 소켓의 핸들이다.

lpOverlapped : OVERLAPPED 구조체에 대한 포인터. 이 소켓 핸들이 overlapped하게 열렸

다면, 이 인자를 명시해줌으로써 비동기 IO 작업으로 처리할 수 있다.

dwFlags: 함수 호출에 대한 플래그 설정.

reserved: 예약된 인자. 0 외의 다른 값은 쓰지 맙시다.

02

PAGE-LOCKING

02 PAGE-LOCKING

•Page-LockingIOCP에서 WSASend 혹은 WSARecv를 할 때 기본적으로 Overlapped IO 방식으로 IO를 수

행한다. 함수 호출할 때를 잘 생각해보면 알겠지만 WSASend, WSARecv를 할 때 결과를 기록

할 버퍼를 우리가 직접 제공한다(WSABUF). 이 때 이 버퍼는 IO 과정에서 커널이 직접 접근해

서 읽고 쓸 수 있어야한다. IO 작업은 우리가 하는게 아니라 커널 단에서 하는 거니까.

이 때문에 해당 버퍼가 있는 위치의 physical memory에는 page lock이 걸린다. 이 부분 메모

리는 커널이 계속 읽고 쓸 수 있어야하므로 해당 영역에서 Page out같은게 일어나면 큰일나기

때문이다.

02 PAGE-LOCKING

•LockedPageLimit비동기 IO 작업을 할 때 Page-locking이 일어난다고 했다. 근데, OS 입장에서 봤을 때 이

Page-locking이 문제가 될 수 있다. 만약에 동시에 엄청난 양의 send - recv가 진행돼서

physical memory 전체에 락이 걸렸다고 하자. 그럼 OS는 다른 프로그램을 실행시킬 수가 없

다. 일부 페이지를 내리고 메모리 공간을 확보해서 다른 프로세스를 돌려야하는데 죄다 락이 걸

려있으니 방법이 없는 것이다.

그래서 영리한 OS는 Locked Page Limit라는 것을 두었다. 한 프로세스가 페이지에 락을 걸 수

있는 양에 한계를 둔 것이다. 이 제한은 대략 램의 8분의 1정도 양이라고 하는데, 이 제한에 도달

하면 IOCP를 사용한 작업들은 ERROR_INSUFFICIENT_RESOURCES 에러를 내며 실패하게

된다.

02 PAGE-LOCKING

•Non-pagedpoollimitnon-paged pool이라는 특별한 영역이 있다.

이 영역은 항상 물리적 메모리 위에 존재하며, 절대 page out되지 않는다. 이 영역은 보통 커널

이 드라이버 정보 등 다양한 커널 모드 컴포넌트들에게 필요한 정보를 저장하는데 쓰인다.

문제는 소켓을 생성할 때도 이 풀의 공간을 약간씩 소모한다는 것이다. bind/connect/

WSASend/WSARecv 등의 함수 호출시에도 조금씩 소모가 되는데, 접속자 수가 굉장히 많아

지면 이 공간이 가득차서 문제를 일으킬 수 있다. 문제는 이 영역의 크기를 정확히 알 수가 없다는

것이다. 결국 에러를 피하기 위해서는 한 번에 연결을 맺을 수 있는 최대 세션 개수를 적절히 정해

서 그걸 잘 관리하는 방법 밖에 없다.

02 PAGE-LOCKING

•PageLocking최소화일단 Lock이 걸리는 단위가 버퍼 크기가 아니라 page 크기라는게 중요하다. 버퍼를 최대한 페

이지 크기에 맞춰서 페이지 공간을 낭비없이 사용하면 일단 좀 더 효율적이긴 할 것이다.

Zerobyterecv

최대 연결 세션 개수가 중요한 서버에서 쓸 수 있는 방법이다. 0바이트를 읽는 recv 작업을

보내는 기법인데, 일단 읽는 크기가 0바이트기 때문에 recv 작업을 하면서도 page-locking

이 일어나지 않는다. IOCP는 읽는 크기가 0바이트라 해도 실제 IO 작업이 일어나기 전까진 IO

Completion으로 생각하지 않는다. 따라서 뭔가 읽기 작업이 일어나면 0바이트 읽었다는 IO

Completion이 도착하게 되고 이제 다시 이걸 읽어내면 엄청나게 많은 연결에 대한 읽기 작업도

모두 0바이트만 갖고 일단 읽기 작업이 일어났는지 확인한 후, 실제 읽기 작업에 들어갈 수 있기

때문에 Page-Locking을 줄일 수 있는 굉장히 효율적인 방식이다. IOCP는 Proactor 방식이기

때문에 실제 읽기 작업이 일어나지 않고 있어도 WSARecv하고 있어야하고, 이 때문에 많은

메모리가 아무 작업도 안하는데 Page-Locking될 수 있다는 걸 생각해보면 Zero byte recv가

굉장히 효율적인 방식이 될 수 있다.

02 PAGE-LOCKING

•SO_RCVBUF,SO_SNDBUF커널 레벨에서의 송수신 버퍼 크기를 조정하는 인자이다.

SO_RCVBUF, SO_SNDBUF 크기를 0으로 지정하면 커널 레벨에서의 송수신버퍼를 이용하지

않고 어플리케이션에서 제공하는 버퍼로 바로 복사를 해버리기 때문에 복사 횟수가 한 번 줄어들

어서 속도가 굉장히 빨라진다. 물론 이 커널에서 어플리케이션이 제공한 버퍼로의 직접 복사 때

문에 Page-Locking이 발생하기는 한다.

03

Travel Of Packet

03 Travel Of Packet

•SendApplication

user

Kernel

User Data

우선 Application 레벨에서 상대에게 보낼 데이터를 생성한다.File

Sockets

TCP

IP

Ethernet

Driver

Device

NIC

03 Travel Of Packet

•SendApplication

user

Kernel

User Data

File

Sockets

TCP

IP

Ethernet

Driver

Device

NIC

WSASend 와 같은 Write 시스템 콜을 하면 보내고자 하는 데이터가 커널 메모리로 복사되고 순서대로 전송하기 위해 Send socket buffer 뒤에 추가된다

send buffer: User Data

03 Travel Of Packet

•SendApplication

user

Kernel

User Data

File

Sockets

TCP

IP

Ethernet

Driver

Device

NIC

send buffer: User Data

TCP

여기서 TCP Segment(packet)을 생성한다. 전송할 데이터 및 TCB(TCP Control Block)을 추가한다. connec-tion state, receive window, con-gestion window, sequence 번호, 재전송 타이머, 체크섬 등등 TCP 연결 처리에 필요한 정보가 덧붙는다.

03 Travel Of Packet

•SendApplication

user

Kernel

User Data

File

Sockets

TCP

IP

Ethernet

Driver

Device

NIC

send buffer: User Data

TCP

TCPIP

이제 여기에 IP 헤더를 추가한 후 IP routing(목적지 IP 주소로 가기 위한 다음 장비의 IP 주소를 찾는 과정)을 한다.

03 Travel Of Packet

•SendApplication

user

Kernel

User Data

File

Sockets

TCP

IP

Ethernet

Driver

Device

NIC

send buffer: User Data

TCP

TCPIP

Ethernet 레이어에서는 ARP(Ad-dress Resolution Protocol) 를 사용해서 next hop Ip(루팅 경로에서 가장 가까운 루터의 IP 주소)의 MAC주소를 찾는다. 그리고 이 Ethernet 헤더를 패킷에 덧붙인다. 이걸로 호스트의 패킷은 완성.

TCPIPEth.

03 Travel Of Packet

•SendApplication

user

Kernel

User Data

File

Sockets

TCP

IP

Ethernet

Driver

Device

NIC

send buffer: User Data

TCP

TCPIP

TCPIPEth.

TCPIPEth.Pre.IFG CRC

NIC는 패킷 전송 요청을 받고 메인 메모리의 패킷을 자신의 메모리로 복사한 후 네트워크로 전송한다. 이 때 표준에 따라 IFG(inter-frame Gap), preamble, CRC를 패킷에 추가한다. IFG, preamble은 패킷의 시작 판단(framing), CRC는 데이터 보호를 위해 사용.

03 Travel Of Packet

•RecvApplication

user

Kernel

File

Sockets

TCP

IP

Ethernet

Driver

Device

NIC TCPIPEth.Pre.IFG CRC

반대로 Recv 과정을 살펴보자. 우선 NIC가 패킷의 내용을 자신의 메모리에 기록. CRC 검사로 패킷이 올바른 지 검사후, 호스트의 메모리 버퍼로 전송한다. 드라이버가 할당해놓은 호스트 버퍼가 없을 경우 packet drop이 일어날 수 있다.

03 Travel Of Packet

•RecvApplication

user

Kernel

File

Sockets

TCP

IP

Ethernet

Driver

Device

NIC

TCPIPEth.

TCPIPEth.Pre.IFG CRC

드라이버는 이 패킷이 자신이 처리할 수 있는 패킷인지 검사한 후 운영체제가 이해할 수 있도록 받은 패킷을 운영체제가 사용하는 패킷 구조체로 포장한다. 윈도우즈의 경우 NET_BUFFER_LIST.

skb

03 Travel Of Packet

•RecvApplication

user

Kernel

File

Sockets

TCP

IP

Ethernet

Driver

Device

NIC

TCPIPEth.

TCPIPEth.Pre.IFG CRC

Ethernet 레이어에서도 패킷이 올바른지 검사한 후, 상위 네트워크 프로토콜을 찾는다(de-multiplex) 그리고 나서 Ethernet 헤더 제거 후 IP 레이어로 전달.

skb

TCPIPEth.

03 Travel Of Packet

•RecvApplication

user

Kernel

File

Sockets

TCP

IP

Ethernet

Driver

Device

NIC

TCPIPEth.

TCPIPEth.Pre.IFG CRC

IP 레이어에서도 체크섬을 통해 패킷이 올바른지 확인한 후 IP routing을 통해 이 패킷을 여기서(로컬 장비) 처리해야 하는지 다른 장비로 전달해야하는 지 판단한다. 여기서 처리해야 한다면 TCP에서의 프로토콜을 찾은 후 IP 헤더를 제거하고 TCP 레이어로 전달.

skb

TCPIPEth.

TCPIP

03 Travel Of Packet

•RecvApplication

user

Kernel

File

Sockets

TCP

IP

Ethernet

Driver

Device

NIC

TCPIPEth.

TCPIPEth.Pre.IFG CRC

TCP 에서도 checksum확인으로 패킷이 올바른지 검사. 이제 이 패킷이 속하는 연결(TCB)을 찾는다. 소스 IP, 소스 port, 타깃 Ip, 타깃 Port를 식별자로 이용해 검사. 찾으면 프로토콜을 수행해서 받은 패킷을 처리한다.

skb

TCPIPEth.

TCPIP

TCP

03 Travel Of Packet

•RecvApplication

user

Kernel

File

Sockets

TCP

IP

Ethernet

Driver

Device

NIC

TCPIPEth.

TCPIPEth.Pre.IFG CRC

이제 새로운 데이터를 받았다면 그 데이터를 receive socket buffer에 추가한다.

skb

TCPIPEth.

TCPIP

TCP

recv buffer:

03 Travel Of Packet

•RecvApplication

user

Kernel

File

Sockets

TCP

IP

Ethernet

Driver

Device

NIC

TCPIPEth.

TCPIPEth.Pre.IFG CRC

skb

TCPIPEth.

TCPIP

TCP

recv buffer:

어플리케이션에서 WSARecv등의 read 시스템 콜이 발생하면 socket buffer에 있는 데이터를 유저 공간의 메모리로 복사하고 buffer에 있는 데이터는 제거한다.