Postgres에서 내구성 워크플로 구축하기
요약
Postgres 환경에서 내구성 있는 워크플로를 구축하기 위한 다양한 도구(DBOS, Restate, Cloudflare Workflows, Temporal)의 특성과 활용 사례를 비교 분석합니다. 특히 DB 트랜잭션과 연동된 원자적 메시징의 중요성과 각 도구의 운영상 장단점을 다룹니다.
핵심 포인트
- DBOS는 Postgres 트랜잭션과 연동된 원자적 메시징을 지원하여 신뢰성이 높음
- Restate는 중앙 오케스트레이터가 필요하지만 서버리스 환경과 결합하기 용이함
- Cloudflare Workflows는 저렴한 비용으로 낮은 중요도의 작업에 적합함
- Temporal은 강력한 오케스트레이션 기능을 제공하나 페이로드 크기 제한 등의 제약이 있음
처리량이 아주 많이 필요하지 않다면 absurd와 Rust 파생 구현인 durable은 클라이언트 쪽을 매우 단순하게 유지하는 좋은 선택지라고 봄
가벼워서 코딩 에이전트가 전체 구조를 쉽게 머릿속에 넣고, 필요하면 쿼리로 상태를 조회하면 됨
dbos.dev, restate.dev, cf workflows를 쓰고 있는데, 우리 Agents.md에는 이렇게 적어둠 Restate.dev는 northflank의 결제 연동에 사용함. cf workflows보다 빠르고, Cloudflare와 그 장애에 독립적이며, 자체 호스팅이 가능해 벤더 종속이 없음 Cloudflare workflows는 CSV/PDF 보고서 생성처럼 중요도가 낮은 작업에 사용함. 매우 저렴하기 때문 DBOS.dev는 Postgres 트랜잭션과 묶인 원자적 메시징이 필요해 100% 신뢰성/내구성이 필요한 워크플로에 사용함. 예를 들면 materialized row 채우기나 상인에게 중요한 이메일/푸시 보내기
DBOS와 Restate는 겉보기엔 비슷하지만, Restate는 중앙 “orchestrator”가 필요해서 장단점이 있고 cf/vercel의 서버리스 워커와 함께 만들기는 쉬워짐
또 VirtualObject가 있어서 Cloudflare의 단일 스레드 DurableObject에 대한 벤더 종속 없는 오픈소스 대안으로 괜찮음
DBOS가 특히 빛나는 지점은 두 가지임. 1) dbos.enqueue_workflow로 비즈니스 로직과 같은 DB 트랜잭션 안에서 원자적 메시징을 할 수 있음. 이 부분이 어떤 해법에서도 가장 취약한 경우가 많아서, 비즈니스 로직을 실행한 같은 트랜잭션에서 원자적이고 내구성 있게 처리하면 복잡도가 크게 줄어듦
2) DBOS는 워크플로 상태를 DB에 저장하므로 metabase/looker로 관측성 대시보드를 만들기 쉬울 것 같음. Restate도 rocksdb 인스턴스를 노출해서 metabase에 연결할 수 있으면 좋겠음
스키마 업데이트는 어떻게 처리하는지 궁금함. 작업을 마이그레이션하는지, 워커 배포를 특정 방식으로 다루는지?
DBOS와 Temporal을 써본 사람들의 체감이 궁금함
예전에 Temporal을 써봤고 꽤 잘 동작했지만, 요청 페이로드나 이벤트 크기 제한 때문에 해법을 만들 때 불편했던 적이 있음
좋은 엔지니어링 관행을 강제한다는 장점도 있지만, CSV 파일이 2MB보다 크다고 해서 S3에 올리고 링크를 넘긴 뒤 워크플로에서 다시 내려받는 특별 로직을 항상 쓰고 싶지는 않음
DBOS 경험은 어떤지, 운영 복잡도나 기능 동등성 등에서 Temporal과 어떻게 비교되는지 궁금함
DBOS는 안 써봤지만 현재 직장과 이전 직장에서 Temporal을 써서 총 1.5년 정도 다뤄봄
집에서도 시간 민감도가 높지 않은 홈 자동화 작업을 처리하는 데 돌리고 있음. 워크플로 지연 시간이 아주 나쁘진 않지만, 집의 동작 감지 이벤트처럼 즉시 반응해야 하는 트리거에는 쓰지 않을 것 같고, 비활동 이후 무언가를 끄는 타임아웃 정도라면 괜찮음
VPC나 Kubernetes 클러스터 안에서 Temporal 앞단에 얇은 REST API를 두는 방식을 꽤 좋아함. 이벤트 기반 트리거가 Temporal 인증이나 워크플로 상태 확인을 신경 쓰지 않아도 되기 때문이고, 이벤트를 가능한 한 로직 없이 유지하는 데 도움이 됨
예를 들어 DB 트리거가 직접 동작하거나 이벤트를 큐에 넣고, 핸들러가 필요한 이벤트 세부정보로 얇은 REST API를 호출함. REST API는 이것이 워크플로를 시작할지, 기존 워크플로에 signal을 보낼지, 무시할지 결정할 수 있음. 패턴은 상황마다 다르지만 내 경우 SignalWithStart를 자주 쓰거나, 시작할 가치가 없고 기존 워크플로도 없으면 그냥 버림
또 단일 객체의 생명주기에서 서로 독립적인 동작을 오케스트레이션해야 할 때 부모/자식 워크플로 기능이 매우 유용하고, 외부 요인이 객체의 진행 경로를 바꿀 때 취소 가능하다는 점도 좋음
길고 모호하게 말하자면, 매우 강력하고 다루기 쉬우며 생명주기 로직을 API 밖으로 빼내는 데 큰 도움이 됨. API 안에 넣어두면 기술 부채가 쉽게 쌓이고 관리가 위태로워짐. 쉽게 보이는 곳에 로직을 던져 넣었다가 나중에 숨은 함정이 되는 것보다 모범 관행을 따르게 해준다는 데 동의함
Temporal은 지나치게 복잡하다고 느꼈지만, 말한 대로 가장 좋은 부분은 좋은 엔지니어링 관행을 강제한다는 점임
그런데 Cloud 상품을 써보고 가격에 경악했음. 프로덕션에 올리기도 전에 무료 크레딧 1,000달러를 다 써버림. 로컬 Temporal을 직접 운영하고 싶지도 않았음
개인적으로 최선은 아키텍처에서 아이디어만 가져와서 Postgres로 직접 구현하는 것이라고 봄
큰 페이로드 문제를 해결하려고 외부 저장소 접근법을 막 출시했음
100% 마음에 들지는 않음. 본질적인 일부라기보다 덧붙인 느낌이고 아직 초기 릴리스임. 그래도 지금은 사실상 해결된 것으로 봐도 됨
대규모 온프레미스 Temporal 구성을 운영 중인데, 이 계정은 들킬까 봐 임시 계정임
1년 넘게 프로덕션에서 운영해본 입장에서 Temporal은 설계가 좋지 않고, 느리며, 인프라 측면에서 터무니없이 무거움
사소하지 않은 작업, 예를 들어 워크플로당 이벤트가 200개 이상이고 동시에 몇백 개만 하루 종일 돌려도 인프라 비용으로 수백만 달러를 쓰게 될 수 있고, 그래도 여전히 별로임
자체 벤치마크를 돌려보면 숫자가 형편없음
영업팀도 정말 끔찍하고 절박해 보임
개발자 관점에서는 SDK가 꽤 좋음 nexus에 갇히지 말고, 영업팀이 전화하면 반드시 법무팀을 방에 같이 두는 게 좋음
우리는 AI 생성 워크플로와 비디오 파일 처리에 dbos를 쓰고 있음
Celery에서 어떻게 마이그레이션할지 이해하는 데 시간이 걸렸지만, 우리 사례에서는 그만한 가치가 있었음
우리 프로덕션에서는 큐에 Redis를 쓰지만, 사용자 중에는 큐로 Postgres와 MySQL을 쓰는 경우도 봤음
데이터 저장소, 상태 기계, 유효 상태 제약, 유효 상태 사이를 전이하는 로직을 분리하는 대신, 이들을 앱 상태의 어떤 커널로 통합할 수 있으면 좋겠음
솔직히 Postgres에는 이미 이런 능력이 많이 있지만, 앱이나 제품 수준에서 앱이 전이할 수 있는 증명 가능한 상태 집합을 제공하고, 이를 클라이언트에 유익한 방식으로 자동 노출하는 뚜렷한 그림은 아직 보이지 않음. 예를 들면 이 사용자는 이 글에 좋아요는 누를 수 있지만 수정은 못 한다는 식
내 눈에는 컬러 페트리 넷 형태로 보이지만, 데이터베이스가 성공적인 경계를 명확히 가진 것처럼 단순한 앱 상태 패러다임은 아직 보이지 않음
Temporal.io는 query, signal, update 기능으로 여기에 어느 정도 가까이 감
다만 완전한 통합인지는 확신이 없음
개념은 완전히 이해하고 동의함. 이런 종류의 내구성을 워크플로 시스템에 넣는 훌륭한 방법임
다만 게이머 뇌로는 이걸 “대규모 세이브 스커밍”이라고 부르고 싶음. 이미 많은 사람이 이 접근이 작동한다는 걸 알고 있지만, 추상적인 컴퓨터과학 개념과 연결하지 못했을 수 있음
견고성을 높이는 또 다른 전략은 워크플로를 멱등 연산들로 구성하는 것임. 워크플로 상태가 너무 커서 백업하기 어려운 상황에 유용할 수 있음. 대신 작업을 처음부터 다시 실행하면, 다시 진전이 생기는 지점까지는 전부 no-op이 됨
Postgres가 도구상자에 들어 있기만 하면 적은 도구로 할 수 있는 일이 계속 놀라움
최근 분산 큐를 개발했는데 정말 잘 동작하고 벤치마크도 좋으며, 경쟁 상태나 충돌도 없음. 워커들이 안전하게 경쟁할 수 있도록 SKIP LOCKED를 사용했음
노드 여러 개에 걸친 워커들이 충돌을 피하게 하려면 세션 범위 mutex, 즉 pg advisory lock도 쓸 수 있음
어차피 이런 경우에는 advisory lock이 선호됨. SELECT FOR UPDATE를 많이 잡고 있으면 확장이 잘 안 되기 때문임
수정: 다시 확인해보니 이제는 조언이 반대로 바뀐 것 같음
그냥 레코드에 actor ID로 예약을 걸면 됨
Rails에는 데이터베이스 기반 작업 백엔드가 여러 개 있지만, 관례상 작업은 항상 한 가지 일만 하고 가능하면 아주 짧게 끝나야 함
이 때문에 워크플로를 만들기가 다소 억지스러워짐. 첫 번째 작업의 마지막 줄에서 두 번째 작업을 큐에 넣고, 두 번째 작업의 마지막 줄에서 세 번째 작업을 큐에 넣는 식이 됨
작업 백엔드는 이들을 연결된 워크플로로 보여주지 않고 독립적인 작업으로 다루며, 워크플로를 높은 수준에서 이해하려 해도 여러 작업 클래스를 읽어야 함
Rails는 최근 작업 안에서 단계별로 체크포인트를 찍고 재개할 수 있는 continuable 개념을 도입했지만, 여전히 작업을 단일 책임으로 유지하는 관례가 강해서 진짜 워크플로에 쓰기엔 어색함
다른 사람들도 이런 걸 겪었는지, 해결책을 찾았는지 궁금함
이건 훌륭한 패턴임. 가능한 한 많은 일을 데이터베이스 안에서 하는 게 좋음
외부 Spanner는 change streams를 제공함. 내부 Spanner는 다르며, 주로 어떤 경우에는 극단적인 확장 요구가 있기 때문이고, “이미 잘 돌아가니까”라는 이유와 “임의 change stream은 무섭다”는 이유도 섞여 있음
내부 Spanner는 어떤 트랜잭션이든 큐 엔트리를 쓸 수 있게 함. 여기서 큐는 대략 특별한 시간 인식을 가진 테이블임. 전달을 예약할 수 있고, 엔트리는 큐에서 핸들러로 푸시되며, 핸들러도 dequeue 트랜잭션 안에서 DB 쓰기를 할 수 있음. 그리고 같은 확장성이 모두 유지됨
AI 자동 생성 콘텐츠
본 콘텐츠는 RSS: GeekNews (한국어)의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기