개발놀이터

데이터베이스 쿼리를 실행하면 내부적으로는 어떤 일이 벌어질까? 본문

CS 지식/운영체제

데이터베이스 쿼리를 실행하면 내부적으로는 어떤 일이 벌어질까?

마늘냄새폴폴 2024. 10. 27. 23:30

오랜만에 포스팅을 쓰는 것 같습니다. 거의 열흘만인 것 같은데 요즘 쿠버네티스를 공부하느라고 실습을 위주로 공부하고 있느라 공부할 시간이 마땅히 나지 않았네요... 

 

이번 포스팅은 평소에도 너무나도 궁금했던 데이터베이스의 연산과 그 속사정입니다. 이번 포스팅에선 OS레벨에서의 속사정과 데이터베이스 내부의 속사정 두가지 관점에서 톺아보도록 하겠습니다. 

 

쿼리를 실행하면 OS레벨에선 어떤 일이 벌어질까?

우리는 아래와 같은 쿼리를 날렸습니다. 

 

SELECT * FROM USER u LEFT JOIN ORDER o ON u.user_id = o.user_id WHERE u.user_id = 20;

 

그럼 데이터가 어떻게 우리 눈에 보일 수 있게 될까요? 

 

이 질문이 머리속에서 떠오르고 난 뒤 곰곰히 생각해봤을 때 정말 알 수 없었습니다. 

 

"그러게..? 어떻게 가져오는거지?"

 

데이터베이스는 한낱 소프트웨어... 하지만 실제 저장되어 있는 데이터는 하드웨어에 저장되어있을 터.. 분명 OS랑 짜고치고 내가 모르는 일이 뒤에서 벌어진다고 생각했죠. 

 

이 부분을 깊이있게 공부해봤습니다. 

 

1. System Call 호출

소프트웨어인 데이터베이스는 컴퓨터의 하드웨어에 접근하기 위해 System Call을 호출합니다. 그럼 이 System Call이라는 놈을 분석해볼 필요가 있죠. 

 

System Call이란?

System Call은 일반적인 방법으로는 접근할 수 없는 OS 자원들 (하드웨어나 파일 시스템 등) 에 접근하는 방법을 제공해줍니다. 듣기에도 매우 강력한 기능이기에 보안과 안정성이 매우 중요한데요. 이 부분은 뒤에서 더 자세히 다루도록 하겠습니다. 

 

System Call은 애플리케이션이 로우 레벨 동작들 (예를 들면 파일 접근, 프로세스 할당, 메모리 할당 등) 을 하드웨어와 직접적인 소통하지 않고 추상화한 계층입니다. 

 

System Call의 동작 원리

System Call이 호출되면 CPU를 OS의 유저모드에서 커널모드로 변경시킵니다. 이 전환이 필요한 이유는 본질적으로 프로그램에게 시스템 자원에 접근할 수 있는 권한을 부여하기 위해서입니다. 

 

CPU가 커널모드에 들어가게 되면 이제부터 시스템 자원에 접근할 수 있게 되는데 첫 번째로 System Call 테이블에 접근해서 어떤 시스템과 연결되어야 하는지 확인하고 그에 상응하는 핸들러를 호출하게 됩니다. 이 작업을 통해 시스템의 자원에 본격적으로 접근할 수 있게 됩니다. 

 

System Call에 대한 요청이 모두 마무리 되고 나면 CPU를 다시 유저모드로 변경해서 다시 원래 상태로 돌려보냅니다. 

 

System Call의 존재의의

왜 OS는 애플리케이션이 시스템 자원에 직접적으로 접근하는 것 대신 System Call이라는 중간 단계를 넣어놓은 것일까요? 

 

이에는 두가지 이유가 있습니다. 

 

첫 번째로, 이 처럼 시스템 자원에 접근해서 실행하는 동작들은 로우레벨의 동작들이기 때문에 개발자가 애플리케이션을 개발함에 있어서 큰 난관이 될 수 있습니다. 때문에 추상화를 통해 애플리케이션이 OS에 접근해서 시스템 자원을 사용해야하는 경우 쉽게 시스템 자원에 접근할 수 있도록 하기 위함입니다. 

 

두 번째 이유가 제일 중요한데요, 애플리케이션이 민감한 자원에 접근할 때 (메모리에 접근하는 등) 절대 실수하면 안되기 때문입니다. 특히 메모리의 경우 애플리케이션은 각각 다른 메모리 자원에 접근할 수 없고 시스템의 안정성과 보안을 위해 직접적으로 하드웨어를 조작할 수 없기 때문입니다. 

 

때문에 OS 커널은 권한이 다른 두가지 모드 (유저모드, 커널모드) 를 사용함으로써 System Call이 시스템의 민감한 자원에 접근하고 통제할 수 있도록 비인가 접근을 막는 것입니다. 

 

System Call을 사용할 때의 고려사항

각각의 System Call은 유저모드와 커널모드를 오가면서 컨텍스트 스위칭 비용이 발생하게 됩니다. 이것은 오버헤드를 불러일으키고 이를 최소화하기 위해서는 최소한의 System Call만을 호출해야합니다. 

 

또한, 컨텍스트 스위칭 비용 뿐만 아니라 System Call 그 자체에도 오버헤드가 존재하기 때문에 System Call을 남발하면 프로그램 성능에 악영향을 미칠 수 있습니다. 

 

 

뒤에서 언급할 내용이지만 System Call이 이런 오버헤드를 가지고 있기 때문에 프로그램의 성능을 위해서는 반드시 최소한의 System Call을 요구하고 이를 위해 데이터베이스는 다양한 장치들을 두고 있습니다. 

 

특히 데이터베이스에서 가장 많이 호출되는 System Call로 예상되는 read()와 write() (읽기와 쓰기) 를 최소화하는 것이 데이터베이스의 성능에 직접적인 영향을 주는 것이죠. 

 

이 내용에 대해서는 조금 뒤 언급할 데이터베이스의 속사정에서 확인해보도록 하겠습니다. 

 

2. VFS 계층

VFS는 Virtual File System의 약자로서 OS와 파일시스템 사이에서 애플리케이션이 다양한 파일시스템들을 사용할 때 스토리지 타입의 변경 없이 파일시스템을 동작할 수 있도록 추상화한 계층입니다. 

 

만약 애플리케이션이 System Call을 호출하면 VFS는 이 System Call을 중간에서 인터셉트하고 사용하려는 파일시스템에 일치하는 동작으로 재해석해서 파일시스템에 접근합니다. 

 

VFS를 지나면 파일시스템에 접근해서 디스크에 적혀있는 데이터 덩어리의 정확한 위치와 파일시스템 (리눅스의 경우 inodes, 윈도우의 경우 file allocation tables) 을 매핑하면서 파일에 저장되어있는 위치를 가져옵니다. 

 

3. I/O 스케줄러

VFS를 지난 데이터베이스 쿼리는 OS에 의해 디스크 I/O를 최적화하기 위해 스케줄러를 편성시키는데 이 스케줄러는 스토리지 디바이스 (HDD나 SSD) 에 효율적으로 접근할 수 있도록 도와줍니다. 

 

예를 들어서 HDD의 경우 헤드가 움직이면서 데이터를 가져와야 하기 때문에 서로 멀리 떨어져 있는 데이터를 가져오기 위해 많은 물리적인 이동이 있어야하고 이 때문에 성능이 느려지기도 합니다. (이렇게 헤드가 움직이면서 파일시스템의 read/write를 실행시키는 것을 seek라고 합니다.)

 

이런 성능 이슈를 해결하기 위해 스케줄러는 물리적으로 가까이 붙어있는 데이터를 뭉탱이로 가져오는 것이 성능상 이점이기에 가까이 붙어있는 데이터를 위주로 갖오게 됩니다. 

 

하지만 SSD의 경우 전기신호로 데이터를 가져오기 때문에 seek에 해당하는 움직임이 없어 이런 스케줄러가 필요없을 것 같지만 SSD에서도 성능이 향상되는 부분이 있다고는 하네요. 

 

4. 스토리지 디바이스에 접근

스케줄러는 스케줄링을 통해 스토리지 디바이스에 접근하고 최종적으로 HDD/SSD에 접근하게 됩니다. 만약 데이터가 파일시스템의 캐시에 존재하지 않는다면 OS는 스토리지 컨트롤러에게 물리적인 저장소로부터 데이터를 가져오라고 명령합니다. 

 

이 때 HDD와 SSD의 동작이 조금 다릅니다. 

 

HDD : HDD의 경우 스토리지 컨트롤러가 디스크의 헤드를 움직여서 실제 데이터가 위치한 곳으로 이동하도록 하고 버퍼에 이 데이터를 적재합니다. 여기서 버퍼란 애플리케이션에 의해 접근되는 디스크의 데이터를 OS가 임시로 저장해놓는 공간입니다. 

SSD : SSD의 경우 스토리지 컨트롤러에게 정확한 셀의 위치로 접근하게 하는 전기적인 신호를 보내고 이 데이터를 버퍼에 담아냅니다. 

 

이 과정 속에서 디스크와 메모리 (버퍼) 사이에서 데이터를 전송하는 것은 Direct Memory Access에 의해 관리되는데 이 DMA라는 것은 데이터를 가져올 때마다 CPU에게 일일히 명령받지 않아도 효율적으로 데이터를 가져올 수 있도록 해주는 개념입니다. 

 

5. 데이터베이스에게 전달

이렇게 버퍼에 담겨져 있는 데이터를 OS는 데이터베이스에게 System Call을 이용해서 건네줍니다. 만약 데이터가 한번에 전달하기에 너무 큰 경우 OS는 모든 요청된 데이터를 전달해주기 위해 여러번 읽기 작업을 진행하기도 합니다. 

 

데이터를 데이터베이스에게 건네준 뒤로는 데이터베이스 엔진이 where 조건이나 join 연산등을 실행하게 되죠. 

 

 

여기까지가 OS레벨에서의 데이터베이스 쿼리 실행이었습니다. 이제 이 가져온 데이터를 어떻게 데이터베이스가 활용하는지 정리해봤습니다!

 

 

쿼리를 실행하면 데이터베이스 내부에선 어떤 일이 벌어질까?

0. 사전지식

  • Page (페이지) : 페이지는 데이터베이스의 데이터가 행단위로 저장되어 있는 데이터베이스의 가장 작은 단위입니다. 보통 16KB로 되어있습니다. 
  • Buffer Pool : 자주 사용되는 쿼리를 메모리에 올려두고 캐시처럼 사용하는 공간입니다. 

1. Buffer Pool 확인

쿼리가 실행되고 데이터가 이미 버퍼에 담겨져 있는 것인지 확인하는 작업이 들어갑니다. 만약 이미 버퍼가 가지고 있는 데이터라면 그 정보를 바로 메모리에서 가져와서 성능적으로 이점을 볼 수 있습니다. 

 

2. 쿼리에 인덱스가 포함되어 있는지 확인

만약 쿼리에 인덱스가 포함되어 있다면 스토리지 엔진은 연관된 데이터 페이지를 통해서 찾아냅니다. 이 작업은 디스크 I/O를 최소한으로 실행하여 성능을 최적화시킵니다. 

 

인덱스 알고리즘으로 B- tree나 hash index와 같은 알고리즘이 사용되어 불필요한 데이터를 조회하기 않고 효율적으로 데이터를 조회할 수 있게 됩니다. 

 

3. 행 단위로 락을 걸고 동시성 컨트롤을 진행한다.

여러개의 쿼리가 동시에 데이터를 요청한다면 스토리지 엔진은 행에 락을 걸어서 동시성을 보장합니다. 동시성 컨트롤의 대표적인 방법론은 MVCC로 스토리지 엔진별 다양한 방법으로 MVCC를 구현하고 있습니다. 

 

MySQL의 InnoDB 스토리지 엔진은 Undo 테이블을 이용해서 MVCC를 구현하였고 PostgreSQL은 Xmin (가장 최근에 접근한 트랜잭션 ID), Xmax (가장 마지막에 접근한 트랜잭션 ID) 를 이용해 Vaccum 이라는 개념으로 MVCC를 구현했습니다. 

 

이 MVCC에 대한 자세한 내용은 아래의 링크에 잘 정리되어있습니다!

 

https://coding-review.tistory.com/301

 

MVCC (Multiversion Concurrency Control)

오늘 포스팅에서는 흔히 Read Committed 격리 수준에서 생기는 부정합 문제인 Non-Repeatable Read 문제를 해결하기 위해 사용하는 방법이라고 알려져있는 MVCC에 대해서 알아보도록 하겠습니다. 이 포스

coding-review.tistory.com

 

또한, MySQL의 경우 gap lock과 record lock을 조합한 next key lock을 이용해서 행간 락을 이용해 동시성 컨트롤을 진행합니다. 

 

4. 데이터베이스 조회 및 필터링

스토리지 엔진이 연관된 데이터 페이지를 찾으면 그 행에 매칭되는 데이터를 가져옵니다. 만약 쿼리가 where 조건을 포함하고 있다면 스토리지 엔진은 초기 데이터에서 필터링을 거쳐서 데이터를 리턴해줍니다. 

 

5. 트랜잭션 로그

만약 쿼리가 트랜잭션을 포함하고 있다면 스토리지 엔진은 먼저 메모리에 있는 데이터를 업데이트하고 recovery 프로세스를 위해 redo 로그를 작성합니다. 

 

redo 로그는 데이터 정합성을 지키기 위해 만약 트랜잭션 두개가 충돌하는 경우 재실행하기위해 사용됩니다. 이와 비슷하게 undo 로그는 롤백과 MVCC를 위해 데이터를 스냅샷으로 찍어두고 유지, 관리합니다. 

 

즉, redo로그는 트랜잭션이 충돌하는 경우 재실행하기 위해, undo로그는 트랜잭션이 충돌하는 경우 롤백을 위해 사용되는 로그입니다. 

 

6. 디스크에 데이터 쓰기

만약 쿼리가 쓰기 연산이라면 메모리에 적혀있는 데이터 (반영되기 전의 데이터 : Buffer Pool에 있는 데이터) 는 곧장 디스크에 데이터를 쓰지 않습니다. 

 

스토리지 엔진은 디스크 I/O를 최소화하여 성능을 향상하기 위해 텀을 두고 flush연산을 진행합니다. 

 

이 과정을 체크포인팅 연산이라고 하며 체크포닝트를 두고 이 텀동안은 디스크에 쓰기 작업을 진행하지 않습니다. 이 체크포인터는 트랜잭션 롤백 상황에서 롤백을 위해 사용되기도 합니다. 

 

체크포인터에 대한 자세한 설명은 아래의 링크에 정리되어 있습니다!

 

https://coding-review.tistory.com/539

 

PostgreSQL이 WAL (Write Ahead Log) 를 활용하는 방법

이번 포스팅은 PostgreSQL의 로드맵인 아래의 링크에서 영감을 받았습니다.  https://roadmap.sh/postgresql-dba DBA Roadmap: Learn to become a database administrator with PostgreSQLCommunity driven, articles, resources, guides, inter

coding-review.tistory.com

 

 

마치며

이번 포스팅은 데이터베이스 쿼리가 실행될 때 OS레벨에서 어떤 일이 벌어지는지 궁금해서 공부하고 정리한 포스팅입니다. 

 

이로 인해 데이터베이스가 저도 모르게 뒤에서 무슨 짓을 하고 있는지 속 시원하게 까발려졌습니다. 데이터베이스 쿼리를 날릴 때마다 오늘 공부한 내용을 상기시키면서 날려야겠습니다. 

 

긴 글 읽어주셔서 감사합니다. 오늘도 즐거운 하루 되세요!