본문 바로가기
카테고리 없음

[Linux] 제로 카피(Zero-Copy)

by 왕 달팽이 2018. 12. 17.
반응형

아파치 카프카(Apache Kafka)에 대한 자료를 읽다가 제로카피(Zero-copy)를 언급한 부분을 보게 되었다. 카프카 서버의 성능 개선을 위해 사용한 기법이라고 나와있어서 관련 자료를 찾아보게 되었다. 


제로카피(Zero-copy)에 대한 자료들에서 가장 많이 언급되는 자료는 IBM 개발자 페이지에 게재된 "Efficient data transfer through zero copy"라는 글이다. 제로카피라는 기법이 운영체제의 어떤 비효율을 개선했는지 잘 설명하고 있다. 2008년에서 시간이 흘러 지금은 흔히 사용되는 기법으로 다양한 운영체제에서 관련 시스템 콜을 제공하고 있다. ​

일반적인 파일 ​전송

파일 서버나 정적 파일을 서비스하는 웹 애플리케이션은 디스크에서 파일 컨텐츠를 읽어 네트워크 소켓으로 데이터를 전송하는 일을 반복적으로 수행한다. 간단한 이 동작에는 운영체제 내부에서의 불필요한 컨텍스트 스위칭(Context Switching)과 데이터 복사(Data Copy)가 수반된다.


일반적으로 파일에서 데이터를 읽어 네트워크로 전송하는 서버 코드는 다음과 같을 것이다. 

1
2
3
4
byte[] applicationBuffer = new byte[1024];
 
read(fileFd, applicationBuffer, len);
send(socketFd, applicationBuffer, len);
cs


버퍼를 할당 받고 디스크에서 파일을 버퍼로 읽어들여서 다시 소켓으로 전송하는 형태다. 1024 바이트 이상의 데이터를 위해 while 구문을 사용할 수도 있지만 근본적으로 read() 시스템 콜과 send() 시스템 콜이 반복되는 형태다. 

read() 이후 send()를 수행하는 간단한 코드지만 코드를 수행하기 위해서 4번의 컨텍스트 스위칭과 4개의 메모리 복사본이 생기게 된다.

그림 1. 일반적인 상황에서의 파일 전송 (출처 : IBM 문서)



1) 유저가 read() 시스템 콜을 호출해 파일을 읽어달라고 요청하면, DMA 엔진에 의해서 디스크에 존재하는 파일의 내용이 커널 주소공간에 위치한 Read buffer에 복사되어 진다. 


2) 커널 주소공간에 위치한 Read buffer는 사용자가 접근할 수 없기 때문에 사용자가 read() 함수 호출시 파라미터로 전달한 Application buffer에 Read buffer의 내용을 복사해준다. 복사가 완료되면 함수 호출에서 리턴한다. 


3) 사용자는 Application 버퍼로 읽어들인 데이터를 소켓으로 전송하기 위해 send() 함수를 호출한다. send() 함수 호출시 Application buffer를 파라미터로 전달하면 커널 영역에 위치한 Socket buffer로 데이터를 복사한다. 


4) Socket buffer에 있는 데이터는 실제 네트워크로 전송하기 위해 네트워크 장비(NIC)에 있는 버퍼로 다시 복사된다. 

4개의 버퍼에 동일한 데이터가 복사되는 이 과정은 애플리케이션의 CPU 자원을 소모한다. 이런 버퍼링은 본래 메모리와 디스크, 메모리와 네트워크 장비 사이의 속도 차이를 만회하고자 만들어진 성능 개선 장치다. 예를 들어 1KB 정도의 작은 데이터를 파일에서 읽으면 커널은 1KB 이후 데이터까지 한번에 읽어서 페이지 캐시에 로드한다. 이 후, 그 다음 데이터를 요청하면 물리적인 I/O 를 수행하지 않고 페이지 캐시에서 읽어서 사용자에게 준다. 데이터를 미리 읽어서 (Read Ahead) 성능 개선을 도모한 것이다. 


안타깝게도 이런 버퍼링이 성능 저하를 유발하는 병목(bottleneck)으로 작용할 수 있다. 사용자가 요청하는 데이터의 크기가 커널이 유지하는 버퍼의 사이즈보다 큰 경우 미리 읽는(Read Ahead) 성능 개선 효과보다 여러 단계에 걸쳐 데이터를 복사하는 비효율이 더 커지게 된다. 


또 한, 시스템 콜을 수행하기 위해 유저모드와 커널모드를 오가는 컨텍스트 스위칭(Context Switching)이 발생하게 된다. 컨텍스트 스위칭은 수행 정보들을 백업하는 등의 오버헤드(Overhead)를 수반한다. 파일에서 읽어 소켓으로 전송하는 동작은 read() 함수의 호출과 반환, 소켓으로 데이터를 전송하는 send() 함수의 호출과 반환 등 총 4번의 컨텍스트 스위칭을 발생시킨다. 


제로카피(Zero-Copy)는 이런 비효율적인 동작을 개선하기 위해 소개된 기법이다. 

제로카피(Zero-copy) 동작​

​리눅스 2.2 버전에서 처음 소개된 sendfile() 시스템 콜이 제로카피(Zero-copy) 동작을 구현했다. 


1
2
3
#include <sys/sendfile.h>
 
ssize_t sendfile(int out_fd, int in_fd, off_t * offset ", size_t" " count" );
cs


자바(Java)에서는 nio 패키지에 transferTo(), transferFrom() 메소드로 구현되어있다. 


1
public void transferTo(long position, long count, WritableByteChannel target);
cs


자바의 transferTo(), transferFrom() 메소드 역시 sendfile() 시스템 콜을 이용해 구현되었다. 


1
toChannel.transferTo(position, to, toChannel);
cs


read(), send() 두 번의 시스템 콜이 transferFrom() 한번의 호출로 가능해졌다. 


그림 2. transferTo() 메소드를 이용한 파일전송 (출처 : IBM 문서)



제로카피를 이용한 sendfile(), transferTo()는 다음과 같이 동작한다. 


1) 사용자가 transferTo() 메소드를 이용해 파일 전송을 요청한다. read()와 send() 함수가 하나로 합쳐진 형태의 시스템 콜이다. read() 시스템 콜과 마찬가지로 DMA 엔진이 디스크에서 파일을 읽어 커널 주소 공간에 위치한 Read buffer로 데이터를 복사한다. 


2) 커널 모드에서 유저 모드로 컨텍스트 스위칭하지 않고 바로 Socket buffer로 데이터를 복사한다. 


3) Socket buffer에 복사된 데이터를 DMA 엔진을 통해 NIC buffer로 복사되어 진다. 


4) 데이터가 전송되고 transferTo() 메소드에서 리턴한다. 


이 모든 동작이 transferTo() 메소드 내에서 발생한다. 즉 컨텍스트 스위칭이 4번에서 2번으로 줄어들었다. transferTo() 메소드 호출시 커널 모드로 한번, 종료시 유저모드로 한번 총 2번의 컨텍스트 스위칭이 발생한다. 


또 이 동작들이 유저 주소공간에 있는 Application Buffer로 복사되어지지 않기 때문에 데이터의 복사본이 4군데에서 3군데로 줄어들었다. 따라서 데이터를 복사하는 동작도 한 번 줄어들었다. 컨텍스트 스위칭 회수와 복사본의 개수가 줄어든만큼 CPU 자원의 낭비가 줄어들게되어 성능이 향상된다. 


더 개선된 제로카피(Zero-copy) 

커널의 노력은 여기서 멈추지 않고 더 나아갔다. 리눅스 커널 2.4 이후 버전에서는 NIC 장비가 "Gather Operation"을 지원할 경우 복사본을 더 줄일 수 있게 되었다. 


그림 3. NIC의 도움을 받은 제로카피가 적용된 transferTo() 메소드 (출처 : IBM 문서)



사용자가 추가로 명시하거나 고려해야할 사항은 없다. 커널의 내부 동작이 좀 더 개선된 것이다. 


1) 사용자가 transferTo() 메소드를 호출한다. DMA 엔진이 디스크에서 파일을 읽어 커널에 위치한 Read buffer로 데이터를 복사한다. 여기까지는 동일하다. 


2) 데이터가 소켓 버퍼로 복사되어지지는 않는다. 대신 데이터가 저장된 위치와 데이터 사이즈에 대한 정보와 함께 디스크립터(descriptor)가 소켓 버퍼에 추가된다. DMA 엔진은 이 정보를 이용해 Read buffer에 있는 데이터를 NIC 버퍼에 바로 복사하고, 네트워크로 데이터를 전송한다.


이런 추가적인 최적화는 NIC의 gather operation과 프로토콜의 checksum 기능이 필요하다. TCP와 UDP의 경우 Checksum 기능이 구현되어 있다. NIC 장비의 "Gather operation" 지원의 이유는 stackoverflow에서 힌트를 얻을 수 있다. 


성능상 이점

IBM 문서에서 제로카피를 이용해 얼마나 성능 효과를 얻을 수 있는지 실험 결과를 공개하고 있다. 성능 측정은 리눅스 2.6 커널 버전을 사용했으며 전통적인 방식의 파일 전송과 transferTo() 메소드를 이용한 파일 전송의 성능을 비교했다. 


 File size

 Normal file transfer (ms)

transferTo (ms) 

7MB 

156

45

21MB

337

128

63MB

843

387

98MB

1320

617

200MB

2124

1150

350MB

3631

1762

700MB

13498

4422

 1GB

18399

8537



전통적인 방식과 비교하여 수행 시간이 대략 65% 줄어든 것을 확인할 수 있다. transferTo() 혹은 transferFrom() 메소드, sendfile() 시스템 콜 등을 이용하여 성능을 개선할 수 있는 포인트가 있는지 확인해 볼 필요가 있겠다. 


요약

디스크에서 파일을 읽은 후 별다른 처리 없이 바로 네트워크로 전송하는 파일 서버나 정적 파일을 전송하는 웹 서버의 경우 제로 카피를 사용하면 성능 개선 효과를 얻을 수 있다. 특히 네트워크 속도가 매우 빨라서 서버의 성능상 병목점(bottleneck)이 CPU로 몰릴 수록 불필요한 데이터 복사를 제거해 성능 개선을 얻을 수 있다. 

반응형

댓글