1P by GN⁺ | ★ favorite | 댓글 1개
  • 내구성(Durable) 워크플로는 실행 상태를 데이터베이스에 체크포인트해, 충돌 뒤 마지막 완료 단계부터 복구하게 함
  • Temporal, Airflow, AWS Step Functions식 외부 오케스트레이션은 중앙 오케스트레이터와 저장소를 추가해 복잡성이 커짐
  • Postgres 기반 구조는 애플리케이션 서버가 워크플로 테이블을 폴링하고 단계 출력을 직접 저장해 복구 가능하게 만듦
  • 여러 서버는 잠금과 무결성 제약으로 중복 실행을 막고, 확장성·가용성·관측성 문제를 Postgres 해법으로 다룸
  • 단일 Postgres는 초당 수만 개 워크플로까지 수직 확장 가능하며, 복제·멀티 AZ·SQL 분석을 그대로 활용할 수 있음

내구성 워크플로의 기본 모델

  • 내구성 워크플로는 프로그램 실행 중 진행 상태를 데이터베이스에 주기적으로 체크포인트로 저장해, 충돌이나 실패 뒤 마지막 완료 단계부터 복구할 수 있게 하는 방식임
  • 비디오 게임의 저장과 불러오기처럼, 프로그램 진행 상황을 정기적으로 “저장”하고 충돌 뒤 마지막 체크포인트에서 “불러오는” 모델로 볼 수 있음
  • 일반적인 구현은 Temporal, Airflow, AWS Step Functions 같은 외부 오케스트레이션 시스템임
  • 외부 오케스트레이션에서는 내구성 프로그램을 여러 단계로 구성된 워크플로로 작성하고, 중앙 오케스트레이터가 실행을 조율함

외부 오케스트레이션의 동작 방식

  • 클라이언트가 워크플로를 제출하면 오케스트레이터가 데이터 저장소에 레코드를 만들고, 실행을 위해 워커에 디스패치함
  • 워커가 각 단계를 완료할 때마다 결과를 오케스트레이터로 보내고, 오케스트레이터는 그 출력을 데이터 저장소에 체크포인트한 뒤 다음 단계를 디스패치함
  • 워커가 충돌하거나 실패하면 오케스트레이터가 해당 워크플로를 다른 워커에 다시 디스패치하고, 마지막 체크포인트 단계부터 시작하게 함
  • 이 구조는 워크플로 상태를 데이터베이스에 저장한다는 핵심 아이디어에 별도 오케스트레이터 서버를 더해 복잡성을 키움

Postgres를 오케스트레이터로 사용하는 구조

  • 내구성 워크플로의 핵심이 데이터베이스에 프로그램 상태를 체크포인트하는 것이라면, 별도 오케스트레이터 서버 없이 데이터베이스 자체를 오케스트레이터로 쓰는 편이 더 단순하고 효율적임
  • Postgres는 인기, 확장성, 풍부한 생태계 때문에 내구성 워크플로를 구축하기에 적합한 선택지임
  • Postgres 기반 내구성 워크플로 시스템에서는 애플리케이션 서버가 중앙 오케스트레이터를 거치지 않고 Postgres와 직접 통신해 워크플로를 실행함
  • 클라이언트는 Postgres의 워크플로 테이블에 항목을 생성해 실행을 제출하고, 애플리케이션 서버는 해당 테이블을 폴링해 워크플로를 디큐하고 실행함
  • 서버는 워크플로를 실행하는 동안 각 단계의 출력을 Postgres에 체크포인트하고, 실행 중인 서버가 충돌하거나 실패하면 다른 서버가 체크포인트에서 워크플로를 복구함

중앙 오케스트레이터가 불필요해지는 이유

  • 애플리케이션 서버들은 Postgres를 통해 조율할 수 있으므로 중앙 오케스트레이터가 워크플로를 워커에 디스패치할 필요가 없음
  • 서버들은 Postgres 테이블에서 협력적으로 워크플로를 디큐하고, 잠금 절 같은 메커니즘으로 각 워크플로가 정확히 하나의 워커에 의해 디큐되도록 보장할 수 있음
  • 단계 출력 체크포인트도 오케스트레이터가 아니라 워커가 직접 Postgres에 기록함
  • 여러 워커가 같은 워크플로를 동시에 실행하려 하면, Postgres의 무결성 제약이 체크포인트 시점에 중복 작업을 감지해 물러나게 할 수 있음
  • 중앙 오케스트레이터를 Postgres나 다른 데이터베이스로 대체하면 확장성, 가용성, 관측성, 보안 같은 문제를 잘 알려진 Postgres 네이티브 해법으로 다룰 수 있음

확장성과 가용성

  • 데이터베이스 기반 내구성 워크플로 시스템의 확장성과 가용성은 근본적으로 기반 데이터베이스에 의해 결정됨
  • 워커 서버를 더 추가해 수평 확장할 수 있으므로, 최대 처리 용량은 데이터베이스가 워크플로를 얼마나 빠르게 처리할 수 있는지에 달려 있음
  • 워커는 상호 대체 가능하고 서로의 상태를 복구할 수 있으므로, 시스템은 기반 데이터베이스가 사용 가능한 동안 가용 상태를 유지함
  • Postgres를 사용할 때는 확장성과 가용성이 오랫동안 연구된 문제이고 견고한 해법이 존재한다는 점이 장점임
  • 단일 Postgres 서버는 초당 수만 개 워크플로를 처리하도록 수직 확장할 수 있음
  • 추가 확장은 CockroachDB 같은 분산 Postgres 또는 샤딩된 Postgres를 사용해 달성할 수 있음
  • Postgres는 자동 장애 조치가 포함된 스트리밍 복제를 지원하고, 관리형 서비스는 멀티 AZ 배포와 고가용성 SLA를 기본 제공함
  • Postgres를 대규모로 운영하기 위해 수십 년간 축적된 엔지니어링과 연구가 내구성 워크플로 운영에도 직접 활용될 수 있음

관측성

  • Postgres 기반 내구성 실행에서는 워크플로와 단계가 Postgres 테이블에 체크포인트되므로, 해당 체크포인트를 스캔해 워크플로를 실시간으로 모니터링하고 실행을 시각화할 수 있음
  • 거의 모든 워크플로 관측성 질의를 SQL로 표현할 수 있다는 점에서 Postgres가 강점을 가짐
  • 지난 한 달 동안 오류가 발생한 모든 워크플로를 찾는 질의처럼, 복잡한 필터링과 분석 작업을 SQL로 선언적으로 표현할 수 있음
  • 이런 방식은 Postgres의 관계형 모델과 수십 년간의 질의 최적화 연구를 활용하기 때문에 가능함
  • 인기 있는 외부 오케스트레이터가 사용하는 키-값 저장소처럼 더 단순한 데이터 모델을 가진 많은 시스템은 같은 수준의 SQL 기반 분석 지원을 제공하지 못함
  • 워크플로와 단계 데이터를 Postgres 테이블에 저장하고 빠른 분석 질의를 위한 보조 인덱스를 추가하면, 내구성 실행에서 효율적인 관측성을 얻을 수 있음

신뢰성과 보안

  • 외부 오케스트레이터를 사용하는 내구성 실행에서는 오케스트레이터와 그 데이터 저장소가 모두 단일 장애 지점이 됨
  • 오케스트레이터와 데이터 저장소가 워크플로 실행을 직접 조율하므로, 둘 중 하나라도 다운되면 전체 애플리케이션을 사용할 수 없게 됨
  • 이들은 워크플로와 단계 체크포인트를 처리하고 저장하므로 민감한 애플리케이션 데이터에 접근할 가능성이 있으며, 민감한 인프라처럼 강화, 접근 제어, 감사가 필요함
  • Postgres 기반 내구성 실행에서는 유일한 장애 지점이 Postgres 자체이며, 모든 워크플로 데이터가 Postgres에 직접 저장되고 다른 시스템을 통과하지 않음
  • 애플리케이션이 이미 Postgres에 의존하고 있다면, 내구성 실행 도입은 새로운 장애 지점을 추가하지 않고 보호해야 할 새 공격 표면도 만들지 않음
  • 데이터베이스는 이미 핵심 인프라이므로, 오케스트레이션을 위해 새 핵심 인프라를 추가하기보다 기존 데이터베이스를 재사용하는 편이 더 타당함

더 알아보기

댓글과 토론

Hacker News 의견들
  • Armin Ronacher의 absurd는 Postgres용 내구성 있는 워크플로 구현체임
    https://lucumr.pocoo.org/2025/11/3/absurd-workflows/
    https://github.com/earendil-works/absurd
    https://earendil-works.github.io/absurd/
    직접 써보진 않았지만 다른 선택지와 비교해볼 만함

    • 처리량이 아주 많이 필요하지 않다면 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에서 어떻게 마이그레이션할지 이해하는 데 시간이 걸렸지만, 우리 사례에서는 그만한 가치가 있었음
  • Conductor OSS도 이걸 꽤 잘함 https://docs.conductor-oss.org/devguide/ai/index.html
    https://github.com/agentspan-ai/agentspan은 본질적으로 Conductor용 에이전트 SDK 계층인데, langgraph, OpenAI, vercel, ADK 에이전트를 코드 변경 없이 내구성 있게 만들고 오케스트레이션을 추가할 수 있음

    • 우리 프로덕션에서는 큐에 Redis를 쓰지만, 사용자 중에는 큐로 Postgres와 MySQL을 쓰는 경우도 봤음
  • 데이터 저장소, 상태 기계, 유효 상태 제약, 유효 상태 사이를 전이하는 로직을 분리하는 대신, 이들을 앱 상태의 어떤 커널로 통합할 수 있으면 좋겠음
    솔직히 Postgres에는 이미 이런 능력이 많이 있지만, 앱이나 제품 수준에서 앱이 전이할 수 있는 증명 가능한 상태 집합을 제공하고, 이를 클라이언트에 유익한 방식으로 자동 노출하는 뚜렷한 그림은 아직 보이지 않음. 예를 들면 이 사용자는 이 글에 좋아요는 누를 수 있지만 수정은 못 한다는 식
    내 눈에는 컬러 페트리 넷 형태로 보이지만, 데이터베이스가 성공적인 경계를 명확히 가진 것처럼 단순한 앱 상태 패러다임은 아직 보이지 않음

    • Temporal.io는 query, signal, update 기능으로 여기에 어느 정도 가까이 감
      다만 완전한 통합인지는 확신이 없음
    • 이런 시도는 있었지만, 천 줄짜리 저장 프로시저는 정말 악몽임
    • 이건 convex.dev나 https://spacetimedb.com/처럼 들림. 둘 다 직접 쓰지는 않음
  • DBOS가 Rust를 지원하지 않아서, 이와 비슷한 매우 최소한의 Rust 버전을 https://github.com/tensorzero/durable에 구현했음
    꽤 안정적이고 확장 가능했지만, 당연히 SQL 구현에는 매우 조심해야 함. 여기 독자들에게 흥미로우면 좋겠음

  • 개념은 완전히 이해하고 동의함. 이런 종류의 내구성을 워크플로 시스템에 넣는 훌륭한 방법임
    다만 게이머 뇌로는 이걸 “대규모 세이브 스커밍”이라고 부르고 싶음. 이미 많은 사람이 이 접근이 작동한다는 걸 알고 있지만, 추상적인 컴퓨터과학 개념과 연결하지 못했을 수 있음
    견고성을 높이는 또 다른 전략은 워크플로를 멱등 연산들로 구성하는 것임. 워크플로 상태가 너무 커서 백업하기 어려운 상황에 유용할 수 있음. 대신 작업을 처음부터 다시 실행하면, 다시 진전이 생기는 지점까지는 전부 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 쓰기를 할 수 있음. 그리고 같은 확장성이 모두 유지됨