6. 데이터베이스 · 테이블 운영

이 장을 마치면: GritDB가 관리할 테이블을 등록하고 데이터를 받고, 상태를 보고, 분석 세션으로 조회한다. 등록 테이블에 하지 않을 작업을 안다.

  • 이 장의 순서: 등록 → 데이터 받기 → 상태 보기 → 분석 세션 → 하지 않는 작업. 그 뒤에 데이터 넣기(6.8)와 분석 조회 관리(6.9)가 이어진다.

6.1 데이터베이스 만들기

새 데이터베이스는 template1을 복제해 만든다(PostgreSQL 기본값, 템플릿 데이터베이스). GritDB는 template1에 gritsurfer 확장을 넣어 두므로 새 데이터베이스에서 바로 등록할 수 있다.

CREATE DATABASE <db>;
  • ~/gritdb1/bin/psql -p 5432 -d <db>로 접속한 뒤 \dx gritsurfer를 실행해 gritsurfer 줄이 있는지 확인한다.
  • SHOW search_path;를 실행해 값에 gritdb가 있는지 확인한다. 없으면 등록 함수와 조회 함수에 gritdb.를 붙여 부른다(예: gritdb.register_gritsurfer_table).

TEMPLATE template0이나 다른 템플릿으로 만든 데이터베이스에는 확장이 없다. 그 데이터베이스에 서비스 계정(superuser)으로 접속해 확장을 만든다.

CREATE EXTENSION gritsurfer;
  • \dx gritsurfer를 실행해 gritsurfer 줄이 있는지 확인한다. 명령 설명은 CREATE EXTENSION에 있다.
  • gritsurfer 확장을 지우지 않는다(DROP EXTENSION). 지우면 그 데이터베이스의 GritDB 함수(등록, 해제, 조회)가 함께 지워진다. CREATE EXTENSION gritsurfer로 다시 만들면 함수 실행 권한이 설치 기본값(모든 계정)으로 돌아간다. 이때는 그 데이터베이스에서 11.5의 거두기를 다시 하고 11.10의 4행으로 본다.

⚠ postgres 데이터베이스를 지우거나 이름을 바꾸지 않는다

  • 이유: GritDB는 기동할 때 postgres 데이터베이스에 접속한다. 없으면 GritDB가 기동하지 않는다.
  • 대신: 업무 데이터는 새 데이터베이스(CREATE DATABASE <db>;)에 둔다.

6.2 등록 조건

등록할 테이블을 고른다.

  • 함께 조회하는 테이블은 모두 등록한다. 하나라도 빠지면 그 조회는 PostgreSQL이 실행한다(6.6).
  • 쓰기가 아주 많은 테이블은 등록한 뒤 쓰기마다 GritDB 엔진의 수신 확인을 기다린다(1.1).
  • 행 단위 보안(RLS) 정책이 있는 테이블과 권한으로 나눠 보여야 하는 테이블은 등록하지 않는다(11.5, 11.6). 권한으로 나눠 보여야 하는 테이블은 계정마다 읽을 수 있는 행이나 열이 다른 테이블이다.

등록 조건은 아래와 같다.

  • REPLICA IDENTITY FULL이 필수다. 설정하지 않으면 register_gritsurfer_table이 그 테이블을 거부한다.
  • REPLICA IDENTITY FULL이 이미 설정돼 있으면 다시 실행하지 않는다. 이 ALTER TABLE은 그 테이블을 잠그고, 돌던 autovacuum을 멈춘다.
    • SELECT relreplident FROM pg_class WHERE oid = '<s>.<t>'::regclass;를 실행해 f가 나오는지 확인한다.
  • 등록 대상은 일반 테이블과 파티션 테이블이다(6.3). 뷰와 외부 테이블은 Table <s>.<t> must have REPLICA IDENTITY FULL. Current setting: NOTHING. …으로 먼저 거부된다. 이 객체에는 REPLICA IDENTITY를 둘 수 없으므로 문구 끝의 ALTER TABLE을 따라 하지 않는다. 구체화 뷰와 그 밖의 객체는 GritSurfer: cannot register relation "<t>"로 거부된다.
  • 지원하는 열 형식은 아래 15종이다. 그 밖의 형식이 한 열이라도 있으면 거부한다(GritSurfer: Table '<t>' has unsupported column types - cannot proceed).
종류 형식
논리 boolean
정수 smallint, integer, bigint
실수와 숫자 real, double precision, numeric
날짜와 시간 date, time, timestamp, timestamptz, interval
문자 char(n), varchar, text
  • 거부되는 형식의 예: uuid, bytea, json, jsonb, 배열, enum, 도메인, timetz

  • 형식 점검(등록 전)은 지원 밖 형식의 열을 찾는다. 운영 DB나 GritDB 어디서나 실행한다.

    SELECT a.attname, a.atttypid::regtype
    FROM pg_attribute a
    WHERE a.attrelid = '<s>.<t>'::regclass AND a.attnum > 0 AND NOT a.attisdropped
      AND a.atttypid NOT IN ('boolean'::regtype, 'smallint'::regtype, 'integer'::regtype, 'bigint'::regtype,
        'real'::regtype, 'double precision'::regtype, 'numeric'::regtype, 'date'::regtype, 'time'::regtype,
        'timestamp'::regtype, 'timestamptz'::regtype, 'interval'::regtype, 'bpchar'::regtype,
        'varchar'::regtype, 'text'::regtype);
    

    결과가 0행이면 정상이다. char(n)은 bpchar다.

  • 따옴표가 붙은 1바이트 형 "char"은 지원하지 않는다. char(n)과 다른 형식이다.

  • numeric은 정밀도를 38 이하로 정한 열(numeric(p, s), p ≤ 38)만 분석 세션 값이 정확하다. 아래 정밀도 점검을 본다(10.3).

  • 빈 테이블일 때 등록한다. 등록 전에 있던 행은 분석 세션 결과에서 빠진다(6.3).

  • 기본 키나 NOT NULL 고유 제약이 없으면 등록은 된다. 다만 WARNING(… has no PRIMARY KEY or UNIQUE(NOT NULL) constraint. Registering in degraded mode …)을 보이고, 수정과 삭제의 반영이 느리다.

  • 분석계(6.4)에서는 운영 DB와 같은 기본 키를 꼭 둔다. 없으면 수정과 삭제의 적용이 오류로 멈춘다(6.4.1의 6번).

  • 테이블은 스키마 이름을 붙여 만든다(CREATE TABLE <s>.<t> …). 서비스 계정으로 스키마 없이 만들면 gritdb 스키마에 생긴다(11.5).

  • 스키마 이름과 테이블 이름은 대소문자를 구분해 정확히 쓴다. 따옴표 없이 만든 이름은 소문자다(CREATE TABLE Sales → 'sales', CREATE TABLE "Sales" → 'Sales'). 틀리면 Relation <s>.<t> not found로 거부한다.

  • 열을 지운 적이 있는 테이블(DROP COLUMN)은 형식 점검을 통과해도 cache lookup failed for type 0으로 등록이 거부된다. 이때는 6.7의 테이블 다시 만들기(4번 빼고)로 새로 만든 뒤 등록한다. SELECT count(*) FROM pg_attribute WHERE attrelid = '<s>.<t>'::regclass AND attisdropped;를 실행해 0이 나오는지 확인한다.

정밀도 점검(등록 전)은 정밀도를 정하지 않았거나 38을 넘는 numeric 열을 찾는다. 운영 DB나 GritDB 어디서나 실행한다.

SELECT table_schema, table_name, column_name, numeric_precision, numeric_scale
FROM information_schema.columns
WHERE data_type = 'numeric'
  AND (numeric_precision IS NULL OR numeric_precision > 38)
  AND table_schema NOT IN ('pg_catalog', 'information_schema', 'gritdb')
ORDER BY 1, 2, 3;
  • 결과가 0행이면 정상이다. 나온 열은 분석 세션에서 근사값이다. 금액이나 수량 열이면 numeric(p, s)(p ≤ 38)로 만든 테이블에 받는다.
  • 받을 테이블만 보려면 AND table_name IN ('<t1>', '<t2>')를 더한다.
  • 운영 DB가 PostgreSQL이 아니면 CDC 도구나 ETL 도구가 원본의 정밀도 없는 숫자 열을 정밀도 없는 numeric으로 만드는지 확인한다.

6.3 등록 · 해제

작업 SQL
등록 SELECT register_gritsurfer_table('<s>', '<t>');
해제 SELECT unregister_gritsurfer_table('<s>', '<t>');
  • 테이블은 비어 있을 때 등록한다. 등록 전에 있던 행은 GritDB 엔진으로 옮기지 않는다(10.4).
  • ⚠ 등록하거나 해제한 뒤에는 그 테이블을 쓰는 접속을 다시 연다. 각 접속은 테이블에 처음 쓸 때 확인한 등록 여부를 접속이 끝날 때까지 쓴다. 그래서 등록 전에 그 테이블에 쓴 접속의 변경은 GritDB 엔진으로 넘어가지 않는다.
    • 응용 프로그램에서는 연결 풀을 다시 시작한다.
    • CDC 도구나 ETL 도구로 받는 구성에서는 등록이나 해제 뒤에 도구의 적용 프로세스를 다시 시작한다.
    • 분석계(내장 복제로 받는 구성)에서는 subscription을 만들기 전에 등록한다. 이미 있는 subscription은 ALTER SUBSCRIPTION <sub> DISABLE;을 실행하고 작업자가 멈춘 것을 본다. 그 뒤 ALTER SUBSCRIPTION <sub> ENABLE;로 복제 작업자를 다시 띄운다(6.4.2의 6번).
  • 등록과 해제는 트랜잭션 블록(BEGIN … COMMIT), DO 블록, 함수 안에서 하지 않는다. ROLLBACK해도 등록과 해제는 되돌아가지 않는다.
  • 해제하기 전에는 그 테이블 쓰기를 멈춘다. 반영은 기다리지 않아도 된다. 해제하면 그 테이블의 반영되지 않은 행은 GritDB 엔진이 버린다. unify checkpoint는 멈추지 않으므로 다른 등록 테이블의 반영은 늦어지지 않는다. unify checkpoint는 GritDB 엔진이 영구 반영을 마친 WAL 위치이며, PostgreSQL checkpoint와 다르다.

⚠ 해제는 되돌릴 수 없다

  • 이유: 해제하면 그 테이블의 cold file(GritDB 엔진이 분석 조회용으로 열 단위로 저장한 파일)을 지운다. 다시 등록하면 빈 상태부터 시작한다.
  • 대신: 분석을 잠시 멈추려는 것이면 해제하지 않는다. 다시 만들 때는 6.7 “테이블 다시 만들기” 순서를 따른다.
  • 해제한 테이블의 cold file은 GritDB가 해제 뒤 따로 지운다. 같은 테이블을 곧바로 다시 등록하면 그 정리가 끝날 때까지 기다린다. 기다리는 동안 30초마다 register_gritsurfer_table: waiting for tombstone cleanup … NOTICE가 나온다.
    • 300초 안에 끝나지 않으면 ERROR로 멈춘다. 이때는 새로 만든 테이블로 등록한다(6.7 테이블 다시 만들기).
  • 그 테이블의 cold file 디렉토리는 등록 때가 아니라 첫 행이 반영될 때 생기므로 등록 직후에는 없을 수 있다.
  • 등록 테이블을 지울 때는 먼저 해제한 뒤 DROP TABLE을 실행한다. 해제 없이 DROP TABLE해도 그 트랜잭션이 commit되면 등록이 풀린다. 등록 목록(6.5)에 그 테이블이 없는지 확인한다. ROLLBACK이나 ROLLBACK TO SAVEPOINT이면 등록이 그대로 남는다. DROP DATABASE를 실행하면 그 데이터베이스의 등록이 모두 풀린다. 이때는 postgresql.log(9.1)에 GritSurfer: dropped database <oid>: unregistered <n> relations (failed 0, …) 줄이 있는지 확인한다.
    • commit 때 등록 풀기에 실패하면 끝이 (registration left in place), (registrations left in place), (tombstone written, row left)인 GritSurfer: WARNING이 나온다. DROP은 끝나지만 등록은 남는다. 이때는 지원 문의(0장)를 하고, HINT에 나온 함수는 쓰지 않는다.
    • 이 WARNING은 postgresql.log(9.1)에도 남으므로 응용 프로그램이 흘려도 여기서 찾는다. grep -c -e 'left in place' -e 'row left' ~/gritdb1/log/postgresql.log를 실행해 0이 나오는지 확인한다(7.3).
    • 등록 테이블이나 데이터베이스를 DROP한 트랜잭션은 PREPARE TRANSACTION을 할 수 없다(cannot PREPARE a transaction that dropped a GritDB registered relation or database).
  • 테이블 DROP은 모두 GritDB 카탈로그에 등록 여부를 묻는다. 등록하지 않은 테이블, 임시 테이블, DROP SCHEMA … CASCADE로 지워지는 테이블도 같다. 카탈로그가 5초 안에 답하지 않으면 그 DROP이 GritSurfer: cannot tell whether relation <oid> is registered (catalog did not answer within 5000 ms)로 거부된다. 이때는 4.5로 상태를 본 뒤 다시 한다.

파티션 테이블은 부모와 모든 파티션(중간 단계와 DEFAULT 파티션 포함)을 각각 등록한다. 부모에 건 REPLICA IDENTITY는 파티션으로 번지지 않는다.

  1. 파티션을 모두 찾는다.

    SELECT relid, parentrelid, level, isleaf
    FROM pg_partition_tree('<s>.<t>')
    ORDER BY level;
    
  2. 1번에 나온 테이블마다 ALTER TABLE <테이블> REPLICA IDENTITY FULL;을 실행한다.

  3. 1번 순서(부모 → 파티션, level이 작은 것부터)로 테이블마다 register_gritsurfer_table을 실행한다.

  4. SELECT schema_name, table_name, is_partition FROM get_gritsurfer_tables();를 실행해 1번의 테이블이 모두 있는지 확인한다.

  • 해제는 반대 순서(파티션 → 부모)로 한 뒤 DROP TABLE한다.
  • 등록한 뒤에는 파티션을 붙이거나 떼지 않는다(6.7).

여러 테이블을 등록할 때는 테이블마다 문장 하나씩 실행한다(문장마다 commit). psql에서 이름 목록으로 차례로 등록하는 예:

SELECT format('SELECT register_gritsurfer_table(%L, %L);', schemaname, tablename)
FROM pg_tables
WHERE schemaname = '<s>' AND tablename IN ('<t1>', '<t2>')
ORDER BY tablename
\gexec
  • SELECT table_name FROM get_gritsurfer_tables() WHERE schema_name = '<s>';를 실행해 넣은 테이블이 모두 있는지 확인한다. psql \gexec은 결과의 각 칸을 문장으로 실행한다.
  • 파티션 테이블은 이 방법으로 하지 않고 위 순서를 따른다.

등록과 해제의 출력은 아래와 같다. 등록 한 번에 INFO 줄과 NOTICE 줄이 여러 개 나온다. 성공이면 마지막에 함수 이름과 빈 줄 1행이 나온다(QuickStart 4.4).

등급 문구(앞부분) 뜻과 할 일
INFO GritSurfer: Loaded <n> constraints / <n> indexes for table <s>.<t> 제약과 인덱스를 읽었다
NOTICE GritSurfer: Registered relation - …, GritSurfer: Registered toast relation - … 등록했다(TOAST 테이블도 함께)
WARNING GritSurfer: Table '<s>.<t>' has no PRIMARY KEY or UNIQUE(NOT NULL) constraint. … 기본 키 없이 등록했다. 수정과 삭제의 반영이 느리다(6.2)
WARNING → ERROR GritSurfer: Table '<t>' has unsupported column type: <형식 번호> → … has unsupported column types - cannot proceed 지원 밖 열 형식이 있어 등록하지 않았다(6.2 형식 점검). 형식 이름은 SELECT <형식 번호>::regtype;으로 확인한다
NOTICE register_gritsurfer_table: waiting for tombstone cleanup … 해제 정리를 기다린다(위)
ERROR register_gritsurfer_table: tombstone for OID <oid> not cleared after 300s (GC stalled?) 해제 정리가 300초 안에 끝나지 않았다. 새로 만든 테이블로 등록한다(6.7 테이블 다시 만들기)
NOTICE GritSurfer: relation <oid> (<s>.<t>) is already registered 이미 등록돼 있다. 할 일 없음
WARNING GritSurfer: relation <oid> is already registered as <s>.<t> and keeps that name 이름을 바꾼 등록 테이블이다. 등록은 옛 이름으로 남는다(6.7)
ERROR GritSurfer: relation <oid> (<s>.<t>) is registered with relfilenode <n> but now has <m> VACUUM FULL, CLUSTER, 테이블을 다시 쓰는 ALTER TABLE을 한 테이블이다. 6.7 테이블 다시 만들기를 따른다. 오류의 HINT에 나온 함수는 쓰지 않는다
ERROR GritSurfer: OID <oid> already has a GritDB catalog row for another relation <s>.<t> 지원 문의(0장)를 한다. 오류의 HINT에 나온 함수는 쓰지 않는다
ERROR Table <s>.<t> must have REPLICA IDENTITY FULL. …, Relation <s>.<t> not found 등록 조건이나 이름 문제다. QuickStart GDB-RUNTIME-001을 본다
NOTICE GritSurfer: Unregistering relation - … → GritSurfer: table deleted from catalog … 해제했다
WARNING GritSurfer: metadata absent, deleting without tombstone … 등록돼 있지 않은 테이블을 해제했다. 등록 목록(6.5)을 본다

6.4 분석계로 받기

분석계는 운영 DB의 변경을 받아 분석을 맡는 GritDB 구성이다(1.1). 이 절은 운영 DB가 PostgreSQL이고 내장 복제(PostgreSQL 논리 복제, publication → subscription)로 받는 경우를 다룬다.

  • 운영 DB: PostgreSQL 17. 다른 주 버전이나 관리형 서비스면 붙이기 전에 지원 문의(0장)를 한다. 자세한 것은 6.4.5를 본다.
  • 운영 DB 쪽 명령은 운영 DB 담당이 운영 DB에서 실행한다. GritDB 쪽 명령은 서비스 계정이 GritDB 서버에서 실행한다(~/gritdb1/bin/psql -p 5432 -d <db>).
  • 운영 DB가 PostgreSQL이 아니면 PostgreSQL을 대상으로 쓰는 CDC 도구나 ETL 도구로 받는다. GritDB 쪽 순서(6.4.1의 6~7)는 같다. 등록이나 해제 뒤에는 도구의 적용 프로세스를 다시 시작한다(6.3). 도구의 commit 단위는 초기 적재를 포함해 한 트랜잭션 상한(6.8)의 절반 아래로 둔다. 도구 설정은 도구 문서를 따른다.
  • 크기 확인(붙이기 전)은 받을 테이블의 행 수와 운영 DB 배치가 한 트랜잭션에서 바꾸는 행 수를 한 트랜잭션 상한(6.8)과 비교한다. 상한의 절반을 넘는 것이 있으면 붙이기 전에 지원 문의(0장)를 한다.
  • 물리 대기 서버를 논리 복제의 받는 쪽(subscriber)으로 바꾸는 pg_createsubscriber는 쓰지 않는다. GritDB는 물리 복제로 받지 않는다(1.1). 큰 테이블은 위 크기 확인을 하고 지원 문의를 한다.
  • 표기: <pub> publication, <sub> subscription, <db> 데이터베이스(운영 DB 쪽과 GritDB 쪽 이름이 다르면 각각), <s>.<t> 스키마와 테이블, <복제 계정>, <암호>, <운영 DB 주소>, <포트>(운영 DB 포트), <GritDB 서버 주소>

6.4.1 붙이기

단계 누가 할 일
1 운영 DB 담당 운영 DB 설정 확인
2 운영 DB 담당 복제 키가 없는 테이블 정리
3 운영 DB 담당 publication 만들기
4 운영 DB 담당 복제 계정 만들기와 접속 허용
5 GritDB 담당 작업자 수 맞추기
6 GritDB 담당 같은 테이블 만들기
7 GritDB 담당 등록
8 GritDB 담당 subscription 만들기
9 둘 다 초기 복사 확인과 건수 대조

1. 운영 DB 설정을 확인한다 (운영 DB)

SHOW wal_level;
SHOW max_replication_slots;
SHOW max_wal_senders;
SHOW max_slot_wal_keep_size;
  • wal_level이 logical이면 정상이다. 아니면 ALTER SYSTEM SET wal_level = logical;을 실행하고 운영 DB를 한 번 재시작한다.
  • max_replication_slots와 max_wal_senders는 지금 쓰는 수에 subscription마다 (1 + GritDB의 max_sync_workers_per_subscription, 기본 2)를 더한 값 이상으로 둔다. 모자라면 ALTER SYSTEM SET …으로 바꾼 뒤 운영 DB를 재시작한다. 두 설정 모두 재시작해야 반영되므로 wal_level을 바꿀 때 함께 바꿔 재시작을 한 번에 한다.
  • 운영 DB 쪽 설정 전체: 논리 복제 설정(보내는 쪽)
  • max_slot_wal_keep_size는 운영 DB 담당이 고른다. -1(기본)이면 분석계가 멈춘 동안 운영 DB 디스크가 찰 때까지 WAL이 쌓인다. 상한을 두면 디스크는 지키지만, 넘으면 처음부터 다시 받는다(8.6).

2. 복제 키가 없는 테이블을 찾는다 (운영 DB)

SELECT c.oid::regclass AS table_name
FROM pg_class c
JOIN pg_namespace n ON n.oid = c.relnamespace
WHERE n.nspname = '<s>'
  AND c.relkind IN ('r', 'p')
  AND (c.relreplident = 'n'
       OR (c.relreplident = 'd'
           AND NOT EXISTS (SELECT 1 FROM pg_index i
                           WHERE i.indrelid = c.oid AND i.indisprimary)));
  • 받을 테이블이 목록에 없으면 정상이다.
  • 목록에 있는 테이블에는 3번 전에 ALTER TABLE <s>.<t> REPLICA IDENTITY FULL;을 실행한다.
  • REPLICA IDENTITY FULL이면 그 테이블의 UPDATE와 DELETE가 옛 행 전체를 WAL에 남기므로 WAL이 는다. 또 ALTER TABLE이 잠금을 잡으므로 한가한 때 한다.
  • 고유 인덱스로 REPLICA IDENTITY USING INDEX <인덱스>를 쓰면 GritDB 테이블에 그 인덱스 열로 기본 키를 만든다(6번).

⚠ 복제 키가 없는 테이블을 publication에 넣지 않는다

  • 이유: 운영 DB에서 그 테이블의 UPDATE와 DELETE가 오류로 멈춘다(cannot update table "<t>" because it does not have a replica identity and publishes updates).
  • 대신: 2번에서 복제 키를 먼저 정한다.

3. publication을 만든다 (운영 DB)

CREATE PUBLICATION <pub> FOR TABLE <s>.<t1>, <s>.<t2>;
SELECT schemaname, tablename FROM pg_publication_tables WHERE pubname = '<pub>';
  • 넣은 테이블만 나오면 정상이다. 명령과 뷰 설명은 CREATE PUBLICATION과 pg_publication_tables에 있다.
  • 분석할 테이블만 이름으로 넣는다. FOR ALL TABLES나 FOR TABLES IN SCHEMA를 쓰지 않는다. 이것을 쓰면 나중에 만든 테이블이 2번 점검과 GritDB 준비 없이 들어온다.
  • publish 옵션은 기본값인 insert, update, delete, truncate 그대로 둔다(6.4.2).
  • 운영 DB 배치가 TRUNCATE로 비우고 다시 채우는 테이블은 넣지 않는다. 넣으면 TRUNCATE마다 그 테이블 다시 받기(6.4.2, 초기 복사 포함)를 해야 하고, 하지 않으면 분석 세션에 지운 행이 남는다. 그래서 붙이기 전에 운영 DB 담당에게 TRUNCATE를 쓰는 배치를 묻는다.
  • 행 수가 많은 테이블은 처음 publication에 하나만 넣는다. 다른 테이블은 9번이 r이 된 뒤 하나씩 더한다(6.4.2 “기존 분석계에 테이블 더하기”, 6.8).
  • 운영 DB에 gritdb 스키마가 있으면 그 스키마의 테이블은 넣지 않는다. GritDB가 거부한다.

4. 복제 계정을 만들고 접속을 연다 (운영 DB)

CREATE ROLE <복제 계정> WITH LOGIN REPLICATION;
\password <복제 계정>
GRANT USAGE ON SCHEMA <s> TO <복제 계정>;
GRANT SELECT ON <s>.<t1>, <s>.<t2> TO <복제 계정>;
  • \password를 쓰면 psql이 암호를 암호화해 보내므로 운영 DB 로그와 ~/.psql_history에 암호가 남지 않는다(11.3). psql이 아닌 도구면 그 도구의 암호 바꾸기 창을 쓴다.
  • 운영 DB에서 PUBLIC의 CONNECT를 거뒀으면 GRANT CONNECT ON DATABASE <db> TO <복제 계정>;도 한다.

pg_hba.conf에 한 줄을 더한 뒤 SELECT pg_reload_conf();를 실행한다.

host  <db>  <복제 계정>  <GritDB 서버 주소>/32  scram-sha-256
  • 방화벽은 GritDB 서버에서 운영 DB 포트로 오는 접속만 연다.

  • GritDB 서버에서 ~/gritdb1/bin/psql "host=<운영 DB 주소> port=<포트> dbname=<db> user=<복제 계정> replication=database" -c "IDENTIFY_SYSTEM;"을 실행해 확인한다. 암호를 넣으면 systemid, timeline, xlogpos, dbname 한 줄이 나온다. 이 확인으로 로그인, pg_hba.conf, 복제 권한을 함께 본다(복제 접속).

  • 1.1에는 연결 암호화(TLS)가 없으므로 같은 사설망 안에서만 붙인다(11.8). hostssl이나 관리형 서비스의 SSL 강제 설정으로 운영 DB가 암호화 접속만 받으면 붙을 수 없다.

  • 운영 DB 테이블에 행 단위 보안 정책(RLS)이 있는지 본다. 복제 계정에 BYPASSRLS가 없으면 초기 복사가 그 정책에 걸린 행만 읽어, GritDB에 행이 오류 없이 빠진다.

    SELECT c.oid::regclass
    FROM pg_class c
    JOIN pg_namespace n ON n.oid = c.relnamespace
    JOIN pg_publication_tables p ON p.schemaname = n.nspname AND p.tablename = c.relname
    WHERE p.pubname = '<pub>' AND c.relrowsecurity;
    

    결과가 0행이면 정상이다. GritDB에서 분석 계정 모두가 나온 테이블의 모든 행을 읽어도 되면(11.5 등록 테이블의 보안 조건) 복제 계정에 BYPASSRLS를 준다. 아니면 publication에서 뺀다. 분석 세션은 RLS를 적용하지 않기 때문이다(11.6). 8번 접속 문자열의 options=-crow_security=off는 이런 테이블을 만나면 행을 빼지 않고 복사를 오류로 멈춘다(논리 복제 보안).

5. 작업자 수를 맞춘다 (GritDB)

SHOW max_worker_processes;
SHOW max_logical_replication_workers;
SHOW max_sync_workers_per_subscription;
SHOW max_parallel_workers;
SHOW max_replication_slots;
  • 필요한 값
    • max_logical_replication_workers ≥ subscription 수 × (1 + max_sync_workers_per_subscription)
    • max_worker_processes ≥ 6(GritDB 작업자: 늘 도는 5 + 기동 때 1) + 1(논리 복제 관리 작업자) + max_logical_replication_workers + max_parallel_workers
    • max_replication_slots(GritDB) ≥ subscription 수 × (1 + max_sync_workers_per_subscription). 받는 쪽 진행 기록도 이 값으로 센다.
  • 설치 기본값(PostgreSQL 기본): max_worker_processes 8, max_logical_replication_workers 4, max_sync_workers_per_subscription 2, max_parallel_workers 8, max_replication_slots 10. 이 기본값이면 subscription 하나여도 max_worker_processes가 19 이상이어야 한다. 그래서 처음 붙일 때는 올린다(예: ALTER SYSTEM SET max_worker_processes = 24;). 값은 256 이하로 둔다(아래 ⚠).
  • 모자라면 ALTER SYSTEM SET <설정> = <값>;을 실행하고 5.4의 재기동 순서를 따른다. 재기동 뒤에는 5.6의 shm_limit_mb 경고를 본다(5.4).
  • 초기 복사 둘이 한 트랜잭션 상한을 함께 쓰지 않게 8번 전에 함께 복사하는 테이블을 하나로 줄인다(6.8). ALTER SYSTEM SET max_sync_workers_per_subscription = 1;과 SELECT pg_reload_conf();를 차례로 실행한다(재기동 없음). 9번이 모두 r이 되면 ALTER SYSTEM RESET max_sync_workers_per_subscription;과 SELECT pg_reload_conf();를 차례로 실행한다.
  • 설정: max_logical_replication_workers, max_sync_workers_per_subscription. 받는 쪽 설정 전체: 논리 복제 설정(받는 쪽)
  • ⚠ 작업자 칸이 모자라면 GritDB가 기동하지 못할 수 있으므로 subscription을 만들거나 늘리기 전에 맞춘다.
  • ⚠ max_worker_processes를 256보다 크게 하지 않는다(GritDB가 기동하지 않는다).

6. 같은 테이블을 만든다 (GritDB)

  • 먼저 받을 데이터베이스를 만든다(6.1). postgres 데이터베이스를 쓰지 않는다. 6~9번의 GritDB 쪽 작업은 모두 그 데이터베이스에 접속해 한다(~/gritdb1/bin/psql -p 5432 -d <db>). subscription은 만든 데이터베이스의 테이블에만 적용된다(CREATE SUBSCRIPTION).
  • 운영 DB와 같은 스키마, 테이블 이름, 열 이름, 열 형식, 기본 키로 만든다. 열 순서는 달라도 된다.
  • 예: 운영 DB에서 pg_dump --schema-only --no-owner --no-privileges --table=<s>.<t> <db>로 정의를 뽑아 GritDB에서 실행한다. 스키마가 없으면 먼저 CREATE SCHEMA <s>;를 실행한다. 두 옵션은 운영 DB의 소유자 줄과 권한 줄을 뺀다. 트리거 줄은 환경에 맞게 고친다.
  • pg_dump 출력의 ENABLE ROW LEVEL SECURITY 줄과 CREATE POLICY 줄은 지운다. RLS 정책이 있는 테이블은 등록하지 않는다(11.6). 7번 뒤에 11.6의 확인 쿼리를 실행해 결과가 0행인지 확인한다.
  • 열 형식은 15종만 쓴다(6.2).
  • numeric 열은 p를 38 이하로 정한 numeric(p, s)로 만든다(6.2 정밀도 점검). 운영 DB 열이 정밀도 없는 numeric이면 운영 DB 값의 최대 자릿수를 먼저 재고 그보다 넉넉하게 정한다. 넘는 값이 오면 적용이 멈추고(numeric field overflow), 모자란 소수 자릿수는 반올림된다.
  • 등록 조건: ALTER TABLE <s>.<t> REPLICA IDENTITY FULL;
  • \d+ <s>.<t>를 실행해 기본 키와 Replica Identity: FULL 줄이 있는지 확인한다. \d에는 이 줄이 없다.

⚠ 기본 키 없이 만들지 않는다

  • 이유: 운영 DB가 기본 키나 고유 인덱스로 복제하면 GritDB에서 수정과 삭제의 적용이 오류로 멈춘다(logical replication target relation "<s>.<t>" has neither REPLICA IDENTITY index nor PRIMARY KEY and published relation does not have REPLICA IDENTITY FULL). 적용이 멈춘 동안 운영 DB에 WAL이 쌓인다.
  • 대신: 운영 DB와 같은 기본 키를 둔다(운영 DB가 REPLICA IDENTITY USING INDEX면 그 인덱스 열로). 운영 DB 테이블이 REPLICA IDENTITY FULL이면 멈추지는 않지만 수정과 삭제의 적용이 느리다.

7. 등록한다 (GritDB, subscription 전에)

SELECT register_gritsurfer_table('<s>', '<t>');
SELECT * FROM get_gritsurfer_tables();
  • 받을 테이블이 모두 목록에 있으면 정상이다. 등록 출력은 6.3에 있다.
  • subscription을 만든 뒤 등록하면 그 테이블의 변경이 분석 데이터에 들어가지 않을 수 있다(6.3, 10.4).

8. subscription을 만든다 (GritDB)

CREATE SUBSCRIPTION <sub>
  CONNECTION 'host=<운영 DB 주소> port=<포트> dbname=<db> user=<복제 계정> password=<암호> options=-crow_security=off'
  PUBLICATION <pub>
  WITH (streaming = off);

예상 출력:

NOTICE:  created replication slot "<sub>" on publisher
CREATE SUBSCRIPTION
SELECT oid, subname, subenabled, substream FROM pg_subscription;
  • subenabled가 t이고 substream이 f면 정상이다.
  • oid 값을 적어 둔다. 치울 때 이 값으로 운영 DB의 슬롯을 가린다(6.4.4). 명령 설명은 CREATE SUBSCRIPTION에 있다.
  • options=-crow_security=off는 운영 DB의 행 단위 보안 정책에 걸려 행이 빠진 채 받지 않게 한다(4번).
  • streaming = off는 기본값이지만 적는다. on이나 parallel을 쓰지 않는다.
  • 암호를 접속 문자열에 넣지 않으려면 서비스 계정의 ~/.pgpass(권한 600)에 <운영 DB 주소>:<포트>:<db>:<복제 계정>:<암호> 한 줄을 두고 접속 문자열에서 password=<암호>를 뺀다. 서비스 계정이 만든 subscription은 이 파일의 암호로 붙으므로 pg_subscription, 로그, ~/.psql_history에 암호가 남지 않는다. 암호를 바꾸면 이 파일만 고친다(.pgpass).
  • 접속 문자열에 암호를 넣으면 GritDB의 pg_subscription에 남는다. superuser만 읽을 수 있다(11.5).
  • log_statement가 ddl 이상이면 이 문장의 암호가 postgresql.log에 남는다. 문장이 실패해도(예: could not connect to the publisher) log_statement 값과 상관없이 그 문장 전체가 STATEMENT: 줄로 남는다. log_min_error_statement의 기본값이 error이기 때문이다. 그래서 subscription을 만드는 세션에서 먼저 SET log_statement = 'none';과 SET log_min_error_statement = 'panic';을 실행한다. ~/.psql_history도 확인한다(11.7, 11.9).
  • 암호를 넣은 문장이 이미 로그에 남았으면 운영 DB에서 복제 계정 암호를 바꾼다(\password <복제 계정>). subscription이 이미 있으면 서비스 계정의 ~/.pgpass 줄을 고치거나 ALTER SUBSCRIPTION <sub> CONNECTION '<새 접속 정보>';를 실행해 새 암호를 넣는다(6.4.5). 이 문장도 위 두 SET 뒤에 실행한다. 그 뒤 9번으로 확인한다. 로그를 밖으로 보내기 전에 로그에서 암호가 든 줄을 지운다(9.7, QuickStart 7.4).
  • 초기 복사는 테이블마다 한 트랜잭션으로 들어온다(아래 경고, 6.8).

⚠ 행 수가 한 트랜잭션 상한의 절반을 넘는 테이블을 subscription으로 받지 않는다

  • 이유: 초기 복사는 테이블 하나를 한 트랜잭션으로 넣는다. 상한(6.8)을 넘으면 GritDB가 멈추고, 다시 띄우면 같은 복사를 처음부터 다시 해서 또 멈춘다.
  • 대신: 3번 전에 운영 DB에서 SELECT count(*) FROM <s>.<t>;로 잰다. 상한의 절반을 넘으면 붙이기 전에 지원 문의(0장)를 한다.

9. 초기 복사를 확인한다 (GritDB → 운영 DB)

SELECT srrelid::regclass AS table_name, srsubstate FROM pg_subscription_rel;
  • 모든 테이블의 srsubstate가 r이면 정상이다. i, d, f, s는 복사 중이라는 뜻이다(pg_subscription_rel).
  • 건수 대조는 아래 차례로 한다.
    1. 운영 DB에서 그 테이블 쓰기를 멈추거나 쓰기가 없는 때를 고른 뒤 SELECT pg_current_wal_lsn(); 값을 적는다.
    2. 운영 DB에서 SELECT confirmed_flush_lsn >= '<1 번 값>'::pg_lsn FROM pg_replication_slots WHERE slot_name = '<sub>';를 실행해 t가 나오는지 확인한다(pg_replication_slots).
    3. GritDB에 반영됐는지 확인한다(6.6.1).
    4. 운영 DB, GritDB 일반 세션, GritDB 분석 세션에서 잰다.

운영 DB와 GritDB 일반 세션:

SELECT count(*), sum(<숫자 열>) FROM <s>.<t>;

GritDB 분석 세션(6.6):

/*+ engine=burst, fallback=false */ SELECT count(*), sum(<숫자 열>) FROM <s>.<t>;
  • 세 값이 같으면 정상이다.
  • <숫자 열>에는 정수 열이나 numeric(p, s) 열을 고른다. real 열의 합과 avg는 두 세션에서 다를 수 있다(6.6).

6.4.2 운영 DB 에서 TRUNCATE · 열 변경을 할 때

운영 DB 작업 GritDB 1.1에서 할 일
TRUNCATE 일반 세션에서는 비어 있다. 분석 세션에는 지운 행이 남는다 그 테이블 다시 받기
열 추가나 열 이름 변경 적용이 멈춘다(is missing replicated column). 그동안 운영 DB에 WAL이 쌓인다 그 테이블 다시 받기(작업 전에)
열 삭제 GritDB의 그 열이 NOT NULL이고 기본값이 없으면 새 행이 올 때 적용이 멈춘다(null value in column "<열>" of relation "<t>" violates not-null constraint). 그동안 운영 DB에 WAL이 쌓인다. 그 밖에는 멈추지 않고, 그 열은 새 행부터 빈 값(기본값이 있으면 그 값)이다 그 테이블 다시 받기(작업 전에). 멈추는 쪽인지는 GritDB에서 \d <s>.<t>로 본다. 그 열의 Nullable이 not null이고 Default가 비었으면 멈추는 쪽이다
열 형식 변경 멈추거나 값이 어긋날 수 있다 그 테이블 다시 받기(작업 전에)
테이블 이름 변경 적용이 멈출 수 있다(does not exist) 그 테이블 다시 받기(작업 전에)
시퀀스 사용(nextval) 시퀀스 값은 넘어오지 않는다(테이블 행은 넘어온다) 할 일 없음(분석에는 영향 없다)

“그 테이블 다시 받기”를 하는 동안 다른 테이블은 계속 받는다. 그 테이블은 초기 복사를 다시 한다. 그래서 행 수가 한 트랜잭션 상한(6.8)의 절반을 넘으면 하기 전에 지원 문의를 한다(6.4.1의 8번 경고). CDC 도구나 ETL 도구로 받으면 이 절 끝을 본다. 1번 전에는 운영 DB 담당에게 바뀔 정의(더할 열과 바꿀 형식)를 받는다. 그 정의를 6.2의 15종과 numeric(p, s)(p ≤ 38)에 맞춘다. 지원 밖 형식의 열이 생기면 5번 등록이 거부된다. 이때는 1번을 하기 전에 지원 문의(0장)를 한다.

단계 어디서 할 일 정상
1 운영 DB ALTER PUBLICATION <pub> DROP TABLE <s>.<t>;를 실행한다. 같은 세션에서 SELECT now(); 값(DROP한 시간)을 적는다. 그 전부터 열려 있던 트랜잭션이 끝나기를 기다린다. SELECT pg_current_wal_lsn(); 값을 적는다 SELECT count(*) FROM pg_stat_activity WHERE backend_type = 'client backend' AND xact_start < '<DROP 한 시간>';이 0이 될 때까지 기다린다
2 GritDB ALTER SUBSCRIPTION <sub> REFRESH PUBLICATION; pg_subscription_rel에 그 테이블이 없다
3 운영 DB SELECT confirmed_flush_lsn >= '<1 번 값>'::pg_lsn FROM pg_replication_slots WHERE slot_name = '<sub>'; t가 될 때까지 기다린다
4 운영 DB 작업(TRUNCATE, 열 변경, 이름 변경) 명령이 끝난다
5 GritDB 권한과 딸린 객체(뷰, 구체화 뷰, 외래 키)를 적고 지운다. 그 뒤 해제 → DROP TABLE → 새 정의로 CREATE TABLE(같은 기본 키) → REPLICA IDENTITY FULL → 등록 → 권한과 뷰 다시 만들기 순서로 한다. 새 정의는 4번 작업 뒤 운영 DB에서 pg_dump --schema-only --no-owner --no-privileges --table=<s>.<t> <db>로 뽑는다(6.4.1의 6번). 구체화 뷰와 외래 키는 9번 뒤에 다시 만든다 등록 목록에 그 테이블이 있다
6 GritDB ALTER SUBSCRIPTION <sub> DISABLE;을 실행하고 작업자가 멈춘 것을 본다. 그 뒤 ALTER SUBSCRIPTION <sub> ENABLE;을 실행한다 SELECT pid FROM pg_stat_subscription WHERE subname = '<sub>';가 빈 값이면 멈춘 것이다
7 운영 DB ALTER PUBLICATION <pub> ADD TABLE <s>.<t>; pg_publication_tables에 그 테이블이 있다
8 GritDB ALTER SUBSCRIPTION <sub> REFRESH PUBLICATION; 그 테이블만 초기 복사를 한다(6.4.1의 9번 srsubstate)
9 둘 다 6.4.1의 9번(그 테이블) 세 값이 같다
  • 넘어오는 것과 안 넘어오는 것(DDL, 시퀀스, TRUNCATE)의 PostgreSQL 규칙: 논리 복제 제약. 명령: ALTER PUBLICATION, ALTER SUBSCRIPTION. 작업자: pg_stat_subscription
  • 1번의 기다림은 DROP TABLE 전에 그 테이블에 쓰고 아직 commit하지 않은 트랜잭션의 변경까지 3번이 받게 한다. 운영 DB가 2단계 commit을 쓰면(SHOW max_prepared_transactions;가 0보다 크다) 위 값이 0이 된 뒤 SELECT count(*) FROM pg_prepared_xacts;도 0인 때를 본다. 준비된 트랜잭션은 pg_stat_activity에 없기 때문이다(pg_prepared_xacts).
  • 3번은 1번 전에 쓴 변경을 GritDB가 다 받았는지 본다. 건너뛰고 5번을 하면 그 변경이 지운 테이블로 와서 subscription 전체가 멈춘다(logical replication target relation "<s>.<t>" does not exist).
  • 5번에서는 새로 만든 테이블로 등록한다. 같은 테이블을 비우고 바로 다시 등록하면 해제 정리를 기다리다 300초 뒤 ERROR가 날 수 있다.
  • 5번의 권한은 GritDB에서 \dp <s>.<t>로 본다. 딸린 객체는 6.7 테이블 다시 만들기 5번의 쿼리(pg_depend)로 찾는다. 뷰와 구체화 뷰는 pg_dump --schema-only --table=<뷰>로 적고 지운다. 다른 테이블의 외래 키는 정의를 적고 지운다(같은 5번). 딸린 객체가 있으면 DROP TABLE이 거부된다(cannot drop table … because other objects depend on it). CASCADE를 쓰지 않는다. 뷰는 5번 끝에 다시 만든다. 구체화 뷰와 외래 키는 9번에서 세 값이 같아진 뒤에 다시 만든다. 구체화 뷰는 REFRESH MATERIALIZED VIEW <뷰>;까지 한다. 스키마에 ALTER DEFAULT PRIVILEGES를 두었으면(11.5) 분석 계정의 SELECT는 저절로 붙는다.
  • 6번은 복제 작업자가 새 등록을 읽게 한다(6.3). 작업자가 바쁘면 DISABLE과 바로 뒤의 ENABLE 사이에 내려가지 않을 수 있다. 그래서 멈춘 것(pid가 빈 값)을 보고 켠다.
  • 운영 DB에서 이미 작업했고 subscription이 멈추지 않았으면(TRUNCATE나 NOT NULL이 아닌 열 삭제) 4번만 빼고 한다. 열 추가, 형식 변경, 이름 변경, NOT NULL 열 삭제로 이미 멈췄으면(is missing replicated column, does not exist, violates not-null constraint), 이 절차만으로 풀리지 않는다. 이때는 지원 문의(0장)를 한다.

“기존 분석계에 테이블 더하기”에서는 먼저 운영 DB에서 행 수, 복제 키(6.4.1의 2번), 행 단위 보안을 확인한다. 행 수가 상한의 절반을 넘으면 지원 문의를 한다(6.4.1의 8번 경고). 행 단위 보안은 SELECT relrowsecurity FROM pg_class WHERE oid = '<s>.<t>'::regclass;를 실행해 f가 나오는지 확인한다(4번). 복제 계정에 GRANT SELECT를 준다(4번). 다른 스키마면 GRANT USAGE ON SCHEMA <s>도 준다. 그 뒤 GritDB에 만들고 등록한다(6번과 7번). 그다음 이 표의 6~9번을 한다. 한 번에 한 테이블씩 더한다(6.8).

CDC 도구나 ETL 도구로 받을 때:

  1. 도구에서 그 테이블을 뺀다(도구 문서).
  2. 해제한 뒤 DROP TABLE을 실행한다.
  3. 새 정의로 CREATE TABLE(같은 기본 키와 REPLICA IDENTITY FULL)을 실행한 뒤 등록한다.
  4. 도구의 적용 프로세스를 다시 시작한다.
  5. 도구로 그 테이블을 처음부터 적재한다. commit 단위는 한 트랜잭션 상한의 절반 아래로 둔다(6.8). 그 뒤 6.4.1의 9번처럼 대조한다.

⚠ subscription을 멈추고(DISABLE) 그 사이에 운영 DB에서 TRUNCATE하지 않는다

  • 이유: 멈춘 동안과 멈추기 전에 아직 받지 않은 그 테이블의 변경이 다시 켠 뒤 들어온다. 분석 세션에 지운 행이 남는다.
  • 대신: 위 “그 테이블 다시 받기”를 따른다.

6.4.3 운영 DB 에 생기는 일

  • 평소에는 운영 DB의 전송 프로세스(walsender)가 변경을 풀어 GritDB로 보낸다.
  • 큰 트랜잭션은 streaming = off라서 운영 DB가 트랜잭션이 끝날 때까지 모았다가 보낸다. 메모리 한도 logical_decoding_work_mem을 넘으면 디스크에 모은다. 그동안 운영 DB 디스크(pg_replslot)를 쓴다. GritDB에서도 한 트랜잭션으로 적용된다.
    • 운영 DB 배치, 대량 UPDATE, 대량 DELETE도 GritDB의 한 트랜잭션 상한(6.8)의 절반 아래로 나눠 commit한다. 이것은 운영 DB 담당과 정한다.
    • 넘으면 GritDB가 멈추고, 다시 띄우면 같은 트랜잭션을 다시 받아 또 멈춘다. 이때는 지원 문의(0장)를 한다.
  • 초기 복사 때는 운영 DB에서 긴 읽기 트랜잭션이 돈다. 그동안 운영 DB의 VACUUM이 그 뒤에 생긴 죽은 행을 지우지 못한다. 그래서 큰 테이블은 한가한 때 붙인다.
  • 복제 키를 REPLICA IDENTITY FULL로 정한 테이블은 UPDATE와 DELETE마다 옛 행 전체가 WAL에 남는다(6.4.1의 2번).
  • subscription이 멈추면(GritDB 정지나 적용 오류) 운영 DB 슬롯이 WAL을 붙잡아 운영 DB 디스크가 찬다.
    • 상한은 운영 DB의 max_slot_wal_keep_size다(6.4.1의 1번). 넘으면 운영 DB 슬롯이 무효가 된다. 이때는 분석계를 처음부터 다시 받는다(8.6).
    • ⚠ GritDB 서버에는 max_slot_wal_keep_size를 설정하지 않는다(10.5).
  • 감시는 7.5를 본다.

6.4.4 치우기 (subscription 을 끝낼 때 · PoC 끝 포함)

단계 어디서 할 일 정상
1 GritDB SELECT oid FROM pg_subscription WHERE subname = '<sub>'; 값을 적은 뒤 DROP SUBSCRIPTION <sub>;를 실행한다 NOTICE: dropped replication slot "<sub>" on publisher
2 운영 DB SELECT slot_name FROM pg_replication_slots WHERE slot_name = '<sub>' OR slot_name LIKE 'pg\_<1 번 oid>\_sync\_%'; 0행
3 운영 DB 2번에 나온 슬롯만 SELECT pg_drop_replication_slot('<슬롯>');으로 지운다 2번을 다시 실행하면 0행이 나온다
4 운영 DB DROP PUBLICATION <pub>;를 실행하고 복제 계정과 pg_hba.conf 줄을 정리한다 —
5 GritDB 테이블을 해제한 뒤 DROP TABLE을 실행한다(6.3) 등록 목록에 없다
  • 1번에서 운영 DB에 닿지 못하면 ALTER SUBSCRIPTION <sub> DISABLE; → ALTER SUBSCRIPTION <sub> SET (slot_name = NONE); → DROP SUBSCRIPTION <sub>; → 2번과 3번 순서로 한다(슬롯 다루기).
  • 명령과 함수: DROP SUBSCRIPTION, pg_drop_replication_slot
  • GritDB를 잃어 1번을 못 하면 6.4.1의 8번에서 적어 둔 oid로 2번을 한다.
  • wal_level은 그대로 둬도 된다. 되돌리면 운영 DB를 1회 재시작해야 한다.

⚠ 운영 DB에 슬롯을 남기지 않는다

  • 이유: GritDB를 먼저 치우거나 subscription을 지우지 않고 끝내면 운영 DB 슬롯이 WAL을 계속 붙잡아 디스크를 채운다.
  • 대신: 1 → 2 순서로 치우고 2번으로 확인한다.

⚠ 2번에 나오지 않은 슬롯을 지우지 않는다

  • 이유: 대기 서버, 다른 subscription, CDC 도구의 슬롯이면 그 복제가 끊기고 되살릴 수 없다.
  • 대신: 2번 조건에 맞는 슬롯만 지운다.

6.4.5 운영 DB 가 바뀔 때 · 관리형 서비스

운영 DB에서 바뀌는 것 분석계에 생기는 일 할 일
재시작 subscription이 이어 받는다 확인만 한다(아래)
전환(failover나 switchover) subscription 슬롯은 옛 primary에 있으므로 분석계가 멈춘다 새 primary에서 처음부터 다시 받는다(8.6)
major 업그레이드 subscription과 슬롯 처리를 1.1에서 시험하지 않았다 하기 전에 지원 문의(0장)를 한다
주소 옛 주소로 붙으려다 실패한다 접속 문자열을 바꾼다(아래)
  • PostgreSQL 17의 failover 슬롯(failover = true)은 1.1에서 시험하지 않았다. 쓰려면 먼저 지원 문의(0장)를 한다.
  • 재시작이나 주소 변경 뒤에는 GritDB에서 SELECT pid FROM pg_stat_subscription WHERE subname = '<sub>';를 실행해 값이 나오는지 확인한다. 그 뒤 6.4.1의 9번 건수 대조를 한다.

운영 DB 주소가 바뀌면 GritDB에서 접속 문자열을 바꾼다.

ALTER SUBSCRIPTION <sub>
  CONNECTION 'host=<새 운영 DB 주소> port=<포트> dbname=<db> user=<복제 계정> password=<암호> options=-crow_security=off';
  • 이 문장에도 암호가 들어 있으므로 6.4.1의 8번처럼 로그와 ~/.psql_history에 남지 않게 한다. .pgpass를 쓰면 접속 문자열에 암호가 없다. 이때는 그 파일의 주소 줄도 고친다(6.4.1의 8번).
  • GritDB 서버 주소가 바뀌면 운영 DB의 pg_hba.conf 줄과 방화벽을 새 주소로 고친다(6.4.1의 4번). GritDB 쪽 접속 허용은 11.4를 본다.

관리형 서비스(클라우드 사업자의 PostgreSQL)면 붙이기 전에 지원 문의(0장)를 한다. 일반 PostgreSQL과 다른 점:

  • 논리 복제는 wal_level 대신 서비스의 매개변수 설정으로 켠다(재시작 필요).
  • 복제 계정에는 서비스가 정한 복제 전용 역할을 준다.
  • SSL 강제가 기본이면 1.1은 붙을 수 없다(6.4.1의 4번).

6.5 상태 보기

함수 보여 주는 것
gritsurfer_version() 확장 SQL 버전이며 판 번호가 아니다. 판 번호는 0장을 본다
get_gritsurfer_tables(), show_gritsurfer_tables() 등록 테이블 목록과 요약(TOAST 테이블 행 포함)
get_gritsurfer_columns(), get_gritsurfer_constraints(), get_gritsurfer_indexes() 등록 테이블의 열, 제약, 인덱스
get_gritsurfer_coldfiles(), show_gritsurfer_coldfiles() cold file 현황
get_gritsurfer_unify_checkpoints(), show_gritsurfer_unify_checkpoints() unify checkpoint 현황
get_gritsurfer_active_queries(), show_gritsurfer_active_queries() 처리 중인 분석 질의
get_gritsurfer_query_history(max_rows integer DEFAULT NULL, since timestamptz DEFAULT NULL), show_gritsurfer_query_history(같은 인자) 분석 질의 이력. 인자가 없으면 전부를 보여 준다. max_rows는 최근 그 행 수만 보여 준다(NULL이면 상한 없음). since는 그 시간 뒤만 보여 준다(NULL이면 전체 기간). 이력이 크면 두 인자로 창을 준다
  • get_* 함수는 SQL로 가공할 표 형태로 보여 준다. show_* 함수는 사람이 읽는 텍스트로 보여 준다.
  • 조회 함수는 접속한 데이터베이스의 것만 보여 준다. get_gritsurfer_unify_checkpoints()만 모든 데이터베이스에 공통이다.
  • 같은 이름의 등록이 여러 줄이면 옛 등록이 정리되지 않고 남은 것이다(6.3의 left in place나 row left WARNING). register_ts가 가장 큰 줄이 지금 등록이다. 이때는 지원 문의(0장)를 한다.
  • postgres 데이터베이스의 gritdb.grit_surfer_heartbeat는 GritDB 내부 등록 테이블이다(10초마다 쓴다). 해제하거나 삭제하지 않는다(10.5).
  • 분석 질의 이력은 조회가 끝난 뒤 조금 늦게(보통 1초 안) 쌓인다.
  • 이력 함수의 인자는 설치 파일 판마다 다를 수 있으므로 쓰기 전에 \df get_gritsurfer_query_history로 확인한다(3.5).
  • get_gritsurfer_constraints()와 get_gritsurfer_indexes()는 등록할 때 읽은 정의다. 등록 뒤 바꾼 제약과 인덱스는 보이지 않는다. 지금 정의는 \d <s>.<t>로 본다.

cold file 상태(get_gritsurfer_coldfiles()의 status):

상태 뜻
STAGED 만들어졌지만 아직 조회에 쓰이지 않는다(unify checkpoint 전)
ACTIVE 조회에 쓰이고, 다음 compaction의 대상이다. compaction은 작은 cold file을 큰 cold file로 합치는 GritDB 엔진의 일이다
FROZEN 조회에 쓰이고, 더 바뀌지 않는다
INACTIVE 지우는 중이다

테이블별, 상태별 파일 수와 크기를 본다.

SELECT schema_name, table_name, status, count(*) AS files,
       pg_size_pretty(sum(file_size_bytes)) AS size
FROM get_gritsurfer_coldfiles()
GROUP BY schema_name, table_name, status
ORDER BY schema_name, table_name, status;
  • STAGED가 계속 늘면 unify checkpoint가 멈춘 것이다. 이때는 7장을 본다. 임계치는 7.2에 있다.

unify checkpoint(6.3 풀이)의 멈춤은 최신 행의 시간으로 본다.

SELECT max(created_at) AS last_checkpoint,
       now() - max(created_at) AS age
FROM get_gritsurfer_unify_checkpoints();
  • 몇 번 다시 실행해도 age가 계속 커지지 않으면 정상이다. 쓰기가 없어도 unify checkpoint는 나아간다(6.6.1).
  • age가 계속 커지면 7장을 본다.

6.6 분석 세션

세션 종류(gritsurfer_session_mode)를 접속할 때 정한다. 접속한 뒤에는 바꿀 수 없다(SET은 거부된다).

값 동작
pg(기본값) 모든 쿼리를 PostgreSQL이 실행한다. 힌트 주석(/*+ ... */)은 일반 주석이다
surfer 등록 테이블을 읽는 쿼리 가운데 엔진 조건에 맞는 쿼리를 GritDB 엔진이 실행한다. 그 밖의 쿼리는 PostgreSQL이 실행한다

분석 세션으로 접속한다.

PGOPTIONS='-c gritsurfer_session_mode=surfer' ~/gritdb1/bin/psql -p 5432 -d <db>

예상 출력(첫 세 줄):

NOTICE:  GritSurfer session started: default_transaction_read_only is on
DETAIL:  Cold-file synchronization is asynchronous, so a query inside a transaction that mixes writes and reads is not guaranteed to see the written data.
HINT:  Read-only usage is recommended. For write workloads, connect with gritsurfer_session_mode=pg.

응용 프로그램에서는 접속 옵션에 -c gritsurfer_session_mode=surfer를 넣는다. 넣는 곳만 드라이버마다 다르다.

드라이버나 도구 넣는 곳 값
libpq, psql, psycopg 접속 문자열의 options(psql은 PGOPTIONS도) options='-c gritsurfer_session_mode=surfer'
libpq URI(postgresql://…) 쿼리 인자 options=-c%20gritsurfer_session_mode%3Dsurfer
JDBC(PostgreSQL JDBC 드라이버) 접속 속성 options(URL에 넣으면 공백은 %20) options=-c%20gritsurfer_session_mode=surfer
.NET(Npgsql 5.0 이상) 접속 문자열의 Options Options=-c gritsurfer_session_mode=surfer
BI 도구와 SQL 도구 연결 설정의 추가 접속 속성, JDBC URL, 드라이버 속성 그 도구가 쓰는 드라이버의 값
  • 그 접속에서 SHOW gritsurfer_session_mode;를 실행해 surfer가 나오는지 확인한다.

  • 접속한 뒤 실행하는 SQL(BI 도구의 초기 SQL 등)로는 켤 수 없다. SET이 거부된다. 넣을 칸이 없는 도구는 일반 세션으로 붙는다.

  • 분석 세션은 접속 옵션으로 켠다. 켤 수 있는 계정을 정하는 설정은 1.1에 없다. 그래서 등록 테이블의 보안 조건은 11.5를 본다.

  • ⚠ 계정 기본값이나 데이터베이스 기본값(ALTER ROLE, ALTER DATABASE … SET)으로는 켤 수 없다. 그 문장이 거부된다(parameter "gritsurfer_session_mode" cannot be set after connection start).

  • ⚠ postgresql.conf나 ALTER SYSTEM으로 전역 설정하지 않는다. GritDB 내부 작업 접속까지 분석 세션이 된다.

  • 분석 세션은 읽기 전용으로 쓴다.

    • gritsurfer_write_visibility_guard가 on(기본값)이면 세션이 default_transaction_read_only = on으로 시작하고 안내 NOTICE를 보인다. 이 상태에서 쓰면 ERROR가 난다(cannot execute INSERT in a read-only transaction). 세션에서 default_transaction_read_only를 끄면 첫 쓰기에 WARNING을 보인다.
    • 이유: 분석 세션에서 쓴 데이터는 cold file에 바로 반영되지 않는다. 분석 세션의 조회가 그 데이터를 보지 못할 수 있다.
    • 쓰기(INSERT, UPDATE, DELETE, DDL)는 일반 세션(pg)에서 한다.
    • 이 설정은 superuser만 바꿀 수 있다.

문장 힌트는 분석 세션에서만 읽는다. 문장 안에서 처음 나오는 /*+ ... */ 주석 하나만 본다. 키와 값은 대소문자를 가리지 않는다. 형식이 틀리면 ERROR로 멈춘다.

힌트 뜻
/*+ engine=pg */ 이 문장은 PostgreSQL이 실행한다
/*+ engine=burst */ 이 문장을 GritDB 엔진으로 보낸다. SELECT가 아니거나 등록하지 않은 테이블을 읽으면 ERROR로 멈춘다. 지원하지 않는 함수나 형식이 있으면 WARNING을 보이고 그대로 보낸다
/*+ fallback=false */ 엔진 실행이 실패하면 PostgreSQL로 다시 실행하지 않고 오류를 낸다
  • 두 키를 함께 쓸 때는 /*+ engine=burst, fallback=false */처럼 쓴다.
  • fallback의 기본값은 gritsurfer_session_fallback이다. 이 값이 on이면 엔진 실행이 실패할 때 PostgreSQL로 다시 실행한다.

GritDB 엔진이 실행하는 문장은 아래 조건을 모두 만족하는 문장이다. 하나라도 어긋나면 메시지 없이 PostgreSQL이 실행한다.

조건 내용
문장 SELECT이고 커서가 아니다
읽는 테이블 모두 등록 테이블이다(서브쿼리, CTE, 뷰 안의 테이블 포함). 구체화 뷰를 읽지 않는다. FROM 절에 함수(예: generate_series)를 두지 않는다
함수와 연산자 PostgreSQL 기본 제공 함수와 연산자만 쓴다. 사용자 정의 함수는 쓰지 않는다
결과 열 형식 15종(6.2)

분석 세션에서 어느 엔진이 실행하는지 EXPLAIN으로 확인한다.

EXPLAIN SELECT count(*) FROM <s>.<t>;
  • 첫 줄이 engine: GritBurst면 GritDB 엔진이 실행한다(EXPLAIN ANALYZE에서는 engine: GritBurst (analyze)). 그 줄이 없으면 PostgreSQL이 실행한다.
  • 분석 질의 이력(6.5)에는 GritDB 엔진으로 보낸 문장만 남는다. 보낸 뒤 PostgreSQL로 다시 실행한 문장은 FAILED로 남는다.

다시 실행(fallback):

  • fallback이 켜져 있으면 GritDB 엔진이 결과를 보내기 전에 실패한 문장만 PostgreSQL로 다시 실행한다. 이때 WARNING GritSurfer: falling back to standard PostgreSQL plan: <이유>를 보인다.
  • 결과를 보내기 시작한 뒤의 실패는 다시 실행하지 않고 ERROR로 멈춘다(SQLSTATE 38000).
  • PREPARE와 확장 프로토콜(드라이버의 바인드 실행)은 준비할 때 정한 엔진으로 실행하고, 다시 실행하지 않는다. 실행할 때 힌트를 바꿔도 엔진은 그대로다(WARNING … (prepare-time routing is sticky)).

동시 실행:

  • GritDB 엔진은 분석 조회를 vCPU 수 × 4개까지 함께 실행한다. 넘으면 자리가 날 때까지 기다린다.
  • grit_burst.session.connect_timeout_ms(기본 300초, 5.6)를 넘겨 기다리면 PostgreSQL로 실행한다.
  • 단, 드라이버의 바인드 실행(JDBC를 쓰는 BI 도구 등), PREPARE, fallback=false는 시한을 넘기면 PostgreSQL로 실행하지 않는다. 이때는 ERROR로 멈춘다(GritSurfer: failed to send query to burst engine, SQLSTATE 38000).
  • 자리를 기다리는 동안에는 기다림이 끝날 때까지 취소(Ctrl-C)와 statement_timeout이 듣지 않는다. 바로 끊으려면 pg_terminate_backend를 쓴다(6.9).
  • 한도는 아래 줄의 cap= 값으로 확인한다.
grep -h 'channel capacity: service Session' ~/gritdb1/log/grit_admin_main.log* | tail -1

반영 지연:

  • 분석 세션은 commit한 변경을 조금 늦게 본다. 보통 약 12초, 100번 중 99번은 약 22초 안에 보인다(자체 시험).
  • 분석계면 운영 DB에서 복제되는 시간이 더해진다.
  • 최신 값이 꼭 필요한 조회는 일반 세션(분석계면 운영 DB)에서 한다.
  • 방금 commit한 변경이 보이는지 확인하려면 6.6.1을 본다.

결과가 PostgreSQL과 다를 수 있는 경우:

  • avg의 정밀도, numeric의 자릿수, real(float4) 열의 sum, char(n)의 octet_length, concat
  • timestamptz 값을 보여 줄 때 GritDB 엔진은 세션 시간대(TimeZone)를 쓰지 않는다. 그래서 AT TIME ZONE '<시간대>'로 바꿔 조회한다(1.8). BI 도구를 포함한 분석용 연결에서 SHOW timezone;을 실행해 서버 시간대(5.3)와 같은지 확인한다. 다르면 도구의 시간대 설정을 맞춘다. JDBC 도구는 JVM 시간대를 맞춘다(예: -Duser.timezone=Asia/Seoul). 시간대가 다르면 한 세션 안에서도 GritDB 엔진이 실행한 문장과 PostgreSQL이 실행한 문장의 날짜와 시각 값이 다르다.
  • 정확한 값이 필요하면 일반 세션에서 조회하거나 /*+ engine=pg */를 붙인다.

연결 풀러:

  • 연결 풀러가 필요하면 Pgpool-II를 쓴다. Pgpool-II는 접속 옵션(options)을 GritDB에 그대로 넘기므로 풀러를 거쳐도 분석 세션이 열린다.
  • PgBouncer는 접속 옵션을 넘기지 않는다. PgBouncer를 거친 연결로는 분석 세션이 열리지 않는다. 그래서 분석용 연결은 PgBouncer를 거치지 않고 GritDB에 바로 붙인다.
  • 풀러를 거칠 때 분석용 연결과 일반용 연결은 계정(또는 데이터베이스)을 나눈다. 풀러는 계정과 데이터베이스가 같은 연결을 다시 쓴다. 그래서 접속 옵션만 다르면 분석 세션 연결과 일반 세션 연결이 섞일 수 있다(6.9, 11.5).
  • 풀러를 거친 분석용 연결에서 SHOW gritsurfer_session_mode;를 실행해 surfer가 나오는지 확인한다. 일반용 연결에서는 pg가 나오는지 확인한다.

6.6.1 반영 확인 · 분석 세션 값 재기

반영 확인에서는 방금 commit한 변경이 분석 세션에 보이는지 확인한다. 일반 세션에서 WAL 위치(pg_current_wal_lsn())를 적은 뒤, unify checkpoint가 그 위치를 넘었는지 본다.

SELECT pg_current_wal_lsn();
SELECT max(ckpt_lsn) >= '<위 값>'::pg_lsn AS caught_up
FROM get_gritsurfer_unify_checkpoints();
  • t가 나오면 정상이다. f면 몇 초 뒤 다시 실행한다. t면 그 전에 commit한 변경이 분석 세션에 보인다. 쓰기가 없어도 GritDB가 10초마다 내부 기록을 commit하므로 unify checkpoint가 그 위치를 넘어간다.
  • 분석 세션 값을 잴 때는 문장 앞에 /*+ engine=burst, fallback=false */를 붙인다. 붙이지 않으면 GritDB 엔진이 실행하지 못한 문장을 PostgreSQL이 대신 실행해도 같은 값이 나온다. ERROR가 나면 등록 상태(6.5)부터 본다.

6.7 등록 테이블에 하지 않는 작업

1.1은 등록 테이블에 한 아래 작업을 GritDB 엔진에 제대로 반영하지 않는다. 등록 테이블에는 하지 않는다. 꼭 해야 하면 “대신 할 일”을 따른다.

작업 영향 대신 할 일
TRUNCATE 분석 세션 결과에 지운 행이 남는다 테이블 다시 만들기(표 아래). 데이터 보관과 다시 넣기는 빼도 된다. 분석계는 6.4.2를 본다
열 추가, 삭제, 이름 변경 분석 세션 조회가 실패하거나 새 열이 보이지 않는다 테이블 다시 만들기(표 아래). 분석계는 6.4.2를 본다
열 형식 변경 분석 세션 결과가 틀어질 수 있다 같음
REPLICA IDENTITY 변경이나 SET UNLOGGED 변경이 GritDB 엔진으로 넘어가지 않을 수 있다 하지 않는다
VACUUM FULL, CLUSTER, SET TABLESPACE 비정상 종료 뒤 재기동 때 일부 변경이 분석 데이터에서 빠질 수 있다 하지 않는다. 공간을 되찾아야 하면 테이블 다시 만들기(표 아래)를 따른다
파티션 붙이기나 떼기 분석 세션 결과에서 행이 빠지거나 남는다 파티션을 모두 등록한 뒤 바꾸지 않는다
등록 테이블이 있는 데이터베이스의 테이블스페이스 변경 분석 데이터에 반영되지 않을 수 있다 테이블을 모두 해제한 뒤
이름 변경(RENAME이나 SET SCHEMA) 등록 목록(6.5)에는 옛 이름이 남는다. 해제할 때는 새 이름을 쓴다 기록해 둔다

테이블 다시 만들기(한 데이터베이스 구성):

  1. 그 테이블 쓰기를 멈춘다(응용 프로그램과 작업 일정).
  2. 데이터를 CREATE TABLE <s>.<t>_keep AS TABLE <s>.<t>;로 보관한다.
  3. 보관본 건수를 원본과 비교한다. SELECT (SELECT count(*) FROM <s>.<t>) = (SELECT count(*) FROM <s>.<t>_keep);을 실행해 t가 나오는지 확인한다.
  4. 해제한다(6.3).
  5. DROP TABLE <s>.<t>;를 실행한다.
    • 먼저 그 테이블에 딸린 객체를 SELECT DISTINCT pg_describe_object(classid, objid, 0) FROM pg_depend WHERE refclassid = 'pg_class'::regclass AND refobjid = '<s>.<t>'::regclass AND deptype = 'n';으로 본다.
    • 뷰와 구체화 뷰는 ~/gritdb1/bin/pg_dump -p 5432 --schema-only --table=<뷰> <db> > <뷰>.sql(정의, 권한, 인덱스)로 적고 지운다. 뷰에 딸린 뷰는 위의 것부터 한다.
    • 다른 테이블의 외래 키는 SELECT format('ALTER TABLE %s ADD CONSTRAINT %I %s;', conrelid::regclass, conname, pg_get_constraintdef(oid)) FROM pg_constraint WHERE contype = 'f' AND confrelid = '<s>.<t>'::regclass; 결과를 적고 그 제약을 지운다. 그 다른 테이블 쓰기도 1번부터 멈춘다.
    • CASCADE를 쓰지 않는다. 뷰는 7번에 ~/gritdb1/bin/psql -p 5432 -d <db> -f <뷰>.sql로 다시 만든다. 구체화 뷰와 외래 키는 8번 건수 대조 뒤에 다시 만든다. 구체화 뷰는 WITH NO DATA로 만들어지므로 REFRESH MATERIALIZED VIEW <뷰>;까지 한다.
  6. 새 정의로 CREATE TABLE(같은 기본 키)을 실행한 뒤 ALTER TABLE <s>.<t> REPLICA IDENTITY FULL;을 실행한다.
  7. 등록한다(6.3). 그 테이블에 딸린 권한과 뷰를 다시 만든다.
  8. 데이터를 INSERT INTO <s>.<t> SELECT … FROM <s>.<t>_keep;으로 다시 넣는다(6.8). 건수를 대조한 뒤 DROP TABLE <s>.<t>_keep;을 실행한다.
    • 행 수가 6.8의 나누는 기준(상한의 절반, 넓은 행은 더 작게)을 넘으면 기본 키 범위로 나눠 여러 번 넣는다. 문장마다 commit한다(예: INSERT INTO <s>.<t> SELECT … FROM <s>.<t>_keep WHERE <키> >= <a> AND <키> < <b>;).
    • serial이나 identity 열이 있으면 GENERATED ALWAYS AS IDENTITY 열은 8번을 INSERT INTO <s>.<t> OVERRIDING SYSTEM VALUE SELECT … FROM <s>.<t>_keep;으로 넣는다. 생성 열(GENERATED ALWAYS AS (…) STORED)은 8번 열 목록에서 뺀다. 값은 다시 계산된다. 9번 전에 serial이나 identity 열마다 SELECT setval(pg_get_serial_sequence('<s>.<t>', '<열>'), max(<열>)) FROM <s>.<t>;로 시퀀스를 맞춘다(시퀀스 함수, pg_get_serial_sequence).
  9. 그 테이블을 쓰는 접속을 다시 연다(6.3).

⚠ 보관본 건수를 확인하기 전에 DROP TABLE하지 않는다

  • 이유: 지운 행은 되살릴 수 없다.
  • 대신: 3번 결과가 t일 때만 5번으로 간다.
  • 같은 테이블을 비우고 바로 다시 등록하면 해제 정리를 기다리다 300초 뒤 ERROR가 날 수 있으므로 새로 만든 테이블로 등록한다.
  • 등록은 빈 테이블에 한다. 데이터를 넣은 뒤 등록하면 그 행은 분석 세션 결과에서 빠진다(6.3, 10.4).
  • 등록 테이블에 해도 되는 작업은 인덱스 만들기와 지우기, VACUUM, ANALYZE, COPY다(7.8).

6.8 데이터 넣기 (대량 적재)

  1. 빈 테이블을 만든다(6.2 조건과 REPLICA IDENTITY FULL).

  2. 등록한다(6.3).

  3. 적재한다(COPY나 INSERT … SELECT). 문장 하나(COPY 한 번, INSERT … SELECT 한 번, 덤프 파일의 테이블 하나)는 한 트랜잭션이다. 그래서 행 수가 나누는 기준(아래 한 트랜잭션 상한의 절반, 넓은 행은 더 작게)을 넘으면 적재용 테이블을 거쳐 나눠 넣는다.

    CREATE UNLOGGED TABLE <s>.<t>_load (LIKE <s>.<t>);
    \copy <s>.<t>_load FROM '<파일>' (FORMAT csv)
    SELECT format('INSERT INTO <s>.<t> SELECT * FROM <s>.<t>_load WHERE <키> BETWEEN %L AND %L', min(<키>), max(<키>))
    FROM (SELECT <키>, ntile(<묶음 수>) OVER (ORDER BY <키>) AS n FROM <s>.<t>_load) x
    GROUP BY n ORDER BY n \gexec
    
    • 위 명령은 ~/gritdb1/bin/psql -p 5432 -d <db> -v ON_ERROR_STOP=1 안에서 실행한다(오류가 나면 거기서 멈춘다).
    • 적재용 테이블(_load)은 등록하지 않는다. UNLOGGED라 WAL이 적게 쌓인다. 원본만큼 공간이 더 든다.
    • 비정상 종료나 즉시 정지 뒤에는 적재용 테이블이 남고 내용만 비워진다. 이때는 \copy 줄부터 다시 한다. 이미 넣은 묶음은 건너뛴다. <s>.<t>에 행이 있으면 ntile이 든 안쪽 SELECT의 FROM <s>.<t>_load 뒤에 WHERE <키> > (SELECT max(<키>) FROM <s>.<t>)를 붙인다. 묶음은 키 차례로 하나씩 commit된다.
    • \copy는 머리 줄이 있으면 (FORMAT csv, HEADER)로 실행한다. CSV 칸 안의 줄바꿈도 그대로 읽으므로 파일을 split으로 나누지 않는다.
    • <키>는 기본 키 열 하나다. <묶음 수>는 적재용 테이블 행 수 ÷ 나누는 기준(올림)이다. \gexec은 만든 INSERT를 하나씩 실행한다(문장마다 commit).
    • 여러 문장을 한 트랜잭션으로 묶지 않는다. BEGIN 안에서, psql -1로, pg_restore --single-transaction으로 넣지 않는다. 응용 프로그램이나 드라이버로 넣으면 autocommit을 켜거나 묶음마다 commit한다(예: Python psycopg는 autocommit이 기본으로 꺼져 있다).
    • 생성 열이나 GENERATED ALWAYS identity 열이 있으면 6.7 “테이블 다시 만들기”의 8번처럼 넣는다.
    • 명령: \gexec, ON_ERROR_STOP, ntile
  4. 통계를 ANALYZE <s>.<t>;로 모은다.

  5. 반영을 확인한다(6.6.1).

  6. 건수와 합계를 일반 세션과 분석 세션에서 대조한다(6.4.1의 9번 두 쿼리). 적재용 테이블을 거쳤으면 그 값(SELECT count(*), sum(<숫자 열>) FROM <s>.<t>_load;)과도 같아야 한다. 같으면 DROP TABLE <s>.<t>_load;를 실행한다.

  • 6번의 값이 모두 같은지 확인한다.
  • 등록 테이블을 UNLOGGED로 만들거나 바꾸지 않는다. 변경이 GritDB 엔진으로 넘어가지 않을 수 있다(6.7).
  • 등록 테이블 쓰기는 쓰기마다 GritDB 엔진의 수신 확인을 기다리므로 같은 적재도 등록하지 않은 테이블보다 느리다(1.1).

등록 테이블에 한 트랜잭션으로 쓰는 행 수(INSERT, UPDATE, DELETE 합)에는 서버마다 상한이 있다. 이것이 한 트랜잭션 상한이다. 1.1에는 이 상한을 바꾸는 설정이 없다.

상한을 확인한다(GritDB 서버, 서비스 계정).

ls -tr ~/gritdb1/log/grit_unify_main.log* | xargs grep -h 'Tier-3 (RowVersion' | tail -1

예상 출력(예):

[…][INFO][…]   Tier-3 (RowVersion,  80%): max=18686611, initial=9343305, sizeof(PoolChunk)=512 + tuple=2229 = 2741
  • 상한은 max= 값이다(위 예에서는 메모리 186 GiB 서버에서 18,686,611행). 서버 메모리에 비례한다.
  • 이 줄은 GritDB 엔진이 기동할 때 쓰는 줄이다. 줄이 없고 로그를 지웠으면 다음 기동 뒤 다시 본다.
  • 한 트랜잭션은 상한의 절반 아래로 나눠 commit한다. 같은 상한을 다른 트랜잭션과 아직 반영하지 않은 변경과 함께 쓰므로 큰 적재를 동시에 여러 개 돌리지 않는다.
  • 행이 넓으면(행 하나가 2 KB를 넘으면) 그 비율만큼 더 작게 나눈다. 예를 들어 4 KB 행은 상한의 4분의 1 아래로 나눈다. 행 크기는 적재용 테이블(3번, 없으면 넣을 데이터가 있는 테이블)에서 압축을 푼 크기로 잰다. SELECT avg(octet_length(t::text))::int FROM <s>.<t>_load t;를 쓴다(큰 테이블은 FROM <s>.<t>_load t TABLESAMPLE SYSTEM (1)). 긴 글자와 bytea 열은 압축을 푼 크기 이상으로 나온다(더 작게 나누는 쪽).
  • 분석계에서는 초기 복사와 운영 DB 배치도 같은 상한을 쓴다(6.4.1의 8번 경고, 6.4.3). 초기 복사는 테이블 하나가 한 트랜잭션이다.
  • 상한에 다가간 신호는 7.1을 본다.

⚠ 상한을 넘는 트랜잭션을 등록 테이블에 쓰지 않는다

  • 이유: GritDB가 멈춘다(PostgreSQL 포함). 그 트랜잭션은 반영되지 않는다.
  • 대신: 상한의 절반 아래로 나눠 commit한다. 나눌 수 없으면 지원 문의(0장)를 한다.

적재 중 감시에서는 늘어나는 추세를 본다. 지표는 7.1을, 임계치는 7.2를 본다.

볼 것 확인 방법
디스크 df -h ~/gritdb1/datafiles/ ~/gritdb1/datafiles/pgdata/. 두 줄의 Filesystem이 같으면 한 볼륨이다. --pgdata로 설치했으면 둘째 줄이 pg_wal 볼륨이다(2.4). 차면 디스크가 찰 때를 본다
pg_wal과 buffer_spill 크기 du -sh ~/gritdb1/datafiles/pgdata/pg_wal ~/gritdb1/datafiles/buffer_spill
복구용 복제 슬롯 보존량 아래 SQL
GritDB 엔진 메모리와 한 트랜잭션 상한 접근 7.1 표의 두 줄
SELECT pg_size_pretty(pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn)) AS retained
FROM pg_replication_slots
WHERE slot_name = 'gritsurfer_slot';

분석계 초기 복사에서는 큰 테이블을 하나씩 나눠 붙인다.

  • 처음 publication에는 일부 테이블만 넣는다. 남은 테이블은 앞 테이블의 초기 복사가 끝난 뒤(srsubstate가 r, 6.4.1의 9번) 하나씩 더한다. 더할 때는 6.4.2 “기존 분석계에 테이블 더하기”를 따른다(ALTER PUBLICATION … ADD TABLE → REFRESH PUBLICATION).
  • 한 subscription이 함께 복사하는 테이블 수는 max_sync_workers_per_subscription 값이다(기본 2).
  • 함께 복사하는 테이블을 하나로 줄이려면 GritDB에서 ALTER SYSTEM SET max_sync_workers_per_subscription = 1;과 SELECT pg_reload_conf();를 차례로 실행한다(재기동 없음, 5.4). 초기 복사가 모두 r이 되면 ALTER SYSTEM RESET max_sync_workers_per_subscription;과 SELECT pg_reload_conf();를 차례로 실행한다.
  • 운영 DB가 한가한 때 붙인다(6.4.3).

6.9 분석 조회 관리

실행 중인 분석 조회를 본다. 함수는 A.3에 있다.

SELECT pid, status, query_start_time,
       now() - query_start_time AS running, query_text
FROM get_gritsurfer_active_queries()
ORDER BY query_start_time;
  • 오래 걸리는 조회는 running이 큰 줄이다.

조회를 끊는다. <pid>는 위 결과의 pid다.

차례 SQL 쓰는 때
1 SELECT pg_cancel_backend(<pid>); 실행 중인 조회를 취소한다
2 SELECT pg_terminate_backend(<pid>); 1로 끝나지 않을 때나 GritDB 엔진 자리를 기다리는 조회일 때. 그런 조회에는 취소와 statement_timeout이 듣지 않는다(6.6)
  • SELECT pid, state FROM pg_stat_activity WHERE pid = <pid>;를 실행해 확인한다. 결과가 0행이면 접속을 끊은 것이다. idle이면 조회만 취소한 것이다.
  • 함수와 뷰: pg_cancel_backend와 pg_terminate_backend, pg_stat_activity
  • gritdb_catalog_* 내부 접속과 GritDB 작업자는 끊지 않는다(7.1).

기다린 시간과 실행 시간은 분석 질의 이력의 단계 시간으로 본다. 아래는 최근 1시간의 100행이다.

SELECT query_text, status,
       dispatched_time - query_start_time AS wait_admit,
       worker_taken_time - dispatched_time AS wait_engine,
       pg_end_time - query_start_time AS total
FROM get_gritsurfer_query_history(100, now() - interval '1 hour');
  • wait_admit은 GritDB 엔진 자리를 기다린 시간이다. wait_engine은 엔진이 작업자를 붙이기까지 걸린 시간이다. total은 전체 시간이다.
  • 기다리다 시한을 넘겨 PostgreSQL이 실행한 조회는 이력에 행이 없다(6.6 동시 실행).
  • 동시 실행은 vCPU 수 × 4까지다. 넘으면 기다리다가 시한(5.6) 뒤 PostgreSQL로 실행한다. 바인드 실행, PREPARE, fallback=false는 오류로 멈춘다(6.6).
  • 분석 세션용 계정과 연결 풀을 일반 세션용과 따로 둔다. 풀러는 계정과 데이터베이스가 같은 연결을 다시 쓰기 때문이다. 연결 풀러는 6.6을, 권한은 11.5를 본다.
  • 긴 분석 조회는 cold file 정리를 늦추므로 끝나지 않는 조회를 오래 두지 않는다(7.8).
  • 분석 조회 자원 한도(작업 부하 그룹)는 1.1에 없다(10.6).

문제가 생기면

  • 등록이 거부되면 QuickStart 7장의 GDB-RUNTIME-001과 TroubleShooting의 RUNTIME 계열을 본다.
  • 분석 세션 값이 일반 세션과 다르면 먼저 반영 확인(6.6.1)을 한다. 그 뒤 하지 않는 작업을 했는지 본다(6.7). 분석계면 6.4.2를 본다.
  • subscription 적용이 멈췄으면 6.4.2와 7.5를 본다.
  • 큰 적재나 초기 복사 중에 GritDB가 멈췄으면 다시 띄우기 전에 QuickStart 7.4로 자료를 모으고 지원 문의를 한다(6.8).

더 알아보기