[TIL] 소켓 프로그래밍 - Overlapped I/O or 비동기 I/O

KYJ의 Tech Velog·2024년 2월 28일
0

앞서 이야기했던 논블록 소켓에도 여러 보완해야될 점은 있습니다. 이를 보완한 여러가지 방식이 있는데 그 전에 논블록 소켓의 장단점을 한 번 정리해보도록 하겠습니다.

먼저 장점은 다음과 같습니다.

  • 스레드 블로킹이 없어서 중도 취소 같은 통제가 가능
  • 스레드 개수가 적어도 소켓을 여러 개 활용 가능
  • 스레드 개수가 적기 때문에 연산량과 호출 스택 메모리가 절약

단점은 다음과 같습니다.

  • 소켓 I/O 함수가 would block을 리턴했을 때, 재시도 호출 낭비 발생(추후 설명)
  • 소켓 I/O 함수를 호출할 때, 입력하는 데이터 블록에 대한 복사 연산이 발생(추후 설명)
  • 연결 함수는 재시도 호출을 하지 않지만, 송수신 함수는 재시도 호출을 해야 하는 API가 일관되지 않는 문제

재시도 호출로 인한 낭비에 대해서 자세히 알아보도록 하겠습니다.

TCP 소켓의 전송 함수 처리 부분을 보면, 송신 버퍼에 1바이트라도 비어 있으면 I/O 가능이 됩니다. 이 상태에서 송신 함수를 호출하면 would block을 볼 일은 없습니다. 보낼 데이터의 크기가 1바이트보다 커도 상관없습니다. TCP에서는 송신 버퍼의 빈 공간만큼 데이터를 채우고 리턴합니다.

TCP 소켓의 수신 함수 처리 부분을 보면, 수신 버퍼에 1바이트라도 들어 있으면 I/O 가능이 됩니다. 송신과 마찬가지로 이 상태에서 수신 함수를 호출하면 would block을 볼 일은 없습니다. 수신의 경우에는 UDP 소켓도 똑같습니다.

다만, UDP 소켓의 전송 함수 처리에서는 문제가 생깁니다. UDP는 TCP와 다르게 데이터를 일부만 보낼 수가 없습니다. 보내려는 데이터의 크기가 5바이트라면 버퍼도 5바이트가 비어있어야 하죠. 따라서, would block 오류 코드를 보게 됩니다. 만약 버퍼에 보내고자 하는 데이터의 크기만큼 빈 공간이 계속 생기지 않는다면 계속 would block을 보게 될 것이고 이는 CPU 낭비로 이어질 것입니다.

논블록 소켓은 함수 내부의 데이터 복사 부하라는 단점도 가지고 있습니다. 소켓 송수신 함수에 들어가는 데이터 블록 인자를 성공적으로 실행하면 메모리 복사 연산이 발생합니다.

RAM은 캐시 메모리에 비해 굉장히 느린 메모리입니다. 고성능 서버를 개발할 때에는 이 복사 연산의 속도도 절대 무시할 수가 없습니다.


언급드렸던 문제들을 모두 해결할 수 있는 방법이 바로 Overlapped 또는 비동기 I/O 입니다. Overlapped I/O는 데이터를 다음과 같이 다룹니다.

  1. 소켓에 대해 Overlapped 액세스를 "겁니다".
  2. Overlapped 액세스의 성공 여부를 확인한 후 성공하면 결과값을 얻어 와서 나머지를 처리합니다.

Overlapped I/O를 어떻게 다루는지 알아보겠습니다.

  1. Overlapped I/O를 걸 때 '진행 중인 상태 현황'을 담을 구조체를 준비
  2. 블로킹 소켓을 사용
  3. 소켓에 대한 Overlapped I/O 전용 함수 호출. (항상 즉시 리턴)
  4. 성공했다면 OK, 그렇지 않으면 완료를 기다리는 중이라는 I/O pending이라는 값을 즉시 리턴
  5. Overlapped I/O 완료 여부를 확인하는 함수를 통해 확인
  6. Overlapped I/O를 수신했다면 데이터 객체에 수신된 데이터가 자동으로 채워져있을 것이기 때문에 그냥 액세스

Overlapped I/O는 성능상 유리한 점은 있지만 주의해야할 부분이 있습니다. Overlapped I/O 함수는 즉시 리턴되지만, 운영체제로 해당 I/O 실행이 별도로 동시에 진행되고 있습니다. 운영체제는 소켓 함수에 인자로 들어갔던 데이터 블록을 백그라운드에서 접근합니다. 사용자가 작성한 코드가 뭔가 하고 있는데, 별개로 운영체제가 마음대로 사용자의 데이터를 건드리고 있다는 말입니다.

따라서, 사용자가 호출한 함수가 비동기로 하는 일이 완료될 때까지 소켓 API에 인자로 넘긴 데이터 블록을 제거하거나 내용을 변경해서는 안됩니다. 또한 함수의 인자로 Overlapped status 구조체가 포함되는데, 완료 여부는 이 구조체를 통해 알 수 있습니다.

이 구조체도 운영체제에서 백그라운드로 접근중입니다. 따라서, 중간에 제거하거나 내용을 변경해서는 안됩니다.

그렇다면 Overlapped I/O를 중첩하면 안되는 것인가라는 생각이 들 수 있습니다. 대답은 '아니오'입니다. 다만, Overlapped status 객체가 서로 달라야 하고 입력한 데이터 블록도 서로 달라야 합니다. 중첩하게 되면 두 스레드에서 블로킹 I/O를 한 것과 같기 때문입니다.

소켓의 송수신 버퍼의 크기를 0으로 설정하면 Overlapped I/O는 다르게 작동합니다. 송신 함수를 호출하면 운영체제는 송신할 데이터가 있는 메모리 블록 자체를 송신 버퍼로 사용합니다. 수신도 마찬가지입니다.

Overlapped I/O의 장단점을 알아보도록 합시다. 장점은 다음과 같습니다.

  • 소켓 I/O 함수의 결과가 would block일 경우 재시도 호출 낭비가 없습니다.
  • 소켓 I/O 함수를 호출할 때 입력하는 데이터 블록의 복사 연산을 없앨 수 있습니다.
  • 송수신, 연결, 승인 함수를 한 번 호출하면 완료 신호도 딱 한 번만 옵니다.
  • IOCP와 결합하면 최고 성능의 서버 개발이 가능

단점은 다음과 같습니다.

  • 완료되기 전까지 Overlapped status 객체가 데이터 블록을 중간에 훼손하지 말아야 합니다.
  • 윈도우 플랫폼에서만 지원합니다.
  • 연결, 승인 함수 계열의 초기화가 복잡합니다.

논블록 소켓에서의 I/O 실행 상태는 다음과 같습니다.

  • 송신 버퍼에 1바이트라도 여유 공간이 있으면 송신 가능(send available)
  • 수신 버퍼에 1바이트라도 여유 공간이 있으면 수신 가능(receive available)
  • 이를 통합하여 I/O 가능이라 표현

Overlapped I/O에서의 상태는 다음과 같습니다.

  • 송신이 진행중이고 완료가 안 되었다면 'Overlapped 송신이 아직 완료 대기 중(pending)'
  • 수신이 진행중이고 완료가 안 되었다면 'Overlapped 수신이 완료 대기 중'
  • 이를 통합하여 'I/O 완료 대기 중' 또는 'I/O 실행 중'

논블록 소켓에서는 상태를 확인한 후에 행동을 한다면, Overlapped I/O에서는 일단 일을 저지른 후 결과를 확인합니다. 그래서 논블록 소켓을 '뒤늦게...'라는 의미의 리액터 패턴, Overlapped I/O는 '미리...'라는 의미의 프로액터 패턴이라고 합니다.


마지막으로 두 패턴 모두 소켓 개수에 따라 성능 문제가 발생합니다. 소켓 개수 그대로 반복문을 돌기 때문입니다. 만약 소켓이 1만 개이고 각 소켓이 초당 100번씩 I/O 가능 혹은 완료 이벤가 발생한다고 하면, 초당 100만 번의 처리를 하게 됩니다. 이러한 반복문없이 한 번에 끝내는 방법을 다음에 알아보고자 합니다.

바로 IOCPepoll입니다.

0개의 댓글