Protobuf를 제거하고 Rust↔C 직접 바인딩으로 성능 5배 개선
(pgdog.dev)- PostgreSQL 확장 프록시인 PgDog가 SQL 파싱 성능을 높이기 위해 Protobuf 직렬화 대신 Rust 직접 바인딩을 도입
- 기존 Protobuf 기반 구조를 C–Rust 직접 변환(bindgen + Claude 생성 래퍼) 으로 교체해 파싱 5.45배, 디파싱 9.64배 속도 향상
- 성능 병목은 pg_query_parse_protobuf 함수에서 발견되었으며, 캐싱 시도 후에도 근본적 개선을 위해 구조 변경 수행
- Claude LLM을 활용해 6,000줄의 Rust–C 변환 코드를 자동 생성하고,
parse,deparse,fingerprint,scan등 주요 함수에 적용 - 이 최적화로 PgDog의 CPU 사용량과 지연시간이 감소, PostgreSQL 수평 확장 프록시로서의 효율성이 크게 향상됨
PgDog과 Protobuf의 한계
-
PgDog은 PostgreSQL을 확장하기 위한 프록시로, 내부적으로 libpg_query를 사용해 SQL 쿼리를 파싱함
- Rust로 작성되어 있으며, 기존에는 Protobuf 직렬화/역직렬화를 통해 C 라이브러리와 통신
- Protobuf는 빠르지만, 직접 바인딩 방식이 더 빠름
- PgDog 팀은
pg_query.rs를 포크해 Protobuf를 제거하고 C–Rust 직접 바인딩을 구현 - 결과적으로 쿼리 파싱은 5.45배, 디파싱은 9.64배 빨라짐
- PgDog 팀은
벤치마크 결과
- 벤치마크는 PgDog의 포크 저장소에서 재현 가능
-
pg_query::parse(Protobuf): 613 QPS -
pg_query::parse_raw(직접 C–Rust): 3357 QPS -
pg_query::deparse(Protobuf): 759 QPS -
pg_query::deparse_raw(직접 Rust–C): 7319 QPS
-
성능 병목 분석과 캐싱 시도
- samply 프로파일러를 사용해 CPU 사용 시간을 분석한 결과, pg_query_parse_protobuf 함수가 병목으로 확인됨
- 캐싱을 통해 일부 개선을 시도
- LRU 알고리듬 기반 해시맵 캐시를 사용, 쿼리 텍스트를 키로 AST를 저장
- 준비된 문장을 사용하는 경우 재사용 가능
- 그러나 일부 ORM이 수천 개의 고유 쿼리를 생성하거나, 오래된 PostgreSQL 드라이버가 준비된 문장을 지원하지 않아 캐시 효율이 낮았음
LLM을 활용한 Protobuf 제거
- PgDog 팀은 Claude LLM을 활용해 Protobuf를 제거한 Rust 바인딩을 생성
- 명확하고 검증 가능한 작업 범위 내에서 AI가 효과적으로 작동
- Claude는
libpg_query의 Protobuf 스펙을 기반으로 C 구조체를 Rust 구조체로 매핑- 2일간의 반복 작업 끝에 6,000줄의 재귀적 Rust 코드 완성
-
parse,deparse,fingerprint,scan함수에 적용해 pgbench 기준 25% 성능 향상 확인
구현 세부 구조
- Rust와 C 간 변환은 unsafe 함수를 사용해 구조체를 직접 매핑
- C 구조체를 Postgres API에 전달해 AST를 생성하고, Rust로 재귀 변환
- 각 AST 노드는 convert_node 함수로 처리되며, SQL 문법의 수백 개 토큰을 매핑
- SELECT, INSERT 등 각 노드 타입별로 별도 변환 함수 존재
- 변환 결과는 기존 Protobuf 구조체(
protobuf::ParseResult)를 재활용해 테스트 시 byte 단위 비교 검증 가능 - 재귀 알고리듬은 메모리 할당이 적고 CPU 캐시 효율이 높아 반복문 기반 구현보다 빠름
- 반복문 기반 구현은 불필요한 메모리 할당과 해시맵 조회로 인해 오히려 느림
결론
- Postgres 파서의 오버헤드를 줄여 PgDog의 지연시간, 메모리, CPU 사용량을 모두 절감
- 이 최적화로 PgDog은 더 빠르고 저비용으로 운영 가능한 PostgreSQL 확장 프록시로 발전
- PgDog은 PostgreSQL의 수평 확장(next iteration) 을 함께 구축할 엔지니어를 모집 중임
제가 원문을 곡해하고 있는 것일지도 모르겠지만, 유독 러스트 관련 글 들은 본질을 떠나 마치 "러스트라서" 빨라진 것 같이 글을 쓰는 것 같아요.
이번 글의 주요 포인트는 불필요한 직렬화 오버헤드를 줄여 성능이 개선된건데 말이죠.
지금 다시 보니 또 그렇게 러스트를 찬양하는 글은 아닌 것 같은데, 다른 글들에 의해 제가 부정적인 인식이 생겨버러셔일까요?
저도 이거 원문 제목이 실제 내용과 달리 너무 Rust 라서 성능 향상에 초점을 두는 것처럼 보여져서, 살짝 수정했습니다.
Rust 글들이 그런 경향이 자주 보여서 살짝 필터링하면서 볼 필요는 있는 것 같아요.
Hacker News 의견들
-
제목이 마치 Rust가 5배 성능 향상을 준 것처럼 보이지만, 실제로는 느려졌다는 점이 아이러니함
문제는 Rust로 작성된 소프트웨어가 C로 된 libpg_query를 써야 했는데, 직접 연결할 수 없어 Protobuf 기반의 Rust–C 바인딩을 사용한 것임
이 방식이 느려서, 결국 LLM의 도움으로 비이식성이지만 훨씬 최적화된 바인딩을 새로 작성했음
만약 애초에 C로 썼다면 변환 과정이 필요 없었을 것임. 즉, 제목은 “Rust 사용으로 인한 성능 손실을 줄였다”가 더 정확했을 것임
변환 계층은 이식성과 보안을 주지만, 결국 복사·변환·직렬화가 반복되며 앱을 느리게 만드는 원인 중 하나라고 생각함- Rust가 느린 게 아니라, 외부 라이브러리의 비효율적 설계가 문제였음
Rust에서 C 라이브러리를 호출하는 건 매우 쉽고, 안전한 래퍼도 이미 많이 존재함
Protobuf를 중간에 둔 구조는 거의 본 적이 없으며, 그게 병목이었음
제목은 단순히 클릭을 유도하기 위한 “Rust로 다시 썼다”식 밈에 가깝다고 봄 - C로 썼다면 더 빠를 거라는 건 공정하지 않음
원래 라이브러리가 직렬화/역직렬화를 반복하는 잘못된 설계를 하고 있었고, 그걸 제거한 게 핵심임
제목은 “Protobuf를 일반 API로 교체해 5배 빨라졌다”가 더 정확함 - 왜 바로 FFI를 쓰지 않았는지 궁금함
Rust에서 C 바인딩은 가장 쉽고, 큰 API가 아니라면 단순함
Protobuf는 메모리 내 데이터 교환에는 부적절한 도구라고 생각함 - LLM으로 최적화를 했다면, 차라리 C 라이브러리를 Rust로 완전히 포팅하는 데 썼으면 어땠을까 생각함
앞으로 LLM 덕분에 다양한 언어로의 포팅이 폭발적으로 늘어날 것 같음 - Rust와 Postgres 사이에 Protobuf를 둔 건 성능 악몽 수준임. 그런 라이브러리가 인기를 얻은 게 놀라움
- Rust가 느린 게 아니라, 외부 라이브러리의 비효율적 설계가 문제였음
-
제목이 다소 오해를 부름
사실상 “Protobuf 직렬화 단계를 제거했더니 빨라졌다”는 내용임- Protobuf는 단순 복사로는 불가능한 버전 호환성을 제공함
클라이언트와 서버가 독립적으로 업데이트돼도 동작하게 해주며, 여러 언어 간 통신을 쉽게 함
대규모 시스템에서는 이런 유연성이 매우 중요함 - Protobuf 직렬화가 단순 메모리 복사보다 5배 느리다는 건 오히려 생각보다 빠르다는 인상임
- memcpy나 mmap이 훨씬 빠르지만, Rust 진영에서는 그런 비안전한 방식을 꺼림
- 이런 경우엔 Arrow 같은 표준화된 zero-copy 포맷을 쓰면 좋을 것 같음. 언어별 패딩 문제나 보안 검사를 자동으로 처리해줌
- Protobuf는 단순 복사로는 불가능한 버전 호환성을 제공함
-
Rust 때문이 아니라, Protobuf를 일반화된 저장 포맷으로 쓴 게 느린 원인일 수 있음
결국 특정 목적에 맞게 단순화한 게 핵심임- “Protobuf를 네이티브 최적화 구현으로 교체했다”는 제목은 주목을 덜 받았을 것임
Rust를 제목에 넣은 건 클릭 유도를 위한 선택 같음 - 기사 제목이 논란을 부르지만, 본문은 그걸 인식하고 있음
- 실제로는 Rust와 거의 관계가 없지만, Rust가 없었다면 메인 페이지에 오르지 못했을 것임
- “Protobuf를 네이티브 최적화 구현으로 교체했다”는 제목은 주목을 덜 받았을 것임
-
pg_query의 원 저자가 배경을 설명함
원래 pganalyze에서 Postgres 쿼리를 파싱해 테이블 참조를 찾고, 쿼리를 재작성·포맷하는 용도로 사용했음
초기엔 JSON을 썼지만, 이후 Protobuf로 전환해 여러 언어(Ruby, Go, Rust, Python 등)에서 타입 안정성 있는 바인딩을 쉽게 제공하기 위함이었음
Rust 같은 언어는 FFI가 낫지만, 다른 언어들은 유지보수 부담이 큼
Lev의 시도는 지지하며, 앞으로 libpg_query에 직접 FFI로 접근할 수 있는 함수를 추가할 예정임
다만 성능이 중요하지 않은 경우엔 Protobuf가 여전히 더 편리한 선택임 -
“5배 빠르다”는 말이 Cap’n Proto의 “무한히 빠르다”는 농담을 떠올리게 함
- Cap’n Proto는 Protobuf 제작자가 만든 것으로, 파싱이 필요 없는 구조라서 그런 표현을 쓴 것임
- 하지만 실제로 써보면 Cap’n Proto는 사용성이 떨어짐
-
제목은 과장됐지만, 실제 작업은 인상적임
Protobuf를 완전히 없앤 게 아니라 사용 방식을 최적화한 것임
“X로 바꿨더니 5배 빨라졌다”는 문구는 대개 “엉망이던 구현을 고쳤다”는 뜻임
핵심 교훈은- 직렬화/역직렬화는 숨은 병목이 되기 쉬움
- 기본 구현은 대부분 특정 상황에 최적화돼 있지 않음
-
프로파일링을 통해 병목을 정확히 찾아야 함
Rust FFI에도 오버헤드가 있으므로, 진짜 성과는 언어가 아니라 데이터 흐름 재설계와 최적화 노력 덕분임
-
FlatBuffers가 더 빠르지만, Protobuf를 쓰는 이유는 대기업이 유지보수하기 때문임
- 하지만 FlatBuffers도 Google이 유지보수함
결국 “Google이 만든 것이라 안전하다”는 인식이 근거 없음 - 나도 예전에 Google 플랫폼(code.google.com)에 코드를 올렸다가 망한 경험이 있음
단순히 메모리 공유와 버전 필드만 있는 zero-copy 구조면 충분한데, 굳이 Protobuf를 쓸 이유가 없다고 생각함 - Google은 아직도 문자열 필드 zero-copy 최적화를 공개하지 않았음
- 하지만 FlatBuffers도 Google이 유지보수함
-
Protobuf 성능은 농담 수준이라 생각함
직렬화가 사실상 무료인 zero-copy 포맷을 써야 함
예를 들어 내가 만든 Lite³는 FlatBuffers보다 242배 빠름- 하지만 그 라이브러리는 2025년 11월 이후에야 등장했음
Protobuf를 쓰는 이유는 생태계, 스키마, 언어별 툴링 등 수많은 현실적 이유 때문임
- 하지만 그 라이브러리는 2025년 11월 이후에야 등장했음
-
실제로는 Rust나 Protobuf 문제가 아니라, PostgreSQL 추상화 계층의 비효율적 직렬화 구현이 원인이었음
pgdog는 그 계층을 제거하고 C API로 직접 데이터를 전달했음
필요 없는 기능을 제거하면 당연히 빨라짐
하지만 어떤 사람들에게는 여전히 직렬화가 필요한 상황이 있음
그런 이들에게 “Rust로 바꾸라”는 제목은 잘못된 메시지임
결국 대부분의 경우엔 JSON이면 충분하고, 정말로 더 빠른 게 필요하다면 직렬화 자체를 피해야 함 -
이건 불공정한 비교임
IPC 통신에 직렬화 프로토콜을 쓰는 건 당연히 오버헤드가 있음
“20% 빨라지면 개선, 10배 빨라지면 처음부터 잘못 만든 것”이라는 말이 딱 들어맞는 사례임