본문으로 건너뛰기
전체 글

HTTP는 응답하는데 합의는 멈춰 있다면

· 약 8분
Bankware Global Engineering

거래를 제출했는데 확정되지 않는다. 운영 화면은 열리고, 상태 조회도 성공한다. 합의 엔진에는 running=true가 표시된다. 서버가 응답하고 엔진도 실행 중이라면, 조금 더 기다리기만 하면 되는 걸까?

이 정보로는 아직 답할 수 없다. 할 일이 없어 기다리는 노드일 수도 있고, 다른 검증자의 표를 기다리거나 저장 작업 안에서 멈춘 노드일 수도 있다. 화면에 남은 값이 장애가 나기 전 마지막 응답일 가능성도 있다.

GC의 안전성과 진전에서는 자료를 보호하며 멈춘 결과와 정리를 끝낸 결과를 구분했다. 합의 관측에도 비슷한 질문이 생긴다. 실패가 보이지 않는다는 것과, 해야 할 일이 끝나고 있다는 것을 어떻게 구별할까?

nigo-protocol은 이 질문을 응답 여부 하나에 맡기지 않았다. 최소 실행 상태를 읽는 경로를 분리하고, 거래 대기·투표·로컬 확정을 각각 관측해 진행을 판단한다. 이 글은 그 설계와 2026년 9월의 로컬 검증에서 확인한 범위를 다룬다.

같은 높이에 머물러도 같은 상황은 아니다​

설명을 위해 검증자 네 대가 높이 10까지 같은 블록을 확정했다고 하자. 높이는 블록의 순번이다. 이 네트워크는 처리할 거래가 없으면 빈 블록을 만들지 않는다. 지금은 대기 거래도, 처리 중인 블록 제안도 없다.

1분 뒤에도 높이가 10이라면 장애일까? 이 조건에서는 자연스러운 대기다. 확정할 일이 없었으므로 새 블록이 없어도 된다. “마지막 블록 이후 시간이 많이 지났다”만으로 경보를 내면, 거래가 드문 시간마다 정상 상태를 장애로 부르게 된다.

이제 거래 T를 제출한다. 한 노드의 거래 풀에 들어갔고, 그 노드는 T를 담은 높이 11의 블록 제안 P를 받아들였다. 여기서부터는 기다리는 대상이 생겼다. 이 예제에서 확정에 필요한 표는 세 개인데, 같은 제안에 대한 유효한 확정 표는 두 개만 관측됐다고 하자. 네트워크 연결 수가 세 개여도, 필요한 세 번째 표를 받았다는 뜻은 아니다.

이 상태가 관측용 지연 기준을 넘으면 “표를 기다리는 중”이라는 근거를 제시할 수 있다. 그러나 그 원인이 상대 노드의 장애인지, 통신 지연인지, 아직 도착하지 않은 처리 결과인지는 이 노드의 표 개수만으로 확정할 수 없다.

이후 필요한 표를 받고, 로컬 원장 확정과 합의 안전성 기록의 저장까지 마쳤다면 높이 11의 확정을 관측한다. T가 풀에서 빠지고 다음 제안도 없다면 다시 대기 상태가 된다. 앞의 대기와 뒤의 대기는 모두 조용하지만, 그사이에 제출된 일을 실제로 끝냈다는 증거가 남는다.

이 높이와 거래 이름은 설명용이다. 실제 실험 결과는 뒤에서 별도로 살펴본다.

HTTP 성공과 엔진 실행은 각각 다른 질문에 답한다​

이 예제에서 운영자가 읽는 정보는 세 층으로 나뉜다.

관측알 수 있는 것그것만으로 알 수 없는 것
HTTP 응답 성공이번 조회가 응답했음합의가 진행되는지
엔진 실행 중로컬 실행 조건이 유지됨필요한 표를 모았는지
새 로컬 확정 관측해당 높이의 확정·저장을 마침이후 거래도 계속 끝낼지

응답 시각이 계속 바뀌어도 그것은 조회가 새로 이루어졌다는 뜻이다. 합의가 새 결과를 냈다는 신호는 아니다. 마찬가지로 running은 엔진이 시작돼 있고 명시적인 실패나 중단으로 판정되지 않았다는 실행 상태다. 예외를 던지지 않은 채 오래 기다리는 엔진도 이 값만으로는 걸러낼 수 없다.

반대로 새 블록이 없다는 이유만으로 엔진이 멈췄다고 해도 안 된다. 높이 10에서 할 일이 없던 예제처럼 정상 대기가 있기 때문이다. “최근에 무엇이 바뀌었나?”에 더해 지금 끝내야 할 일이 있는지를 함께 물어야 한다.

새 확정 관측도 한 노드가 확인한 결과다. 이를 네트워크 모든 노드의 현재 상태나 장기적인 가용성 보장으로 확대하지 않는다. 운영 화면은 관측한 사실을 제공해야지, 보지 못한 범위까지 정상이라고 대신 선언해서는 안 된다.

장애를 읽는 길이 장애와 함께 막히지 않도록​

진행을 구분하기 전에 해결할 문제가 있었다. 상태를 조회하는 요청이 상세 원장 정보까지 읽는다면, 저장소가 느려진 순간 상태 조회도 같은 저장소에서 기다릴 수 있다. 합의 엔진 내부의 잠금을 잡아야 하는 진단도 마찬가지다. 장애를 설명할 경로가 장애가 발생한 작업의 완료를 기다리는 구조가 되는 것이다.

nigo-protocol은 최소 상태를 별도의 읽기 경로로 분리했다. 엔진이 실행 상태와 최초 실패 정보를 작은 메모리 표본으로 공개하고, 조회는 이미 공개된 표본을 읽는다. 합의 처리 잠금이나 데이터베이스를 다시 거치지 않으며, 거래 풀과 다른 노드의 연결 정보도 조회하지 않는다.

실패를 알리려고 실패한 상태 머신의 잠금을 다시 얻는 일도 피한다. 전이가 실패해 그 이후 상태를 확인하지 못했다면 그 부분은 비워 둔다. 모든 칸을 채우기 위해 실패 공개까지 늦추는 것보다, 확인한 실패를 먼저 보여주는 편이 낫다.

상세 조회 자체를 없앤 것은 아니다. 원장의 내용이 필요한 화면은 여전히 저장소를 읽는다. 대신 그 요청이 실패하거나 기다리는 동안에도 최소 실행 상태를 별도로 볼 수 있게 했다.

9월 14일의 격리 검증에서는 실제 HTTP 요청을 보내면서 이 분리를 확인했다. JDBC 연결 풀이 모두 점유돼 상세 요청이 기다리거나 RocksDB의 잠금 때문에 읽기가 막힌 동안에도 최소 상태 조회는 응답했다. 저장소가 닫혀 상세 조회가 실패하는 경우도 따로 확인했다. 이 결과는 관측 경로가 해당 저장소 의존성에서 분리됐다는 근거다. JVM 자체가 멈추거나 CPU·HTTP 처리 자원이 모두 고갈된 경우까지 응답을 보장한다는 뜻은 아니다.

기다리는 일을 세고, 실제로 끝난 일을 센다​

최소 상태 분리만으로는 표를 기다리는 예제를 설명할 수 없다. 그래서 진행 판정에는 세 가지 근거가 더 필요하다.

첫째는 풀에 남아 있는 거래와 대기 시간이다. 다만 풀에 들어왔다는 것이 바로 실행할 수 있다는 뜻은 아니다. 대기 거래 수가 많다는 사실만으로 합의 장애를 선언하지 않고, 오래 남은 거래가 있는지를 별도로 드러낸다. 다른 거래는 계속 확정되는 중일 수도 있다.

대기 시간은 거래 안에 적힌 서명 시각이 아니라 이 노드가 거래를 처음 받아들인 뒤의 경과 시간으로 센다. 같은 거래를 반복해서 받았다는 이유로 대기 시작을 뒤로 미루지 않는다. 그래야 오래 기다린 T가 중복 제출될 때마다 새 거래처럼 보이지 않는다.

둘째는 같은 높이·라운드·제안에 모인 유효한 표다. 라운드는 같은 높이의 합의를 다시 시도하는 차수다. 검증자 한 명이 같은 표를 여러 번 보내도 한 표로 세며, 서로 다른 제안에 대한 표를 합쳐 정족수를 채운 것처럼 보이지 않게 한다. 높이 11의 합의 작업이 기다린 시간은 라운드가 바뀌었다는 이유만으로 처음부터 다시 세지 않는다.

셋째는 로컬에서 확정을 완료한 높이와 그 이후의 경과 시간이다. 표를 충분히 모았다는 사실과 저장까지 끝냈다는 사실 사이에도 작업이 남아 있다. 원장 확정에 이어 필요한 합의 안전성 기록의 저장을 마쳐야 확정 완료로 관측한다. 타이머가 실행되거나 같은 메시지를 다시 처리한 횟수를 확정 횟수로 대신 세지 않는다.

현재 진행 판정은 이런 근거를 함께 읽는다. 명시적인 실패나 로컬 작업·타이머 지연이 있으면 그 사실을 먼저 드러낸다. 그렇지 않은 상태에서 끝내야 할 합의 작업이 오래 남았다면 표 대기와 확정 지연을 구분한다. 대기 거래만 오래 남은 경우도 따로 표시한다. 제안도 대기 거래도 없으면 정상 대기로 읽을 수 있다.

현재 기본 진행 지연 기준은 60초다. 이는 관측을 분류하는 기준이며, 합의의 정족수나 거래 유효성을 바꾸거나 60초 뒤 강제로 확정하라는 규칙은 아니다. 운영 화면이 지연을 발견했다고 자동으로 재시작하거나 저장 기록을 우회하지도 않는다.

알 수 없는 순간을 정상으로 채우지 않는다​

앞의 정보는 각각 일관된 표본으로 공개되지만, 거래 풀과 합의 상태 전체를 한순간에 멈춰서 찍은 사진은 아니다. 합의가 다음 라운드로 바뀌는 동안 읽으면 서로 맞지 않는 시점의 정보가 잠시 함께 보일 수 있다. 이런 경우에는 그럴듯한 정상 판정을 만드는 대신 UNKNOWN, 즉 현재 근거로 판정할 수 없음을 표시한다.

프로세스를 다시 시작한 직후에도 이전 실행에서 마지막으로 확정한 시점의 경과 시간을 추측해서 채우지 않는다. 새 실행에서 아직 관측하지 않았다면 값이 없는 상태로 둔다. “아직 본 적 없음”과 “확정이 오래 지연됨”은 다르기 때문이다.

화면에서도 같은 원칙이 필요하다. 마지막에 받은 정상값이 15초를 초과해 오래됐다면 현재 정상의 근거로 사용하지 않는다. 조회 실패·갱신 일시정지·재조회 중인 경우에도 이전 표본과 현재 판정을 구분한다. 15초는 화면 표본의 신선도 기준이며 앞의 60초 진행 기준과 역할이 다르다.

이때 UNKNOWN은 장애를 확정했다는 뜻도 아니다. 연결이 끊긴 화면이 “합의 정상”이라고 계속 말하지 않게 하는 장치다. 상태를 모른다는 사실을 표현할 수 있어야, 실제 실패와 단순한 관측 공백을 구별할 수 있다.

조용한 네 노드에 거래를 넣어 확인한 것​

9월 16일에는 같은 노드 JAR로 네 개의 검증자 프로세스를 띄워, 거래가 없는 대기와 새 거래 확정이 구분되는지 확인했다. 로컬 macOS 환경의 RDB와 상호 TLS 연결을 사용했고, 빈 블록을 만들지 않는 조건이었다.

약 65초의 대기 구간에서 63회에 걸쳐 네 노드의 진행 상태를 읽었다. 총 252개 표본은 모두 IDLE이었다. 대기 거래는 없었고, 아직 관측한 확정이 없으므로 마지막 확정 이후 시간도 값이 없었다. 합의 라운드는 바뀌었지만 그것을 새 확정이나 진행 지연으로 잘못 세지 않았다.

그다음 서로 다른 노드로 거래 두 건을 제출했다. 네 노드는 모두 같은 높이 2까지 확정했고, 로컬에서 관측한 마지막 확정 높이도 2였다. 거래를 처리한 뒤에는 다시 대기 거래가 없는 상태로 돌아갔다. 거래가 없을 때의 조용함과 들어온 거래를 끝낸 뒤의 조용함을 구분해 본 것이다.

이 실행은 정상 대기와 새 확정을 확인한 실험이다. 표 부족과 저장 지연의 분류는 별도의 조건을 고정한 Java 테스트와 HTTP 격리 검증으로 확인했다. 252개 표본을 여러 장애가 난 실운영 환경의 결과로 읽거나, 장기간의 네트워크 가용성과 자동 복구까지 검증했다고 말할 수는 없다. 이번 글을 쓰면서 실험을 새로 실행한 것도 아니다.

운영 관측이 해야 할 일은 초록색 표시를 오래 유지하는 것이 아니다. 무엇을 기다리고 있는지, 무엇을 실제로 끝냈는지, 지금은 무엇을 모르는지를 구별하는 것이다. 그 구분이 있어야 정상 대기를 불필요하게 깨우지 않고, 응답하는 서버 뒤에 남은 지연도 놓치지 않는다.

저장 함수가 돌아오면 서명을 보내도 될까에서는 이 글에서 짧게 지난 저장 경계를 살펴본다. 합의 메시지를 다른 노드에 보내기 전에 무엇이 저장돼 있어야 하며, 저장 함수가 반환했다는 사실만으로 그 조건을 충족했다고 말할 수 있을까?