Redis가 필요할까? PostgreSQL이 큐잉, 락, Pub/Sub을 제공함 (2021)
(spin.atomicobject.com)- 웹 서비스에서 흔한 PostgreSQL+Redis 조합은 편리하지만, 백그라운드 작업 큐·분산 락·Pub/Sub 중 일부는 PostgreSQL만으로도 처리 가능함
- PostgreSQL 9.5의
FOR UPDATE SKIP LOCKED는 잠긴 행을 기다리지 않고 건너뛰어, 여러 워커가 같은 작업을 집어가는 상황을 막는 큐 구현에 활용됨 - 애플리케이션 수준의 분산 락은 PostgreSQL의 advisory locks로 만들 수 있으며, 내부 락 엔진을 애플리케이션 정의 목적에 재사용함
- PostgreSQL 9의
LISTEN/NOTIFY는 임의 문자열 채널 구독과 알림 전송을 지원해 Pub/Sub 계층으로 쓸 수 있고, Rails ActionCable도 PostgreSQL 사용을 기본 지원함 - Redis는 TTL 캐싱과 임시 데이터 조작에 여전히 강점이 있지만, 일부 시스템은 Redis 의존을 줄여 운영 비용과 개발 복잡도를 낮출 수 있음
PostgreSQL로 흡수할 수 있는 Redis 역할
- 일반적인 웹 서비스는 PostgreSQL을 데이터 저장소로 쓰고, Redis를 백그라운드 작업 큐 조정이나 제한적인 원자적 연산에 사용함
- Redis 자체는 유용하지만, 이 조합에서 Redis가 맡는 일부 역할은 PostgreSQL 기능만으로도 대체 가능함
백그라운드 작업 큐
- Redis는 웹 서비스에서 백그라운드 워커 풀로 작업을 전달하는 작업 큐 조정에 자주 쓰임
- 실행해야 할 백그라운드 작업과 입력 데이터를 기록함
- 여러 워커 중 하나만 해당 작업을 가져가도록 보장해야 함
- Redis는 자료구조에 대한 원자적 연산이 풍부해 이 용도에 잘 맞음
- PostgreSQL 9.5부터
SELECT ... FOR ...문에SKIP LOCKED옵션을 사용할 수 있음- 이 옵션을 지정하면 PostgreSQL은 락 해제를 기다려야 하는 행을 무시함
FOR UPDATE SKIP LOCKED를 지정하면 반환된 행에 대해 행 수준 락이 암묵적으로 획득됨SKIP LOCKED덕분에 다른 트랜잭션의 락에서 블로킹될 가능성이 없음- 처리할 다른 작업이 있으면 그 작업이 반환됨
- 여러 워커가 같은 명령을 실행해도 행 수준 락 때문에 같은 행을 받지 않음
- 기본 흐름은 트랜잭션 안에서
pending상태의 작업 하나를 선택하고, 해당 작업을running으로 바꾼 뒤 반환하는 방식임BEGINSELECT ... FOR UPDATE SKIP LOCKEDUPDATE jobs SET status = 'running' ... RETURNING jobs.*COMMIT
- 주의할 점은 워커와 작업 수가 모두 많을 때 큐를 훑으며 락 획득을 시도하는 비용이 커질 수 있다는 점임
- 실제로 다뤄본 대부분의 앱은 백그라운드 워커가 12개 미만이어서, 이 비용이 크지 않을 가능성이 높았음
애플리케이션 락
- 서드파티 서비스와 동기화하는 루틴처럼, 모든 서버 프로세스에서 특정 사용자에 대해 한 인스턴스만 실행되게 해야 하는 경우가 있음
- 이런 분산 락은 Redis의 또 다른 흔한 사용 사례임
- PostgreSQL은
advisory locks로 같은 목적을 달성할 수 있음- advisory locks는 PostgreSQL이 내부적으로 사용하는 락 엔진을 애플리케이션 정의 목적에 활용하게 해줌
Pub/Sub
- 활성 클라이언트에 이벤트를 푸시할 때도 Redis가 자주 쓰임
- 사용자가 새 메시지를 읽을 수 있음을 알림
- 데이터가 준비되는 대로 클라이언트에 스트리밍함
- 일반적으로 웹소켓이 이벤트 전송 계층이고 Redis가 Pub/Sub 엔진 역할을 함
- PostgreSQL 9부터
LISTEN과NOTIFY문으로 Pub/Sub 기능을 제공함- PostgreSQL 클라이언트는 임의 문자열인 특정 메시지 채널을
LISTEN으로 구독할 수 있음 - 다른 클라이언트가 해당 채널에
NOTIFY를 보내면 구독 중인 모든 클라이언트가 알림을 받음 - 선택적으로 작은 메시지를 함께 첨부할 수 있음
- PostgreSQL 클라이언트는 임의 문자열인 특정 메시지 채널을
- Rails와 ActionCable을 사용 중이라면 PostgreSQL 사용이 기본으로 지원됨
Redis를 남겨둘 만한 영역
- Redis는 PostgreSQL과 다른 영역을 담당하며, PostgreSQL이 목표로 하지 않는 작업에서 강함
- TTL이 있는 데이터 캐싱
- 임시 데이터 저장과 조작
- PostgreSQL은 단순한 SQL 데이터베이스나 ORM 뒤의 불투명한 구성요소로 보기에는 기능 폭이 넓음
- Redis에 맡긴 작업 중 일부는 PostgreSQL에도 적합할 가능성이 있음
- Redis를 생략하면 여러 데이터 서비스에 의존할 때 생기는 운영 비용과 개발 복잡도를 줄이는 선택지가 될 수 있음
댓글과 토론
Hacker News 의견들
-
모두가 과도하게 분산된 아키텍처를 고집하니 Redis의 진짜 장점을 못 보는 경우가 많음. 애플리케이션과 같은 머신에서 돌리면 1밀리초보다 훨씬 짧게 응답할 수 있고, 그래서 Postgres로는 하기 어려운 일을 애플리케이션에서 할 수 있음
Postgres가 훌륭한 건 맞지만, 애플리케이션과 같은 머신의 메모리에서 도는 건 아님. 큐 같은 것만 필요하면 인메모리 키-값 저장소가 필요 없을 수도 있음. 인메모리 키-값 저장소의 핵심은 RAM의 성능 특성이 필요한 일을 하는 것이고, 네트워크 연결 너머로는 그 특성을 얻을 수 없음- 로컬 프로세스 하나만 머신 안의 Redis를 인메모리 캐시로 쓸 거라면, 그냥 사용하는 프로그래밍 언어의 자료구조를 쓰는 편이 낫다
- 로컬에서 실행할 때 Postgres와 Redis의 오버헤드가 얼마나 다른지 궁금함. 왜 Postgres는 로컬에서 안 돈다고 보는지도 모르겠음
Postgres에 특별한 마법이 있는 건 아니고 Redis처럼 다른 프로세스에서 도는 프로그램일 뿐임. 로컬 연결에서는 지연을 줄이기 위해 빠른 파이프를 쓰고, 더 빠른 대량 데이터 전송 방식도 쓸 수 있음. 이런 식으로 여러 번 써봤다 - Django에는 Redis를 지원하는 캐시가 내장되어 있고, 인메모리 캐시 옵션도 있지만 “프로덕션용 아님”이라고 표시되어 있음. Django 인스턴스가 여러 개면 각 인메모리 캐시가 서로 달라지기 때문임
하지만 내부 업무 도구 같은 경우에는 단일 인스턴스를 오래 키워 쓸 수 있고, 이 인메모리 캐시 덕분에 매우 빨라짐. django-cachalot은 테이블에 쓰기가 발생할 때마다 캐시 무효화를 자동으로 처리하는 라이브러리임. 다소 거친 방식이지만 거의 노력 없이 성능 향상을 주고, 업데이트가 드문 내부 업무 앱은 사실상 RAM에서 돌다가 캐시에 없을 때만 일반 데이터베이스 질의로 돌아감
https://github.com/noripyt/django-cachalot - 이 구성에서 Redis보다 더 빠른 건 해시맵임. Postgres가 앱과 같은 서버에서 돌지 못할 이유도 없고, 실제로 꽤 흔한 구성임
- 과도한 설계와 성급한 분산화는 실제 문제지만, Redis는 “Remote Dictionary Server”의 약자임. 본래 목적은 로컬에서 돌리는 게 전혀 아님. 물론 언어 기본 딕셔너리가 범위 질의를 지원하지 않는 경우처럼 로컬 실행도 정당한 설계 선택일 수 있음
-
여기 논의는 Redis 관점에서 나온 방어적인 얘기가 많지만, 물론 Redis가 더 나은 특정 영역은 있음
다만 글의 핵심은 그게 아니라고 봄. 핵심 문장은 “PostgreSQL은 단순한 SQL 데이터베이스나 ORM 뒤에 숨어 있는 신비한 존재로 접근할 때 예상하는 것보다 훨씬 많은 기능을 갖고 있다”로 요약할 수 있음. 데이터베이스를 ORM 뒤에서만 쓰고 있다면 어떤 데이터베이스든 기능을 놓치고 있을 가능성이 큼. Redis 같은 다른 서비스를 추가해야 한다면, 새 의존성을 더하기보다 이미 구성한 데이터베이스를 쓰는 편이 나을 수도 있음 -
Postgres가 할 수 있는 일을 이해하는 건 좋음. 강력한 데이터베이스임
반론은 Redis를 쓰는 장벽이 매우 낮고, 그 대가로 성능이 높고 라이브러리 지원이 풍부하며 기본 데이터베이스의 부하를 덜 수 있다는 점임. 예를 들어 API 응답 캐시를 Postgres로 만들 수는 있음. TTL은 cron 작업으로 오래된 캐시 값을 쓸어내면 됨. 아니면 그냥 Redis를 쓰면 됨
권고 잠금은 멋지고 유용하지만, PgBouncer 같은 것을 쓰려면 세션 권고 잠금과 트랜잭션 인터리빙 사이에서 문제가 생길 수 있음. 별도 시스템에는 네트워크 호출, 가용성, 도메인 지식 같은 단점이 있지만 Redis 정도의 절충은 꽤 낮은 편임- 프로덕션에서 관리할 것이 하나 줄어드는 게 장점임. PostgreSQL로 시작하고, 성능·확장·비용 때문에 필요해질 때 특화 시스템을 추가하면 됨
-
꽤 오래된 글이지만, 이제는 매우 흔한 패턴이 됨. 이메일 발송이나 보고서 생성용 작업 큐만 필요한 프로젝트 90%는 초당 수백만 메시지를 처리하지 않으니, 스택을 단순화하는 방식은 검토할 가치가 있음
Celery에서 겪은 문제를 우회하려고 이런 패턴을 자주 쓰다가 별도 프레임워크로 분리했음: https://github.com/TkTech/chancy 피드백 환영
이런 도구는 많고, 그중 몇몇은 상업 서비스라 확실한 수요가 있음
https://worker.graphile.org/ (Node.js)
https://riverqueue.com/ (Go)
https://github.com/acaloiaro/neoq (Go)
https://github.com/contribsys/faktory (Go)
https://github.com/sorentwo/oban (Elixir)
https://github.com/procrastinate-org/procrastinate (Python)- 간단한 작업 큐를 찾고 있었음. Huey도 괜찮지만 이미 Postgres를 쓰고 있고, 큐에 넣는 작업이 한 시간에 한 번 정도라 Redis 기반은 늘 과하게 느껴졌음
- Node 쪽에는 PG-Boss도 있음: https://github.com/timgit/pg-boss
-
PGQueuer는 PostgreSQL의 FOR UPDATE SKIP LOCKED와 LISTEN/NOTIFY를 사용해 작업 큐, 잠금, 실시간 알림을 제공함
이미 PostgreSQL을 쓰는 경우 Redis가 필요 없는 미니멀한 대안임
https://github.com/janbjorge/PGQueuer
참고로 내가 만든 것임 -
Postgres를 좋아하지만 몇 가지 한계가 있음
키-값 저장소가 필요하다면 autovacuum을 알고 있는지, 커넥션 풀 한계를 알고 있는지, 처리량과 안전성 중 무엇을 원하는지 따져야 함. 큐가 필요하다면 순차 처리인지, 속도 제한이 있는지, 팬아웃인지, 주제별 분리인지 봐야 함. 발행/구독이 필요하다면 중복 수신을 신경 쓰는지, 메시지 손실을 신경 쓰는지, 재생이 필요한지 따져야 함. 잠금이 필요하다면 커넥션 풀 한계와 statement_timeout을 알아야 함. 위 문제 대부분은 해결 가능하지만 그렇게 단순하지 않음- 오래 도는 트랜잭션이 vacuum의 튜플 제거를 막을 때 구현이 무너지지 않는지도 봐야 함
-
Postgres 발행/구독에서 큰 걸림돌은 메시지 최대 크기가 8000바이트라는 점임
권장 우회책은 데이터를 테이블에 넣고 ID만 보내는 방식인데, 영원히 남기고 싶지 않으면 그 데이터를 가비지 컬렉션해야 하고 메시지마다 작업이 추가됨. 물론 괜찮은 경우도 있지만, 많은 Redis 사용 사례에서는 이 제한 때문에 동등하다고 보기 어려움 -
pgsql이 클라이언트 연결 15000개를 처리하는지 보자
- Postgres와 MySQL의 차이가 여기서는 정말 놀라울 정도로 큼
Planetscale 쪽 사람이 팟캐스트에서 GitHub의 MySQL 인스턴스는 각각 5만 개 이상 연결을 처리한다고 했음. 반면 Postgres에서 100개 넘는 연결이 필요하면 이미 PgBouncer가 필요해짐 - 커넥션 풀링을 쓰면 됨. AWS RDS Proxy처럼 클릭 몇 번으로 쓸 수 있음
- Postgres와 MySQL의 차이가 여기서는 정말 놀라울 정도로 큼
-
큐, 잠금, 발행/구독은 가능함. 하지만 Redis의 가장 중요한 용도인 캐싱은 빠져 있음
Postgres의 업데이트는 삽입보다 비싸기로 악명 높고, 쓰레기를 만들며 vacuum이 필요함. 캐싱에는 중요하지 않은 내구성 보장 때문에 쓰기도 상당히 느려짐. 자동 만료는 매우 편하고 실수를 줄여줌- 필요 없는 기능은 대부분 끌 수 있음. 예를 들어 테이블을 unlogged로 만들 수 있음: https://www.postgresql.org/docs/current/sql-createtable.html...
동기 커밋, autovacuum 등도 끌 수 있음. 물론 Redis가 여전히 더 빠르겠지만, 일반적인 회사가 신경 쓸 정도의 차이는 아닐 수 있음
- 필요 없는 기능은 대부분 끌 수 있음. 예를 들어 테이블을 unlogged로 만들 수 있음: https://www.postgresql.org/docs/current/sql-createtable.html...
-
글의 요지를 더 분명히 말하면, Postgres로 시작하고 필요가 생기면 Redis로 옮기면 됨
움직이는 부품 수를 최대한 적게 유지하는 게 좋음- Redis는 배포가 딱히 어렵지 않음. 나중에 어떤 예기치 못한 부작용이 생길지 모르는 전환을 계획하느니 그냥 처음부터 Redis를 쓰는 편도 괜찮음