TL;DR
- ClickHouse의 네 가지 Postgres 확장 프로그램은 Postgres의
MemoryContext와 오류 처리 방식이 C++의 예외·소멸자 모델과 충돌하므로, 각각 C++ 제거·경계 격리·별도 프로세스 분리·예외 처리를 적용함. palloc/pfree와MemoryContext는 트랜잭션이나 집계 종료 시 메모리를 해제하고 콜백으로 정리 작업을 수행하는 Postgres 방식임.- Postgres의
setjmp/longjmp기반 오류 처리는 C++ 소멸자를 건너뛰며, 비자명한 자동 객체의 소멸자를 건너뛰는 동작은 메모리 누수만이 아니라 정의되지 않은 동작임. pg_clickhouse는 C++를 완전히 제거하고clickhouse-c와pg-clickhouse-c를 사용하며,pg_re2는 C++ 코드를 별도 래퍼에 한정함.pg_chdb는 C++ 런타임을 헬퍼 프로세스로 분리하고,pg_stat_ch는 예외를FATAL로 처리하지만 이는 일반적인 크래시 격리 보장이 아니며 장기적으로 헬퍼 프로세스 분리를 검토함.
Postgres 확장 프로그램에서 C와 C++가 충돌하는 이유
- ClickHouse에서 개발한 Postgres 확장 프로그램은
pg_clickhouse(ClickHouse 외부 데이터 래퍼),pg_re2(ClickHouse와 같은 정규식 엔진인 RE2 통합),pg_chdb(COPY용 ClickHouse 객체 스토리지 기능),pg_stat_ch(메트릭과 로그 전송)임. - 이 확장 프로그램에는 C++를 Postgres 확장 프로그램에 도입하는 작업이 포함됨. Postgres는 C를 사용하지만, 두 언어의 메모리 관리와 오류 처리 방식은 서로 잘 맞지 않음.
- Postgres는
malloc/free대신palloc/pfree를 사용하며, 할당은MemoryContext에 연결됨.MemoryContext는 트랜잭션이나 집계 등이 끝날 때 할당 메모리를 한꺼번에 해제하는 아레나 할당자처럼 동작하고, 소멸자 역할을 하는 콜백도 지원함. - 메모리 할당 실패 시 Postgres는 오류를 발생시키며,
setjmp/longjmp를 기반으로 한PG_TRY/PG_CATCH/PG_FINALLY매크로로 처리함.PG_FINALLY는 RAII와 유사한 정리 로직을 구성하는 또 다른 방법임. - C++는 보통 생성자와 소멸자를 호출하는
new/delete를 사용하고, 할당 실패 시std::bad_alloc을 발생시킴.setjmp/longjmp는 C++ 정리 작업을 건너뛰며, 비자명한 소멸자를 가진 자동 객체를 건너뛰는 것은 단순한 메모리 누수가 아니라 정의되지 않은 동작임. PG_FINALLY와MemoryContext콜백도 이런 점프를 안전하게 만들지 못하며, C++ 예외는PG_CATCH/PG_FINALLY를 우회함. 처리되지 않은 예외는 프로세스를 중단시키고, 백그라운드 워커에서도 공유 메모리 손상 우려로 Postgres의 포스트마스터가 전체 클러스터를 재시작하게 만들 수 있음.
`pg_clickhouse`
pg_clickhouse에서는 C++ 사용을 완전히 없앰.clickhouse-cpp를 새 C 라이브러리인clickhouse-c로 대체한 뒤, Postgres 관련 로직을 통합하면서clickhouse-c를 범용 라이브러리로 유지하기 위해pg-clickhouse-c로 한 겹 더 감쌈.clickhouse-c는 전송 방식에 종속되지 않도록 설계되어, Native 프로토콜 외부에서도 Native 형식을 지원함. 이에 따라 HTTP 드라이버는 HTTP를 통한 Native 형식을 사용해 바이너리 드라이버와 디코딩·인코딩 로직 상당 부분을 공유함.- HTTP에는 Postgres 환경과 함께 사용하기에 충분히 낮은 수준의 인터페이스를 제공하는
libcurl을 사용함. - 이전 구현은 ClickHouse 문자열을
std::string에 담은 뒤cstring_to_text_with_len으로 Postgres 텍스트 값으로 변환했음. 이 변환 함수는palloc을 사용하므로, 할당 실패 시 Postgres가 오류 처리기로 점프하면서std::string의 소멸자를 건너뜀. - 주변의 C++
try/catch는 Postgres의 점프를 잡을 수 없으므로, 이 경로에서 C++ 객체를 사용하는 방식은 안전하지 않음.
`pg_re2`
pg_re2에서는 C++ 코드를re2_wrapper.cpp에 한정함.- C++ 할당은 모두 C++
try/catch내부에서 이뤄지며, 해당 코드는 Postgres C 코드로 호출을 넘기지 않음. 이 관심사 분리가 두 메모리·오류 처리 체계 사이에 명확한 경계를 만듦.
`pg_chdb`
pg_chdb는 Native 블록 디코딩·인코딩에pg-clickhouse-c를 재사용함.- 처음에는 백그라운드 워커 안에서
libchdb를 로드했지만, 이후libchdb를 별도의 헬퍼 실행 파일로 옮김. 핵심은 C++ 런타임, 스레드, 메모리 할당, 실패를 Postgres 백엔드나 관리 대상 백그라운드 워커 바깥에 두는 것임. - 백엔드가 헬퍼를 포크하고 실행하므로 헬퍼는 포스트마스터의 자손 프로세스이지만, 포스트마스터가 백그라운드 워커로 관리하지는 않음.
- 헬퍼에는 ClickHouse 쿼리를 실행하고 Native 블록을 교환하는 데 필요한 정보만 전달되므로 Postgres 헤더가 필요하지 않음. 따라서
libchdb충돌은 호출 중인 작업을 실패시킬 수 있지만, 그 자체로 Postgres 클러스터의 크래시 복구를 촉발하지 않음.
`pg_stat_ch`
pg_stat_ch는 OpenTelemetry 라이브러리와 Arrow의 C++ 바인딩에 의존함.nanoarrow에는 IPC 기능이 다수 부족하기 때문임.- C++ 코드는 Postgres 백그라운드 워커에 한정하고, 해당 워커에서 C++ 예외 처리와 Postgres 예외 처리를 함께 적용함.
- 종료 처리기(terminate handler)는 처리되지 않은 예외와
std::terminate를 기본 동작인abort()/SIGABRT대신ereport(FATAL)로 전달함.FATAL은 일반적으로 Postgres 프로세스 종료 정리를 실행하고 상태 코드 1로 종료함. - 공유 메모리 분리도 정상적으로 완료되면 포스트마스터는 이를 크래시가 아닌 종료로 받아들이며,
bgw_restart_time에 따라 워커를 재시작함. 반면 비정상적인 워커 종료는 클러스터 전체의 크래시 복구를 촉발하고 다른 세션의 연결을 끊을 수 있음. - 이는 완화책이지 일반적인 크래시 격리 보장이 아님.
FATAL은 일관성이 깨진 공유 메모리를 복구할 수 없으며, 정리 실패 시 포스트마스터가 종료를 크래시로 판단할 수 있음. - 프로세스 전체 종료 처리기가 라이브러리 스레드에서 호출될 가능성도 검토해야 함. 이 처리기는 임의의 스레드에서 Postgres 오류 처리와 종료 콜백을 안전하게 호출할 수 있게 만들지 않음.
- 이 글을 작성하는 과정에서 구체적인 소유권 문제가 드러났으며, 이는 PR #126에서 해결함. 중단된 큐 제거 작업이 슬롯에 이미 해제된 오류 텍스트나 이미 반환된 쿼리 텍스트를 가리키게 둘 수 있었음.
- 복구 과정은 꼬리 위치가 이동하기 전에 해당 슬롯을 재시도할 수 있어, 같은 참조를 두 번 해제하거나 반환할 가능성이 있었음.
- 장기적으로는
pg_chdb처럼 C++ 의존성을 헬퍼 실행 파일로 옮겨pg_stat_ch를 더 견고하게 만들 계획임. PR #129는 첫 단계로, Postgres 의존성이 없는src/exporter에 C++를 격리하고bgworker.c가 C/C++ 경계를 처리하도록 함.
댓글 (0)
로그인하면 이 기사에 내 생각을 남길 수 있어요