들어가며
관계형 DB를 다뤄봤다면 이런 상황이 익숙할 겁니다.
대량 UPDATE가 도는 동안 조회 쿼리가 느려지거나 대기한다.
그런데 OpenSearch는 초당 수만 건을 색인하는 중에도 검색이 멀쩡히 나갑니다. 색인과 검색이 서로를 거의 방해하지 않아요.
비결은 정교한 락 설계가 아닙니다. 정반대예요.
애초에 락을 걸 일이 없게 만들었습니다.
이번 글의 범위
- 다루는 것: 불변성과 락의 관계, tombstone 삭제, merge의 진짜 역할, 스냅샷 읽기
- 다루지 않는 것: refresh 주기와 NRT(5편), 복제 시의 동시성(6편)
락은 왜 필요한가
먼저 락이 왜 필요한지부터 봅시다. 이유는 하나입니다.
누군가 데이터를 바꾸는 중인데 다른 사람이 그걸 읽으면, 반쯤 바뀐 상태를 볼 수 있으니까.
칠판에 적힌 글씨를 고치는 상황을 떠올려 보세요. 지우고 다시 쓰는 중간에 누가 보면 뜻이 깨진 문장을 읽게 됩니다. 그래서 “고치는 동안 보지 마세요”라고 막아야 하죠.
그런데 아무도 안 고친다면? 막을 이유가 없습니다.
Lucene은 오른쪽 길을 골랐습니다. 세그먼트를 절대 수정하지 않기로 한 겁니다.
변경이 아니라 추가
2편에서 봤듯, 역색인은 사전순 정렬을 유지해야 해서 중간 삽입이 매우 비쌉니다. 그래서 기존 세그먼트를 고치는 대신 새 세그먼트를 하나 더 만듭니다.
색인 전: _0 [doc1, doc2]
↓ 새 문서 doc3 추가
색인 후: _0 [doc1, doc2] ← 그대로. 한 글자도 안 바뀜
_1 [doc3] ← 새로 생김_0은 한 글자도 바뀌지 않았습니다. 그저 _1이 새로 생겼을 뿐이죠.
이것이 결정적입니다. 지금 _0을 읽고 있는 검색이 있어도 아무 문제가 없어요. 읽는 대상이 변하지 않으니까요. 락 없이 동시에 읽어도 안전합니다.
“추가”는 왜 안전한가?
새 세그먼트가 만들어지는 동안, 그 파일은 아직 “유효한 세그먼트 목록”에 등록되지 않은 상태입니다. 완성된 뒤에야 목록에 원자적으로 추가되죠.
그래서 검색은 “완성 안 된 세그먼트”를 볼 일이 없습니다. 보이거나(완성 후) 안 보이거나(완성 전) 둘 중 하나이지, 반쯤 만들어진 걸 보는 경우는 없어요.
그럼 삭제와 수정은 어떻게 하나
여기서 자연스러운 의문이 생깁니다.
추가는 그렇다 치고, 삭제는? 파일을 못 고치는데?
정답은 “안 지웁니다” 입니다.
삭제 = 죽었다고 표시만 하기
문서를 삭제하면 세그먼트 파일은 그대로 두고, 옆에 “이 문서는 죽었다”는 표시만 남깁니다. 이 표시를 tombstone(묘비) 라고 부릅니다.
_0.cfs [doc1, doc2, doc3] ← 파일은 그대로
_0_1.liv doc2 = 삭제됨 ← 묘비 파일검색할 때는 세그먼트를 읽으면서 묘비에 적힌 문서를 결과에서 빼버립니다. 사용자 눈에는 사라진 것처럼 보이지만, 디스크에는 그대로 남아있어요.
수정 = 삭제 + 추가
사실 Lucene에는 “수정”이라는 개념이 없습니다.
문서 수정 = 기존 문서에 묘비를 세운다 + 수정된 내용을 새 문서로 추가한다OpenSearch의 _update API도 내부적으로는 이걸 합니다. 겉보기엔 수정이지만 실제로는 삭제 후 재색인이에요.
그래서 문서를 지워도 디스크 사용량이 즉시 줄지 않습니다.
“1000만 건 삭제했는데 왜 용량이 그대로죠?”라는 질문의 답이 여기 있습니다. 묘비만 세워졌을 뿐 실제 데이터는 남아있으니까요.