신뢰성 없는 네트워크, GC 이야기
데이터 중심 애플리케이션 설계 8장 '분산 시스템의 골칫거리' 에서 GC 이야기가 나온다.
원격 노드가 일시적으로 응답하기를 멈췄지만(가비지 컬렉션 휴지가 길어졌을 수 있다), 나중에는 다시 응답하기 시작할 수 있다(p278).
심지어는 I/O 중단과 GC 중단이 공모해서 지연을 결합하기도 한다(p297).
원문: I/O pauses and GC pauses may even conspire to combine their delays
cf) conspire 는 실제로 공모(직역)했다기 보다 두 가지 독립적인 요소가 서로 의도적으로 협력하는 것처럼(비유적 표현) 보이는 상황을 설명하는 데 사용됐다고 보는게 맞을거 같다. 두 가지 지연 요소가 어떻게 시스템의 전반적인 성능에 더 큰 부정적 영향을 미치는지 강조한다.
이 책에서는 JVM GC 상황만 설명했는데, 사실 GC 는 File System 에서도 하고 SSD 에서도한다. 그래서 각각의 GC 에 대해서 정리해봤다.
먼저 책에서 설명한 JVM GC 때문에 네트워크 지연이 발생하는 문제부터 살펴보자(원문). JVM 옵션에 따라 다양한 타입의 GC 와 JVM 활동들이 GC log 파일들에 로깅되는데, background IO traffic 때문에 GC stop the world(모든 스레드가 일시적으로 멈춤) 시간이 길어질 수 있다.
다시말해 background IO traffic 이 높은 상태에서는, GC logging write() system call 이 OS 에 의해 block 된다.
cf) write() operation 을 asynchronous mode((i.e. buffered IO or non-blocking IO) 로 사용해도 block 된다. 코드를 살펴봐도 파일 변경 사항을 디스크에 즉시 반영하도록 강제하는 system call 인 fsync 는 없었으니 asynchronous 한게 맞는데도 block 된다.
그렇다면 background IO 는 어떤것들이 있을까?
1. OS page cache writeback
OS 메모리 관리 기능 중 하나로, disk I/O 를 최적화하기 위해 일부 메모리를 page cache 로 사용한다. 이 cache 는 disk 에서 읽거나 disk 에 쓸 데이터를 일시적으로 저장한다. page cache 에 저장된 데이터를 dirty data 라고 한다.
2. 관리 및 유지보수 SW
3. 같이 위치한 다른 애플리케이션들의 IO 작업
4. 동일한 JVM instance 의 다른 IO 작업
그런데 왜 cache 된, 다시말해 buffered writes 들이 block 될까? 원문에선 이렇게 설명했다.
We realized that buffered writes could be stuck in kernel code. There are multiple reasons including:
(1) stable page write; and (2) journal committing.
이 문장은 OS kernel 에서 data write 과정이 예상치 못하게 지연될 수 있음을 의미한다. data 를 즉시 disk 에 기록하는 것이 아니라, buffer 에 저장한 후 kernel 이 결정한 적절한 시점에 disk 에 쓰는 과정에서 발생할 수 있는 문제를 나타낸다. '막혀버릴 수 있다'는 표현은 이러한 지연이 장애가 될 수 있음을 강조한다.
Stable Page Write: OS가 page cache 에 있는 data 를 disk 에 안정적으로 쓰기 위한 메커니즘이다. 만약 page 가 이미 OS 에 의해 disk 로 writeback 중이라면, 이 page 에 대한 추가적인 write 작업은 writeback 이 완료될 때까지 기다려야 한다. 이로 인해 buffer 된 write 작업이 지연될 수 있다.
Journal Committing: file system 에서는 file write 작업 중 data 일관성과 무결성을 보장하기 위해 Journal 영역에 data 를 생성하고 커밋(저장)한다. 새로운 data block 이 할당될 때, file system 은 먼저 Journal data 를 disk 에 커밋해야 한다. 이 과정 중에 다른 IO 작업들이 수행되고 있다면, Journal 커밋은 기다려야 하며, 이로 인해 buffer 된 write 작업이 지연될 수 있다.
cf) EXT4 file system 에는 "지연 할당"(delayed allocation)이라는 기능이 있어서 일부 Journal data 를 OS writeback 시간까지 연기할 수 있어 문제를 완화시킬 순 있지만 완전히 해결하지는 못한다.
마지막으로 해결책은 무엇일까?
background IO 작업들을 줄이는 방법도 있고, SSD 를 쓰는 방법도 있다. 그리고 마지막으로 GC 로그 file 을 tmpfs에 두는 방법을 소개한다(i.e: -Xloggc:/tmpfs/gc.log). tmpfs는 disk file 백업이 없으므로, tmpfs file 에 쓰기는 disk 활동을 발생시키지 않으므로 disk IO에 의해 차단되지 않는다. 물론 이 방식도 문제가 있다. crash 가 나면 GC 로그 file 들이 loss 될 수 있다. 이에 대한 해결책은 정기적으로 영구 저장소에 백업하는거다.
다음으로 File System GC 에 대해 이야기해보자. 먼저 전통적인 file system 의 layout 을 보면 아래와 같이 구성되어 있다.
1. B : boot block, OS 가 부팅됐을 때 실행되는 boot code 들이 포함되어 있다.
2. S : super block, file system 의 metadata 가 들어 있다. i.e: block size, inode 갯수 등
3. Inode(Index node 라고도 함) : data block 에 저장된 file 에 대한 description 을 갖고 있다. file data 가 어디에 저장되어 있고 file owner, create time, access time, permission, file size 등등. file 과 1:1 mapping 관계다. file 만들때마다 inode 가 만들어진다.
4. Data blocks : 실제 file 들이 저장되는 공간이다.
cf) B 에 대해 좀 더 엄밀하게 말하면 OS 가 부팅될 때 boot code 가 MBR(Sector 0 of disk is called Master Boot Record) 을 로드해서 수행시키고 MBR 이 boot block 을 로드하는거다.
cf) file system layout 에 대해서 좀 더 엄밀히 말해보면(Linux Ext 기준) 아래 그림과 같이 block group 별로 모아놔서 HDD disk head 움직임을 적게 최적화하고, 중요한 super block 은 여러개 copy 시킨다. 그리고 inode 와 data block 이 사용 여부를 확인하는 bitmap 도 존재한다.
LFS(Log-Structured File System) 는 write 에 특화된 file system 이다.
기존 file system 들은 inode block position 이 고정되어 있다. file 이 write 될 때마다 inode 가 update 되야 하므로(access time 등) sequential 한 write 가 안된다. random I/O 는 HDD disk head 오버헤드가 크다.
sequential 한게 성능이 좋으니 write 만이라도 sequential 하게 할 순 없을까 라는 아이디에서부터 시작됐다. read 는 고려하지 않았다. read 는 DRAM 에 캐시해서 성능을 끌어올릴 수 있다고 봤다. write 가 더 중요하다고 봤다.
하지만 HDD 는 회전하니까 sequential 한 write 가 잘 안됐다. 그래서 write buffering 기법을 써서 in-memory 에 계속 쌓아두고 적절할 때 disk 에 쓰게 했다. 그럼에도 LFS 는 HDD 에서는 그렇게 각광받지는 못했다.
그러나 SSD 에서 대세가 된 F2FS(Flash Friendly File System) 가 LFS 기반이다. 아래 그림을 보면 sequential 한 write 만 하기 때문에 새로운 inode 와 새로운 file 들이 쭉 그대로 쓰여진다.
문제는 file 들이 어딨는지 알려주는 inode 들이 가변적으로 바뀐다. sequential write 니까 inode 들도 fixed 하게 있을 수 없게 됐다. data block 이 바뀌면 그에 mapping 된 inode 들도 수정해줘야하는데 기존 table index 로는 찾을 수가 없게 됐다. 결국 아래 두가지가 중요해졌다.
1. position 이 계속 변하는 걸 어떻게 tracking 할거냐
2. old 한 것들을 어떻게 제거할거냐
inode tracking 에 대한 얘기까지 하자면 너무 길어진다. 이 글의 주제인 GC 와는 달라서 생략하겠다. 간단히 설명하면 imap 이라는 자료구조를 이용해서 inode position 을 follow 한다. 재밌는건 imap 을 disk 맨 앞으로 고정시킨다. LFS 철학과는 맞지 않지만, file 을 찾기 위한 시작점은 있어야하므로 어쩔 수 없다.
다음으로, old 한 것들을 제거하기 위해 GC 가 필요해졌다.
위 그림에선 segment 단위로 사용량을 파악해서 합치고 GC 하는걸 설명했지만 내부적으로는 각 inode 와 data block 마다 state 를 갖게 된다. 새로운 data 들이 추가되서 old 가 되면 state 가 invalid 가 되고, 나중에 invalid 들만 따로 모여 GC 를 수행하고 compaction 한다.
마지막으로 SSD GC 에 대해 이야기해보자. 아래 그래프를 보면 앞으로 2년 뒤엔 HDD 와 SSD 가격이 같아지고 그 후론 더 싸진다. 그래서 앞으로 서버 환경에서 SSD 를 더 많이 쓸 수 있다. 물론 wearout 문제가 있긴하다. wearout 문제는 SSD cell 에 전자를 채웠다 뺐다 계속 반복하면 언젠가 전자가 잘 안채워지는 문제를 뜻한다. 일반 사용자 IT 기기에서는 쉽게 발생하지 않지만, write 작업이 많은 서버 환경에서는 문제가 될 수 있다. cell 이 하나라도 죽으면 SSD 전체가 죽은거다.
SSD GC 를 얘기하기전에 먼저 SSD 동작 방식부터 간단히 살펴보자. 전자를 채우는게 비우는 행위다. 요즘 SSD 는 거의 다 아래 그림과 같은 multi level cell 이다. 0과 1만 있는 single level cell 보다 집접도가 2배지만, single level cell 이 더 빠르고 비싸다. multi level cell 은 error 가 많이 발생해서(전자를 채우는 단위가 많아지니 미묘한 차이에 따른 에러) error collecting code 가 있다보니 좀 더 느리다.
공장에서 처음 나오면 아래와 같이 모두 1로 되어 있는(전자가 채워져 있는) 상태다.
write 를 program operation 이라 하는데, page 단위로 쓴다.
근데 재밌는건 1 에서 0으로 바꾸는건 page 단위로 되는데, 0 에서 1로 바꾸는(지우는)건 block 단위밖에 안된다. 한 page 에 있는 데이터만 지우고 싶어도 block 전체를 1로 다 채워줘야 한다. 결국 남의것까지 지워지게 되니 이 문제는 SW 로 해결한다.
FTL(Flash Translation Layer) 를 이용해서 SW 로 해결한다. FTL 은 논리적 메모리 주소와 물리적 NAND 플래시 메모리 주소를 mapping 한다. 그리고 SSD 안에 있는 SRAM 에 mapping 관계를 저장한다.
cf) FTL 은 이외에도 GC process 를 관리하고 cell 에 쓰이는 횟수를 균등하게 분배하는 기능도 담당한다.
아래 그림에서 논리적 주소 1에 맵핑된 physical data 를 update 하기 하기 위해서 새로운 Free 영역 page 에 write 한다(in place update 하지 않는다).
그리고 기존 page 를 invalid state 로 바꿔 GC 대상이 되게한다. GC 는 SSD 안에서 자체적으로 수행한다. GC 주기는 제조사마다 다르고 공개하지 않는다. GC 가 수행될 동안 read write operation 이 무조건 멈추는건 아니지만 이 또한 제조사 구현 나름이고 공개하지 않는다. 제조사는 OS 제어를 받는걸 원하지 않는다. 솔루션으로 팔길 원하지 단순히 칩만 팔려고 하지 않기 때문에 영업 기밀인 것이다.
마지막 결론 : SSD 는 최대한 용량 큰거로 사야된다(거거익선). 용량 다 채워져가면 GC 많이 발생한다.