고가용성을 위한 프로덕션급 Postgres 레플리케이 달성하기

필 이튼

10월 07, 2024

복제 이해: 확장 가능하고 안정적인 Postgres 클러스터의 열쇠

프로덕션급 Postgres를 찾고 계십니다. 고가용성을 위해 데이터의 복사본을 여러 개 보관해야 합니다. 그리고 이 데이터는 복사본 간에 일관성을 유지해야 합니다. 클러스터의 롤링 업그레이드를 처리해야 합니다. 유지보수를 위해 클러스터에서 노드를 제거해야 합니다. 클러스터 내 노드 간 DDL 변경을 처리해야 합니다. 그리고 너무 앞서 나간 것일 수도 있지만, 지연 시간이 짧은 읽기 복제본이 필요하거나 데이터 거버넌스 정책을 준수하면서 데이터를 지리적으로 분산시켜야 할 수도 있습니다.

사실 이러한 작업은 바닐라 Postgres에서는 할 수 없습니다. 일반적인 고가용성(HA) 도구로도 불가능합니다. 그 이유를 이해하려면 Postgres에서 복제를 수행할 수 있는 두 가지 방법, 즉 물리적 복제와 논리적 복제에 대해 이야기해야 합니다.

먼저 물리적 복제에 대해 알아보겠습니다. 두 개의 Postgres 노드가 있다고 가정해 보겠습니다. 사용자로부터 쿼리를 직접 수신하는 “업스트림” 노드와 업스트림 노드에 변경 사항을 구독하는 “다운스트림” 노드가 있습니다. 물리적 복제는 업스트림 노드의 디스크에서 바이트를 가져와 네트워크를 통해 다운스트림 노드로 복사합니다. 그러면 다운스트림 노드는 그 바이트를 디스크에 그대로 다시 씁니다.

물리적 복제는 매우 낮은 수준에서 이루어지므로 두 노드 모두 원래 사용자로부터 받은 데이터의 측면에서 실제로 무엇을 복사하고 있는지 전혀 알 수 없습니다. 따라서 업스트림 노드와 다운스트림 노드가 서로 다른 버전의 Postgres를 실행하는 경우, 다운스트림 노드가 업스트림 노드가 전송한 물리적 데이터를 이해할 수 있다는 보장이 없습니다.

일반적으로 사용되는 많은 HA 도구는 물리적 복제를 기반으로 합니다. 따라서 롤링 업그레이드와 같은 요구 사항(예: 클러스터를 업그레이드하는 동안 Postgres 12 노드에서 Postgres 13 노드로 일시적으로 복제)은 이러한 도구를 자동으로 배제합니다.

논리적 복제는 어떻게 되나요? 논리적 복제는 물리적 복제와 달리 사용자 데이터에 대한 인식을 유지합니다. 업스트림 노드에서 다운스트림 노드로 불투명한 바이트를 복제하는 대신, 업스트림 노드에서 다운스트림 노드로 논리적 변경(삽입, 업데이트, 삭제)을 복제합니다. 논리적 복제는 이 상위 수준에서 작동하기 때문에 실제로 혼합 버전의 Postgres를 실행하는 클러스터를 허용합니다.

하지만 Postgres의 논리적 복제에는 몇 가지 큰 한계가 있습니다. 가장 큰 한계는 DDL을 복제할 수 없다는 것입니다. 이 제한은 Postgres의 DDL이 트랜잭션이기 때문에 특히 어렵습니다! 테이블을 만든 다음 변경 사항을 롤백할 수 있습니다. 하지만 바닐라 Postgres의 논리적 복제에서는 쉽지 않습니다.

더 자세히 알고 싶으시다면 제 동료인 Peter가 복제의 역사에 대한 훌륭한 포스팅을 작성했습니다.

EDB Postgres 배포


지금까지 프로덕션급 Postgres 요구 사항이 어렵다는 것을 확인했습니다. 하지만 EDB Postgres Distributed(PGD)를 사용하면 이 모든 것을 아주 쉽게 수행할 수 있는 방법을 보여드리고자 합니다. 특히, 바닐라 Postgres 논리적 복제를 기반으로 PGD가 구축한 가장 멋진 기능 중 하나는 DML 외에도 트랜잭션 DDL을 복제하는 기능입니다. 따라서 단일 머신에서 PGD를 사용하여 세 가지 버전의 Postgres를 실행하는 3노드 Postgres 클러스터를 구축하겠습니다. 그리고 이 노드들 간에 DDL과 DML이 복제되는 것을 볼 수 있습니다.

서로 다른 버전의 Postgres를 영구적으로 실행하는 것은 권장하지 않지만, PGD는 롤링 업그레이드를 지원하도록 명시적으로 설계되었습니다. 혼합 버전 클러스터를 아예 실행하는 것은 흥미로운 일이므로 이 글에서는 어쨌든 계속 진행하겠습니다.

모든 것을 수동으로 설정하는 방법을 보여드릴 것이므로 숨겨져 있는 것은 없습니다. 그러나 이것이 꽤 쉽게 스크립트화할 수 있다는 것을 알게 될 것입니다.

다른 Debian 기반 및 RHEL 기반 Linux 배포판도 지원하지만 이 글의 지침은 Debian 12를 사용한다고 가정합니다. 그러니 데비안 12를 실행하는 amd64 머신을 준비해서 시작해 보세요!

Postgres 14, 15, 16 가져오기

먼저 postgresql.org의 패키지 리포지토리 구성 지침을 따릅니다.

# Import the repository signing key:
sudo apt install curl ca-certificates
sudo install -d /usr/share/postgresql-common/pgdg
sudo curl -o /usr/share/postgresql-common/pgdg/apt.postgresql.org.asc --fail https://www.postgresql.org/media/keys/ACCC4CF8.asc
  
# Create the repository configuration file:
sudo sh -c 'echo "deb [signed-by=/usr/share/postgresql-common/pgdg/apt.postgresql.org.asc] https://apt.postgresql.org/pub/repos/apt $(lsb_release -cs)-pgdg main" > /etc/apt/sources.list.d/pgdg.list'
  
# Update the package lists:
sudo apt update

이제 세 가지 버전의 Postgres를 설치할 수 있게 되었습니다.

sudo apt install -y postgresql-16 postgresql-client-16
sudo apt install -y postgresql-15 postgresql-client-15
sudo apt install -y postgresql-14 postgresql-client-14

PGD 받기

EDB 리포지토리 2.0 페이지에서 EDB 리포지토리 토큰을 받으세요. 아직 EDB 계정이 없는 경우, 무료로 가입하여 EDB의 소프트웨어 리포지토리에 대한 평가판 구독을 받을 수 있습니다.

해당 토큰으로 환경 변수를 설정합니다:

export EDB_SUBSCRIPTION_TOKEN=<your-repo-token>

그리고 헬퍼 스크립트를 사용하여 패키지 리포지토리를 설정합니다:

curl -1sLf "https://downloads.enterprisedb.com/$EDB_SUBSCRIPTION_TOKEN/postgres_distributed/setup.deb.sh" | sudo -E bash

이제 각 Postgres 버전에 대해 PGD를 설치합니다:

sudo apt install -y edb-bdr5-pg14
sudo apt install -y edb-bdr5-pg15
sudo apt install -y edb-bdr5-pg16

3개의 Postgres 인스턴스 시작하기

이제 로컬에 세 개의 Postgres 버전이 설치되었으므로 데이터베이스를 만들고 시작할 수 있습니다. 하지만 각 데이터베이스를 시작하기 전에 데이터베이스의 postgresql.conf를 편집하여 bdr이라는 PGD 확장자를 로드하도록 해야 합니다.

Postgres 14 데이터베이스 만들기

Debian 패키지가 몇 가지 디렉터리를 설정하고 initdb를 실행했지만, 이제 깨끗한 환경을 만들어 보겠습니다.

admin@localhost $ sudo su postgres
postgres@localhost $ rm -rf /var/lib/postgresql/14
postgres@localhost $ /usr/lib/postgresql/14/bin/initdb /var/lib/postgresql/pg14
The files belonging to this database system will be owned by user "postgres".
This user must also own the server process.

The database cluster will be initialized with locale "C.UTF-8".
The default database encoding has accordingly been set to "UTF8".
The default text search configuration will be set to "english".

Data page checksums are disabled.

creating directory /var/lib/postgresql/pg14 ... ok
creating subdirectories ... ok
selecting dynamic shared memory implementation ... posix
selecting default max_connections ... 100
selecting default shared_buffers ... 128MB
selecting default time zone ... Etc/UTC
creating configuration files ... ok
running bootstrap script ... ok
performing post-bootstrap initialization ... ok
syncing data to disk ... ok

initdb: warning: enabling "trust" authentication for local connections
You can change this by editing pg_hba.conf or using the option -A, or
--auth-local and --auth-host, the next time you run initdb.

Success. You can now start the database server using:

/usr/lib/postgresql/14/bin/pg_ctl -D /var/lib/postgresql/pg14 -l logfile start

PGD를 위한 postgresql.conf 구성하기

이제 다음을 실행하여 PGD를 실행할 데이터베이스를 설정합니다.

postgres@localhost $ echo "
shared_preload_libraries = 'bdr'
wal_level = 'logical'
track_commit_timestamp = on

# For bigger clusters you may want to decrease shared buffers or the
# OS may complain you are using too much shared memory.
shared_buffers = 1MB

# These depend on the size of the cluster.
max_worker_processes = 12
max_wal_senders = 6
max_replication_slots = 6" | tee -a /var/lib/postgresql/pg14/postgresql.conf

Postgres 14 데이터베이스 시작


제안된 대로 시작하되 포트 8014에서 실행합니다:

postgres@localhost $ /usr/lib/postgresql/14/bin/pg_ctl -o "-p 8014" -D /var/lib/postgresql/pg14 -l /tmp/logfile14 start
waiting for server to start.... done
server started

Postgres 15 데이터베이스 만들기


이제 Postgres 15 데이터베이스를 생성하고 시작하겠습니다.

postgres@localhost $ rm -rf /var/lib/postgresql/15
postgres@localhost $ /usr/lib/postgresql/15/bin/initdb /var/lib/postgresql/pg15
The files belonging to this database system will be owned by user "postgres".
This user must also own the server process.

The database cluster will be initialized with locale "C.UTF-8".
The default database encoding has accordingly been set to "UTF8".
The default text search configuration will be set to "english".

Data page checksums are disabled.

creating directory /var/lib/postgresql/pg15 ... ok
creating subdirectories ... ok
selecting dynamic shared memory implementation ... posix
selecting default max_connections ... 100
selecting default shared_buffers ... 128MB
selecting default time zone ... Etc/UTC
creating configuration files ... ok
running bootstrap script ... ok
performing post-bootstrap initialization ... ok
syncing data to disk ... ok

initdb: warning: enabling "trust" authentication for local connections
initdb: hint: You can change this by editing pg_hba.conf or using the option -A, or --auth-local and --auth-host, the next time you run initdb.

Success. You can now start the database server using:

/usr/lib/postgresql/15/bin/pg_ctl -D /var/lib/postgresql/pg15 -l logfile start

PGD를 위한 postgresql.conf 구성하기


이제 PGD를 실행하도록 데이터베이스를 설정합니다.

postgres@localhost $ echo "
shared_preload_libraries = 'bdr'
wal_level = 'logical'
track_commit_timestamp = on

# For bigger clusters you may want to decrease shared buffers or the
# OS may complain you are using too much shared memory.
shared_buffers = 1MB

# These depend on the size of the cluster.
max_worker_processes = 12
max_wal_senders = 6
max_replication_slots = 6" | tee -a /var/lib/postgresql/pg15/postgresql.conf

Postgres 15 데이터베이스 시작


이제 Postgres 15 데이터베이스를 실행합니다.

postgres@localhost $ /usr/lib/postgresql/15/bin/pg_ctl -o "-p 8015" -D /var/lib/postgresql/pg15 -l /tmp/logfile15 start
waiting for server to start.... done
server started

Postgres 16 데이터베이스 만들기


마지막으로 Postgres 16 데이터베이스를 생성합니다.

postgres@localhost $ rm -rf /var/lib/postgresql/16
postgres@localhost $ /usr/lib/postgresql/16/bin/initdb /var/lib/postgresql/pg16
The files belonging to this database system will be owned by user "postgres".
This user must also own the server process.

The database cluster will be initialized with locale "C.UTF-8".
The default database encoding has accordingly been set to "UTF8".
The default text search configuration will be set to "english".

Data page checksums are disabled.

creating directory /var/lib/postgresql/pg15 ... ok
creating subdirectories ... ok
selecting dynamic shared memory implementation ... posix
selecting default max_connections ... 100
selecting default shared_buffers ... 128MB
selecting default time zone ... Etc/UTC
creating configuration files ... ok
running bootstrap script ... ok
performing post-bootstrap initialization ... ok
syncing data to disk ... ok

initdb: warning: enabling "trust" authentication for local connections
initdb: hint: You can change this by editing pg_hba.conf or using the option -A, or --auth-local and --auth-host, the next time you run initdb.

Success. You can now start the database server using:

/usr/lib/postgresql/16/bin/pg_ctl -D /var/lib/postgresql/pg16 -l logfile start

PGD를 위한 postgresql.conf 구성하기


다시 한 번 다음을 실행하여 PGD를 실행하도록 데이터베이스를 설정합니다.

postgres@localhost $ echo "
shared_preload_libraries = 'bdr'
wal_level = 'logical'
track_commit_timestamp = on

# For bigger clusters you may want to decrease shared buffers or the
# OS may complain you are using too much shared memory.
shared_buffers = 1MB

# These depend on the size of the cluster.
max_worker_processes = 12
max_wal_senders = 6
max_replication_slots = 6" | tee -a /var/lib/postgresql/pg16/postgresql.conf

Postgres 16 데이터베이스 시작


마지막으로 Postgres 16 데이터베이스를 시작합니다.

postgres@localhost $ /usr/lib/postgresql/16/bin/pg_ctl -o "-p 8016" -D /var/lib/postgresql/pg16 -l /tmp/logfile16 start
waiting for server to start.... done
server started

3개의 데이터베이스를 클러스터로 전환하기


세 개의 노드를 설정하고 PGD 확장을 실행하고 있습니다. 하지만 서로 연결되어 있지 않습니다. 연결해 봅시다!

먼저 현재 위치를 상기시키기 위해 실행 중인 각 데이터베이스의 버전을 쿼리해 보겠습니다.

postgres@localhost $ /usr/lib/postgresql/14/bin/psql -p 8014 -c 'select version()'
                                                        version
-----------------------------------------------------------------------------------------------------------------------
  PostgreSQL 14.12 (Debian 14.12-1.pgdg120+1) on x86_64-pc-linux-gnu, compiled by gcc (Debian 12.2.0-14) 12.2.0, 64-bit
(1 row)

postgres@localhost $ /usr/lib/postgresql/15/bin/psql -p 8015 -c 'select version()'
                                                        version
---------------------------------------------------------------------------------------------------------------------
  PostgreSQL 15.7 (Debian 15.7-1.pgdg120+1) on x86_64-pc-linux-gnu, compiled by gcc (Debian 12.2.0-14) 12.2.0, 64-bit
(1 row)

postgres@localhost $ /usr/lib/postgresql/16/bin/psql -p 8016 -c 'select version()'
                                                        version
---------------------------------------------------------------------------------------------------------------------
  PostgreSQL 16.3 (Debian 16.3-1.pgdg120+1) on x86_64-pc-linux-gnu, compiled by gcc (Debian 12.2.0-14) 12.2.0, 64-bit
(1 row)

좋아 보입니다.

PGD 클러스터 구축


클러스터의 각 노드에 대해 CREATE EXTENSION bdr CASCADE; 를 호출한 다음, 클러스터의 각 노드에서 bdr.create_node를 호출하여 PGD 노드로 등록해야 합니다. 마지막으로 노드 그룹을 생성할 단일 노드를 선택합니다. 그런 다음 다른 두 노드에 우리가 만든 노드 그룹에 가입할 수 있도록 세부 정보를 제공합니다.

따라서 Postgres 14 서버에서 테이블을 복제할 데이터베이스를 만들고, 확장자를 만들고, PGD 노드를 만들고, PGD 노드 그룹을 만듭니다.

postgres@localhost $ /usr/lib/postgresql/14/bin/psql -p 8014 postgres -c "CREATE DATABASE pgdtest;"
CREATE DATABASE
postgres@localhost $ /usr/lib/postgresql/14/bin/psql -p 8014 pgdtest -c "CREATE EXTENSION bdr CASCADE;"
CREATE EXTENSION
postgres@localhost $ /usr/lib/postgresql/14/bin/psql -p 8014 pgdtest -c "SELECT bdr.create_node(node_name := 'pg14', local_dsn := 'port=8014 dbname=pgdtest host=localhost user=postgres');"
  create_node
-------------
  1151355342
(1 row)

postgres@localhost $ /usr/lib/postgresql/14/bin/psql -p 8014 pgdtest -c "SELECT bdr.create_node_group(node_group_name := 'pgdtest-group');"
  create_node_group
-------------------
          117160161
(1 row)

잘됐네요! PGD 노드와 PGD 노드 그룹이 생겼습니다.

이제 Postgres 15 노드에 대해서도 동일한 작업을 수행할 수 있습니다. 하지만 새 노드 그룹을 만드는 대신 기존 노드 그룹에 가입합니다.

postgres@localhost $ /usr/lib/postgresql/15/bin/psql -p 8015 postgres -c "CREATE DATABASE pgdtest;"
CREATE DATABASE
postgres@localhost $ /usr/lib/postgresql/15/bin/psql -p 8015 pgdtest -c "CREATE EXTENSION bdr CASCADE;"
CREATE EXTENSION
postgres@localhost $ /usr/lib/postgresql/15/bin/psql -p 8015 pgdtest -c "SELECT bdr.create_node(node_name := 'pg15', local_dsn := 'port=8015 dbname=pgdtest host=localhost user=postgres');"
  create_node
-------------
  2481762504
(1 row)

postgres@localhost $ /usr/lib/postgresql/15/bin/psql -p 8015 pgdtest -c "SELECT bdr.join_node_group(join_target_dsn := 'port=8014 dbname=pgdtest host=localhost user=postgres');"
  join_node_group
-----------------

(1 row)

그리고 이제 Postgres 16 노드도 마찬가지입니다.

postgres@localhost $ /usr/lib/postgresql/16/bin/psql -p 8016 postgres -c "CREATE DATABASE pgdtest;"
CREATE DATABASE
postgres@localhost $ /usr/lib/postgresql/16/bin/psql -p 8016 pgdtest -c "CREATE EXTENSION bdr CASCADE;"
CREATE EXTENSION
postgres@localhost $ /usr/lib/postgresql/16/bin/psql -p 8016 pgdtest -c "SELECT bdr.create_node(node_name := 'pg16', local_dsn := 'port=8016 dbname=pgdtest host=localhost user=postgres');"
create_node
-------------
  1945575858
(1 row)

postgres@localhost $ /usr/lib/postgresql/16/bin/psql -p 8016 pgdtest -c "SELECT bdr.join_node_group(join_target_dsn := 'port=8014 dbname=pgdtest host=localhost user=postgres');"
join_node_group
-----------------

(1 row)

사용해 보기

여러 Postgres 버전이 혼합된 단일 클러스터를 실행하고 있다는 점을 상기시켜드리겠습니다. 이제 이 클러스터 내에서 DDL과 DML을 복제할 수 있을 뿐만 아니라 복제할 수 있습니다. 읽기 및 쓰기를 위한 노드 간 전환. (실제 환경에서는 쓰기 리더에 대한 지원을 사용하고 다른 노드에서 쓰기를 혼합하지 않는 것이 좋습니다.)

Postgres 14 노드에 테이블을 만들고 몇 가지 데이터를 추가해 보겠습니다.

postgres@localhost $ /usr/lib/postgresql/14/bin/psql -p 8014 pgdtest -c "CREATE TABLE x (a int primary key);"
CREATE TABLE
postgres@localhost $ /usr/lib/postgresql/14/bin/psql -p 8014 pgdtest -c "INSERT INTO x VALUES (1), (32), (19), (0);"
INSERT 0 4

그리고 동일한 노드에 쿼리하세요:

postgres@localhost $ /usr/lib/postgresql/14/bin/psql -p 8014 pgdtest -c "SELECT * FROM x;"
a
----
  1
32
19
  0
(4 rows)

정상적인 포스트그레스입니다! 아직까지 흥미로운 것은 없습니다.

Postgres 15 노드를 쿼리해 보겠습니다.

postgres@localhost $ /usr/lib/postgresql/15/bin/psql -p 8015 pgdtest -c "SELECT * FROM x;"
a
----
  1
32
19
  0
(4 rows)

이제 이야기를 해보겠습니다. Postgres 14 클러스터에서 Postgres 15 클러스터로 데이터를 원활하게 복제했습니다.

Postgres 16은 어떨까요?

postgres@localhost $ /usr/lib/postgresql/16/bin/psql -p 8016 pgdtest -c "SELECT * FROM x;"
a
----
  1
32
19
  0
(4 rows)

Postgres 16 노드에 데이터를 추가하면?

postgres@localhost $ /usr/lib/postgresql/16/bin/psql -p 8016 pgdtest -c "INSERT INTO x VALUES (10000);"
INSERT 0 1

그리고 Postgres 14 노드에서 읽는다면?

postgres@localhost $ /usr/lib/postgresql/14/bin/psql -p 8014 pgdtest -c "SELECT * FROM x;"
  a
-------
    1
    32
    19
    0
10000
(5 rows)

꽤 멋지네요.

지금까지…


우리는 PGD의 프로덕션급 복제 기능을 사용하여 다양한 Postgres 노드 클러스터로 데이터를 복제했습니다. DDL을 복제할 수 있습니다.

지루한 단계를 많이 거쳤습니다. 하지만 이러한 단계는 간단하게 스크립트로 작성할 수 있습니다. 이러한 단계를 스크립트로 작성하고 나면 Postgres를 위한 아주 멋진 프로덕션급 설정이 완성됩니다.

PGD 문서를 확인하고 구독자 전용 노드로 읽기 전용 쿼리 확장 등 다른 PGD 기능을 다루는 향후 포스팅을 기대해 주세요.

본문: Achieving Production-Grade Postgres Replication for High Availability

EDB 영업 기술 문의: 02-501-5113

이메일: salesinquiry@enterprisedb.com

홈페이지 문의하기