Apache Kafka에서는 "Zero Copy"라는 기술을 통해 효율적인 데이터 전송과 I/O를 이뤄낸다. 본격적으로 Zero Copy를 이야기하기 전에 일반적인 운영체제에서 커널 공간을 활용하여 데이터를 전송하는 과정을 살펴보자.

 

 

  • 먼저, 애플리케이션의 요청에 따라 디스크에서 데이터를 커널 읽기 버퍼에 복사한다. 첫 번째 복사는 디스크에서 파일 내용을 읽고 커널 주소 공간 버퍼에 저장하는 DMA(엔진)에 의해 수행된다. 애플리케이션이 요청이 이뤄지는 시점에서는 User mode였으나, DMA를 통해 복사가 이뤄지는 시점에서는 Kernel mode이다. 즉, 컨텍스트 스위치가 일어난다. (1 번의 복사, 1 번의 컨텍스트 스위치)

  • 이후 데이터를 커널 공간에서 애플리케이션 읽기 버퍼로 복사한다. (1 번의 복사, 1 번의 컨텍스트 스위치)

  • 다시 데이터를 사용자 공간에서 다시 커널 공간의 소켓 버퍼로 복사한다. 복사된 데이터는 소켓 버퍼에 임시로 저장된다.
    (1 번의 복사, 1 번의 컨텍스트 스위치)

  • 마지막으로 데이터를 커널 공간의 소켓 버퍼에서 커널 공간의 네트워크 버퍼로 전송한다. (1 번의 복사)

     

강조하였듯, 운영체제의 전통적인 방식으로 데이터를 네트워크를 통해 전송하거나 데이터 I/O를 수행한다면 중복된 복사가 일어난다. 디스크에서 데이터를 네트워크를 통해 전송해야 한다면, 먼저 해당 데이터는 커널 버퍼로 복사된다. 그 후, 일정 작업을 수행한 이후 데이터는 애플리케이션의 사용자 공간 버퍼로 다시 복사된다. 이후 데이터는 네트워크를 통해 전달되기 위해 커널 공간의 네트워크 버퍼로 복사된다. 간략하게 표현하였지만, 눈에 띄는 복사 작업이 3번이나 발생했다. 


중복 복사는 기본적으로 CPU 오버헤드와 비교적 큰 메모리 대역폭을 사용한다. CPU의 경우에는 실제 애플리케이션 로직 처리보다 데이터를 복사하는데 더 많은 자원을 소모하기도 하며, 복사 작업을 수행하기 위해 전송하고자 하는 데이터의 양보다 훨씬 더 많은 메모리가 필요하기도 한다. 그리고 복사를 위해서 메모리에 I/O 작업을 수행해야 하는데, 이는 데이터 전송에 더 많은 작업을 소요하게 만들고 전체 시스템의 반응 속도를 느려지게 하는 원인으로 동작한다.


Zero Copy 기술은 이러한 비효율성을 최소화하기 위해 개발되었다. Zero Copy의 핵심 아이디어는 데이터의 실제 복사를 최소화함으로써 전체 경로의 비효율성을 줄이는 것이다. 이때 그림을 잘 살펴보면 "도대체 왜 유저 공간에 복사를 해야하지?"라는 궁금증이 들 수 있다. Zero Copy는 이러한 비효율을 제거한다. 


Zero Copy는 비단 Kafka만의 솔루션은 아니다. Java 클래스 라이브러리는 java.nio.channels.FileChannel에 위치한 transferTo() 메서드를 통해 Linux와 UNIX 시스템에서 제로 복사를 지원한다. Zero Copy를 사용하는 애플리케이션은 데이터를 User Space에 올리지 않고 디스크 파일에서 소켓으로 곧바로 복사하도록 커널에 요청한다. 이는 복사 횟수와 컨텍스트 스위치의 횟수의 감소로 이어진다. 그림을 통해 살펴보자.





그러나 여전히 Copy는 총 3번에 걸쳐 일어나며 이를 Zero Copy라고 부르기에는 무리가 있다. 


특정 네트워크 인터페이스 카드에서는 gather 작업을 지원한다. gather 작업은 파일의 내용이 여러 메모리 페이지에 걸쳐 분산되어 있을 때, 각 페이지의 주소와 크기에 대한 정보만을 NIC에 전달하면 이 정보를 기반으로 직접 메모리에서 데이터를 가져오는 형태로 네트워크에 데이터를 전송한다. 이 경우 중간에 데이터를 소켓 버퍼로 다시 복사하지 않아도 되므로 복사 작업이 줄어들게 된다. 문제는 각 페이지에 대한 주소와 크기에 대한 정보를 전달하는 방식인데, 이는 Linux 커널의 2.4 버전에서 지원하는 소켓 버퍼 스케터리스트(Socket Buffer Scatterlist)를 통해 가능하다. 소켓 버퍼 스케터리스트는 메모리의 여러 조각들에 대한 정보를 담고 있다. 스케터리스트에는 고유한 데이터 구조를 가지고 있는데, 이 데이터 구조에는 각각의 데이터 조각의 시작 주소와 크기에 대한 정보가 담긴다. 때문에 메모리에 분산되어 있는 여러 데이터 조각들을 연속적으로 저장하거나 접근할때 스케터리스트는 굳이 별도의 소켓 버퍼에 모든 데이터를 저장하지 않고 메타데이터만을 담는 정도로 복사 작업을 간소화시킨다. 




특히 NIC가 DMA 작업으로 인해 쪼개진 데이터들에 대해서 gather 작업을 지원한다면, 스케터 정보에 담긴 정보들을 바탕으로 NIC는 메모리에 여러 위치에 분산된 네트워크 조각들을 직접 읽어올 수 있다. 앞서 Read Buffer에서 Socker Buffer로의 복사는 CPU를 거치게 되는데, 이 과정을 없앰으로써 CPU의 관여를 없앤다. 특히나 그 이외의 작업들의 경우 DMA를 통해서 데이터가 전송되는데, DMA의 목적 자체가 데이터 전송과정에서 CPU의 개입을 최소화하는 것인 만큼, Zero Copy를 달성하게 되면 CPU의 개입이 현저하게 줄어들게 된다.


그림의 transferTo 메서드 또한 위의 설명을 따른다. 우선 맨 처음의 그림에 따라 유저 공간으로의 전환은 일어나지 않는다. 이때 추가적으로 소켓 버퍼에 대한 복사를 제거하는데 이는 소켓 버퍼 Descriptor(Scatter, 엄밀하게 말하면 다르다)에 의해 가능해진다. NIC의 경우 gather 기능을 가지고 있어야 하는데, gather 기능이 있다면 NIC는 Read Buffer 상의 페이지에 산발적으로 위치한 데이터들을 Descriptor에 기록된 각 페이지들의 시작 주소와 크기 등을 기반으로 DMA를 거친 데이터를 모아서 네트워크 카드에 전송하게 된다.

 

'Data Engineering' 카테고리의 다른 글

Kafka 환경에서의 Zookeeper의 Cluster Coordination  (0) 2023.05.18
Kafka와 Zookeeper의 상호작용  (0) 2023.05.18
Zookeeper의 znode  (0) 2023.05.18
Coursera 데이터 엔지니어링 강의 목록  (0) 2022.12.08
Spark  (0) 2022.11.20