블로그

썰프티어) 카테시안 곱: 단 하나의 SELECT문이 불러온 재앙

등록일
2024-01-03 13:55:38
조회수
1,161


🚀 소프티어 2.0을 한창 개발하던 어느 날이었습니다. 🚀


여느 때와 다름없는 일상 속에 갑자기 개발 서버에 DB 경보 가 떴고, 그 내용은 너무나도 낯선 "저장 공간 부족".


모니터링 콘솔을 확인해 보니 150 MB/s 라는 무서운 속도로 disk write가 일어나고 있었고,

당황한 저희 팀이 손을 써보기도 전에, 10여 분 만에 평소 5%도 채 사용되지 않았던 저희 DB는 가득 차버렸고 DB 읽기 및 쓰기를 요하는 모든 기능이 마비되었습니다.


DB가 마비되어 모든 개발은 중지되었고, 불행 중 다행으로 DB 재시작 이후 저장공간과 서비스는 정상화되었습니다.


하지만 사건의 원인을 파악하지 못한 이상 언제든 재발할 수 있는 문제였기에, 저희는 불안감 속에서 원인 찾기에 나섰습니다.




🤔 가능한 원인들 🤔


1. 누군가의 공격?

제일 처음 떠오른 생각이었습니다. 하지만 서버 모니터링 솔루션을 통해 확인한 바로 특이하거나 반복되는 트랜잭션은 확인되지 않았습니다.

(모두가 좋아하는 소프티어를 공격할 이유도 없고요!)


2. 코딩 실수 또는 많은 요청으로 인한 DB용량 소모?

아닐 것 같았습니다. 그도 그럴 게 제아무리 많은 write 요청을 보내더라도, 그 짧은 시간 안에 String 만으로 DB 용량을 모두 채우려면(파일 등은 S3에 저장되므로) DB가 차기 전에 요청을 보내는 서버가 터졌을 것이라 생각했습니다.

결정적으로 재시작 이후 복원된 DB는 데이터 손실이 없었음에도, 사건이 일어나기 이전, 즉 5% 미만이 사용된 상태였습니다.


그렇다면 저희 DB를 가득 채웠던 그 많은 데이터는 어디로 갔던 걸까요?



저희는 그 데이터가 모두 휘발성 데이터, 그중에서도 Temporary Table 이었다고 잠정적으로 결론 내렸습니다.


임시 테이블(Temporary Table)은 보통 빠른 액세스를 위해 메모리 상에서 만들어집니다.

하지만 몇 가지 경우에는 디스크에 쓰여지기도 합니다.




🧐 임시 테이블의 디스크 생성 원인 🧐


1. 임시 테이블이 굉장히 큰 경우

메모리에 담을 수 없을 만큼 큰 임시 테이블을 요구하는 경우, 데이터베이스 시스템은 임시 테이블을 디스크에 생성합니다.


2. DB 설정

어떤 데이터베이스들은 임시 테이블을 디스크에 쓰도록 설정하기도 합니다. 복잡하고 큰 용량을 요구하는 계산을 처리하기 위해서죠.


3. 세션 간 지속성이 필요한 경우

임시 테이블은 주로 일시적인 데이터에 사용되지만, 일부 데이터베이스 시스템은 사용자 세션 동안 지속되는 세션 별 임시 테이블을 지원합니다.

세션이 장시간 지속되거나 시스템이 메모리를 확보해야 할 필요가 있을 때, 이러한 임시 테이블의 내용은 디스크에 쓰여질 수 있습니다.


저희 DB에는 2번 과 같은 설정을 하지 않았었고,

3번 의 경우 DB용량을 모두 채울 정도의 많은 세션을 유지하지는 않았을 것 같았습니다. (소프티어 SQL 문제 사용자가 엄청 많아진다면 언젠가는?!)

때문에 저희는 1번 가능성에 더 집중했습니다.




🧩 복잡한 쿼리의 파급효과: Cartesian Product 🧩


그렇다면 매우 엄청나게 큰 임시 테이블을 요구하는 경우는 무엇일까요?


'매우 엄청나게 큰' 에서 가장 먼저 떠오른 것은 바로 Cartesian Product , 즉 곱집합 이었습니다.


쿼리에서 카테시안 곱 은 FROM 절에 두 개 이상의 테이블이 있고, join 조건이 주어지지 않을 경우 발생할 수 있는 현상입니다.


다음의 쿼리를 예시로 들면,


SELECT * FROM A JOIN B 혹은 SELECT * FROM A, B



위와 같은 경우, 명확한 join 조건이 주어지지 않았기에 결과는 A x B , 즉 A 의 모든 행에 B 의 모든 행이 결합된 행들이 됩니다.

만약 A 테이블에 5개의 행, B 테이블에 6개의 행이 존재한다면, 해당 쿼리는 30개의 행을 반환합니다.


하지만 실제 운영 중인 DB에서 한 테이블에 몇 천, 몇 만 개의 행이 쌓이는 건 흔한 일입니다.

게다가 여기에 테이블 두 개가 아니라 세 개, 네 개씩 불러오는 복잡한 쿼리였다면 훨씬 더 큰 결과를 불러오겠죠.


만약 각 테이블에 5,000행의 데이터가 들어있는 테이블 세 가지를 곱집합 쿼리를 통해 불러온다면 어떻게 될까요? 계산의 편의를 위해 한 행을 1byte라 두면


5,000 x 5,000 x 5,000 = 125,000,000,000 byte


무려 125GBResult Set 가 나오게 됩니다.

(DB에 큰 타격을 주기 충분한 크기이죠!)



😱 갑작스러운 깨달음과 실험의 시작 😱


이야기가 여기까지 진행됐을 때, 저희 팀원 중 한 명은 오한을 느끼게 됩니다.

왜냐하면 예시로 든 쿼리가 사건이 터지는 당시에 본인이 실험하고 있던 매우 복잡한 쿼리와 유사하다고 느꼈기 때문입니다.


하지만 해당 팀원이 해당 쿼리에 대한 내용을 말할 때까지만 해도 저희 팀 모두 "설마~" 하는 마음이었습니다.

로컬 환경에서 실행된 단 하나의 쿼리, 그것도 SELECT문이 전체 시스템을 마비시킬 정도의 영향을 불러왔다는 것이 이론적으로는 가능해도 상식적으로 받아들여지지 않았기 때문입니다.


저희 팀은 바로 해당 쿼리 분석에 들어갔고, 해당 쿼리에 사용된 3개 테이블을 식별했습니다.

계산기를 두드려보니 사이즈는 얼추 소프티어 DB를 폭파시키고도 남을 정도였습니다.


남은 건 재현 뿐.. 상남자 소프티어 개발진은 해당 쿼리를 직접 실행시키기로 합니다 .

계획은 문제가 되는 쿼리를 실행해 DB 디스크 용량이 줄어드는 것이 확인되면 바로 해당 프로세스를 kill하는 것이었습니다.

이론상 DB용량이 가득 차기 전에 프로세스를 중단시키기만 하면 서비스에는 영향이 없을 것이라는 결론이었습니다.


떨리는 마음으로 쿼리를 실행했고(딸깍), DB 모니터링을 시작했습니다.

CPU사용량이 튀고, writing to net 프로세스가 오랜동안 실행됐지만, 디스크 용량에는 별다른 변화가 보이진 않았습니다.

10여 분간 실행 뒤 별다른 수확이 없자 프로세스를 종료시켰고, 팀원A는 안도의 한숨을 쉬었습니다.


단순 곱집합을 SELECT하는 경우 그저 순서대로 테이블의 데이터를 불러올 뿐 별다른 계산이 요구되지 않았기 때문에,

데이터베이스 시스템이 똑똑하게도 실행 결과를 버퍼링하며 스트림 한 것이었습니다.

때문에 매우 큰 결과를 불러옴에도, 메모리 이슈 없이 그저 일정한 속도로 끊임없이 client에게 결과를 보내줬던 것이죠.


하지만 실험은 아직 끝나지 않았습니다.

문제의 쿼리는 단순 곱집합 SELECT문이 아니라, 복잡한 group by, order by 절 등 추가적인 계산을 요하는 요소들이 잔뜩 있었기 때문입니다.

그저 순서대로 불러올 뿐인 SELECT와는 다르게, sorting을 요하는 계산 요소가 들어간다면 모든 데이터가 메모리 혹은 디스크에 쓰인 후에야 결과를 낼 수 있겠죠. (이는 paging을 해도 마찬가지입니다!)


바로 다음 실험이 강행되었고, 팀원A는 다시금 식은땀을 흘렸습니다. 이번에는 order by 절이 추가된 곱집합 쿼리였습니다.

이쯤 와서는 모두들(한 명 빼고) 오히려 터졌으면 좋겠다는 마음가짐이었고, 쿨하게 쿼리 실행을 눌렀습니다. (딸깍2)


~긴장되는 순간이 흐르고~


"어!? 어!!" 금지된 감탄사와 함께 관측된 것은 바로 빠르게 줄어드는 DB 용량! 무려 200 MB/s 의 속도로 DB용량이 줄어들기 시작했습니다. (이전보다 속도가 빠른 이유는 몇 가지 group by 절 등의 계산 요소가 빠지면서 disk write 속도가 더 빨라진 것으로 예상됩니다.)


바로 프로세스를 kill 했고, 다행히 DB 용량은 조금 더 줄어들다가 바닥나기 전 빠른 속도로 원상복구되었습니다.

모두 원인을 찾는데 고생했기에 쾌재를 불렀고, 팀원A 도 손들고 벌을 서며 기뻐하는 눈치였습니다. (🎉 happy ending 🎉)




📚 모든 사고에는 교훈이 있다 (Dare to Fail) 📚


이 사건을 통해 저희는 단순한 쿼리 하나가 데이터베이스 시스템에 얼마나 큰 영향을 미칠 수 있는지 실감했습니다.


또한, 복잡한 쿼리 작성 시 주의 깊게 접근해야 할 필요성을 깨달았습니다. 이는 특히 대용량 데이터를 다루는 환경에서 더욱 중요합니다.

쿼리 성능 분석 및 최적화는 저희의 주요 관심사가 되었고, 이는 앞으로의 개발 프로세스에 큰 영향을 미칠 것입니다.


마지막으로, 이 사건은 우리 모두에게 기술적 겸손함을 일깨워줬습니다.

아무리 경험이 풍부한 개발자라도 예상치 못한 결과에 직면할 수 있으며, 이는 항상 새로운 학습의 기회가 될 것입니다.


앞으로도 🚀소프티어 개발팀🚀 은 지속적인 학습과 성장을 통해 더 나은 소프트웨어 개발자가 되기 위해 노력할 것입니다!

최신 블로그