6P by GN⁺ | ★ favorite | 댓글 1개
  • 별도 큐 시스템을 들이기 전, 이미 쓰는 Postgres만으로 배경 작업 큐를 구성할 수 있다면 운영 단순성과 팀의 익숙함을 크게 활용할 수 있음
  • Postgres 9.5 이후의 NOTIFY/LISTENFOR UPDATE SKIP LOCKED는 새 작업 알림과 작업자 간 중복 처리 방지를 함께 해결함
  • Redis, Kafka, RabbitMQ, Amazon SQS는 강력한 선택지지만, 새 의존성은 개발·테스트·운영 환경에 실패 모드와 학습 비용을 추가함
  • Postgres 큐도 만능은 아니어서 Redis보다 push/pop이 느릴 수 있고, 별도 큐 데이터베이스나 서버, 더 잦은 VACUUM 같은 운영상 대가가 생길 수 있음
  • 배경 작업 코드는 큐 백엔드에 묶이지 않게 추상화하고, 익숙한 기술이 요구를 충족하지 못할 때만 다른 큐 기술을 검토하는 편이 안전함

Postgres 큐가 동작하는 방식

  • Postgres 큐 기술은 두 요소로 구성됨
    • 새 작업을 알리고 받는 pub/sub
    • 여러 작업자가 같은 작업을 처리하지 않게 하는 행 잠금
  • 두 기능은 2016년에 출시된 Postgres 9.5부터 기본 제공됨
  • NOTIFYLISTEN을 함께 쓰면 애플리케이션에 pub/sub를 붙일 수 있음
  • FOR UPDATE SKIP LOCKED는 조건에 맞는 레코드에 행 잠금을 잡고, 이미 잠긴 레코드는 건너뜀
    • 작업 레코드에 적용하면 SELECT * FROM jobs ORDER BY created_at FOR UPDATE SKIP LOCKED LIMIT 1 같은 큐 처리 쿼리를 만들 수 있음
  • SKIP LOCKED는 데이터의 “일관되지 않은” 보기를 제공하지만, 큐에서는 이 특성이 유용함
    • 이미 처리 중인 작업은 행 잠금 때문에 다른 작업자에게 보이지 않음
    • 이 동작으로 분산 상호 배제가 가능해짐
  • NOTIFYLISTEN 중인 작업자에게 새 작업을 알리면 주기적 배치 처리와 실시간 작업 처리를 모두 구성할 수 있음

Redis가 배경 작업의 기본값이 된 흐름

  • 장시간 실행되는 작업을 어떻게 처리할지는 여러 트레이드오프가 걸린 기술 선택임
  • 널리 쓰이는 큐·메시지 브로커 선택지는 다음과 같음
    • Redis: 인메모리 데이터 저장소이자 여러 배경 작업 라이브러리의 백엔드
    • Apache Kafka: Apache Foundation이 관리하는 분산 이벤트 스트리밍 플랫폼
    • RabbitMQ: 널리 배포된 메시지 브로커로 소개됨
    • Amazon SQS: 확장성 높은 큐를 제공하는 Amazon SaaS
  • GitHub의 background jobs topic에서 인기 상위 5개 라이브러리는 모두 Redis 기반임
    1. Sidekiq — Ruby
    2. resque — Ruby
    3. rq — Python
    4. Hangfire — C#
    5. asynq — Go
  • Redis는 데이터를 메모리에 저장해 삽입과 조회가 빠르고, pub/sub API와 list, set 자료구조를 제공해 큐 구현에 잘 맞음
  • Redis의 확장성은 많은 개발자에게 기본 선택지로 작용하며, 기본값은 기술 선택에서 강한 힘을 가짐

확장성보다 먼저 볼 기준

  • 기술 업계의 “scale” 집착은 단순성, 유지보수 용이성, 개발자 인지 부하 감소를 뒤로 밀 수 있음
  • 많은 애플리케이션은 Google, Facebook, Uber 수준의 확장을 요구하지 않음
  • 프로젝트나 비즈니스를 시작할 때는 처음부터 확장성만 최적화하기보다 다음 기준을 먼저 볼 필요가 있음
    • 팀이 잘 아는 기술
    • 사용자 요구를 충족하는 충분히 좋은 기술
    • 팀 역량에 맞고 수고가 가장 적은 해법
  • Postgres도 실제로 잘 확장되지만, 큐 사용 사례에서 가장 크게 확장되는 전문 큐 시스템은 아님
  • 범용 소프트웨어인 Postgres는 여러 작업을 꽤 잘 처리하며, 큐도 그중 하나로 다룰 수 있음

“지루한 기술”로 판단하기

  • 큐 기술을 고를 때 가장 중요한 질문은 현재 사용 중이고 잘 이해하는 기술이 무엇인지임
  • 이미 쓰고 잘 아는 기술은 소프트웨어 스택에 추가되는 부담이 작음
  • 관계형 데이터베이스를 이미 쓰고 있고 그 데이터베이스가 Postgres라면, 다른 소프트웨어보다 먼저 Postgres 큐를 고려할 수 있음
  • “지루한 기술”은 고정된 목록이 아니라 현재 사용 중인 기술에 따라 달라짐
    • 메시지 전달 중심 애플리케이션에는 RabbitMQ가 지루한 기술일 수 있음
    • 캐싱 중심 애플리케이션에는 Redis가 지루한 기술일 수 있음
    • 관계형 데이터가 많은 애플리케이션에는 Postgres가 지루한 기술일 수 있음
  • Redis, Kafka, RabbitMQ, SQS를 배경 작업만을 위해 새로 도입하면 비용이 커짐
    • 개발·테스트·프로덕션 환경에 새 시스템 의존성이 추가됨
    • 향후 Developer, DBA, SRE가 새 시스템의 실패 모드와 설정을 알아야 함
    • 장애 복구, 문제 진단, 성능 모니터링 지식이 필요함
    • 팀이 아직 모르는 unknown unknowns가 위험으로 남음

Postgres 큐의 대가와 선택 기준

  • 지루한 기술이 만능은 아니며, Postgres도 예외가 아님
  • Postgres 큐를 선택하면 익숙함과 알려진 실패 모드, 비용 분산이라는 장점을 얻는 대신 다음 대가를 치를 수 있음
    • Postgres 큐의 push/pop은 Redis보다 상당히 느림
    • 단일 관계형 데이터베이스 대신 애플리케이션 데이터베이스와 큐 데이터베이스가 필요할 수 있음
    • 배경 작업을 독립적으로 확장하기 위해 별도 데이터베이스 서버가 필요할 수 있음
    • 더 잦은 VACUUM이 필요해 성능 부담이 생길 수 있음
  • Postgres, Redis, Kafka, RabbitMQ, SQS 중 어느 것도 자동 기본값이 되어서는 안 됨
  • 기술 선택은 계속되는 트레이드오프이며, 팀과 애플리케이션 요구에 맞춰 판단해야 함
  • 의심스러울 때는 “지루한 기술이 요구를 충족하지 못한다는 점이 입증될 때만 대안을 고려한다”는 기준을 적용할 수 있음

큐를 바꿀 수 있는 배경 작업 구조

  • 배경 작업 처리 코드는 특정 큐 기술에 종속되지 않아야 함
  • 애플리케이션이 성장하면 필요에 따라 memcached나 Redis 같은 기술이 추가될 수 있고, 시간이 지나면 그 기술도 팀에 익숙한 “지루한 기술”이 될 수 있음
  • 큐 기술을 추상화하면 사용자는 작업에 맞는 큐를 선택할 수 있음
  • GitHub 인기 상위 배경 작업 라이브러리 중 Hangfire를 제외하면 Redis 외 큐 기술로 빠져나갈 탈출구를 제공하지 않음
    • 이런 구조에서는 큐를 바꾸려면 애플리케이션 코드를 다시 작성해야 함
  • Neoq는 이 철학에 따라 만든 Go용 라이브러리임
    • 큐 백엔드는 인메모리, Postgres, Redis를 사용할 수 있음
    • 사용자는 애플리케이션 코드를 바꾸지 않고 다른 큐 백엔드로 초기화할 수 있음
    • 인메모리와 Postgres 구현은 1차 제공이고, Redis 구현은 asynq를 사용함
  • Redis, Kafka, RabbitMQ, SQS는 훌륭한 기술일 수 있지만, 항상 해당 작업에 맞는 큐이거나 적절한 복잡도인 것은 아님

댓글과 토론

Hacker News 의견들
  • 지금까지 분산 작업 시스템을 세 번 만들었고, 몇 년째 권하는 경험칙은 “현재 규모의 10배를 기준으로 설계하라”임
    초당 70 요청이면 700 요청을, 배치 작업 서버 20대가 필요하면 200대를 기준으로 잡는 식임. 매년 100% 성장하는 스타트업도 3년 뒤면 8배 규모라, 성장하면서 다시 쓸 시간은 있음
    처음 만든 시스템은 “확장성” 때문에 SQL을 피했지만, 트랜잭션 무결성이 필요한 예외 상황을 많이 만나면서 고생했음
    이후 만든 두 분산 작업 시스템은 PostgreSQL을 코디네이터로 쓰고, 말 그대로 SELECT FOR UPDATE SKIP LOCKED를 중심으로 구성했음. 하나는 보통 워커 350개를 제어하고, 다른 하나는 수천 개 작업에 복잡한 우선순위를 적용함
    둘 다 연 매출 수백만 달러 규모가 될 때까지 문제없이 버틸 것이고, 예를 들어 워커 350개짜리는 조금만 손보면 CPU 약 2,000개까지 확장될 수 있음
    초대규모 기술은 트랜잭션이 필요하다는 걸 깨닫기 전까지는 꽤 저렴해 보임. 하지만 그 순간부터 최종적 일관성 저장소 위에 트랜잭션 의미론을 흉내 내는 일이 엔지니어링 악몽이 됨
    그러니 앉아서 계산해 봐야 함. 회사가 연 1억 달러를 벌면 분산 시스템이 얼마나 커져야 하는지, 그 부하를 처리할 PostgreSQL 인스턴스를 쉽게 구할 수 있는지, 또는 클라이언트별 샤딩으로 충분한지 따져보고, 가능하다면 PostgreSQL을 강하게 고려할 만함. 사소한 것 백 가지가 쉬워짐

    • 비슷한 경험이 있음. SQL 기반 큐를 “SQL은 여기서 무너지고 분산 솔루션이 필수”라고들 하는 규모보다 두 자릿수 배 더 밀어붙인 적이 여러 번 있음
      SQL 해법은 보통 더 단순하고, 계산 자원도 덜 먹고, 운영 지원도 쉬움
      다만 작동시키려면 SELECT FOR UPDATE SKIP LOCKED 같은 기능이 애초에 존재한다는 걸 알 만큼 데이터베이스를 잘 알아야 함. 요즘은 무거운 ORM이 DBMS의 진짜 능력을 가로막는 환경에서 자라는 엔지니어가 많아져서 이런 지식이 꽤 드물어지고 있음
    • 현재 규모의 10배를 기준으로 설계하고 테스트하되, 구현은 지금 필요한 만큼만 하면 됨
      시스템은 피크 부하를 감당해야 하고, 피크를 모른다면 안전 여유를 두거나 필요할 때 작업을 버리거나 뒤로 미룰 방법을 넣어야 함
      모든 것은 절충이며, 최적화가 필요한 것만 최적화해야 하고, 무엇이 그 대상인지 판단하는 능력이 좋은 엔지니어의 특징임
    • “최종적 일관성 저장소 위에 트랜잭션 의미론을 흉내 내는 건 엔지니어링 악몽”이라는 문장은 정말 중요함
      이게 새롭다면 근무 시간에 HN을 일주일 읽은 값어치를 방금 한 셈이니, 종이에 적어서 ATM 카드에 붙여둘 만함
    • PostgreSQL에서 자문 잠금 기반 큐도 만들 수 있고, 훨씬 느리지만 장점도 있음
  • 여러 프로젝트에서 더 단순한 접근을 택했는데, 모든 언어의 ORM/쿼리 DSL 프레임워크에서 바로 되는 일반 테이블과 SELECT FOR UPDATE SKIP LOCKED를 쓰는 방식임
    https://www.pgcasts.com/episodes/the-skip-locked-feature-in-...
    “웹 규모”는 아니지만, 내 경험상 수천 개 백그라운드 작업까지는 쉽게 확장됨

    • 트랜잭션 로직이 없어서 잠금 없이 더 단순하게 한 적도 있음. 행 하나를 고른 뒤 “가져갔다”는 필드를 갱신해 보고, 영향을 받은 행이 1개면 내 작업이고 0개면 누가 먼저 가져간 것이니 새 행을 고르는 식임
      큰 조직의 작업에도 이 방식을 문제없이 썼음. 특별한 배포나 새 인프라가 필요 없고, 앱 안에서 워커 스레드 몇 개만 띄우면 됨. 버려진 작업을 재설정하는 스레드를 둘 수도 있음
      다만 3년 동안 실제로 그런 상황은 없었고, 모든 것이 try/catch 안에 있어 실패하면 큐에 다시 넣었으며 Java 앱도 매우 안정적이었음
    • 최근 PostgreSQL에서 정확히 이 방식을 위한 매니페스토와 코드 조각을 공개했음
      delete from task
      where task_id in
      ( select task_id
      from task
      order by random() -- use tablesample for better performance
      for update
      skip locked
      limit 1
      )
      returning task_id, task_type, params::jsonb as params
      [1] https://taylor.town/pg-task
    • 큐 시스템이 제대로 확장되지 않는다는 사실은 가장 나중에 알면 안 되는 종류의 문제임. 큐 시스템이 구조적으로 확장 불가능하다는 걸 알게 된 뒤에는 데이터 손실 없이 고치기 어렵기 때문임
      “수천 개 백그라운드 작업”이라고 했지만, 큐는 보통 리틀의 법칙 기준으로 봐야 하며, 초당 평균 작업 투입률과 평균 작업 지속 시간 같은 비율을 말해야 함. 원시 개수만으로는 큰 의미가 없음
      처음에는 순진한 UPDATE ... SET으로도 되지만 너무 많은 잠금을 잡음. UPDATESELECT 서브쿼리를 쓰거나, SELECT FOR UPDATE SKIP LOCKED로 빼내기 잠금을 효율화할 수는 있어도, 결국 빼내기 쿼리들이 서로의 잠금을 막아 큐가 멈출 수 있음
      그때 투입을 꺼서 DB에 숨통을 틔울 수는 있지만, 유실된 투입 때문에 데이터 손실이 생기고, 대개는 빼내기끼리 서로 막는 문제가 핵심임
      작업 테이블을 급히 샤딩해 잠금을 피할 수도 있지만, 여러 워커에 걸쳐 배포하기 취약하고 데이터 손실로 이어질 수 있음. 임의의 작업 일부를 버릴 수도 있지만 당연히 데이터 손실임
      이런 선택지는 운영 중에는 극도로 스트레스가 크고, 바닥부터 재설계하지 않으면 복구도 매우 어려움. 고객이 몇 명뿐이고 영원히 초당 수십 작업 수준이라고 확신한다면 모를까, 이런 상황을 직접 겪은 뒤로는 가능하면 데이터베이스보다 진짜 큐 기술을 고름
    • Graphile의 https://github.com/graphile/worker도 대략 이런 방식으로 구현되어 있음
  • “엔지니어들이 자기 필요와 전혀 다른데도 멋있어 보이려고 FAANG 인프라를 베낀다”는 식의 비판을 자주 보지만, 상당 부분은 지식과 문서화 문제라고 봄
    FAANG이나 다른 스타트업을 따라 무한 확장 가능한 큐 기반 아키텍처를 만들고 싶으면, Redis, SQS 같은 확장 가능한 솔루션을 몇 시간 안에 설정할 수 있게 해 주는 고품질 가이드, 튜토리얼, 백서가 수십 개 있음
    유지보수 비용은 더 높지만, 평판 있는 출처의 명령어·코드·설정을 복붙해서 시작할 수 있음
    반대로 PostgreSQL의 NOTIFY를 쓰고 싶어서 “SQLALchemy notify listen postgres”를 검색하면, 답 없는 Stack Overflow 질문 몇 개와 맥락 없는 GitHub Gist 정도만 나옴
    사이드 프로젝트에서 이 방식을 정말 써보고 싶지만, 혼자 2~3일 들여 알아낼 여유가 없음
    겉으로는 “단순하지만 확장 안 됨, 즉 PostgreSQL만 쓰기”와 “복잡하지만 확장됨, 즉 Redis/SQS 등”의 선택처럼 보이고, 그러면 내가 멋진 기술과 FAANG에 눈이 멀어 필요 없는 확장성을 고른다는 식이 됨
    하지만 가이드와 자료까지 고려하면 실제 선택지는 “복잡하고 확장성도 예측 안 됨, 내가 구현법과 함정을 모르기 때문”과 “단순하고 확장 가능, 모두가 실제로 하는 방식”에 가까움. 그러면 FAANG을 따라가는 엔지니어의 선택이 훨씬 합리적으로 보임

    • PostgreSQL 같은 “지루한” 기술의 장점은 문서가 훌륭하다는 점임. NOTIFY를 쓰려면 그냥 https://www.postgresql.org/docs/current/sql-notify.html를 읽으면 되고, 검색 기술이 필요 없음
    • 확장성에는 대가가 따름. 필요하지 않은데 넣으면 유연성이 줄고, 스타트업에게는 바로 그게 원치 않는 상태임
      예를 들어 부하가 낮은 PostgreSQL을 쓰면 스키마 마이그레이션, 새 제약 추가, 분석 등이 거의 사소한 일이 됨
      SQS, Cassandra 등을 쓰면 확장성/가용성은 얻지만, 원래 설계가 안 맞는다는 걸 알았을 때 바꾸는 일이 훨씬 오래 걸림. 비즈니스에서 “foo 타입 사용자는 bar 값을 동시에 조합하면 안 된다” 같은 제약을 추가해 달라고 할 때가 그렇음
      PostgreSQL 없이도 구현은 가능하지만, 특히 변경이 필요하면 쉽거나 단순하지 않음
      그래서 유연성을 유지하려면 PostgreSQL을 쓰고, 바뀌지 않을 것임을 알 때 PostgreSQL 위에 다른 것을 같이 쓰는 편이 낫다고 봄. 물론 이 경우 인프라와 유지보수 오버헤드가 추가됨
      결국 항상 절충이고, 무엇을 언제 무엇과 맞바꿀지 알아야 함
    • 이 글은 애플리케이션 개발자보다는 라이브러리 개발자를 더 겨냥한 글임
      모든 사람이 애플리케이션 안에서 PostgreSQL 백엔드든 다른 큐든 맞춤형 백그라운드 작업 워커를 직접 구현하는 건 이상적이지 않다고 봄
      백그라운드 작업에는 틀리기 쉬운 미묘한 부분과 구현 세부사항이 많아서, 보통은 더 포괄적인 전용 라이브러리나 프레임워크가 맡는 편이 좋음
      모든 Rails 애플리케이션에 Sidekiq/Active Jobs가 없고 각자 맞춤형 백그라운드 워커를 구현했다면, Rails 앱은 신뢰성 면에서 지금보다 훨씬 나쁜 평판을 얻었을 가능성이 큼
    • “유지보수가 더 높다”는 점이 작은 스타트업을 죽임. 물론 얻는 것도 많지만, 회사가 망하면 아무 의미가 없음
      주 목표에서 너무 멀어지지 않으면서 가장 큰 이익을 주는 해법을 고르면 됨
      개발자들이 시간의 약 80%를 도구와 인프라와 싸우는 데 쓰던 스타트업을 본 적이 있음. 활주로는 3개월이었고, 지금은 그 끝에 큰 구멍만 남았음. 생각만 해도 아직 몸서리쳐짐
    • FAANG 수준 설계로 가면 시간이 절약된다는 논리라면, 핵심 근거가 온라인 가이드를 찾을 수 있다는 것뿐임
      복잡성에 대한 존중이 충분하지 않은 것 같음
  • PostgreSQL을 발행/구독 버스로 LISTEN/NOTIFY와 함께 쓸 때 가장 큰 단점은 LISTEN이 세션 기능이라 문장 단위 커넥션 풀링과 맞지 않는다는 점임
    이 방식을 쓴다면 자문 잠금을 쓰는 게 좋음. 다른 명시적 잠금은 데이터베이스에 더 큰 부담을 주지만, 자문 잠금은 의도적으로 매우 가볍게 만들어져 있음
    좋아하는 구현 예시는 여러 언어로 포팅된 que

    • NOTIFY/LISTEN을 싫어하는 이유 중 하나는 문제가 생겼을 때 진단하기 어렵기 때문임
      최근에도 한동안 지나면 모든 NOTIFY/LISTEN이 멈추고, 데이터베이스를 재시작해야만 해결되는 문제가 있어 사용을 중단해야 했음 https://dba.stackexchange.com/questions/325104/error-could-n...
    • Que의 Node 버전이 있는지 궁금함
  • Skype는 수년 전 모든 CDR을 처리하기 위해 작은 플러그인과 함께 PostgreSQL을 큐로 썼음. 지금도 쓰는지는 모르겠지만 10년 전 기준으로는 “웹 규모”였고, 인터넷에서 데이터베이스를 큐로 쓰는 건 안티패턴이라고 논쟁하는 동안 그냥 잘 돌아갔음
    트랜잭션이 있다는 건 꽤 편리함
    https://wiki.postgresql.org/wiki/SkyTools
    업무에서 꽤 많이 썼기 때문에 Sydpy에서 이 주제로 발표도 몇 번 했음. PostgreSQL을 이미 잘 운영하고 지원 중이라면 유용함
    다만 요즘이라면 전용 큐를 쓸 것 같고, RabbitMQ만은 피하겠음

    • “RabbitMQ만은 피하겠다”는 부분을 더 설명해 주면 좋겠음. 다른 사람들의 경험이나 생각도 궁금함
    • 요즘 전용 큐를 쓰겠다는 데 동의함. 주된 이유는 클라우드 환경이라면 대부분의 클라우드 제공자가 매우 쓰기 쉽고 저렴한 호스팅 큐 기술을 제공하기 때문임
      벤더 종속을 걱정하더라도 큐의 기본 연산은 사실상 push/pop 정도로 작아서, 필요하면 옮기기 쉽게 작성하는 것도 비교적 쉬움
    • RabbitMQ에서 나쁜 경험이 있었는지 궁금함. 있었다면 기술 자체 때문인지, 문서나 커뮤니티 같은 주변 요소 때문인지도 궁금함
    • 데이터베이스를 큐로 쓰는 방식은 잘 될 때는 훌륭하지만, 깨지는 방식이 복구하기 매우 어려운 상태로 몰고 감
      이미 데이터베이스를 운영 중이라는 이유로 큐까지 데이터베이스에 넣었다면 그 논리는 양날의 검임. 축하할 일은 아니지만, 큐 난장판이 이제 주 데이터 저장소까지 끌어내릴 수 있음
  • PostgreSQL을 애플리케이션 큐로 쓸 때 가장 큰 장점 중 하나는 예약한 모든 비동기 작업이 트랜잭션성의 혜택을 받는다는 점임
    예를 들어 회원가입 뒤 이메일을 보내는 복잡한 백엔드 변경 작업이 있고, 이메일 전송 작업을 넣은 뒤 같은 트랜잭션의 나중 부분에서 실패해 롤백되면, 그 이메일 작업은 애초에 큐에 들어가지 않음

    • 다른 비멱등 시스템과 연결할 때는 필연적으로 최소 한 번 또는 최대 한 번 의미론 중 하나를 골라야 한다는 점을 분명히 해야 함
      이메일의 경우 전송 확인을 기다리다 실패하면, 트랜잭션을 실패시켜 이메일 중복 가능성을 감수할지, 계속 진행해서 이메일 유실 가능성을 감수할지 선택해야 함
      큰 장점은 비동기로 DB를 수정하는 코드 경로에서 나옴. 작업 소비와 DB 갱신이 같은 트랜잭션 안에 있으므로 정확히 한 번 의미론을 완전히 트랜잭션으로 처리할 수 있음
    • 이건 정말 중요한 지점임. SQS가 작업 처리 클러스터 자동 확장을 쉽게 해 주기 때문에 PostgreSQL과 SQS를 조합해서 자주 씀
      PostgreSQL에는 트리거된 이벤트와 해당 트랜잭션의 pg_current_xact_id()를 담는 트랜잭션 로그 테이블을 둠. 행의 내장 xmin을 쓸 수도 있지만 트랜잭션 랩어라운드를 신경 써야 함. 이 행에 삽입하면 NOTIFY가 발생함
      백그라운드 프로세스가 반복 실행되며, 지난 실행의 xmin과 현재 pg_snapshot_xmin(pg_current_snapshot()) 사이의 트랜잭션 ID를 가진 모든 행을 트랜잭션 테이블에서 고름. 그 이벤트들을 작업으로 매핑해 SQS에 제출하고, xmin을 기록한 뒤 LISTEN으로 다음 NOTIFY를 기다림
    • 우리도 큐 추가를 커밋 이후에 실행되도록 예약해서 이 문제를 조금 완화함. 그래도 안전하지 않은 부분은 남고, RabbitMQ 연결이 내려가면 곤란해짐
    • 데이터베이스에 무언가 처리됐다고 알리는 일과 RabbitMQ 같은 곳에 메시지를 쏘는 일은 100% 트랜잭션이 될 수 없음. 이 방식의 가장 큰 이유가 바로 이것임
      PostgreSQL 큐에서는 이메일 전송 작업을 넣은 뒤 나중에 트랜잭션이 실패하면 이메일이 큐에 들어가지 않는다는 점이 맞음
      완전히 별도여야 하는 데이터베이스 코드는 서로 다른 트랜잭션으로 분리할 가치가 있음. 반대로 사용자가 DB에 생성되지 않았다면 회원가입 이메일도 보내고 싶지 않을 수 있으니 상황에 달려 있음
    • 또 다른 장점은 작업이 집히기 전에 트랜잭션 완료가 보장된다는 것임
      Redis 기반 큐나 사실상 다른 어떤 큐에서도, 트랜잭션이 커밋되기 전에 데이터베이스 레코드가 존재한다고 가정한 작업이 실행되는 상황을 금방 만나게 됨. 이를 고치는 코드는 보통 어색하고 복잡해짐
  • Kafka에서 좋아하는 점은 그냥 추가 전용 로그이고, 클라이언트는 본질적으로 오프셋만 들고 있다는 것임
    개념적으로 이해하기 매우 단순하고, 영속적이며 장애에도 꽤 강함. 어떤 오프셋으로든 돌아가서 다시 읽을 수 있기 때문임
    안타깝게도 Kafka는 분산 특성 때문에 충분한 복잡성을 동반해서, 대부분의 사용 사례에는 결국 그만한 가치가 없어짐
    개인적으로는 운영하기 더 쉬운 비슷한 무언가가 있으면 좋겠음. 단일 노드에서 초당 수백, 어쩌면 수천 이벤트를 처리할 수 있을 것이고, 분산 복잡성이 없다면 꽤 좋을 것임
    이론상 PostgreSQL로도 할 수 있고, 행을 절대 지우지 않으면 됨. 어쩌면 그게 답일 수도 있음

    • PostgreSQL에는 네이티브 “오프셋”에 해당하는 자동 증가 ID가 있고 날짜별 파티셔닝도 가능하니, 단순한 Kafka 대체재로 아주 좋은 후보라고 봄
      소비자 그룹, 파티션 같은 전체 기능이 필요 없다면 소비자 설정도 훨씬 단순해질 것임
    • FasterLog가 언젠가 충분히 성숙해지길 기대하곤 했음
  • Windmill에서 정확히 이 방식을 쓰고 있음. Windmill은 오픈소스 Retool 대안이자 현대적인 Airflow에 가까운 도구이고, 매일 벤치마크를 돌림
    적당한 GitHub CI 인스턴스에서 Windmill 워커 하나와 PostgreSQL을 컨테이너로 실행하면 벤치마크가 초당 1,200 작업을 처리함. 워커를 추가하면 초당 5,000 작업까지 안정적으로 확장됨
    멀티테넌트 인스턴스에서 초당 5,000 작업 장벽을 넘기 위해 Citus 사용을 검토 중임
    https://github.com/windmill-labs/windmill/tree/benchmarks

  • 초당 약 10 메시지 수준이던 시절 일부 큐에 PostgreSQL을 썼고 꽤 확장되긴 했음. 하지만 솔직히 AWS, GCP, Azure에서 SQS나 다른 큐 스택을 설정하는 건 너무 쉽고, 해당 목적에 맞춰 만들어져 있으며, DLQ 같은 것도 내장되어 있음
    굳이 그 시스템이 무너져 나머지 DB 상태까지 영향을 줄 걱정을 하며 이 길을 택할 이유를 모르겠음
    “가장 단순한 도구를 써라”는 원칙을 좋아하지만, 때로는 엔지니어들이 너무 멀리 가서, 주류 대안이 비교적 저렴하고 단순한데도 애매한 주의사항이 붙은 가장 멍청한 도구만 남기는 것 같음

    • 내가 정착한 방식은 대부분의 작업 상태는 DB에 저장하고, 작업 큐는 워커를 깨워 해당 작업을 처리하게 만드는 데만 쓰는 것임
      작업 상태를 DB에 저장하면 상태를 보기 좋게 질의할 수 있음. 실제 상태를 정확히 보여주지는 않더라도, 운영 중 장애를 분석할 때 도움이 됨. 특히 대부분의 작업 큐는 처리된 레코드를 그냥 지워버림
      그리고 백그라운드 작업을 모두 멱등적으로 만들면 “이 작업을 처리하라고 작업 큐에 메시지를 보내기” 같은 방식은 거의 항상 안전함
      메시지 큐에만 의존하면 성능 문제가 생겼을 때 무슨 일이 벌어지는지 알기 어려운 경우가 많음. 예를 들어 RabbitMQ는 큐 크기는 알려줄 수 있어도 내부 데이터 점검 기능은 거의 없거나 매우 제한적임
    • 트랜잭션과 데이터 일관성 때문임. SQS에서는 얻을 수 없는 답임
    • 소프트웨어마다 요구사항이 다르기 때문임. 온프레미스/B2B 어플라이언스에서는 PostgreSQL 외부 서비스 요구사항이 없다는 것 자체가 기능일 수 있음
      어떤 소프트웨어는 PostgreSQL의 능력을 절대 넘지 않을 것으로 예상될 수 있고, 넘더라도 다른 서비스로 옮기기 쉽게 만들 수 있음
      그리고 트랜잭션이 있는 작업 시스템을 PostgreSQL 안에서 단순하게 만들고 싶을 수도 있음
  • 우리는 Elixir의 Oban을 사용해 PostgreSQL 안에서 매일 수십만에서 수백만 작업을 처리함
    트랜잭션이 성공했을 때만 이메일을 예약하는 식으로, 백그라운드 작업 주변에 트랜잭션 의미론이 있다는 건 엄청나게 편리함
    autovacuum 튜닝을 조금 해야 하지만, 정리하고 나면 우리에게는 아주 잘 맞았음