모르지 않다는 것은 아는것과 다르다.

Database

PostgreSQL 아키텍처 이해하기

채마스 2023. 12. 8. 20:51

개요

  • 2년 넘게 PostgreSQL을 사용하면서 PostgreSQL의 아키텍처에 대해서 전혀 알지 못했다.
  • RDBMS와 같은 복잡한 시스템에서는 아키텍처의 이해가 성능 최적화와 효율적으로 관리하는데 큰 도움이 된다.
  • 그래서 이번 글에서는 PostgreSQL에 대해서 간단히 정리해보려고 한다.



PostgreSQL 아키텍처

PostgreSQL의 아키텍처는 아래와 같다.

  • PostgreSQL 구조는 물리적인 관점에서 보면, Shared Memory, 프로세스, 데이터 파일(또는 테이블 스페이스)로 구성된다.
  • 공유 메모리의 주요 요소는 Shared Buffer와 WAL 버퍼이다. (Commit Log를 비롯한 나머지 구성요소는 위 그림에서 생략되었다.)
  • 프로세스는 Postgres 프로세스, 백그라운드 프로세스, Backend 프로세스, Client 프로세스로 구분된다.
    • 백그라운드 프로세스는 에러 로그 기록, 버퍼 기록, WAL 버퍼 기록용이 존재하며 autovacuum을 위한 프로세스가 존재한다.
    • Backend 프로세스는 Client가 요청하는 쿼리를 수행하며, 작업 수행을 위한 로컬 메모리 영역이 존재한다.

 

이제 위 그림의 구성요소를 하나하나 정리해 보자

 

Shared Buffer

PostgreSQL에서 Shared Buffer는 여러 프로세스가 공유하는 메모리 공간이다. Shared Buffer는 데이터베이스 서버의 주 메모리(RAM) 내에 할당된 메모리 영역이고 이 영역에는 데이터베이스 테이블과 인덱스에서 필요한 데이터 페이지들이 캐싱된다.

  • Shared Buffer의 목적은 DISK IO를 최소화하는 것이다.
  • 메모리 액세스는 디스크 액세스보다 훨씬 빠르므로, 자주 사용되는 데이터를 메모리에 유지함으로써 전체 시스템의 성능을 크게 향상 시킬 수 있다.
  • Shared Buffer는 데이터의 쓰기 작업도 관리합니다. 변경된 페이지(더티 페이지)는 일정 시간 후에 디스크에 쓰여진다.
    • 이 과정은 백그라운드 프로세스인 Checkpointer와 Background Writer에 의해 수행된다. (백그라운드 프로세스는 아래에서 좀 더 자세히 설명한다.)
  • 정리하면 Shared Buffer는 DISK IO를 최소화하기 위해서 데이터를 캐싱하는 공간이라고 생각하면 된다.



WAL 버퍼

WAL 버퍼는 데이터베이스 변경 사항(예: INSERT, UPDATE, DELETE 연산)을 물리적인 WAL 파일에 기록하기 전에 일시적으로 저장하는 메모리 내 버퍼를 말한다.

  • WAL은 Write Ahead Log의 약자로 먼저 로그를 쓴다라는 의미를 갖는다. 더 정확한 의미는 아래와 같다.
    • 데이터 파일의 변경된 내용은 해당 변경 사항이 로깅된 이후에 쓰여져야만 한다.
  • 데이터베이스 트랜잭션이 발생할 때, 모든 변경 사항은 먼저 WAL 버퍼에 기록되기 때문에 시스템 장애 발생 시에도 변경 사항을 복구할 수 있다.
  • WAL 버퍼를 사용하면 디스크 I/O 작업을 최적화할 수 있다.
    • 변경 사항을 즉시 디스크에 쓰는 대신, 버퍼를 통해 효율적으로 관리하고 배치 작업으로 디스크에 기록된다.
  • 정리하면 WAL 버퍼를 통해서 특정 시점으로 백업 및 복구가 가능하고 성능 최적화도 할 수 있다.



프로세스

PostgreSQL에서는 다양한 종류의 프로세스가 시스템 운영과 관리에 중요한 역할을 한다. 프로세스는 크게 3가지 종류의 프로세스로 구분된다. 현재 실행 중인 프로세스는 아래와 같은 명령어로 확인할 수 있다.

ps -fu postgres

  • 아래 쿼리를 통해서도 현재 동작중인 프로세스를 확인할 수 있다.
select * from pg_stat_activity

 

Postgres 프로세스

  • PostgreSQL 버전 9.3 이전에는 Postmaster라고 불렸다.
  • 하나의 데이터베이스 클러스터는 하나의 Postgres 프로세스에 의해 관리된다.
  • PostgreSQL 서버가 구동될 때 가장 먼저 생성돠어 복구 작업, Shared 메모리 초기화 작업, 백그라운드 프로세스 구동 작업을 수행한다.
  • 클라이언트로부터 커넥션 생성 요청을 받으면, 백엔드 프로세스를 새로 생성해서 새로운 커넥션에 할당한다.

 

Background 프로세스

  • 데이터베이스를 운영하기 위해서는 쿼리를 처리하는 것뿐만 아니라 다양한 부가 작업이 필요하다.
  • PostgreSQL에서는 아래와 같은 백그라운드 프로세스들이 부가 작업을 수행하고 있다.
    • logger: 에러 메시지와 로그를 관리하고, 로그 파일에 기록한다.
    • checkpointer: 체크 포인트 발생 시, dirty 페이지(수정된 메모리 페이지)를 디스크에 기록한다.
    • background writer: dirty 페이지를 주기적으로 디스크에 기록하여 메모리와 디스크 간의 데이터를 동기화한다.
    • walwriter: WAL 버퍼 내용을 WAL 파일에 기록한다. 따라서 데이터베이스의 내구성과 복구에 중요한 역할을 한다.
    • autovacuum launcher: Vacuum이 필요한 시점에 autovacuum worker를 fork 해서 데이터베이스의 공간 재사용과 성능 최적화한다.
    • archiver: WAL 파일을 지정된 위치에 복사하여 백업한다.
    • stats collector: 세션 수행 정보(pg_stat_activity) 와 테이블 사용 통계 정보(pg_stat_all_tables)와 같은 DBMS 사용 통계 정보를 수집한다.

 

Backend 프로세스

  • Backend 프로세스는 자신에게 배정된 커넥션을 통해 요청된 쿼리 또는 명령을 수행하기 위한 일종의 워커 프로세스다.
  • 따라서 Backend 프로세스는 Client 프로세스의 쿼리 요청을 수행한 후, 결과를 전송하는 역할을 수행한다.
  • Backend 프로세스의 최대 개수는 max_connections 파라미터로 설정하며 기본 설정값은 100이다.
  • 만약, 개발할 때 아래와 같이 쿼리를 수행하면 백엔드 프로세스가 생성되는 것을 확인할 수 있다.

  • 위 사진을 보면, 세션을 하나 더 만들어서 쿼리를 수행하니 추가적인 벡엔드 프로세스가 생성된 것을 확인할 수 있다.
  • 또한, 한번 생성된 프로세스는 쿼리를 수행했다고 하더라도 사라지지 않는다.
    • postgresql.conf 파일에서 idle_in_transaction_session_timeout 값을 통해서 'idle' 상태로 유지되는 최대 시간을 지정할 수 있다.
  • 하나의 백엔드 프로세스가 생성될 때 하나의 커넥션을 점유하기 때문에 백엔드 프로세스가 많다는 건 사용중인 커넥션 수도 많다는 의미가 된다.
  • 현재 사용중인 커넥션 수 유추할 수 있는 쿼리는 아래와 같다.
-- 현재 사용중인 컨넥션 수
select count(*) - (select count(*) from pg_stat_activity where backend_type = 'client backend') as active_connections
from pg_stat_activity;

-- 사용가능한 커넥션 수
select (select setting::int from pg_settings where name = 'max_connections') -
       (select count(*) - (select count(*) from pg_stat_activity where backend_type = 'client backend') as active_connections
        from pg_stat_activity) as remaining_connections;
  • 백엔드 프로세스는 backend_type이 client backend이기 때문에 위와 같이 현재 동작중인 백엔드 프로세스의 수를 확인할 수 있다.
  • 만약 max_connections값 보다 더 많은 백엔드 프로세스를 생성하려고 하면 아래와 같은 오류 메시지를 뱉는다.

  • 만약, 실무에서 DataGrip으로 개발한다고 가정했을 때, 위와 같이 무작정 여러개의 세션을 생성하면 백엔드 프로세스가 과도하게 생성될 수 있으니 주의하자.
  • 백엔드 프로세스는 쿼리를 수행하기 위해서 몇 가지 메모리 구조가 필요한데 이것을 통칭해서 로컬 메모리라고 한다.
  • 백엔드 프로세스 안에는 로컬 메모리가 존재한다. 로컬 메모리의 구성요소는 아래와 같다.

  • Work Memory
    • 정렬 작업, Bitmap 작업, 해시 조인과 Merge 조인 작업 시에 사용되는 공간이다.
    • 기본 설정은 4MiB이다.
    • postgresql.conf의 work_mem값으로 설정 가능하다.
  • Maintenance Work Memory
    • 데이터베이스 유지 관리 작업(vacuum, 인덱스 생성, 테이블 변경, 외래키 추가)에 사용되는 메모리 공간이다.
    • 기본 설정은 64 MiB이다.
    • postgresql.conf의 maintenance_work_mem값으로 설정 가능하다.
  • Temp Buffers
    • Temporary 테이블을 저장하기 위한 공간이다.
    • 기본 설정값은 8 MiB이다.
    • postgresql.conf의 temp_buffers값으로 설정 가능하다.
  • Catalog Cache
    • 데이터베이스의 카탈로그 정보를 캐싱하는 데 사용된다.
    • 조회하는 빈도가 높기 때문에 디스크에서 읽을 경우 성능 저하가 발생할 수 있어 로컬 메모리 영역을 사용된다.
  • Optimizer & Executor
    • 쿼리를 수행할 최적 플랜을 찾는 과정에서 사용하는 메모리 영역이다.

 

PostgreSQL의 아키텍처를 제대로 이해하기 위해서는 'Vacuum' 프로세스에 대해서 알아야 한다.

 

Vacuum

  • 진공청소기라는 뜻을 가진 Vaccum은 PostgreSQL에만 존재하는 특별한 개념이다.
  • PostgreSQL에서 MVCC의 구현 방법상의 특징상 공간 비효율이 발생하기 때문에 Vacuum은 필수적으로 필요하다.
  • Vacuum의 역할은 크게 아래 4가지이다.
  1. 테이블 및 인덱스 Dead 블록 제거를 통한 디스크 공간 확보
    • PostgreSQL에서 행(또는 투플)이 삭제되거나 업데이트될 때, 그 행은 즉시 제거되지 않고 'dead' 상태가 된다.
    • VACUUM은 이러한 dead 투플을 정리하여 FSM (Free Space Map)으로 반환 공간을 반환한다.
  2. Transaction ID Wraparound 방지를 위한 레코드별 XID Frozen
    • PostgreSQL은 4바이트 정수를 사용하여 트랜잭션 ID를 관리한다.
    • 이 정수가 가득 차면, 'transaction ID wraparound' 문제가 발생할 수 있다.
    • VACUUM은 오래된 트랜잭션 ID를 정리하여 이러한 문제를 방지한다.
  3. 테이블 및 인덱스 통계 정보 갱신
    • VACUUM은 테이블과 인덱스의 사용 패턴에 대한 통계를 갱신합니다. 이 정보는 쿼리 플래너가 더 효율적인 실행 계획을 세우는 데 사용한다.
  4. Visibility Map(VM)을 갱신
    • Visibility Map은 각 데이터 페이지에 대한 "visibility" 상태를 추적한다.
    • 여기서 "visible"이란 특정 페이지가 모든 활성 트랜잭션에게 보이는 상태, 즉 더 이상 변경되지 않을 것이 확정된 상태를 의미한다.
    • VACUUM은 이 맵을 갱신하여, 인덱스 스캔이 필요한 페이지만 읽도록 함으로써 성능을 향상시킨다.



References