read와 write는 대칭이 아니다, 오류는 read에서 즉시 드러나고 write에서는 나중에 터진다
- read 계열 호출은 버퍼를 채우는 과정에서 생긴 오류가 fread 반환 전에 모두 드러나는 반면, write 계열 호출은 데이터를 사용자 공간 버퍼에 복사하고 곧바로 반환하므로 디스크 오류가 한참 뒤에야 나타남
- 블록 장치는 블록 단위로만 읽고 써서 바이트 단위 요청을 그대로 받지 못하고, 버퍼 캐시가 이를 블록 정렬 연산으로 바꾸며 정렬되지 않은 쓰기는 덮어쓰지 않는 부분을 채우려고 블록의 기존 내용을 먼저 읽어야 함
- 블록 크기는 구형 하드디스크 512바이트, CD와 DVD 2KB, 최신 저장장치 4KB이고 시스템 콜 오버헤드 때문에 보통 4096바이트(Visual C++ 2013 FILE*, Go bufio)나 8192바이트(GNU libc, Java BufferedReader) 버퍼를 씀
- read와 write 시스템 콜은 커널 전환과 오류 확인에 최소 몇백 클럭이 걸려서, 오버헤드가 실제 작업을 압도하지 않으려면 한 번에 최소 몇 KB를 읽거나 써야 함
Hacker News 의견들
이거 CPU 캐시 레벨에서도 똑같은 얘기라 결국 아래층까지 다 내려가는 문제임. Jane Street에서 Jon G가 발표한 적 있는데, 원인 모를 성능 버그가 알고 보니 코어들이 같은 캐시 라인을 두고 싸운 거였음.
false sharing은 대학에서 배우는 기본 아닌가? 리눅스 dwarves 도구도 그거 하려고 만든 거고, struct 필드 배치가 캐시 라인 크기에 맞는지 확인해서 서로 다른 코어가 쓰는 필드가 한 라인에 붙는 걸 막는 거임.
나는 언어랑 뇌 이야기인 줄 알았는데 I/O 얘기라 좀 아쉬웠음. 그쪽이 더 재밌을 텐데.
나도 같은 생각했음. 나는 언어를 거의 완벽하게 읽고 이해하는데 말은 못 하는 이유를 진짜 토론해보고 싶다.
주제에 대해 쓸모 있게 글을 쓰려면 쓴 걸 읽고 이해하는 것보다 훨씬 높은 전문성이 필요하다는 얘기도 재밌을 텐데.
읽기는 캐시를 무효화하지 않음. 남의 캐시도 안 건드리고.
그건 좀 아닌 듯. 큰 파일을 읽는데 빈 페이지가 없으면 페이지 캐시에서 다른 사용자 파일 데이터가 밀려남. 고성능 앱이면 커널이 임의로 페이지를 고르게 두면 안 되고, mmap이면 madvise, read/write면 posix_fadvise로 미리 정리해야 함.
fsync 관련 얘기는 저자가 명확히 해야 함. 그 문제는 새로 만든 파일에만 해당하고 기존 파일 업데이트에는 적용 안 됨.
read/write/sync 얘기면 write amplification, 그러니까 read-modify-write 사이클도 넣어야 하고, 리눅스에는 파일 일부만 동기화하는 sync_file_range도 있음.
글 잘 봤음. 근데 읽기 쪽은 버퍼링이 거의 항상 좋은데, 읽기는 쓰기보다 지연에 훨씬 민감함. 버퍼 키우고 mmap으로 프리페치 늘리면 지연 때문에 오히려 느려질 수 있고, 줄이면 호출이 늘어서 또 느려짐. 결국 워크로드마다 튜닝해야 함. 쓰기는 반대로 fire and forget이라 지연은 상관없고 대신 데이터가 진짜 도착했는지가 중요함.
리눅스의 동기 I/O 모델 자체가 근본적으로 나쁘고, 하드웨어 제조사가 그걸 그럴듯하게 보이려고 눈속임하는 거임. 요즘 고속 하드웨어는 전부 배치와 비동기라서, 1GB 파일을 읽을 거면 1MB 읽기 1000개를 미리 던져놓고 오는 순서대로 처리하는 게 맞음.
mmap()은 읽기에는 좋지만 쓰기에는 좋지 않다는 것도 짚고 넘어갔으면 함.
fsync 오류로 재밌는 사건이 많았는데, PostgreSQL이 fsync 문제를 겪은 이른바 fsyncgate가 대표적임. 이후 커널에 뭔가 바뀐 걸로 아는데 정확히는 기억 안 남.
읽기랑 쓰기가 대칭이라고 왜 생각하겠음? 코드를 읽어본 경험과 코드를 쓰는 경험은 완전히 다른 과정이라는 걸 몸으로 알지 않나.