Skip to content
Published on

NATS Server 2.14 — 패스트 배치 퍼블리시, 서버 내장 스케줄, 그리고 2.15 전에 해둘 숙제

공유하기
Authors

들어가며 — 2.12 다음이 2.14인 이유부터

NATS Server 2.14.0은 2026년 4월 30일에 릴리스됐습니다. 직전 마이너 릴리스인 2.12.0이 2025년 9월 22일이었으니 약 7개월 만입니다. NATS 팀은 2.12 릴리스 글에서 6개월 릴리스 주기로 전환했다고 밝혔는데, 2.14 릴리스 글은 이번에 한 달쯤 밀린 이유를 "AI 보안 리뷰와 공시 대응 활동이 늘어서"라고 적고 있습니다. AI가 만들어내는 보안 제보가 오픈소스 메인테이너의 릴리스 일정을 미는 시대라는 게, 릴리스 노트에서 읽게 되는 2026년의 풍경입니다.

그리고 버전 번호. 릴리스 노트 첫 줄에 "2.13.x 버전은 건너뛰었다(the 2.13.x version was skipped)"라고 명시돼 있습니다. 이유는 릴리스 노트에도, 블로그에도, 업그레이드 가이드에도 없습니다. git 이력을 보면 2.12.0 릴리스 다음 날인 2025년 9월 23일에 main 브랜치가 곧바로 v2.14.0-dev로 올라갔으니, 나중에 번호를 바꾼 게 아니라 사이클 시작부터 계획된 스킵이었습니다. 뒤에서 다루겠지만 JetStream API 레벨 번호와 서버 마이너 버전이 다시 맞아떨어지게 됐다는 관찰은 할 수 있는데, 이건 제 추정이지 공식 설명이 아닙니다.

이 글은 하이라이트 나열이 아니라, 릴리스 노트와 업그레이드 가이드, 설계 문서(ADR), 서버 소스를 직접 확인해서 "이게 실제로 뭘 하고, 뭘 안 하고, 운영자가 뭘 챙겨야 하는지"를 정리합니다. NATS 자체가 처음이라면 분산 메시징 2026 딥다이브를 먼저 읽는 편이 좋습니다.

패스트 배치 퍼블리시 — async publish의 후계자

2.14의 대표 기능입니다. 배경부터 정리하면, 2.12가 도입한 원자적 배치 퍼블리시(atomic batch publish)는 N개의 메시지를 스테이징해 두고 일관성 검사를 전부 통과해야만 배치 전체를 커밋하는 all-or-nothing 방식이었습니다. KV 스토어에서 주소를 이루는 키 5개를 한꺼번에 갱신하는 류의 문제에는 정확히 맞지만, 스테이징 때문에 배치 크기에 실질적 제한이 있습니다(ADR-50은 원자적 배치의 1,000개 제한을 언급합니다).

2.14의 패스트 배치는 목적이 다릅니다. 원자성이 아니라 처리량과 흐름 제어가 목표입니다.

                원자적 배치 (2.12)          패스트 배치 (2.14)
목적            all-or-nothing 정확성       고속 인제스트 + 흐름 제어
저장 방식       스테이징 후 일괄 커밋        도착하는 대로 즉시 저장
일관성 검사     배치 전체 단위               메시지 단위 인라인
크기 제한       있음 (ADR 기준 1,000개)     사실상 무제한
실패 시         배치 전체 거부               gap:fail — 배치 중단 / gap:ok — 갭 보고 후 계속
리더 교체       배치 포기                    gap:ok 모드면 생존

설계의 핵심은 배치가 열려 있는 동안 유지되는 컨트롤 채널입니다. 클라이언트는 메시지마다 ack을 기다리지 않고, 서버가 대략 10개에 한 번꼴로 흐름 제어 ack(BatchFlowAck)을 보냅니다. 흐름 속도는 서버가 정합니다 — ADR에 따르면 클라이언트가 최대 흐름을 요청해도 서버는 낮은 값(예: 1)에서 시작해 여유가 있으면 대략 2배씩 올리고, 부하가 감지되면 절반씩 줄입니다. 판단 근거는 동시 패스트 퍼블리셔 수, 평균 메시지 크기, 아직 영속화되지 않은 인플라이트 양 같은 서버 내부 지표입니다.

갭(유실) 처리는 두 모드 중 고릅니다. fail 모드는 갭이 감지되는 순간 배치를 중단해 순서 보장이 필요한 경우(예: 오브젝트 스토어의 파일 청크)에 맞고, ok 모드는 갭을 BatchFlowGap 메시지로 보고만 하고 계속 진행해 일부 유실을 감수할 수 있는 고빈도 지표 퍼블리셔에 맞습니다. ADR은 이 설계의 최종 목표를 분명히 적어 뒀습니다 — 기존 클라이언트의 async publish를 이것으로 대체하는 것.

성능 주장에 대해서는 정직할 필요가 있습니다. 공식 블로그는 "설정에 따라 async publish 대비 거의 2배까지 처리량이 나올 수 있다"고 쓰는데, 이는 벤더 자체 서술이고 측정 조건(메시지 크기, 복제 수준, 하드웨어, 클라이언트 수)은 공개돼 있지 않습니다. 재현 가능한 벤치마크 수치는 현재 공개된 게 없습니다. 다만 정성적 동기는 수긍할 만합니다 — 오브젝트 스토어 업로드 두 개가 동시에 붙으면 흐름 제어 부재로 깨지기 쉬웠던 기존 async publish와 달리, 서버가 퍼블리셔들의 속도를 직접 조율하니 동시 퍼블리셔가 수백이어도 각자 흐름 제어를 받는다는 것이 블로그의 주장입니다(이 역시 자체 서술입니다).

클라이언트 지원 현황도 현실적으로 봐야 합니다. 이 기능을 쓰려면 스트림에 AllowBatchPublish 옵션을 켜고 지원하는 클라이언트가 필요한데, 글을 쓰는 시점 기준 공식 구현은 Go 확장 라이브러리 orbit.go의 jetstreamext v0.3.0(2026-04-21, FastPublisher)이 먼저입니다. 그리고 2주 뒤 v0.3.1이 FastPublisher의 스톨·타임아웃·리플라이 프리픽스 버그를 고쳤습니다. 코어 nats.go에는 v1.52.0에 스트림 설정 필드만 들어갔습니다. 첫 구현이 릴리스 직후 버그픽스를 거친, 아직 어린 기능이라는 뜻입니다. 프로덕션의 핵심 경로에 넣기 전에 자기 워크로드로 검증하는 게 맞습니다.

반복 스케줄과 샘플링 — 크론이 서버 안으로

2.12는 Nats-Schedule-TTL 헤더로 "나중에 한 번 배달되는 메시지"(delayed message)를 도입했습니다. 2.14는 이걸 반복 스케줄로 확장합니다. 메시지에 Nats-Schedule: @every 5m 같은 간격이나 @hourly, 혹은 크론 표현식을 붙여 두면 서버가 그 스케줄대로 대상 서브젝트에 메시지를 계속 만들어 냅니다.

nats pub -J 'schedules.orders.hourly' \
  -H "Nats-Schedule: @hourly" \
  -H "Nats-Schedule-TTL: 5m" \
  -H "Nats-Schedule-Target: orders" \
  'body'

클라이언트 쪽에 타이머 돌리는 전용 퍼블리셔를 상주시킬 필요가 없어진다는 게 요지입니다. 파생 기능 두 가지도 같이 들어왔습니다. 서브젝트 샘플링은 Nats-Schedule-Source 헤더로 "스케줄이 발화할 때마다 소스 서브젝트의 마지막 메시지를 읽어 타깃으로 발행"합니다 — 엣지에서 초 단위로 쏟아지는 센서 값을 5분에 한 번만 클라우드로 올리는 서버사이드 다운샘플링이 대표 유스케이스입니다(소스에 메시지가 없으면 스케줄 메시지 자신의 본문이 폴백으로 발행됩니다). 스케줄 롤업은 Nats-Schedule-Rollup 헤더로 발행 시점에 해당 서브젝트를 롤업합니다.

ADR-51에서 확인한, 문서 안 읽으면 밟게 되는 제약들:

  • 스케줄 하나당 고유 서브젝트가 필요합니다. 같은 타깃으로 가는 지연 메시지를 여러 개 두려면 orders.schedule.UUID처럼 스케줄 메시지의 서브젝트를 유일하게 만들고 Nats-Schedule-Target으로 모아야 합니다.
  • @every의 최소 간격은 1초입니다. 그보다 짧으면 거부됩니다.
  • 크론은 초 필드를 포함한 6필드 형식이고 기본 시간대는 UTC입니다. Nats-Schedule-Time-Zone 헤더로 IANA 시간대(Asia/Seoul 등)를 줄 수 있는데, 서버가 호스트의 tzdata로 이름을 해석하므로 크론 스케줄을 평가할 모든 서버에 tzdata가 깔려 있고 최신이어야 한다는 운영 요구가 따라옵니다. 고정 오프셋(+02:00)은 받지 않습니다.
  • ADR은 DST 전환 시각에 걸리는 크론을 권하지 않습니다 — 시계가 앞으로 뛰면 그 회차가 건너뛰어질 수 있고, 뒤로 돌면 두 번 실행될 수 있다고 명시합니다.
  • 서버가 오래 죽어 있다 복구되면 과거로 지나간 스케줄은 즉시 발사됩니다. 한 달 전에 예약된 메시지가 복구 직후 도착하는 걸 원치 않으면 스케줄 메시지에 Nats-TTL을 붙여 만료시키는 게 ADR이 안내하는 방어책입니다.
  • 스케줄을 쓰는 스트림에 Discard New 정책은 지원되지 않는다고 ADR이 명시하며, 2.14.2는 카운터 스트림과 스케줄에 대한 구성 제약 검증을 강화했습니다.

언제 쓰지 말아야 하나 — 이건 분산 크론 스케줄러의 대체재가 아닙니다. 실행 이력 추적, 실패 재시도 정책, 겹침 방지 같은 잡 스케줄러의 책임은 여전히 소비자 쪽 몫이고, 서버가 해 주는 건 "그 시각에 메시지를 만들어 스트림에 넣는 것"까지입니다. 반대로 "연결을 유지한 채 타이머만 돌리는 사이드카"를 없애는 용도로는 딱 맞습니다.

WorkQueue·Interest 소싱이 이제 안 끊긴다

수수한 이름에 비해 실질적인 정합성 수정입니다. 2.14 전까지 스트림 미러·소스는 내부적으로 임시(ephemeral) 컨슈머로 비동기 복제를 했습니다. 문제는 소싱 대상이 WorkQueue나 Interest 보존 정책 스트림일 때입니다 — 예컨대 리프 노드가 Interest 스트림을 소싱하다가 일주일 다운되면, 그 사이 관심(interest)이 사라진 것으로 처리돼 데이터가 자동 삭제되고, 리프가 복구돼도 그 구간은 복제되지 않습니다.

2.14는 WorkQueue·Interest 스트림을 소싱·미러링할 때 자동으로 내구(durable) 컨슈머로 전환합니다. 새 ack 정책 AckFlowControl이 흐름 제어 기반으로 영속화 후 확인응답을 하고, 이 컨슈머는 컨슈머 목록에 보이며 소스 도메인·계정·스트림 메타데이터를 달고 연결이 끊겨도 상태를 유지합니다. 보안이 민감한 배치에서는 상대편이 컨슈머 설정을 건드리지 못하도록 미리 만든 컨슈머를 가져다 쓰는(bring your own consumer) 방식도 지원합니다.

업그레이드 중 주의할 것 하나 — 혼합 버전 클러스터에서는 업그레이드된 서버가 신형 소싱 컨슈머 생성을 시도하다가 구버전 서버가 json: unknown field "sourcing" 경고를 로그에 남길 수 있습니다. 가이드에 따르면 자동으로 구형 방식으로 재시도하므로 전체 업그레이드가 끝나면 사라지는 일시적 로그입니다. 다운그레이드 쪽이 더 아픕니다 — 2.12로 내리면 AckFlowControl 컨슈머는 오프라인으로 마킹되어 2.14로 되돌리기 전까지 못 씁니다.

컨슈머 리셋 API

운영하다 보면 "이 컨슈머를 시퀀스 N부터 다시 돌리고 싶다"는 순간이 옵니다. 지금까지는 컨슈머를 지우고 시작 시퀀스를 지정해 다시 만드는 방법뿐이었고, 그 과정에서 이름·구독자·ACL에 얽힌 것들이 같이 흔들렸습니다. 2.14는 $JS.API.CONSUMER.RESET.stream.consumer API로 컨슈머를 지우지 않고 ack floor나 임의 시퀀스로 배달 상태를 리셋할 수 있게 했습니다. 리셋 후 상태는 "그 시퀀스에서 delete 후 recreate한 것과 동일"하다고 가이드가 정의합니다.

운영자 관점에서 진짜 중요한 세 가지

기능 목록보다 이 절이 업그레이드 판단에 더 중요합니다. 셋 다 업그레이드 가이드에 명시된 동작 변화입니다.

첫째, filestore I/O 에러가 이제 스트림을 얼립니다. 이전 버전은 일부 파일시스템 I/O 에러를 제대로 처리하지 않은 채 스트림과 서버가 계속 돌았습니다. 2.14부터는 에러가 표면화됩니다 — 영향받은 스트림은 얼어붙어(freeze) 더 이상 진행하지 않고, 로그에 에러를 남기고, 헬스체크가 unhealthy를 보고합니다. 같은 서버의 다른 스트림은 영향받지 않고, 복제 스트림이면 다른 레플리카가 투명하게 이어받습니다. 다만 얼어붙은 서버 자체는 재시작해야 복구됩니다. 조용히 썩는 것보다 명백히 낫지만, 모니터링해야 할 새 운영 조건이 생긴 겁니다 — healthz 실패 메시지에서 write error를 감시 대상에 추가하세요.

둘째, Raft 과부하 보호. 이전에는 Raft 로그에 엔트리가 커밋·적용 속도보다 빠르게 쌓이면 메모리와 디스크가 무한정 자랄 수 있었습니다. 2.14는 이 상태를 인식해 자원 사용을 바운드하고, 뒤처진 리더는 스스로 물러나 더 건강한 피어에게 넘깁니다. 가이드는 한계도 분명히 적습니다 — 피어 과반이 똑같이 과부하면 시스템은 성능 저하 상태에 머물며, 이 보호는 일시적 과부하용 안전망이지 용량 부족의 대체재가 아니라고요. 이 문장이 릴리스 문서에 들어 있다는 것 자체가 이 프로젝트 문서의 미덕입니다.

셋째, 2.15 전에 해둘 숙제 — ACK 서브젝트 포맷 마이그레이션. 2.14는 컨슈머 확인응답·흐름제어 리플라이 서브젝트의 v2 포맷(도메인·계정 해시 포함)을 함께 지원하기 시작했습니다.

v1: $JS.ACK.<stream>.<consumer>.<delivered>.<sseq>.<cseq>.<ts>.<pending>
v2: $JS.ACK.<domain>.<acct hash>.<stream>.<consumer>.<delivered>.<sseq>.<cseq>.<ts>.<pending>

2.14의 기본값은 여전히 v1이지만 2.15에서 기본이 v2로 바뀝니다. 계정 import/export나 서브젝트 권한에 스트림 이름까지 박은 ACK·FC 서브젝트를 쓰고 있다면 2.15 전에 ACL을 갱신해야 한다고 가이드가 명시합니다(catch-all 와일드카드를 쓰거나 단일 계정에서만 JetStream을 쓰면 해당 없음). 미리 테스트할 수 있도록 2.14에 서버 설정의 feature_flags 필드가 생겼고, 여기서 js_ack_fc_v2를 켜 볼 수 있습니다. 단, 이 필드를 설정한 채 2.12로 다운그레이드하면 구버전이 필드를 인식하지 못하므로 내리기 전에 지워야 합니다.

이 밖에 조용히 도움이 되는 변화로, info·list 류 조회 API 요청이 생성·갱신·삭제 요청과 분리된 큐로 들어가 우선순위가 낮아졌고(대시보드 폴링이 운영 작업을 밀어내는 상황 완화), 복제 스트림의 상태 스냅샷이 스트림 처리를 멈추지 않고 비동기로 기록됩니다 — 내부 삭제(interior delete)가 많은 스트림의 꼬리 지연에 특히 효과가 있다고 하는데, 구체적 수치는 공개된 게 없습니다.

API 레벨로 보는 기능 게이팅, 그리고 세 개의 릴리스 트레인

2.11부터 NATS는 JetStream 기능을 API 레벨로 게이팅합니다. 서버 소스(server/jetstream_versioning.go)에서 직접 확인한 값으로 2.11이 레벨 1, 2.12.0이 레벨 2, 2.14.0이 레벨 4입니다. 그럼 레벨 3은 어디 갔느냐 — 흥미롭게도 유지보수 트레인인 2.12.5(2026년 3월)가 레벨 3을 쓰고 있습니다. 스트림 백업의 window_size 파라미터 같은 소소한 API 추가가 패치 트레인에 들어오면서 레벨을 하나 소비한 겁니다. 결과적으로 2.13을 건너뛴 덕에 "마이너 버전 − 10 = API 레벨"이 2.14에서 다시 맞아떨어지게 됐는데, 거듭 말하지만 이게 스킵의 이유라는 공식 설명은 없습니다.

이 레벨이 실무에서 갖는 의미는 다운그레이드 안전성입니다. 상위 레벨 기능을 쓰는 스트림·컨슈머는 구버전 서버에서 손상되는 대신 오프라인으로 마킹되고(2.12의 offline assets), 클라이언트는 Nats-Required-Api-Level 헤더로 요구 레벨을 명시할 수 있습니다.

릴리스 트레인 운영도 눈여겨볼 만합니다. GitHub 릴리스 이력을 보면 2.14.0이 나온 뒤에도 2.12.x가 계속 패치를 받고 있고, 같은 날 나란히 릴리스됩니다 — 6월 2일에 2.12.10과 2.14.2가, 6월 29일에 2.12.12와 2.14.3이 함께 나왔습니다. 한편 2.11.x는 2.14.0 릴리스 직전인 4월 27일의 2.11.17을 끝으로 멈췄습니다. 즉 관찰되는 패턴은 "현행 + 직전" 두 트레인 동시 유지보수인데, 공식 지원 기간 정책 문서는 찾지 못했으므로 이건 이력에서 읽히는 패턴이지 보장이 아닙니다.

그래서 언제 올리고, 언제 기다리나

바로 올릴 이유가 있는 경우 — WorkQueue·Interest 스트림을 리프 노드에서 소싱하는 토폴로지(정합성 수정이 실질적입니다), 컨슈머 리셋이 필요한 운영 패턴, filestore I/O 에러의 조기 표면화가 반가운 환경(스토리지가 미덥지 않은 엣지), 그리고 서버 내장 스케줄·다운샘플링이 사이드카 하나를 지워 주는 IoT·엣지 배치.

서두를 이유가 없는 경우 — 패스트 배치가 목적이라면 클라이언트 생태계가 아직 orbit.go(Go) 중심이고 첫 구현이 버그픽스를 갓 거쳤다는 점을 감안해서, 자기 워크로드 벤치마크 없이 코어 경로에 넣지 않는 게 좋습니다. 2.12.x 트레인이 여전히 같은 날짜에 패치를 받고 있으므로, 신기능이 필요 없다면 2.12.12에 머무는 것도 현재로선 방어 가능한 선택입니다. 다만 어느 쪽이든 ACK 서브젝트 ACL을 세밀하게 쓰는 멀티테넌트 환경이라면 2.15에서 기본값이 바뀌기 전에 js_ack_fc_v2 마이그레이션 준비는 지금 시작하는 게 맞습니다.

덧붙여, 같은 시기 메시징 생태계의 다른 축에서는 Kafka가 KIP-1150 디스크리스 토픽으로 저장 계층 자체를 재설계하고 있습니다. Kafka는 브로커 디스크를 오브젝트 스토리지로 밀어내는 방향으로, NATS는 서버가 퍼블리셔의 흐름을 직접 조율하는 방향으로 — 처리량과 비용 문제를 푸는 접근이 갈라지는 걸 나란히 보는 재미가 있습니다. 두 시스템의 포지셔닝 차이는 메시징 시스템 종합 가이드에서 다뤘습니다.

마치며

정리하면 2.14는 화려한 릴리스라기보다 "JetStream을 대량 인제스트와 무인 운영 쪽으로 한 칸 미는" 릴리스입니다. 패스트 배치는 async publish의 구조적 약점(흐름 제어 부재)을 서버 주도 조율로 풀었고, 스케줄은 타이머 사이드카를 서버로 흡수했고, WorkQueue·Interest 소싱은 조용히 데이터를 잃던 구멍을 메웠습니다. 그 대가로 운영자에게는 새 감시 조건(스트림 freeze)과 마이그레이션 숙제(ACK v2 ACL)가 생겼습니다.

성능 숫자는 아직 벤더 자체 서술뿐이니, 도입 판단은 문서의 주장이 아니라 자기 워크로드의 측정으로 하시기 바랍니다. 그리고 이 릴리스의 문서들 — 한계를 명시한 업그레이드 가이드, DST에서 두 번 실행될 수 있다고 적어 둔 ADR — 은 그 측정을 시작하기에 꽤 좋은 출발점입니다.

참고 자료