PostgreSQL의 서버 프로세스의 처리 내용이나 클라이언트/서버 간 통신, 쿼리 실행 흐름, 트랜잭션 제어에 대해 이해하면, PostgreSQL을 블랙박스로 취급하는 것이 아니라, 내부 처리를 생각한 애플리케이션을 설계할 수 있게 됩니다. 또한, 만일 문제가 발생했다고 해도, 원인을 분석하기 쉬워질 것입니다.
「 서버 프로세스의 역할 」
PostgreSQL에서는 데이터베이스 관리 시스템으로 필요한 동작을, 복수의 서버 프로세스와 프로세스 간에 공유하는 리소스에 의해 제어하고 있습니다.

그럼, 각 프로세스가 어떤 처리를 하고 있는지 살펴보도록 하겠습니다.
【 마스터 서버 프로세스 】
마스터 서버는 PostgreSQL을 제어하는 다양한 프로세스(백그라운드 프로세스)를 fork()하여 기동 하는 부모 프로세스입니다. 또한, 외부로부터의 접속을 받아, 접속에 대응하는 프로세스(백엔드 프로세스)를 fork()하여 기동 합니다.
마스터 서버 프로세스 이외의 프로세스는 모두 자식 프로세스로 작동합니다.
【 라이터 】
공유 버퍼 내의 업데이트된 페이지를, 해당 데이터 파일의 블록에 내보내는 프로세스입니다.
라이터 프로세스에 의한 데이터 파일로 내려쓰는 작업은, 클라이언트에서 발행되는 쿼리의 실행을 저해하는 것은 아니지만, 시스템 전체로 I/0량이 증가하여, 쿼리의 응답에 큰 영향을 미칠 수 있습니다. 이 때문에, 라이터 프로세스의 설정에 의해, 내려쓰는 작업을 지연시켜 응답에 미치는 영향을 줄이는 연구가 이루어지고 있습니다.
【 WAL 라이터 】
WAL 라이터 프로세스는 WAL(Write Ahead Logging)을 파일에 내려쓰는 프로세스입니다. WAL은 PostgreSQL의 업데이트 정보가 기록된 로그로, 복구 시나 스트리밍 복제에 사용되는 매우 중요한 정보입니다.
WAL 라이터 프로세스에서는, WAL 버퍼에 기록된 WAL을 설정에 따라 WAL 파일에 기록합니다.
【 체크 포인터 】
체크 포인트(모든 더티 페이지(파일 시스템에 다시 쓸 필요가 있는 데이터를 가진 페이지)를 데이터 파일에 반영하고, 특수한 체크 포인트 레코드가 로그 파일에 기록된 상태)를 설정에 따라 자동으로 실행하는 프로 세스입니다.
체크포인트는, PostgreSQL이 문제가 생겼을 때, 어느 곳에서 복구 처리를 하는지를 나타내는 포인트입니다. 체크포인트 처리는, 모든 더티 페이지를 디스크상의 데이터 파일에 쓰기 위해, 상당히 I/0 부하가 높아질 수 있습니다. 또한, 체크포인트의 빈도와 문제 발생 후 복구 처리 시간과는 서로 연관이 있습니다. 체크포인트가 자주 발생하는 경우, 성능에 영향을 받기 쉬워지지만, 장애 발생 후 복구 처리로 수행해야 할 복구 처리량이 감소하고, 기동까지의 시간이 단축됩니다. 이 때문에, 시스템의 요건에 따라 적절한 체크포인트 설정이 필요합니다.
【 자동 베큠 런처와 자동 베큠 워커 】
자동 베큠을 제어 / 실행하는 프로세스입니다. 자동 베큠 런처는 설정에 따라 자동 베큠 워커를 기동 하고, 자동 베큠 워커는 테이블에 대해 자동으로 베큠과 분석을 실행합니다.
베큠은, 데이터의 갱신이나 삭제에 의해 발생한 데이터 파일이나 인덱스 내의 불필요한 영역을 재사용할 수 있도록 하는 처리로, 분석은, 쿼리를 실행할 때 이용하는 통계 정보(각 열의 전형적인 값과 각 열의 데이터 분포의 개요를 나타내는 도수 분포)를 수집하여 pg_statistic 시스템 카탈로그를 갱신하는 처리입니다. 어느 쪽도 정상적인 운용에는 빼놓을 수 없는 처리입니다. 작업자는, 이러한 처리를 실행하기 전에, 대상 테이블에 대량의 갱신(Insert, Update, Delete)이 있었는지 여부를 (통계 정보 수집기를 이용하여 수집되는) 통계 정보를 참조하여 검사하고, 필요에 따라 처리합니다.
【 통계 정보 수집기 】
데이터베이스의 활동 상황에 관한 통계 정보를 일정 간격으로 수집하는 프로세스입니다. 여기서 수집된 정보는, 자동 베큠 워커에서 사용됩니다.
통계 정보 수집기 프로세스는 설정에 따라 기동 시키지 않는 운용도 가능합니다. 그러나, 보통은 이 프로세스를 시작하여 운용하기를 강력히 추천합니다. 왜냐하면, 이 프로세스가 기동 하지 않으면, 자동 베큠 기능이 유효하게 작동하지 않기 때문입니다.
【 백엔드 프로세스 】
클라이언트에서 접속 요청을 받았을 때 생성되는 프로세스입니다. SQL의 실행은, 이 백엔드 프로세스 내에서 이루어집니다.
PostgreSQL에서는, 사용자가 독자적인 워커 프로세스를 구현하여 PostgreSQL에 통합을 가능하게 하는 프레임워크가 구현되어 있습니다. 이 프레임워크를 '백그라운드 워커 프로세스'라고 합니다.
백그라운드 워커 프로세스 규정의 형식으로 구현한 사용자의 독자적인 워커 프로세스는, 마스터 서버 프로세스 기동의 백그라운드에서 기동 되어, PostgreSQL의 서버 프로세스로 감시되어, 마스터 서버의 종료와 동기화하여 종료합니다. 또한, PostgreSQL의 공유 메모리에 대한 액세스나, 데이터베이스로의 접속도 가능합니다. 독자적인 워커 프로세스의 용도는, 시스템 고유의 감시 기능을 통합하고 싶은 경우 등을 예로 들 수 있습니다.
「 클라이언트와 서버의 연결 / 통신 」
클라이언트에서 PostgreSQL에 접속하면, PostgreSQL은 백엔드 프로세스를 생성하고 클라이언트와 백엔드 프로세스 간의 연결을 설정합니다.
클라이언트로부터 접속 요청을 받았을 경우의 동작은 아래와 같습니다.

❶ 클라이언트에서는 최초로 마스터 서버의 포트(기본값으로는 5432)에 대해, 사용자명과 연결하고 싶은 데이터베이스명을 포함한 메시지를 보냅니다.
❷ 마스터 서버는 그 메시지 내의 정보와, pg_hba.conf의 내용을 비교하여, 접속이 허용되는지 여부를 확인합니다.
❸❹ 해당 접속이 인증을 필요로 하는 경우에는, 인증을 요구하는 메시지를 클라이언트에 보내고, 클라이언트는 인증에 필요한 정보를 서버에 송신합니다. 또한, 인증 방식이 인증을 필요로 하지 않는 경우(인증 방식이 trust 등)는, 이 처리를 건너뜁니다.
❺ 서버는 인증 정보를 받으면, 인증 방식에 따른 처리를 합니다.
❻ 인증이 성공하면 인증 성공 메시지를 클라이언트에 반환하고, 백엔드 프로세스를 fork0에 의해 생성합니다.
❼ 마스터 서버는 백엔드 프로세스를 기동한 후, 클라이언트에 개시 처리 종료 메시지를 보냅니다. 클라이언트는 인증 성공 메시지를 받은 후, 마스터 서버에서 개시 처리 종료 메시지를 받을 때까지 대기하고 있습니다. 개시 처리 종료 메시지를 수신하면, 접속이 확립되어, 클라이언트에서 쿼리를 보낼 수 있게 됩니다.
이러한 인증의 각 처리에서, 클라이언트와 마스터 서버 간에는 PostgreSQL에서 규정된 프로토콜에 따라 메시지를 주고받고 있습니다. psql에 의해 서버에 접속할 때나, libpq / JDBC 라이브러리를 사용하여 서버에 접속할 때는, 라이브러리 내에서 이러한 메시지 처리를 하고 있기 때문에, 이용자는 사용자 / 데이터베이스 / 인증 정보 만을 의식하면 되고, 프로토콜에 대해 의식할 필요는 없습니다.
「 쿼리의 실행 」
쿼리는 아래와 같이, 다양한 처리를 통해 실행됩니다.

【 파서 】
쿼리는 먼저 파서로 처리됩니다. 파서에서는 어휘 해석과 구문 해석을 수행합니다.
• 어휘 해석
어휘 해석이란, SQL이 어떤 토큰(구문 단위)으로 구성되는지를 해석하는 것으로써, 오픈 소스 소프트웨어(OSS)의 'flex*'(*Unix 표준 명령의 lex를 바탕으로 GNU 프로젝트로 개량된 것. 어휘 해석 프로그램의 기반이 되는 소스를 생성하는 도구)를 이용하고 있습니다. 어휘 해석에서는, 확장자가 '.l'의 파일 내용에 따라, SQL을 식별자나 SQL 키워드 등의 토큰으로 분해하여 구문 해석으로 넘어갑니다.
어휘 해석의 규칙은, PostgreSQL의 소스 코드(./backend/parser/scan.l)로 정의되어 있습니다.
• 구문 해석
어휘 해석으로 분해된 어휘의 순서가 PostgreSQL에서 다룰 수 있는 SQL의 기술 규칙에 맞는지 검사하여 질의 트리를 생성합니다. 구문 해석에는, OSS의 'bison*'(*Unix 표준 명령의 yacc를 바탕으로 GNU 프로젝트로 개량된 것. 구문 해석 프로그램의 베이스가 되는 소스를 생성하는 도구)가 이용되고 있으며, 확장자가 '.y'의 파일 내용(확장 BNF 표기법과 비슷한 기술 내용)에 따라, 전달된 SQL이 PostgreSQL에서 규정된 구문에 맞는지 체크합니다.
구문 분석 규칙은 PostgreSQL의 소스 코드(./backend/parser/gram.y)에 정의되어 있습니다.
• 존재하는지 확인
어휘 해석과 구문 해석에 더해, 파서에서는 질의 트리의 내용에서, 테이블명이나 컬럼명이 실제로 데이터베이스상에 존재하는지(접근할 수 있는지)를 판단합니다. 예를 들어, 존재하지 않는 테이블을 FROM 절에 지정한 경우, 에러로 이후의 처리를 하지 않습니다. 구문 분석 단계에서 에러가 발생하는 경우는 아래의 예와 같이 'syntax error'라고 표시됩니다.
< 구문 해석 에러의 예 >
[postgres@postgresql ~]$ psql -d tpch
psql (14.9)
Type "help" for help.
tpch=# \dt
List of relations
Schema | Name | Type | Owner
--------+----------+-------+----------
public | customer | table | postgres
public | lineitem | table | postgres
public | nation | table | postgres
public | orders | table | postgres
public | part | table | postgres
public | partsupp | table | postgres
public | region | table | postgres
public | supplier | table | postgres
(8 rows)
tpch=# select * nation;
ERROR: syntax error at or near "nation"
LINE 1: select * nation;
^
한편, 구문상으로는 맞지만 검색 대상이 되는 테이블이 없는 경우에는, 아래의 예와 같이 다른 에러 메시지가 됩니다.
< 검색 대상 테이블이 없을 때 에러의 예>
[postgres@postgresql ~]$ psql -d tpch
psql (14.9)
Type "help" for help.
tpch=# \dt
List of relations
Schema | Name | Type | Owner
--------+----------+-------+----------
public | customer | table | postgres
public | lineitem | table | postgres
public | nation | table | postgres
public | orders | table | postgres
public | part | table | postgres
public | partsupp | table | postgres
public | region | table | postgres
public | supplier | table | postgres
(8 rows)
tpch=# select * from parts;
ERROR: relation "parts" does not exist
LINE 1: select * from parts;
^
어휘 해석과 구문 해석에 의해 타당한 SQL이라고 해석되면, 파서는 SQL을 트리 구조로 표현한 '질의 트리'를 생성하고, 다음 처리가 되는 리 라이터에게 건네줍니다.
【 리 라이터 】
SQL을 실행하는 데이터베이스에 규칙(SQL을 다시 쓰는 규칙)이 정의되어 있는 경우, 그 규칙을 참조하여 리 라이터에서 질의 트리를 수정합니다. 수정한 질의 트리는, 다음 처리인 플래너에게 전달됩니다.
PostgreSQL의 뷰는, 규칙을 사용하여 정의되어 있습니다. 이 때문에, 뷰에 엑세스 하는 경우에는, 리 라이터에 의한 질의 트리의 갱신이 이루어지고 있습니다.
【 플래너 / 옵티마이저 】
플래너에서는 리 라이터에서 수정된 질의 트리를 바탕으로 최적의 실행 계획을 생성합니다.
실행 계획 작성에는 크게 2단계가 있습니다. 먼저 개별 테이블에 대한 접근 방법을 선택하고, 그다음 결합 방법을 선택합니다.
• 개별 테이블에 대한 접근 방법 선택
먼저, 테이블 전체를 스캔하는 방식(SeqScan)을 검색 방식의 후보로 합니다. 질의 중에 그 테이블에 대한 검색 조건이 설정되고, 그 테이블에 설정된 인덱스가 사용 가능하다면, 인덱스 검색(IndexScan)이나 비트맵 검색(BitmapScan)을 검색 방식의 후보로 합니다.
• 결합 방법 선택
질의 트리가 여러 테이블을 대상으로 하는 경우는 결합 방법을 선택합니다. PostgreSQL은 '중첩 루프 결합', '머지 결합', '해시 결합'의 3가지 결합 방법을 지원하고 있습니다. 플래너에서는 이 3가지에서 적용 가능한 결합 방법을, 통계 정보를 바탕으로 판단합니다.
또한, 결합 대상이 되는 테이블이 3개 이상인 경우에는, 결합 순서도 고려됩니다. 개별 테이블에 대한 액세스 방법, 결합 방법, 결합 순서의 조합의 후보군에서, 가장 효율이 좋다고 판단한(실행 비용이 작은) 방법의 조합이 실행 계획으로 생성되어, 익스큐터에게 전달됩니다.
또한, 플래너가 생성한 실행 계획은, EXPLAIN 명령으로 확인할 수 있습니다.
【 익스큐터 】
익스큐터는 플래너에서 결정된 실행 계획에 따라 필요한 행의 집합을 추출합니다. 익스큐터에서는 DML(Data Manipulation Language:데이터 조작 언어)만을 대상으로 처리합니다. 익스큐터는 실행하는 DML의 종류(SELECT / INSERT / UPDATE / DELETE)에 따라 동작이 다릅니다.

【 SQL의 종류에 따른 동작 】
PostgreSQL에서 사용되는 SQL은, 크게 나누면 데이터 조작을 하는 DML과 데이터 정의를 하는 DDL(Data Definition Language), 트랜잭션 등 의 제어를 실시하는 DCL(Data Control Language)의 3종류가 있습니다.
DDL은 'CREATE', 'DROP', 'ALTER' 등 데이터베이스 오브젝트의 생성이나 삭제, 변경을 하는 명령입니다. DCL은 'BEGIN', 'COMMIT', 'ROLLBACK' 등 트랜잭션 제어를 위한 커맨드입니다.
DDL과 DCL은 DML과 마찬가지로 파서와 리 라이터를 통해 플래너에게 질의 트리를 건네지만, 플랜 선택이 없기 때문에, 아무것도 하지 않고 플래너를 빠져나갑니다. 게다가 익스큐터에서는 처리를 하지 않고, 대응하는 개별 커맨드를 실행합니다.
「 트랜잭션 」
트랜잭션 처리란, 서로 관련된 복수의 처리를, 트랜잭션이라고 불리는 불가분한 처리 단위로 취급하는 것입니다. 트랜잭션 처리는 PostgreSQL을 비롯한 RDBMS의 근간이 되는 기구입니다. 간단히 트랜잭션 처리의 개요와, PostgreSQL에서의 트랜잭션 처리의 대응에 대해 알아보겠습니다.
【 트랜잭션의 특성 】
트랜잭션은, 크게 4가지 요건을 충족해야 합니다.
• 원자성(atomicity)
여러 처리를 하나로 묶어서, 그 처리가 모두 실행되었거나 전혀 실행되지 않거나 둘 중 하나의 결과가 되는 것입니다.
• 일관성(consistency)
트랜잭션의 개시 및 종료 시점에서, 업무로 규정된 정합성을 충족시키는 것입니다.
• 독립성(isolation)
작업 중인 트랜잭션에 의한 갱신은, 확정될 때까지 다른 트랜잭션에서 보이지 않는 것입니다.
• 지속성(durability)
확정된 트랜잭션의 결과는 데이터베이스에 영구적(변하지 않고 오래가는)으로 저장되는 것입니다.
【 트랜잭션 제어 】
PostgreSQL의 트랜잭션 제어에는 'BIGIN'(개시), 'COMMIT'(확정), 'ROLLBACK'(파기)을 사용합니다. 또한, 분리 레벨 지정에는 'SET TRANSACTION'을 사용합니다.
트랜잭션이 이상 상태가 되면, 후속 데이터 조작 명령은 모두 에러가 발생합니다. 이런 경우에는, 트랜잭션을 파기(ROLLBACK)하고 개시 전의 상태로 되돌립니다.
【 트랜잭션의 분리 레벨 】
트랜잭션은, 동시에 1개만 실행된다고는 할 수 없습니다. 복수의 트랜잭션이 동시에 실행될 경우, 각각의 트랜잭션 간에 상호 영향의 정도를 나타내는 것이, 트랜잭션의 분리 레벨입니다. 트랜잭션의 분리 레벨은, 표준 SQL에서는 아래의 표와 같이 규정되어 있습니다.

트랜잭션의 분리 레벨이 약한(다른 트랜잭션의 영향을 받기 쉬운) 경우, 더티 리드, 반복 불능 읽기, 팬텀 리드 등의 영향이 발생하여, 의도하지 않은 결과가 될 수 있습니다.

PostgreSQL에서는, 이러한 분리 레벨 중, '리드 커미티드(READ COMMITTED)', '리피터블 리드(REPEATABLE READ)', '시리얼라이저블(SERIALIZABLE)'을 지정할 수 있습니다. 또한, 리드 언커미티드(READ UNCOMMITTED) 레벨을 지정한 경우에도 리드 커미티드와 같은 동작이 되기 때문에, 사실상, PostgreSQL에서는 더티 리드는 발생하지 않습니다.
트랜잭션 분리 레벨은 반드시 분리 레벨이 높으면 좋다는 것이 아니라, 시스템의 요구 사항에 따라 허용 가능한 분리 레벨을 선택해야 합니다. PostgreSQL에서는, 분리 수준이 비교적 낮은 리드 커미티드를 기본 동작으로 하고 있습니다. 이것은 일반적인 애플리케이션 요건에서는 리드 커미티드의 분리 레벨에서 충분히 다루기 쉽기 때문이라고 생각됩니다.
반복 불능 읽기나 팬텀 리드가 발생함으로써, 애플리케이션으로서 치명적인 문제가 발생하는 경우에는, 리피터블 리드나 시리얼라이저블(직렬화) 레벨의 지정을 검토할 필요가 있습니다. 다만, 리피터블 리드나 시리얼라이저블을 지정한 경우, 동시 실행 중인 트랜잭션의 직렬화에 실패할 가능성이 생기기 때문에, 실패한 트랜잭션의 재실행 등은 별도로 고려할 필요가 있습니다. 리피터블 리드나 시리얼라이저블의 분리 레벨에서, 직렬화가 실패하는 경우를 아래의 예로 확인할 수 있습니다.


postgres_fdw의 트랜잭션 분리 레벨
PostgreSQL 자체의 기본 트랜잭션 분리 벨은 리드 커미티드이지만, PostgreSQL의 확장 기능 contrib/postgres_fdw 모듈 내에서 실행되는 원격 트랜잭션의 분리 레벨은 리드 커미티드가 아니라, 리피터블 리드 또는 시리얼라이저블(로컬 트랜잭션의 트랜잭션 분리 레벨이 시리얼라이저블인 경우)이 됩니다.
「 락(LOCK) 」
트랜잭션에서의 동시 실행을 보장하기 위해, 테이블 / 행에 대해 명시적으로 락을 걸 수 있습니다. 테이블 단위의 락은 LOCK 명령을 사용합니다. 행 단위 잠금은 SELECT 명령의 옵션 지정으로 SELECT FOR UPDATE 또는 SELECT FOR SHARE를 사용합니다.
또한, PostgreSQL은, 명시적인 락을 획득하지 않은 경우에도, SQL 실행 뒤에서 적절한 모드의 락을 자동으로 획득합니다. SQL 실행으로 획득되는 잠금 모드는 아래의 표와 같이 분류됩니다.

또한 각각의 락 모드가 충돌한 경우는 아래의 표와 같습니다.


명시적인 락을 사용하는 경우, 락의 획득 순서에 따라 데드락이 발생할 수 있습니다. PostgreSQL에는 데드락을 감지하는 기구가 있습니다. 데드락을 감지한 경우에는, 기인한 트랜잭션의 한쪽을 중단합니다(중단된 트랜잭션은 필요에 따라 다시 실행해야 합니다).
단순한 데드록의 예는 아래와 같습니다.

리소스에 대한 암묵적인 락이나 LOCK 커맨드에 의한 명시적인 락 외에, 애플리케이션 고유의 상호배제를 하고 싶은 경우에 사용하는 '권고적 락'이라고 불리는 기구가 있습니다. 권고적 락의 사용은 애플리케이션 측에서 책임지고 관리할 필요가 있습니다. 예를 들어, 트랜잭션 내에서 권고적 락을 한 후, 트랜잭션이 롤백해도, 그 권고적 락은 자동으로 해제되지 않으며, 데드록에 상응하는 상태가 되어도 PostgreSQL 측에서 그것을 검사하고 해소하는 일도 하지 않습니다.
「 동시 실행 제어 」
PostgreSQL은 추가형 아키텍처를 채용함으로써, MVCC(Multi Version Concurrency Control: 다중 버전 동시성 제어)라고 불리는 동시 실행 제어 방식을 실현하고 있습니다.
추가형 아키텍처란, 데이터 갱신 시 원래 있던 데이터를 직접 갱신하는 것이 아니라, 갱신 전의 데이터는 그대로 두고 갱신 후의 데이터를 추가하는 구조입니다.

PostgreSQL에서는, 데이터가 삽입 / 갱신되었을 때, 트랜잭션을 식별하기 위한 트랜잭션 ID(XID)가 부여됩니다. 즉, 테이블 데이터의 각 행은 XID를 유지한 상태입니다. 복수의 트랜잭션이 다른 XID의 데이터를 취득함으로써, 각각이 다른 시점의 데이터를 참조할 수 있습니다.
추가형 아키텍처를 바탕으로 구현된 MVCC에 덕분에, 트랜잭션의 동시 실행 제어라는 복잡한 구조를, 비교적 간단하게 구현할 수 있다는 이점이 있습니다. 그 대신, 오래된 행의 데이터가 물리적으로 저장 영역을 사용하거나, 그 영역을 재사용하기 위한 구조가 별도로 필요하다는 단점이 있습니다.
갱신 전의 오래된 행을 참조하는 트랜잭션이 존재하지 않게 되면, 불필요 영역으로 취급됩니다(이 시점에서는 재사용할 수 없는 영역입니다). PostgreSQL에서는 베큠 처리에 의해 갱신 전의 오래된 행이 있던 영역을 빈 영역 맵에 기록하여 재사용 가능하게 합니다.
이전의 PostgreSQL에서는, 이 베큠 처리가 운용상의 중요한 과제였지만, PostgreSQL 8.3에서 채용된 HOT(Heap Only Tuple)나, 자동 베큠 기능 강화로, 베큠 처리를 의식할 필요가 없게 되었습니다.
'database > postgresql' 카테고리의 다른 글
각종 설정 파일과 기본 설정 (0) | 2023.10.10 |
---|---|
아키텍처의 기본 (0) | 2023.10.07 |
PostgreSQL이란? (0) | 2023.10.06 |
최근댓글