Zero-copy in Go: sendfile, splice, and the cost of io.Copy
Go 언어에서 `sendfile`과 `splice`를 활용한 제로 카피(zero-copy) 기술은 파일 데이터를 사용자 공간을 거치지 않고 커널 내부에서 직접 전송하여 성능을 극대화하는 방식입니다. 이 기술은 디스크에서 페이지 캐시를 거쳐 소켓 버퍼로 데이터를 전달하는 과정을 단일 커널 전송으로 압축하여 불필요한 사용자 공간 복사를 제거합니다.
Go 표준 라이브러리는 이러한 제로 카피 경로를 위해 `io.Copy` 호출 시 소스 파일이 `*os.File`이거나 `*io.LimitedReader`인 경우 `sendfile(2)` 시스템 호출을 사용하도록 최적화되어 있습니다. 그러나 파일이나 리더를 감싸는(wrapping) 중간 레이어가 추가되면 런타임이 이 빠른 경로를 인식하지 못하고 사용자 공간에서 데이터를 주고받는 방식으로 전환됩니다.
예를 들어, 단순히 바이트 수를 세기 위해 `io.Reader`를 추가하는 작은 미들웨어만 삽입해도 `sendfile` 호출이 사라지고 대신 수많은 `read` 및 `write` 시스템 호출이 발생하며 데이터가 32 KiB 단위로 왕복하게 됩니다. 이는 제로 카피 경로에서 약 24배 많은 시스템 호출을 유발하며 성능 저하를 초래합니다.
따라서 파일과 연결 사이에 `io.Reader`나 `io.Writer`를 추가할 때는 런타임이 인식하는 범위 내에서만 작업해야 하며, `io.LimitReader`와 같이 런타임이 명시적으로 처리하는 인터페이스를 사용하는 것이 중요합니다. `strace`를 통해 실제 시스템 호출 횟수를 확인하는 것이 직관보다 더 정확한 성능 판단 기준이 됩니다.
Go 표준 라이브러리는 이러한 제로 카피 경로를 위해 `io.Copy` 호출 시 소스 파일이 `*os.File`이거나 `*io.LimitedReader`인 경우 `sendfile(2)` 시스템 호출을 사용하도록 최적화되어 있습니다. 그러나 파일이나 리더를 감싸는(wrapping) 중간 레이어가 추가되면 런타임이 이 빠른 경로를 인식하지 못하고 사용자 공간에서 데이터를 주고받는 방식으로 전환됩니다.
예를 들어, 단순히 바이트 수를 세기 위해 `io.Reader`를 추가하는 작은 미들웨어만 삽입해도 `sendfile` 호출이 사라지고 대신 수많은 `read` 및 `write` 시스템 호출이 발생하며 데이터가 32 KiB 단위로 왕복하게 됩니다. 이는 제로 카피 경로에서 약 24배 많은 시스템 호출을 유발하며 성능 저하를 초래합니다.
따라서 파일과 연결 사이에 `io.Reader`나 `io.Writer`를 추가할 때는 런타임이 인식하는 범위 내에서만 작업해야 하며, `io.LimitReader`와 같이 런타임이 명시적으로 처리하는 인터페이스를 사용하는 것이 중요합니다. `strace`를 통해 실제 시스템 호출 횟수를 확인하는 것이 직관보다 더 정확한 성능 판단 기준이 됩니다.