pgactive는 PostgreSQL 데이터베이스 간에 메시 토폴로지를 사용하는 느슨하게 결합된 비동기/동기 액티브-액티브 논리 복제를 제공하는 확장 기능입니다. 이 문서는 설계, 보안, 노드 관리, 글로벌 시퀀스, 충돌 처리, 논리/물리 복제 차이, 모범 사례와 제한 사항을 다룹니다.
pgactive는 메시 기술을 통해 데이터베이스 간에 느슨하게 결합된 비동기 또는 동기 액티브-액티브 논리 복제를 제공합니다. 이는 pgactive 그룹에 속한 데이터베이스 중 어느 곳에나 쓰기가 가능하며, 먼저 해당 쓰기가 수행된 인스턴스에 커밋된 후 클러스터의 다른 모든 인스턴스로 행 단위로 전송된다는 의미입니다.
pgactive 그룹은 PostgreSQL 인스턴스가 아니라 데이터베이스의 모음입니다. 여기서 차이점은, 인스턴스는 PostgreSQL 백엔드에 의해 관리되는 하나 이상의 데이터베이스 집합이라는 점이고, 데이터베이스는 인스턴스 안에 존재한다는 점입니다. pgactive 그룹에 참여하는 각 데이터베이스는 다른 모든 멤버로부터의 모든 변경을 수신하며, 직접 쓰기가 가능합니다. 모든 인스턴스에 쓰는 것은 필수가 아닙니다. 일부 인스턴스에만 쓰더라도, 나머지 인스턴스는 모든 변경 사항을 수신합니다.
한 pgactive 노드에서 이루어진 변경은 로컬에 커밋되기 전에 다른 노드로 복제되지 않습니다. 그 결과, 어느 시점에 보더라도 모든 노드의 데이터가 정확히 동일하지 않을 수 있으며, 어떤 노드는 아직 다른 노드에 도착하지 않은 데이터를 포함할 수 있습니다. 액티브-액티브와 결합되면, 비동기 복제는 흔히 "최종적 일관성(eventually consistent)" 아키텍처라고 불리지만, 개별 노드 관점에서는 데이터가 일관적입니다. 특정 시점에는 노드마다 데이터를 다르게 볼 수 있지만, 시간이 지남에 따라 노드끼리 동기화됩니다. 쓰기가 멈추면, 일정 시간이 지나 모든 노드의 상태가 동일해집니다. pgactive에서는 이로 인해 외래 키 제약 조건이 데이터가 여러 노드에서 복제되는 동안 일시적으로 위배될 수 있습니다.
pgactive의 노드들은 노드 간 조정 트래픽이 많지 않기 때문에 느슨하게 결합되어 있습니다. 글로벌 트랜잭션 관리자나 글로벌 락 관리자가 존재하지 않습니다. 락은 노드 로컬이며, 노드 간 행 또는 릴레이션 락킹이 없습니다. 노드 간 락킹은 스키마 변경(DDL)에 대해서만 수행됩니다. 이로 인해 노드들은 비교적 독립적으로 동작하며 네트워크 중단과 파티션에 매우 강인합니다. 그러나, 이는 복제 충돌이 발생할 수 있음을 의미하기도 합니다.
논리(행 기반) 복제는 개별 행 값들을 사용하여 복제를 수행하는 방식입니다. 이는 데이터 블록의 변경 사항을 전송하는 물리(블록 기반) 복제와 대조됩니다. 논리 복제는 다른 레벨에서 동작합니다. 여러 개의 파일을 전송하는 것과, 그 파일이 들어있는 하드디스크 전체를 보내는 것의 차이와 유사합니다. 논리 복제는 물리 복제에 비해 장단점이 모두 존재합니다. 자세한 내용은 아래의 "논리 복제와 물리 복제의 차이점" 섹션을 참고하십시오.
복제는 데이터를 한 장소에서 다른 장소로 복사하는 과정입니다. pgactive에서 복제는 pgactive가 공유 스토리지 아키텍처가 아니라는 점을 의미하며, 각 노드는 관련 인덱스를 포함해 데이터베이스의 복사본을 자체적으로 가지고 있습니다. 노드는 다른 노드와 통신하지 않고도 쿼리를 처리할 수 있지만, 데이터베이스의 모든 데이터를 저장하기 위한 충분한 스토리지 공간을 필요로 합니다.
pgactive는 각 노드가 다른 모든 노드와 직접 통신할 수 있는 메시 토폴로지를 사용합니다. 이는 순환(circulation) 복제의 문제점을 피하도록 설계되어 있습니다. 활성 노드에 대해 전달(forwarding)이나 계단식(cascading) 복제를 지원하지 않습니다. 각 노드 쌍은 대부분 단방향인 두 개의 채널을 통해 통신합니다. 하나는 노드 A에서 B로 데이터를 스트리밍하기 위한 채널이고, 다른 하나는 B에서 A로 스트리밍하기 위한 채널입니다. 이는 각 노드가 다른 모든 노드에 직접 연결할 수 있어야 함을 의미합니다. 방화벽, NAT 등은 이 요구 사항에 맞게 설정되어야 합니다.
모든 pgactive 노드는 다른 모든 pgactive 노드에 대해 replication slot을 하나씩 가지고 있어야 하며, 이를 통해 해당 노드의 변경 사항을 재생(replay)할 수 있습니다. 또한 각 노드는 다른 모든 노드에 대해 replication origin을 가지고 있어야 하며, 이를 통해 재생 진행 상황을 추적합니다.
노드가 오프라인이거나 네트워크 파티션으로 인해 도달 불가능한 상태에서 새로운 노드가 그룹에 조인하는 것을 허용한다면, 해당 노드에서 이루어진 변경 사항을 재생할 방법이 없어 pgactive 그룹이 서로 다른 상태가 됩니다. pgactive는 정상 동작 중에 전달 경로를 변경하지 않기 때문에 이러한 비동기 상태는 해결되지 않습니다.
향상된 변경 전달 기능이 추가되면, 메시의 나머지 부분으로부터 분리된 계단식(cascading) 노드를 허용하고, 새 노드가 조인한 뒤 나중에 직접 연결이 가능해졌을 때 그 노드로부터 직접 데이터를 수신하도록 점진적으로 전환하는 등의 동작이 가능해질 것입니다. 노드 조인 시 모든 노드에 도달 가능해야 하는 것은 근본적인 필수 조건은 아니며, 현재 구현체의 요구사항일 뿐입니다. 초기 노드 복제 시에는 이미 제한적인 변경 전달 기능이 사용되고 있습니다. 이를 위해서는 DDL 락킹 기능의 향상도 필요합니다.
DDL 복제는 기본적으로 비활성화되어 있습니다. 필요하다면, 그룹 내 모든 노드에서 설정 파라미터 pgactive.skip_ddl_replication을 false로 설정해야 합니다. 어떤 노드에서 pgactive.skip_ddl_replication=false로 설정하더라도, 수신 노드에 pgactive.skip_ddl_replication=true인 노드가 있다면 해당 노드에는 DDL이 적용되지 않습니다.
연결 DSN에 지정되는 사용자는 슈퍼유저여야 하며, 그렇지 않으면 pgactive.pgactive_nodes 테이블 및 public 권한이 회수된 다른 객체들에 접근할 수 없습니다.
pgactive 확장은 슈퍼유저만 설치할 수 있습니다. 확장에서 제공하는 함수들도 슈퍼유저만 사용할 수 있습니다.
pgactive 노드에서의 변경 사항은 백그라운드 워커에 의해 적용됩니다. 백그라운드 워커에는 supervisor, database worker, apply worker 등이 있습니다. 모든 백그라운드 워커는 슈퍼유저 권한으로 동작해야 합니다.
모든 표준 PostgreSQL 인증 메커니즘을 사용할 수 있습니다. 여기에는 pg_user_mapping을 사용하여 시스템 사용자를 PostgreSQL 사용자에 매핑하는 방법, 또는 .pgpass를 사용하여 비밀번호 파일에 비밀번호를 저장하고 연결 문자열에는 포함하지 않는 방법 등이 포함됩니다.
연결 시 SSL을 사용해 전송 중인 데이터를 보호할 수 있습니다.
pg_catalog에 대한 변경 사항은 복제되지 않습니다.
pgactive는 pgactive.pgactive_create_group 및 pgactive.pgactive_join_group에서 DSN 대신 사용자 매핑을 지정하는 기능을 제공합니다. 이는 pgactive 그룹 내의 노드들이 서로 연결할 때 사용할 연결 정보를 캡슐화하기 위한 것입니다. 사용자는 pgactive 그룹의 각 pgactive 노드에 대한 연결 정보를 기반으로 CREATE FOREIGN SERVER 명령으로 외부 서버를 정의하고, CREATE USER MAPPING으로 정의된 사용자 매핑을 해당 서버에 연결한 뒤, 이들을 DSN 대신 사용할 수 있습니다.
새로운 pgactive 노드를 기존 pgactive 그룹에 조인시키면, 해당 노드는 상류(upstream) 피어에 구독되며, 복제가 시작되기 전에 시스템은 피어 노드(들)의 기존 데이터를 로컬 노드로 복사해야 합니다. 이 복사는 로컬 데이터와 원격 데이터가 완전히 동일 하도록 정교하게 조정되어야 하며, 단순히 사용자가 직접 pg_dump를 사용하는 것으로는 충분하지 않습니다. 확장은 이 초기 복사를 수행하기 위한 내장 기능을 제공합니다.
새 노드를 조인하려 할 때는 모든 pgactive 노드가 온라인 상태이고 도달 가능(reachable) 해야 합니다. 그렇지 않으면 조인 작업이 무기한 대기 상태에 빠지거나 실패할 수 있습니다. pgactive는 각 노드가 다른 모든 노드와 통신 가능해야 하는 메시 구조이며, 네트워크 파티션과 중단에는 관대하지만, 모든 노드는 존재하는 모든 다른 노드를 인지해야 합니다.
새 pgactive 노드를 조인하는 방법에는 논리 복사(logical copy)와 물리 복사(physical copy)의 두 가지가 있습니다. 초기 복사가 완료된 후에는 논리 초기화와 물리 초기화 방식 사이에 본질적인 차이가 없으므로, 어떤 방식을 쓸지는 특정 상황에서 더 빠르고 쉬운 방법을 선택하면 됩니다.
논리 복사에서는, 기존 독립 PostgreSQL 인스턴스 내의 빈 데이터베이스에 SQL 함수 호출을 통해 pgactive를 활성화합니다. pgactive 확장은 사용자가 지정한 상류 노드에 연결한 뒤, 해당 노드의 스키마와 데이터 덤프를 수행합니다. 이 덤프는 복제가 시작되기 전에 로컬 빈 데이터베이스에 적용됩니다. 지정된 데이터베이스만 복사됩니다. 논리 복사를 사용하면 새로운 init 스크립트를 만들거나, 별도 포트에서 별도로 인스턴스를 실행하는 등의 작업이 필요 없으며, 모든 작업을 기존 PostgreSQL 인스턴스 내에서 수행할 수 있습니다.
물리 복사에서는 pgactive_init_copy를 사용해 사용자가 지정한 상류 노드를 복제(clone)합니다. 이 복제본은 재구성 및 기동을 거쳐 새로운 노드로 사용되며, 이후 복제가 시작됩니다. 원격 노드의 모든 데이터베이스가 복사되지만, 처음에는 지정된 데이터베이스만 pgactive용으로 활성화됩니다. (추후 여러 데이터베이스를 한 번에 조인하는 기능이 추가될 수 있습니다.)
물리 노드 조인 또는 구독 후에는 일반적으로 새 PostgreSQL 인스턴스를 운영체제에 별도로 등록하여 자동 시작되도록 설정해야 합니다. PostgreSQL 자체가 이를 자동으로 수행하지는 않습니다. 또한 이미 로컬에 PostgreSQL 인스턴스가 존재하는 경우, 다른 PostgreSQL 포트를 선택해야 할 수도 있습니다.
각 접근 방식의 장단점은 논리 백업(pg_dump, pg_restore 사용)과 물리 복사(pg_basebackup 사용)의 장단점과 대체로 유사합니다. 자세한 내용은 PostgreSQL 백업을 참조하십시오.
일반적으로, 이미 PostgreSQL 인스턴스가 존재하고 데이터베이스 규모가 비교적 작거나, 복사/복제하고 싶지 않은 다른 데이터베이스가 있을 경우 논리 조인을 사용하는 것이 더 편리합니다. 물리 조인은 해당 PostgreSQL 설치에 단일 데이터베이스만 존재하면서 데이터베이스 규모가 큰 경우에 더 적합합니다.
pgactive 노드는 장기간의 장애로부터 복구가 가능하기 때문에, 노드를 완전히 제거할 경우에는 pgactive 그룹에 명시적으로 알려야 합니다. 노드를 폐기(decommission)하면서 다른 노드에 이 사실을 알리지 않으면 남은 노드가 디스크 공간을 소진할 수 있습니다.
각 노드는 일시적으로 도달 불가능한 피어 노드에 변경 사항을 재생할 수 있도록 피어 노드마다 하나씩 replication slot을 사용해 변경 정보를 저장합니다. 피어 노드가 무기한 오프라인 상태로 남을 경우, 축적되는 변경 정보로 인해 해당 노드의 PostgreSQL 트랜잭션 로그(WAL, pg_xlog)가 저장된 디스크 공간이 고갈되고, 다음과 같은 오류와 함께 데이터베이스 서버가 종료될 수 있습니다.
PANIC: could not write to file "pg_xlog/xlogtemp.559": No space left on device
또는 디스크 부족과 관련된 다른 증상이 보고될 수 있습니다.
NOTE
관리자는 노드 장애를 모니터링(참고: Monitoring)하고, 노드에 충분한 여유 디스크 공간이 있는지 확인해야 합니다.
노드 제거는 pgactive.pgactive_detach_nodes() 함수를 사용해 수행합니다. 제거할 노드의 이름(노드 생성 시 지정한 이름)을 명시해야 합니다. 제거할 노드에서가 아니라, pgactive 그룹에 계속 남아 있을 노드에서 pgactive.pgactive_detach_nodes()를 호출해야 합니다. 여러 노드를 한 번에 제거할 수도 있습니다. 이 함수는 값을 반환하지 않으며, 제거 상태는 해당 노드의 pgactive.pgactive_nodes 뷰에서 status 필드를 확인해 알 수 있습니다.
예를 들어 node2를 제거하기 위해, node1에서 다음을 실행합니다.
SELECT pgactive.pgactive_detach_nodes(ARRAY['node-2']);
또는 여러 노드를 한 번에 제거하려면, node1에서 다음을 실행합니다.
SELECT pgactive.pgactive_detach_nodes(ARRAY['node-2', 'node-3']);
이미 제거된 pgactive 노드나, 베이스 백업에서 복원된 노드를 일반 PostgreSQL 데이터베이스로 되돌리려면 pgactive.pgactive_remove() 함수를 사용할 수 있습니다.
pgactive.pgactive_remove()를 실행한 후에는 DROP EXTENSION pgactive를 안전하게 수행할 수 있습니다. 이 시점에서 로컬 데이터베이스의 모든 pgactive 전용 요소가 제거되며, 해당 데이터베이스를 독립형 데이터베이스로 사용할 수 있습니다. 글로벌 시퀀스는 로컬 시퀀스로 변환되며, 일반적으로 사용할 수 있습니다. 로컬 노드에서 pgactive 트리거, 이벤트 트리거, 시큐리티 레이블, 슬롯, replication identifier 등은 모두 제거됩니다.
또한, pgactive.pgactive_remove() 실행 이후에 pgactive.pgactive_create_group()을 사용해 이 데이터베이스를 시작 노드로 하는 새로운 pgactive 그룹을 생성하는 것도 가능합니다. 새 그룹은 기존 그룹과 완전히 독립적입니다.
pgactive가 자신이 여전히 기존 노드 그룹에 조인되어 있다고 판단하는 경우, 실행 중인 노드를 불완전하게 제거하는 것을 방지하기 위해 pgactive.pgactive_remove()는 실행을 거부합니다.
노드가 실제로 그룹에서 제거되었거나, 정상적으로 동작 중인 노드의 복제본(예: PITR 백업이나 디스크 스냅샷에서 복원한 경우)임이 확실하다면, pgactive.pgactive_remove(true)를 호출하여 강제로 제거할 수 있습니다. 그룹에서 이미 분리된 노드에서만 이 작업을 수행해야 합니다. 예를 들어, 노드가 분리(disconnect)된 상태에서 제거되었거나, PITR 백업이나 디스크 스냅샷에서 복원된 경우 등입니다. 그렇지 않을 경우 다른 노드에 미사용 replication slot 등이 남아 있어, 남아 있는 노드들에 문제를 일으킬 수 있습니다. 항상 먼저 pgactive.pgactive_detach_nodes()를 사용해 노드를 제거한 뒤에 위 함수를 사용해야 합니다.
pgactive는 PostgreSQL 9.6+의 기본 n-safe 동기 복제 기능을 사용하도록 설정할 수 있습니다. 각 노드는 synchronous_standby_names에 우선순위 순으로 다른 노드 목록과, 커밋이 상류 노드에서 승인되기 전에 재생을 확인해야 하는 최소 노드 수를 설정할 수 있습니다. PostgreSQL은 목록에서 가장 높은 우선순위를 갖고 현재 연결된 노드가 커밋 재생과 로컬 플러시를 확인할 때까지 클라이언트에 대한 COMMIT 확인을 지연시킵니다.
각 pgactive apply 워커가 상류 노드에 연결할 때 사용하는 application_name은 nodename:send입니다. 이는 피어로부터의 연결이 pg_stat_activity에 어떻게 표시되는지, 그리고 synchronous_standby_names에서 어떤 이름이 사용되는지를 의미합니다. synchronous_standby_names에서 노드 이름은 반드시 이중 따옴표(double quote)로 감싸야 합니다.
일반적인 구성 예시는 두 개의 상호 동기 1-safe 쌍으로 구성된 4개 노드입니다. 노드 이름이 A, B, C, D이고, A는 B와, C는 D와 각각 동기화되도록 구성하려면 각 노드의 설정은 다음과 같습니다.
conf# node A에서: synchronous_standby_names = '1 ("B:send")' pgactive.synchronous_commit = on # node B에서: synchronous_standby_names = '1 ("A:send")' pgactive.synchronous_commit = on # node C에서: synchronous_standby_names = '1 ("D:send")' pgactive.synchronous_commit = on # node D에서: synchronous_standby_names = '1 ("C:send")' pgactive.synchronous_commit = on
이 구성에서 B가 다운되면 A에서의 커밋은 무기한 대기하게 되며, 그 반대의 경우도 마찬가지입니다. 이를 원치 않는다면 각 노드가 다른 노드를 보조 동기 옵션(예: WAN 상 더 높은 지연을 허용)으로 사용할 수 있습니다. 예를 들어:
conf# node A에서 B에 대한 동기 복제를 선호하지만, B가 다운된 경우 # C 또는 D 중 하나가 도달 가능하고 따라잡았으면 COMMIT 확인을 허용: synchronous_standby_names = '1 ("B:send","C:send","D:send")'
세 노드 모두(B, C, D)가 커밋 재생을 확인해야 로컬 커밋이 승인되도록 하려면 3-safe를 사용합니다.
conf# A에서 로컬 커밋이 보이기 전에 B, C, D 모두가 # 커밋 재생을 확인하도록 요구 synchronous_standby_names = '3 ("B:send","C:send","D:send")'
PostgreSQL 매뉴얼의 동기 복제 부분에서 PostgreSQL의 동기 복제가 어떻게 동작하는지에 대한 설명을 참고하십시오. 반대편이 물리적 스탠바이가 아니라 pgactive 노드인 경우에도 대부분의 원리는 동일하게 적용됩니다.
참고: PostgreSQL의 동기 복제는 상류 노드에서 커밋을 수행한 후 다운스트림 노드로 복제하지만, 다운스트림이 완료될 때까지 그 커밋을 다른 동시 트랜잭션으로부터 숨깁니다. 상류 노드가 재시작되면, 다운스트림이 아직 응답하지 않았더라도 숨겨진 커밋이 보이게 되므로, 노드 재시작은 사실상 일시적으로 동기 복제를 비활성화하는 효과를 가집니다.
synchronous_standby_names에 나열된 피어에서 동기 복제를 사용할 경우, 일반적으로 해당 피어들에 pgactive.synchronous_commit = on을 설정하는 것이 좋습니다. 이는 피어의 커밋 확인 속도를 높여 COMMIT이 최소한의 지연으로 반환되도록 돕습니다.
COMMIT 확인 지연을 줄이고 처리량을 높이기 위해, 사용자는 중요하지 않은 트랜잭션에 대해 다음과 같이 실행할 수 있습니다.
SET LOCAL synchronous_commit = off;
이는 개별 트랜잭션에 대해 사실상 동기 복제를 비활성화하는 효과를 갖습니다.
PostgreSQL의 물리 복제와 달리, 논리 디코딩(따라서 pgactive)은 트랜잭션이 원본 노드에서 커밋되기 전에는 피어 노드로 복제를 시작할 수 없습니다. 이 때문에, 큰 트랜잭션은 동기 복제를 사용할 경우 COMMIT 지연이 길어질 수 있습니다. 큰 트랜잭션이 synchronous_commit = off로 실행되더라도, 논리 디코딩이 트랜잭션을 엄격한 커밋 순서대로 처리하기 때문에, 이후에 커밋되는 작은 동기 트랜잭션의 커밋 확인을 지연시킬 수 있습니다.
동기 복제를 활성화해 두더라도, 노드 간 락킹이 수행되지 않기 때문에 2노드 상호 동기 구성에서도 충돌은 여전히 발생할 수 있습니다.
많은 애플리케이션은 데이터베이스 항목에 고유한 값을 할당해야 합니다. 일부 애플리케이션은 외부 프로그램에서 생성한 UUID/GUID를 사용하고, 일부는 데이터베이스가 제공하는 값을 사용합니다. 이는 낙관적 충돌 해결 방식(pgactive에서 사용하는 방식)에서 중요합니다. 고유성 위배는 충돌 해결 중에 삽입이 폐기되는 결과를 초래할 수 있기 때문입니다.
SQL 표준은 고유한 값을 생성하는 SEQUENCE 객체를 요구하며, PostgreSQL의 SERIAL 의사 타입과 마찬가지로 DEFAULT nextval('mysequence')를 사용해 기본값으로 사용할 수 있습니다. PostgreSQL은 시퀀스를 동기화하거나 복제하는 기능을 제공하지 않으므로, 시퀀스는 철저히 노드 로컬입니다.
샤딩 또는 다중 노드 애플리케이션에서 일반적인 접근 방식은 "분할 스텝(partitioned)" 시퀀스를 사용하는 것입니다. 여기서는 모든 노드가 동일한 고정 값만큼 시퀀스를 증가시키고, 각 노드는 시퀀스 내에서 고정된 오프셋을 가집니다. 예를 들어 노드 1은 1, 101, 201, 301, ...을 생성하고, 노드 2는 2, 102, 202, 302, ...와 같이 생성합니다. 이는 PostgreSQL의 기존 시퀀스 기능으로 쉽게 구현할 수 있지만, 성장이 충분히 고려되지 않은 경우 큰 문제가 될 수 있습니다. 예를 들어 위와 같은 방식에서 101개 노드를 사용하려 하면 심각한 문제가 발생합니다.
또한 이 방식은 노드별 DDL 및 설정이 필요하기 때문에 번거롭고, 장애 노드를 교체하는 작업도 까다롭습니다. 장애 전에 각 노드에서 각 테이블의 시퀀스가 어느 값까지 증가했는지 확인하기 위해 테이블을 스캔해야 하거나, 매우 제한적인 새로운 노드 ID를 할당해야 합니다.
동시 삽입에서 액티브-액티브 충돌을 피하기 위해, pgactive는 글로벌 시퀀스 매핑 함수를 제공합니다. 이를 통해 일반 시퀀스 결과에 고유 노드 ID와 타임스탬프를 결합해 글로벌하게 고유한 방식으로 사용할 수 있습니다. 구체적으로는 40비트 타임스탬프, 10비트 node_id, 14비트 시퀀스를 사용합니다. 타임스탬프를 사용함으로써 클러스터 전체의 삽입에 대해 대략적인 시간 순서를 제공합니다.
pgactive는 내부적으로 노드 ID를 관리합니다(pgactive.pgactive_nodes.node_seq_id 컬럼 참조). 분리(detach)된 노드의 ID는 재사용되므로, 정기적으로 노드를 분리·조인하는(예: 로드 밸런싱 목적) 환경에서도 노드 ID 고갈은 문제가 되지 않습니다.
글로벌 시퀀스를 사용하려면, 먼저 일반적인 방식대로 CREATE SEQUENCE ...를 사용해 로컬 시퀀스를 생성합니다. 그리고 nextval(seqname) 대신 pgactive.pgactive_snowflake_id_nextval(seqname)을 사용해 값을 얻습니다. 결과는 64비트이므로 대상 컬럼 타입은 BIGINT여야 합니다.
sqlCREATE TABLE gstest ( id bigint primary key, parrot text ); CREATE SEQUENCE gstest_id_seq OWNED BY gstest.id; ALTER TABLE gstest ALTER COLUMN id SET DEFAULT pgactive.pgactive_snowflake_id_nextval('gstest_id_seq');
일반적으로 BIGSERIAL 컬럼으로 시퀀스를 생성하는 경우에도, 이 방식을 그대로 사용할 수 있습니다. 단, 글로벌 시퀀스를 사용하려면 테이블 생성 후 해당 컬럼의 DEFAULT 표현식을 ALTER로 변경해야 합니다. 현재 이를 자동 및 투명하게 수행하는 기능은 없으므로, 다음과 같은 후속 명령으로 직접 수행해야 합니다.
ALTER TABLE my_table ALTER COLUMN my_bigserial SET DEFAULT pgactive.pgactive_snowflake_id_nextval('my_table_my_bigserial_seq');
pgactive의 액티브-액티브 특성상, 서로 다른 노드에서 동일하거나 관련된 테이블에 동시에 쓰기를 수행하면 데이터 충돌이 발생할 수 있습니다. 일부 클러스터링 시스템은 이러한 동시 접근을 막기 위해 분산 락 메커니즘을 사용합니다. 이 방식은 서버들이 매우 가까이에 있을 경우에는 어느 정도 성능을 낼 수 있지만, 지리적으로 분산된 애플리케이션에서는 낮은 지연 시간이 절대적으로 중요하기 때문에 적합하지 않습니다.
분산 락킹은 본질적으로 비관적(pessimistic) 접근 방식인 반면, pgactive는 낙관적(optimistic) 접근 방식을 채택합니다. 즉, 가능한 한 충돌을 피하되, 일부 종류의 충돌은 발생하도록 허용하고, 발생 시 이를 해결하는 방향입니다.
노드 간 충돌은 관련된 모든 트랜잭션이 단일 노드에서 동시에 실행되었다면 발생할 수 없었을 사건 시퀀스의 결과로 발생합니다. 노드들은 트랜잭션 커밋 이후에만 변경 사항을 교환하므로, 각 트랜잭션은 자신이 커밋된 노드에서는 개별적으로 유효하지만, 그 사이에 다른 작업이 수행된 또 다른 노드에서 실행되면 유효하지 않을 수 있습니다.
pgactive apply는 본질적으로 다른 노드에서 해당 트랜잭션을 재실행(replay)하는 방식이므로, 적용 중인 트랜잭션과 수신 노드에서 이미 커밋된 트랜잭션 간에 충돌이 있으면 재실행이 실패할 수 있습니다.
대부분의 충돌이 단일 노드에서 모든 트랜잭션을 실행했을 때 발생하지 않는 이유는, PostgreSQL이 이를 방지하기 위한 트랜잭션 간 통신 메커니즘을 가지고 있기 때문입니다. 예를 들어, UNIQUE 인덱스, SEQUENCE, 행 및 릴레이션 락킹, SERIALIZABLE 의존성 추적 등입니다. 이 모든 메커니즘은 트랜잭션 간 통신을 통해 바람직하지 않은 동시성 문제를 방지하는 역할을 합니다.
pgactive에는 분산 트랜잭션 관리자나 분산 락 관리자가 없습니다. 이는 지연 시간과 네트워크 파티션 환경에서 우수한 성능을 보이는 이유 중 하나입니다. 그 결과, 서로 다른 노드에서 실행되는 트랜잭션은 완전히 서로 격리된 상태에서 수행됩니다. 일반적으로 "격리가 높을수록 좋다"고 생각하기 쉽지만, 실제로는 충돌을 피하려면 격리를 줄여야 할 때도 있습니다.
대부분의 경우, 적절한 애플리케이션 설계를 통해 충돌을 회피하거나, 애플리케이션을 충돌 허용 방식으로 설계할 수 있습니다. 충돌은 여러 노드에서 동시에 작업이 수행될 때만 발생하므로, 가장 단순한 충돌 회피 방법은 단일 노드에만 쓰기를 수행하거나, 각 노드에서 독립적인 데이터베이스 부분에만 쓰기를 수행하는 것입니다.
예를 들어, 각 노드에 서로 다른 스키마를 두고, 모든 노드가 서로 데이터는 교환하되, 특정 스키마에 대한 쓰기는 해당 스키마를 "소유"한 노드에서만 수행하도록 할 수 있습니다.
INSERT 대 INSERT 충돌에 대해서는, 글로벌 시퀀스를 사용하면 충돌을 완전히 방지할 수 있습니다.
충돌이 허용될 수 없는 경우, pgactive 사용자들은 애플리케이션 레벨에서 분산 락킹을 수행하는 것이 유용할 수 있습니다. 가장 바람직한 접근 방식은 종종 충돌 발생을 허용하되, 애플리케이션을 pgactive의 충돌 해결 메커니즘과 잘 연동되도록 설계해 충돌을 처리하는 것입니다.
액티브-액티브 충돌의 진단 및 처리를 용이하게 하기 위해, pgactive는 각 충돌 사건을 pgactive.pgactive_conflict_history 테이블에 기록하는 기능을 지원합니다. 이 테이블에 대한 충돌 로깅은 pgactive.log_conflicts_to_table이 true일 때만 활성화됩니다.
또한, log_min_messages가 LOG 이하이고 pgactive.log_conflicts_to_logfile이 true인 경우, pgactive.log_conflicts_to_table 값과 무관하게 PostgreSQL 로그 파일에도 충돌이 기록됩니다.
충돌 이력 테이블을 사용해 애플리케이션이 얼마나 자주, 어떤 위치에서 충돌을 발생시키는지 파악할 수 있으며, 이를 통해 애플리케이션을 개선해 충돌 빈도를 낮출 수 있습니다. 또한, 충돌 해결 결과가 원하는 방식으로 적용되지 않은 경우를 탐지해, 사용자 정의 충돌 트리거를 추가하거나 애플리케이션 설계를 변경해야 할 지점을 식별하는 데 도움을 줍니다.
행 충돌에 대해서는, 선택적으로 행 값을 로깅할 수 있습니다. 이는 데이터베이스 전체에 적용되는 글로벌 옵션 pgactive.log_conflicts_to_table에 의해 제어됩니다. 현재 시점에서는 테이블별로 행 값 로깅을 제어할 수 있는 기능이 없습니다. 또한, 행에 포함될 수 있는 필드 수, 배열에 덤프되는 요소 수, 필드 길이 등에 대한 제한이 없으므로, 다중 메가바이트 크기의 행이 자주 충돌을 유발하는 환경에서는 이 기능을 활성화하지 않는 것이 좋습니다.
충돌 이력 테이블에는 데이터베이스 내의 모든 테이블에 대한 정보가 포함되며, 각 행의 스키마가 서로 다를 수 있으므로, 행 값이 로깅될 경우 JSON 필드로 저장됩니다. JSON은 PostgreSQL의 row_to_json() 함수를 사용해 생성되며, 이는 SQL에서 직접 행에 대해 row_to_json()을 호출하는 것과 동일한 결과입니다.
현재 PostgreSQL에는 이 JSON을 다시 튜플로 복원하는 json_to_row() 함수가 없으므로, 저장된 JSON으로부터 복합 타입 튜플을 재구성하려면 테이블별(예: pl/pgsql, pl/python, pl/perl 등) 코드를 작성해야 합니다.
pgactive가 구현한 논리 복제와 물리 복제 간의 주요 차이점은 다음과 같습니다.
VACUUM, hint bit 등의 데이터는 네트워크로 전송되지 않으므로, 특히 full_page_writes를 사용하는 물리 복제에 비해 대역폭 요구량이 줄어들 수 있습니다.hot_standby_feedback을 사용할 필요가 없으며, 핫 스탠바이에서 장기 실행 쿼리를 취소할 필요도 없으므로, ["cancelling statement due to conflict with recovery"] 오류가 발생하지 않습니다.ALTER SYSTEM 또는 CREATE ROLE 같은 명령은 pgactive에 의해 복제되지 않으므로, 관리자에 의해 별도로 관리해야 합니다.pgactive는 노드가 비-pgactive 논리 복제 소스에서 변경을 가져오는 것을 허용하지 않습니다. 즉, pgactive 노드는 PostgreSQL의 기본 논리 복제 또는 pglogical 확장에서 구독자(subscriber)로 사용할 수 없습니다.
그러나 비-pgactive 노드는 여전히 pgactive 노드에서 변경을 가져올 수 있습니다. 예를 들어, pgactive 노드는 PostgreSQL 논리 복제에서 퍼블리셔(publisher)로 사용할 수 있습니다.
pgactive는 모든 애플리케이션에 바로 적용 가능한(drip-in) 솔루션이 아닙니다. 액티브-액티브 데이터베이스 클러스터를 위해 pgactive를 사용하는 애플리케이션은 안전하게 동작할 수 있도록 특정 설계 결정을 내려야 합니다. 쓰기 트래픽을 단일 pgactive 인스턴스로 라우팅하는 경우에도, 애플리케이션이 액티브-액티브 복제 토폴로지를 지원하도록 아키텍처를 설계해야 합니다. 이 섹션에서는 pgactive 기능 개요와, pgactive를 사용하는 애플리케이션 설계 시 고려해야 할 사항을 설명합니다.
pgactive를 사용할 때는 충돌과 그 해결 방안을 미리 계획해야 합니다. pgactive를 사용하는 각 데이터베이스 인스턴스는 독립적인 인스턴스이며, 어떤 소스에서든 데이터 변경을 수용할 수 있습니다. 변경이 데이터베이스 인스턴스로 전송되면, PostgreSQL은 해당 변경을 로컬에 커밋하고, 그 후 pgactive를 사용해 비동기 방식으로 다른 시스템에 복제합니다. 두 PostgreSQL 인스턴스 간에 pgactive를 사용한 비동기 복제는 성능과 가용성 측면에서 유리하지만, 두 인스턴스가 동시에 동일 레코드를 갱신하는 상황이 발생할 수 있습니다.
pgactive는 충돌 감지 및 자동 해결 메커니즘을 제공합니다. pgactive는 양쪽 시스템에서 트랜잭션이 커밋된 시점을 추적하고, 타임스탬프가 더 늦은 변경을 자동으로 적용합니다. 또한, 충돌이 발생하면 pgactive.pgactive_conflict_history 테이블에 기록하므로, 관리자가 다른 해결 방법을 수동으로 선택할 수 있습니다. pgactive가 자동 충돌 해결 기능을 제공하더라도, 애플리케이션 일부는 오래된(stale) 데이터를 수신했을 수 있으므로, 이러한 상황을 고려하여 애플리케이션을 설계해야 합니다.
충돌 감지의 일환으로, pgactive는 변경된 행을 모두 검사해야 합니다. 이 작업에는 충돌 감지를 위해 영향을 받는 행을 로딩해야 합니다. pgactive는 기본 키(primary key)를 통해 이를 수행합니다. 기본 키는 테이블의 각 행을 유일하게 식별하는 제약 조건으로, 행을 효율적으로 조회하기 위한 수단입니다. 따라서 pgactive는 모든 테이블에 기본 키가 존재할 것을 요구합니다. 즉, pgactive를 사용할 애플리케이션은 모든 테이블에 기본 키 제약 조건을 포함해야 합니다.
또한, 중복 또는 누락된 행을 일으킬 수 있는 특정 유형의 충돌을 방지하기 위해, pgactive는 기본 키 제약이 걸린 컬럼에 대한 직접 업데이트를 금지합니다. 대부분의 애플리케이션 개발 프레임워크는 테이블에 기본 키를 자동으로 추가하지만, pgactive를 사용할 애플리케이션이 이 제약 조건을 포함하는지 반드시 확인해야 합니다.
개발자들은 PostgreSQL에서 기본 키로 정수 시퀀스를 자주 사용합니다. 예를 들어 PostgreSQL의 serial, bigserial 타입이나 SQL 표준의 GENERATED BY DEFAULT AS IDENTITY 구문을 사용할 수 있습니다. 그러나 PostgreSQL에서 기본값인 1씩 증가하는 정수 시퀀스는 액티브-액티브 복제 구성에서 충돌을 유발합니다.
pgactive는 충돌 없는 시퀀스를 생성하고, 기존 시퀀스를 충돌 없는 시퀀스로 변환하는 함수(SELECT pgactive.convert_local_seqs_to_snowflake_id_seqs())를 제공합니다. 추가로, pgactive는 시퀀스 래퍼라운드 위험을 줄이기 위해 정수 기반 시퀀스가 64비트(bigint)여야 한다고 요구합니다. 대신 UUID 등 다른 식별자를 사용할 수도 있습니다.
pgactive는 스키마 변경(DDL; data definition language 명령)의 복제를 허용하지 않습니다. 스키마 변경을 적용하려면 2단계 커밋(2PC)을 사용하는 조정된 롤아웃이 필요합니다.
pgactive는 대용량 객체(large object) 관련 명령에 대한 지원을 제공하지 않으므로, 애플리케이션은 대용량 객체 저장을 위해 bytea 타입이나 Amazon Simple Storage Service(Amazon S3) 같은 다른 저장 메커니즘을 사용해야 합니다. pgactive는 모든 인스턴스에서 동일한 버전의 텍스트 정렬(collation) 제공자를 사용하도록 보장하며, 제공자 버전이 일치하지 않을 경우 노드 조인을 차단합니다.
마지막으로, 성능과 정합성을 최적화하기 위한 네트워크 토폴로지 관련 결정이 필요합니다. pgactive를 사용한 액티브-액티브 복제에서 PostgreSQL 인스턴스 간 네트워크 지연은 정합성에 직접적인 영향을 미칩니다. 지연 시간이 길어질수록 충돌 발생 위험이 증가하기 때문입니다. 또한, 큰 트랜잭션은 각 영향을 받은 행이 대상 인스턴스에서 재실행 및 적용되어야 하므로, 복제 지연에 영향을 줄 수 있습니다.
pgactive는 여러 인스턴스에 대한 쓰기 부하 분산(load balancing) 용도로 사용해서는 안 됩니다. 이는 쓰기 성능을 향상시키지 않을 뿐 아니라, 충돌 발생 위험을 크게 증가시키기 때문입니다.
UPDATE와 DELETE가 허용되지 않습니다. 기본 키 컬럼의 값은 변경해서는 안 됩니다.