Zero Copy가 뭘까?

심규민·2024년 7월 19일

최근 카프카 핵심 가이드에서 카프카는 consumer의 요청에 대한 응답을 할 때 zero copy를 이용해서 성능 최적화를 진행했다고 나와있었다. 그런데 zero copy가 뭘까?

데이터 전송(Non-Zero Copy)

우선 간단한 예로 접근해보자.
정적 콘텐츠를 제공하는 서버가 있을 때, 서버가 정적 콘텐츠를 제공하는 과정에서 콘텐츠 데이터를 디스크로부터 읽어와 응답 소켓으로 전송해준다. 이러한 과정에서는 cpu의 사용이 매우 작을거라 생각할 수 있다. 하지만 요청이 들어오고 응답할 때까지의 과정을 자세히 들여다보면 아주 비효율적인 부분이 존재한다.

이 과정을 그림과 코드로 표현하면 다음과 같다.

private long sendWithNonZeroCopy(SocketChannel socketChannel, FileChannel fileChannel) throws IOException {
	long transferSize = 0;
    ByteBuffer buffer = ByteBuffer.allocate(1024);
	int numberOfReadBytes = 0;
	while ((numberOfReadBytes = fileChannel.read(buffer)) != -1) {
		buffer.flip();
		socketChannel.write(buffer);
		buffer.clear();
		transferSize += Math.max(numberOfReadBytes, 0);
      }
	return transferSize;
}
  1. DMA를 통해 디스크로부터 콘텐츠 데이터를 커널 영역에 존재하는 Read Buffer로 복사한다.
  2. 애플리케이션이 커널 영역에 직접 접근할 수 없기 때문에 Read Buffer(커널 영역)의 데이터를 Application Buffer(유저 영역)으로 복사한다.
  3. 데이터 전송을 위해 Application Buffer(유저 영역)의 데이터를 Socket Buffer(커널 영역)으로 복사한다.
  4. 네트워크 통신을 위해 Socket Buffer의 데이터를 NIC Buffer로 복사한다.

위 과정에서 유저 영역으로의 데이터 복사가 발생하는 것을 알 수 있다.
만일 데이터를 조회한 후 다시 사용할 수 있다면 성능상의 이점이 있겠지만, 카프카와 같이 지속적으로 새로운 데이터를 조회해야하는 경우 메모리 사용량에 있어 부담이 될 수 있다. 특히 JVM 환경에서는 메모리를 직접 제어하지 않고 GC가 메모리를 관리하며, 메모리를 정리과정에서 STW가 발생하면 요청에 대한 응답이 지연될 수 있다.

이러한 문제를 zero copy를 통해 해결할 수 있습니다.

데이터 전송(Zero Copy)

zero copy는 커널 영역에서의 버퍼간 데이터 복사를 유저 영역을 거치지 않고 바로 복사하는 방법입니다.
예전에는 JVM 환경에서는 커널 영역의 메모리를 직접 다둘수 있는 방법이 없었기 때문에 zero copy를 사용할 수 없었습니다. 하지만 Java의 NIO 패키지의 추가로 커널 영역을 직접 다룰 수 있는 transferTo와 같은 메소드를 제공하게 됐습니다.
transferTo 메소드는 커널 영역 내에서 데이터 복사를 할 때 사용하는 메소드이며, UNIX에서의 sendfile() 시스템 콜을 사용합니다.
앞선 Non-Zero Copy에서 transferTo를 활용하면 다음과 같은 그림과 코드로 나타낼 수 있습니다.

private long sendWithZeroCopy(SocketChannel socketChannel, FileChannel fileChannel) throws IOException {
     return fileChannel.transferTo(0, fileChannel.size(), socketChannel); // java nio transferTo
}
  1. DMA를 통해 디스크로부터 콘텐츠 데이터를 커널 영역에 존재하는 Read Buffer로 복사한다.
  2. transferTo() 메소드를 통해 Read Buffer에서 Socket Buffer로 데이터를 복사한다.
  3. 네트워크 통신을 위해 Socket Buffer의 데이터를 NIC Buffer로 복사한다.

앞선 복사하는 과정에 비해 코드가 더 간결해졌으며, 유져 영역으로의 복사 과정 또한 없어져 4번의 복사과정을 3번으로 줄였습니다. 추가로 유저 영역으로의 복사가 없기 때문에 메모리를 좀 더 효율적으로 사용하게 됩니다.

여기서 더 최적화를 진행할 수 있을까요?

데이터 전송(Zero Copy With Scatter-Gather DMA)

이전 내용을 통해 Socket Buffer에서 NIC Buffer로의 데이터 복사가 이뤄짐을 알 수 있었습니다. 그런데 Read Buffer에서 NIC Buffer로의 직접 복사가 이뤄지면 좀 더 최적화할 수 있지 않을까?라는 생각이 들 수 있습니다.
이를 NIC의 Scatter-Gather DMA 기능을 통해 이룰 수 있습니다!

NIC에서 gather operation을 지원하는 경우, 이 기능을 통해 여러 메모리 영역에 분산되어 있는 데이터를 효율적으로 모아 네트워크에 전송할 수 있습니다.

즉, Socket Buffer에 Read Buffer에 담긴 데이터의 위치와 크기를 포함하는 descriptor를 생성한 뒤, NIC가 해당 descriptor를 조회하며 DMA 엔진을 통해 데이터를 NIC Buffer로 복사해올 수 있습니다.

위 과정을 그림으로 나타내면 다음과 같습니다.

  1. DMA를 통해 디스크로부터 콘텐츠 데이터를 커널 영역에 존재하는 Read Buffer로 복사한다.
  2. transferTo() 메소드를 통해 Socket Buffer에 descriptor를 생성하고 이를 통해 Read Buffer에서 NIC Buffer의 데이터 복사를 진행한다.

NIC의 gather operation으로 복사를 총 4번에서 2번까지 줄이게 됐습니다.

성능 차이(Non-Zero Copy VS Zero Copy)

대략 1KB크기의 파일을 10000번 전송할 때 성능을 비교해보았습니다.

loop count: 10000, Average time for zero copy: 0ms
Total time for zero copy: 1837ms
loop count: 10000, Average time for non zero copy: 0ms
Total time for non zero copy: 2535ms

대략 약 28% 줄어든 것을 확인할 수 있습니다.

관련 코드는 깃허브에서 확인할 수 있습니다.

요약

  • zero copy는 유저 영역으로 복사를 제거함으로써 CPU, 메모리 사용을 최적화하는 방법입니다.
  • Java에서는 transferTo() 메소드를 통해 쉽게 zero copy를 사용할 수 있습니다.
  • NIC의 gather operation을 통해 복사 수를 더 줄일 수 있다.

참고
https://f-lab.kr/insight/understanding-and-utilizing-buffering
https://m.blog.naver.com/kgw1988/221218267855
https://developer.ibm.com/articles/j-zerocopy/
https://velog.io/@jinii/%EC%A0%9C%EB%A1%9C%EC%B9%B4%ED%94%BCzero-copy

0개의 댓글