콘텐츠로 이동

34.5. 파이프라인 모드 (Pipeline Mode)

출처: PostgreSQL 공식 문서 — libpq 파이프라인 모드 (섹션 34.5)


목차


개요

libpq 파이프라인 모드를 사용하면 이전에 전송한 쿼리의 결과를 읽지 않고도 다음 쿼리를 전송할 수 있습니다. 파이프라인 모드를 활용하면 여러 쿼리/결과를 단일 네트워크 트랜잭션으로 송수신할 수 있기 때문에 클라이언트가 서버를 기다리는 시간이 줄어듭니다.

파이프라인 모드는 상당한 성능 향상을 제공하지만, 대기 중인 쿼리 큐를 관리하고 어떤 결과가 어떤 쿼리에 대응하는지 파악해야 하므로 클라이언트 코드가 더 복잡해집니다.

파이프라인 모드는 또한 클라이언트와 서버 양쪽에서 더 많은 메모리를 소비합니다. 다만 송수신 큐를 주의 깊게 적극적으로 관리하면 이를 완화할 수 있습니다. 이는 연결이 블로킹 모드인지 비블로킹 모드인지와 관계없이 적용됩니다.

파이프라인 API는 PostgreSQL 14에서 도입되었으나, 특별한 서버 지원을 필요로 하지 않는 클라이언트 측 기능으로, v3 확장 쿼리 프로토콜을 지원하는 모든 서버에서 동작합니다.


34.5.1. 파이프라인 모드 사용법

파이프라인을 실행하려면 애플리케이션이 연결을 파이프라인 모드로 전환해야 합니다. 이는 PQenterPipelineMode를 통해 수행됩니다. PQpipelineStatus를 사용하면 파이프라인 모드가 활성 상태인지 확인할 수 있습니다.

파이프라인 모드에서는:

  • 확장 쿼리 프로토콜을 사용하는 함수만 허용됩니다.
  • 여러 SQL 명령이 포함된 커맨드 문자열은 허용되지 않습니다.
  • COPY 명령도 허용되지 않습니다.
  • PQfn, PQexec, PQexecParams, PQprepare, PQexecPrepared, PQdescribePrepared, PQdescribePortal 같은 동기식 실행 함수는 오류 조건입니다.
  • PQsendQuery도 허용되지 않습니다 (단순 쿼리 프로토콜을 사용하기 때문).

모든 디스패치된 커맨드의 결과가 처리되고 마지막 파이프라인 결과가 소비되면, 애플리케이션은 PQexitPipelineMode를 통해 일반 모드로 복귀할 수 있습니다.

주의 파이프라인 모드는 libpq를 비블로킹 모드로 사용할 때 가장 적합합니다. 블로킹 모드에서 사용하면 클라이언트/서버 간 교착 상태(deadlock)가 발생할 수 있습니다.

쿼리 전송 (Issuing Queries)

파이프라인 모드 진입 후, 애플리케이션은 PQsendQueryParams 또는 준비된 쿼리 형제 함수인 PQsendQueryPrepared를 사용하여 요청을 디스패치합니다. 이 요청들은 서버로 플러시될 때까지 클라이언트 측에 큐잉됩니다. 플러시는 다음 두 가지 경우에 발생합니다:

  • PQpipelineSync를 사용하여 파이프라인에 동기화 포인트를 설정할 때
  • PQflush가 호출될 때

PQsendPrepare, PQsendDescribePrepared, PQsendDescribePortal 함수도 파이프라인 모드에서 동작합니다.

서버는 클라이언트가 전송한 순서대로 문장을 실행하고 결과를 반환합니다. 서버는 파이프라인이 끝날 때까지 기다리지 않고 즉시 파이프라인의 커맨드 실행을 시작합니다. 결과는 서버 측에서 버퍼링되며, PQpipelineSync로 동기화 포인트가 설정되거나 PQsendFlushRequest가 호출될 때 플러시됩니다.

어떤 문장에서 오류가 발생하면:

  • 서버는 현재 트랜잭션을 중단합니다.
  • 다음 동기화 포인트까지 큐에 있는 이후 커맨드를 실행하지 않습니다.
  • 해당 각 커맨드에 대해 PGRES_PIPELINE_ABORTED 결과가 생성됩니다.
  • 동기화 포인트 이후에 쿼리 처리가 재개됩니다.

한 작업이 이전 작업의 결과에 의존하는 것도 허용됩니다. 예를 들어, 하나의 쿼리가 테이블을 정의하고 동일 파이프라인의 다음 쿼리가 해당 테이블을 사용할 수 있습니다. 마찬가지로, 애플리케이션이 이름이 있는 준비된 문장을 만들고 동일 파이프라인의 이후 문장으로 실행할 수도 있습니다.

결과 처리 (Processing Results)

파이프라인에서 한 쿼리의 결과를 처리하려면, 애플리케이션이 PQgetResult를 반복 호출하여 PQgetResult가 null을 반환할 때까지 각 결과를 처리합니다. 그런 다음 파이프라인의 다음 쿼리 결과를 PQgetResult로 다시 가져올 수 있습니다.

파이프라인의 모든 쿼리 결과가 반환되면, PQgetResultPGRES_PIPELINE_SYNC 상태 값을 포함하는 결과를 반환합니다.

클라이언트는 파이프라인 전송 완료 후 결과 처리를 지연하거나, 파이프라인에 추가 쿼리를 전송하면서 결과 처리를 인터리브할 수 있습니다.

PQgetResult는 다음과 같은 새 PGresult 타입을 포함할 수 있다는 점을 제외하면 일반 비동기 처리와 동일하게 동작합니다:

상태값 설명
PGRES_PIPELINE_SYNC 파이프라인의 해당 PQpipelineSync 지점에서 정확히 한 번 보고됨
PGRES_PIPELINE_ABORTED 오류 발생 후 다음 PGRES_PIPELINE_SYNC까지 각 결과 대신 발생

PQisBusy, PQconsumeInput 등은 파이프라인 결과 처리 시 정상적으로 동작합니다. 파이프라인 중간에 PQisBusy를 호출하면, 지금까지 발행된 모든 쿼리의 결과가 소비된 경우 0을 반환합니다.

libpq는 현재 처리 중인 쿼리에 대한 정보를 애플리케이션에 제공하지 않습니다 (PQgetResult가 null을 반환하면 다음 쿼리 결과 반환이 시작된다는 것만 알 수 있음). 따라서 애플리케이션은 쿼리를 전송한 순서를 추적하여 해당 결과와 연결해야 합니다. 애플리케이션은 일반적으로 상태 머신(state machine) 또는 FIFO 큐를 사용합니다.

오류 처리 (Error Handling)

클라이언트 관점에서, PQresultStatusPGRES_FATAL_ERROR를 반환하면 파이프라인이 중단(aborted)으로 표시됩니다. PQresultStatus는 중단된 파이프라인의 나머지 큐된 작업 각각에 대해 PGRES_PIPELINE_ABORTED 결과를 보고합니다. PQpipelineSync의 결과는 중단된 파이프라인의 끝과 정상 결과 처리 재개를 알리기 위해 PGRES_PIPELINE_SYNC로 보고됩니다.

오류 복구 중에 클라이언트는 PQgetResult로 결과를 처리해야 합니다.

파이프라인이 암시적 트랜잭션을 사용한 경우: - 이미 실행된 작업은 롤백됩니다. - 실패한 작업 이후에 큐된 작업은 완전히 건너뜁니다.

파이프라인이 단일 명시적 트랜잭션(BEGIN으로 시작, COMMIT으로 종료)을 시작하고 커밋하는 경우, 동일한 동작이 적용되지만 파이프라인 종료 시 세션이 중단된 트랜잭션 상태로 남습니다.

파이프라인에 여러 명시적 트랜잭션이 포함된 경우: - 오류 이전에 커밋된 모든 트랜잭션은 커밋된 상태로 유지됩니다. - 현재 진행 중인 트랜잭션은 중단됩니다. - 이후의 모든 작업(이후 트랜잭션 포함)은 완전히 건너뜁니다.

중단 상태의 명시적 트랜잭션 블록에서 파이프라인 동기화 포인트가 발생하면, 다음 커맨드가 ROLLBACK으로 트랜잭션을 정상 모드로 전환하지 않는 한 다음 파이프라인은 즉시 중단됩니다.

주의 클라이언트는 COMMIT을 전송했다고 해서 작업이 커밋되었다고 가정해서는 안 됩니다. 커밋이 완료되었다는 확인은 해당 결과를 수신했을 때만 가능합니다. 오류는 비동기적으로 도달하기 때문에, 애플리케이션은 마지막으로 수신된 커밋된 변경 지점부터 재시작하고 그 이후에 수행된 작업을 다시 전송할 수 있어야 합니다.

결과 처리와 쿼리 전송 인터리브 (Interleaving)

대규모 파이프라인에서 교착 상태를 방지하려면, 클라이언트를 select, poll, WaitForMultipleObjectsEx 등의 운영 체제 시스템 콜을 사용하는 비블로킹 이벤트 루프 기반으로 구성해야 합니다.

클라이언트 애플리케이션은 일반적으로 다음 두 가지 큐를 유지해야 합니다:

  1. 디스패치 대기 중인 작업 큐
  2. 디스패치되었지만 아직 결과가 처리되지 않은 작업 큐

  3. 소켓이 쓰기 가능하면 더 많은 작업을 디스패치합니다.

  4. 소켓이 읽기 가능하면 결과를 읽고, 해당 결과 큐의 다음 항목과 매핑하여 처리합니다.

가용 메모리를 기반으로 소켓의 결과를 자주 읽어야 합니다. 파이프라인 끝까지 기다릴 필요가 없습니다. 파이프라인은 보통(반드시 그렇지는 않지만) 트랜잭션 단위로 범위를 지정해야 합니다. 파이프라인 사이에 파이프라인 모드를 종료하고 재진입하거나, 다음을 전송하기 전에 하나가 완료될 때까지 기다릴 필요가 없습니다.

select()와 간단한 상태 머신을 사용하여 전송/수신 작업을 추적하는 예제는 PostgreSQL 소스 배포판의 src/test/modules/libpq_pipeline/libpq_pipeline.c에 있습니다.


34.5.2. 파이프라인 모드 관련 함수

PQpipelineStatus

libpq 연결의 현재 파이프라인 모드 상태를 반환합니다.

PGpipelineStatus PQpipelineStatus(const PGconn *conn);

반환 가능한 값:

설명
PQ_PIPELINE_ON libpq 연결이 파이프라인 모드에 있음
PQ_PIPELINE_OFF libpq 연결이 파이프라인 모드가 아님
PQ_PIPELINE_ABORTED libpq 연결이 파이프라인 모드이며 현재 파이프라인 처리 중 오류 발생. PQgetResultPGRES_PIPELINE_SYNC 타입의 결과를 반환하면 중단 플래그가 해제됨

PQenterPipelineMode

연결이 현재 유휴 상태이거나 이미 파이프라인 모드에 있는 경우 파이프라인 모드로 진입합니다.

int PQenterPipelineMode(PGconn *conn);

성공 시 1 반환. 연결이 현재 유휴 상태가 아닌 경우(결과 준비됨, 서버에서 더 많은 입력 대기 중 등) 0을 반환하고 효과 없음. 이 함수는 실제로 서버에 아무것도 전송하지 않으며, libpq 연결 상태만 변경합니다.


PQexitPipelineMode

현재 빈 큐와 보류 중인 결과가 없는 파이프라인 모드인 경우 파이프라인 모드를 종료합니다.

int PQexitPipelineMode(PGconn *conn);

성공 시 1 반환. 파이프라인 모드가 아닌 경우 1을 반환하고 아무 조치도 취하지 않음. 현재 문장의 처리가 완료되지 않았거나 이전에 전송된 모든 쿼리에서 결과를 수집하기 위해 PQgetResult가 호출되지 않은 경우 0 반환 (이 경우 PQerrorMessage를 사용하여 실패에 대한 더 많은 정보 확인 가능).


PQpipelineSync

sync 메시지를 전송하고 전송 버퍼를 플러시하여 파이프라인에 동기화 포인트를 표시합니다. 이는 암시적 트랜잭션의 구분자이자 오류 복구 포인트로 기능합니다.

int PQpipelineSync(PGconn *conn);

성공 시 1 반환. 연결이 파이프라인 모드가 아니거나 sync 메시지 전송이 실패한 경우 0 반환.


PQsendFlushRequest

서버가 출력 버퍼를 플러시하도록 요청을 전송합니다.

int PQsendFlushRequest(PGconn *conn);

성공 시 1 반환. 실패 시 0 반환.

서버는 PQpipelineSync가 호출된 결과로 자동으로 출력 버퍼를 플러시하거나, 파이프라인 모드가 아닐 때 어떤 요청에서든 플러시합니다. 이 함수는 동기화 포인트를 설정하지 않고 파이프라인 모드에서 서버가 출력 버퍼를 플러시하도록 하는 데 유용합니다. 요청 자체는 서버에 자동으로 플러시되지 않으므로 필요한 경우 PQflush를 사용해야 합니다.


34.5.3. 파이프라인 모드를 사용하는 시점

비동기 쿼리 모드와 마찬가지로, 파이프라인 모드 사용 시 의미 있는 성능 오버헤드는 없습니다. 클라이언트 애플리케이션 복잡성이 증가하고 클라이언트/서버 교착 상태를 방지하기 위한 추가 주의가 필요하지만, 파이프라인 모드는 상태를 더 오래 유지함으로 인한 메모리 사용 증가의 대가로 상당한 성능 개선을 제공할 수 있습니다.

파이프라인 모드가 유용한 경우

파이프라인 모드는 다음 경우에 가장 유용합니다:

  • 서버가 원거리에 있을 때 (즉, 네트워크 지연/핑 시간이 높을 때)
  • 많은 작은 작업이 빠르게 연속으로 수행될 때

각 쿼리 실행에 클라이언트/서버 왕복 시간의 수배가 소요되는 경우에는 파이프라인 명령 사용의 이점이 일반적으로 적습니다.

구체적인 예시: 왕복 시간이 300ms인 서버에서 100개의 문장 작업을 실행하면, 파이프라인 없이는 네트워크 지연만으로 30초가 걸립니다. 파이프라인을 사용하면 서버에서 결과를 기다리는 시간이 0.3초에 불과할 수 있습니다.

파이프라인 명령을 사용해야 할 때: 애플리케이션이 집합 연산이나 COPY 작업으로 쉽게 변환할 수 없는 많은 소규모 INSERT, UPDATE, DELETE 작업을 수행할 때.

파이프라인 모드가 유용하지 않은 경우

파이프라인 모드는 한 작업의 정보가 다음 작업을 생성하기 위해 클라이언트에서 필요한 경우 유용하지 않습니다. 이 경우 클라이언트는 동기화 포인트를 도입하고 필요한 결과를 얻기 위해 전체 클라이언트/서버 왕복을 기다려야 합니다.

그러나 필요한 정보를 서버 측에서 교환하도록 클라이언트 설계를 조정하는 것이 가능한 경우가 많습니다. 읽기-수정-쓰기 사이클은 특히 좋은 후보입니다. 예를 들어:

-- 파이프라인 모드에 적합하지 않은 패턴
BEGIN;
SELECT x FROM mytable WHERE id = 42 FOR UPDATE;
-- 결과: x=2
-- 클라이언트가 x에 1을 더함:
UPDATE mytable SET x = 3 WHERE id = 42;
COMMIT;

위 코드는 다음과 같이 훨씬 더 효율적으로 수행할 수 있습니다:

-- 파이프라인 모드에 적합한 패턴
UPDATE mytable SET x = x + 1 WHERE id = 42;

파이프라인 모드는 단일 파이프라인에 여러 트랜잭션이 포함된 경우 덜 유용하고 더 복잡합니다.


관련 섹션

  • 이전: 34.4. 비동기 커맨드 처리 (Asynchronous Command Processing)
  • 다음: 34.6. 쿼리 결과 행별 가져오기 (Retrieving Query Results Row-by-Row)

Copyright © 1996–2022 The PostgreSQL Global Development Group