스왑으로 밀려난 Go GC 메타데이터가 40ms stop-the-world 정지를 만든 사례
- Go 런타임은 힙 밖에 있고 해제되지 않는 GC 메타데이터를 stop-the-world 구간에서 읽는데, 이 페이지가 스왑으로 밀려나면 정지 시간이 중앙값 51us에서 최악 40ms까지 늘어남 (Hetzner, 커널 6.8, MGLRU, NVMe 스왑)
- bpf로 STW 중 page fault를 세보니 39902us 정지 가운데 39013us가 228번의 page fault에서 발생했고, fault 지점은 runtime.(*spanSet).reset, finishsweep_m, nextMarkBitArenaEpoch 같은 GC 부기 코드였음. 30분 동안 STW 정지가 312번 있었고 40ms는 중앙값의 800배임
- 511 KiB 메시지 하나 만드는 시간이 평소 3-5ms에서 NVMe 스왑 105ms, Hetzner 네트워크 볼륨 903ms로 늘었음. 다만 할당하는 고루틴만 지불하는 비용이라 전역 정지와는 다름
- 커널이 접근 시각 기준으로 페이지를 스왑으로 보내고 GC가 다시 읽을 때 do_swap_page가 새 프레임 할당, cgroup 과금, bio 제출, 디스크 대기까지 거치면서 지연이 생김. 작성자는 swap 자체가 악은 아니라는 Chris Down 글에는 동의하지만 GC와는 잘 맞지 않았다고 정리함
- 작성자가 Go 1.26 Green Tea GC를 측정해봤지만 메타데이터 읽기 방식에 미친 영향은 무시할 수준이었음
Hacker News opinions
런타임이 없는 언어가 얼마나 큰 이점인지 또 증명된 셈임. 이런 문제 자체가 안 생기니까
넌 이미 libc랑 커널이라는 런타임 위에서 코딩하고 있는 거임. 넓게 보면 런타임 없는 코드는 없음
Go가 아예 STW 없는 on-the-fly GC를 안 쓰는 이유가 있나?
on-the-fly-gc 프로젝트를 찾아보면 문서상으로도 정지가 있음. GC 노브는 지연 시간과 처리량을 서로 맞바꾸는 구조라 한쪽만 최적화하면 다른 쪽이 나빠짐. Go GC도 이미 concurrent mark-sweep이고 정지 시간이 수십 마이크로초 수준이라 on-the-fly는 다른 맛일 뿐임
GC 지연 SLO에는 OS 메모리 압박을 넣어야 함. 안 그러면 page fault 문제를 컬렉터 문제로 착각하고 엉뚱한 걸 고치게 됨
page fault는 이론상 무한정 길어질 수 있는데, 그걸로 어떻게 의미 있는 SLO를 세우냐?
'이렇게 하면 아프다' '그럼 하지 마라'임. 지연 시간이 중요하면 스왑을 끄면 됨. 시스템 전체든 cgroup 단위든
그건 좀 틀림. 지연이 중요하면 major fault 아무 데서나 멈추는 건 마찬가지임. 스왑을 끄면 데이터 페이지는 보호되지만 코드 페이지는 여전히 밀려날 수 있음. mlock()을 쓰는 게 맞고 스왑을 끄는 게 아님. 스왑이 있어야 커널이 데이터와 코드 페이지를 공평하게 내보낼 수 있음
무서운 건 스왑이 할당만 느리게 만드는 게 아니라, GC 메타데이터가 밀려나면 메모리 압박이 곧바로 stop-the-world 지연 스파이크로 바뀐다는 점임
사람들이 왜 참조 카운팅을 안 쓰는지 모르겠음. 런타임 성능이 훨씬 예측 가능한데
할당마다 오버헤드가 붙고 순환 참조 문제도 있음
예측 가능하다는 것도 사실이 아님. 객체 하나 해제가 임의 길이의 연쇄 해제를 촉발할 수 있음
멀티스레드 원자적 참조 카운팅은 포인터를 덮어쓸 때마다 카운트 두 개를 원자적으로 갱신해야 해서 비쌈. Swift ARC가 런타임 오버헤드 40%까지 나온다는 얘기도 있음. 그래도 고유성 추론이나 빌림 같은 정적 분석으로 RC를 없앨 수 있다는 점은 유망하고, 추적 GC에서는 그런 최적화 사례를 못 봤음
나는 Node, Ruby, Python 쓸 자리에 Go를 쓰고 지연 시간이 중요하면 Rust를 씀
Discord가 2020년에 이걸 겪고 블로그 글을 썼음
구현이 어떻든 GC가 살아있는 객체를 전부 훑어야 한다는 사실만으로 스왑과는 안 맞음. 40ms STW가 없어도 GC마다 LRU 캐시가 날아감. 애플이 GC를 버리고 ARC로 간 것도 같은 이유라고 봄
참조 카운팅도 GC 알고리즘임. '구현과 무관하게'는 아님. gchandbook.org 같은 책도 있고 구현은 여러 갈래임
GC가 OS와 협력하는 연구가 있었음. bc-pldi-2005 논문이고 UMass에서 리눅스에 구현까지 했는데 결과적으로 안 퍼진 게 아쉬움
이 문제는 GC만의 문제가 아님. Go GC에 메타데이터를 읽는 임계 구역이 있고, 자주 안 읽지만 중요한 메모리 청크를 가진 프로그램은 다 취약함
스왑을 인지하는 GC가 가능하지 않을까. 필요한 페이지를 먼저 스왑인시키고 나서 world를 멈추는 식으로. 그런 API가 딱히 보이진 않지만