← 전체 글
MQTT/약 12분 읽기/ 조회

MQTT Store-and-Forward가 장애 중 Historian에 구멍을 내지 않게 하는 법

WAN 장애 중 MQTT edge gateway가 데이터를 잃는 이유와, source timestamp·Sparkplug 방식 sequence number·replay 정책으로 historian의 공백을 막는 방법.

MQTTSCADA히스토리언네트워킹문제 해결

Publish는 성공했는데 history엔 구멍이 남는다

두 시간짜리 WAN 장애 뒤 링크를 복구하면 edge gateway는 초록불로 바뀌고 live dashboard도 곧바로 채워진다. 일주일 뒤 누군가 교대 리포트를 뽑으면 02:00부터 04:00까지 평평한 선이 보인다. 하필 compressor가 trip한 구간이다. Publish 함수는 요란하게 실패한 적이 없다. Broker에 닿지 못했을 뿐이고, gateway는 sample을 둘 곳이 없었으며, 그 공백에서 무슨 일이 일어나야 하는지 아무도 적어두지 않았다.

Store-and-forward는 그 구멍이 생길지 말지를 결정하는 부분이다. Broker가 공짜로 주는 기능이 아니라 데이터 계약이다. MQTT의 session 지속성도 여기서는 도움이 안 된다. Persistent session에 QoS 1/2를 써도 그건 재접속할 subscriber를 위해 broker가 붙들고 있는 message를 큐잉하는 것이다. Publisher 쪽 broker에 닿지 못하면 기댈 session 자체가 없고, edge gateway가 자기 저장소에 buffer해야 한다. Clean Start와 Session Expiry(MQTT 5.0), 3.1.1의 clean-session flag는 subscriber 쪽 이야기지 publisher의 장애를 다루지 않는다.

그래서 쓸 만한 edge 설계는 장애가 나기 전에 다음 질문에 답한다.

  • 어떤 message를 buffer에 남길 것인가.
  • 정상 traffic 기준으로 buffer가 몇 시간 버틸 수 있는가.
  • Timestamp는 장치, edge gateway, broker 중 어디 시간을 쓸 것인가.
  • 수신 측은 누락과 중복을 어떻게 찾을 것인가.
  • 링크가 복구된 뒤 오래된 data가 빠르게 들어오면 어떻게 처리할 것인가.
  • 저장 공간이 부족할 때 어떤 data를 먼저 버릴 것인가.

이 규칙이 없으면 live dashboard는 멀쩡해도 historian record는 비어 있을 수 있다.

모든 데이터를 같은 방식으로 저장하지 않는다

MQTT message마다 같은 persistence가 필요한 것은 아니다. 데이터 종류를 나눠서 본다.

데이터 종류Buffer 규칙비고
Historian용 공정값Source timestamp와 quality를 포함해 buffer한다.보통 양이 가장 많다. 압축은 publish 전이나 historian에서 할 수 있다.
Alarm과 eventEvent timestamp, state, acknowledge context를 포함해 buffer한다.Active와 return-to-normal을 마지막 상태 하나로 뭉개면 안 된다.
설비 상태 전환순서를 보존하고 sequence number를 붙인다.MES와 downtime 계산은 전환 순서에 민감하다.
Heartbeat와 availability오래 buffer하지 않는 편이 낫다.지난 heartbeat가 복구 로직을 헷갈리게 만들 수 있다.
HMI command보통 store-and-forward 대상이 아니다.지연된 command는 명확히 queue job으로 설계하지 않으면 위험하다.
Configuration change신중하게 buffer하거나 journal로 남긴다.사용자, 시간, 이전 값, 새 값, 결과가 필요하다.

SCADA에서 위험한 실수는 모든 message를 똑같이 buffer하는 것이다. 일주일 치 heartbeat가 한 시간 치 생산 측정값을 밀어낼 수 있다.

재접속 시간이 아니라 source timestamp를 쓴다

Gateway가 broker에 재접속하면 오래된 sample 수천 개를 짧은 시간에 보낼 수 있다. Historian은 이 값을 전달된 시간이 아니라 측정된 시간에 저장해야 한다.

Buffered message에는 보통 다음이 들어간다.

  • Source timestamp.
  • 필요한 경우 gateway receive timestamp.
  • Quality 또는 reason code.
  • Topic, asset, stream 단위의 sequence number나 증가 counter.
  • Payload schema version.
  • 진단용 publish attempt count.

Historical data에 broker arrival time만 쓰면 안 된다. Broker 시간은 transport 진단에는 유용하지만 공정 시간은 아니다.

누락을 찾으려면 sequence number가 필요하다

Timestamp만으로는 부족하다. 두 sample이 같은 timestamp를 가질 수 있고, clock이 움직일 수 있으며, 일부 장치는 거친 시간 단위만 보낸다. Sequence number가 있으면 수신 측이 빠진 message와 중복 message를 볼 수 있다.

현장에서 쓰기 쉬운 방식은 다음과 같다.

  • 설비 한 대나 tag group 같은 logical stream마다 counter를 따로 둔다.
  • 해당 stream에서 data message를 publish할 때마다 증가시킨다.
  • 가능하면 gateway restart 뒤에도 counter를 유지한다.
  • Restart 때 counter가 초기화된다면 boot id나 session id를 같이 보낸다.
  • 수신 측은 duplicate를 허용하되 gap은 표시한다.

Duplicate는 있다고 가정한다. QoS 1(at-least-once)은 ACK가 불확실하면 재전송하므로, 링크가 불안정하면 같은 sample이 두 번 들어온다. 정확히 한 번(exactly-once)은 QoS 2뿐인데, 대부분의 broker와 Sparkplug 배포는 throughput 때문에 QoS 1을 쓴다. 그래서 수신 측에는 message가 한 번만 온다는 가정이 아니라 idempotency key가 필요하다. asset + stream + seq면 충분하다.

이미 Sparkplug B를 쓴다면 이 대부분이 규격에 들어 있다. 모든 payload에 seq(0–255, 순환, 접속 뒤 NBIRTH에서 0으로 초기화)가 실리고, NBIRTH/NDEATH 쌍에는 bdSeq가 실려서 host가 MQTT Last Will and Testament로 전달된 death certificate를 그 session을 연 birth와 맞출 수 있다. 위에서 말한 stream별 counter + boot id가 그대로 표준화된 셈이다. 직접 JSON을 만들어 써도 되지만, timestamp만 믿지 말고 이 패턴을 그대로 옮기면 된다.

진단 필드는 이런 형태가 될 수 있다.

{
  "asset": "compressor-01",
  "stream": "process-values",
  "seq": 1842331,
  "bootId": "gw-a-20260618-0915",
  "sourceTs": "2026-06-18T09:24:12.350Z",
  "quality": "good"
}

필드 이름보다 중요한 것은 규칙이 일관되고 시험 가능해야 한다는 점이다.

Buffer 크기는 실제 traffic으로 잡는다

Tag 수만 보고 buffer를 계산하면 틀리기 쉽다. Message 크기, publish rate, 압축 방식, 예상 장애 시간을 같이 봐야 한다.

간단한 산식은 다음과 같다.

buffer size = average payload bytes × messages per second × outage seconds × safety factor

여기에 queue metadata, index, filesystem block size, 진단 기록의 overhead를 더한다. TLS certificate, broker address, store file이 같은 작은 disk에 있다면 OS log 공간도 남겨야 한다.

현장 확인 항목은 다음과 같다.

  • 실제 JSON, Sparkplug, binary payload로 payload 크기를 잰다.
  • 평균 부하만 보지 말고 가장 빠른 scan class를 시험한다.
  • Queue가 남아 있는 상태에서 gateway를 재기동하면 어떻게 되는지 확인한다.
  • 실제 산업용 PC나 embedded gateway에서 queue write 속도를 본다.
  • Flash storage를 쓰면 disk wear도 확인한다.

Replay 속도를 제한한다

네트워크가 복구되면 edge gateway가 broker로 message를 한꺼번에 밀어 넣을 수 있다. Broker는 받아도 historian connector, rule engine, database가 버티지 못할 수 있다.

복구 정책을 둔다.

  • 초당 replay message 수를 제한한다.
  • Priority를 지원한다면 낮은 가치의 sample보다 중요 event를 먼저 보낸다.
  • Stream 내부 순서는 유지한다.
  • 오래된 data를 비우는 동안에도 live data 상태를 볼 수 있게 한다.
  • Queue depth, oldest sample age, replay status를 SCADA 진단값으로 노출한다.

운전자는 "링크가 복구되어 buffer를 비우는 중"인지, "지금 공정이 오래된 값을 만들고 있는 것처럼 보이는지"를 구분할 수 있어야 한다.

현장에서 자주 나는 문제

  • Buffered message가 재접속 시간으로 저장되어 historian에 가짜 생산 spike가 생긴다.
  • Alarm active와 clear가 하나의 마지막 상태로 합쳐져 장애 중 발생한 trip을 아무도 못 본다.
  • Queue가 단순 FIFO라서 낮은 가치의 고속 tag가 중요한 상태 전환을 늦춘다.
  • Queue를 memory에만 둬서 gateway restart 때 buffer가 사라진다.
  • 수신 측에 idempotency key가 없어 duplicate message가 MES event를 두 번 만든다.
  • Gateway disk가 꽉 차면서 buffered data와 현재 진단 log를 같이 잃는다.
  • Broker는 burst를 받았지만 downstream historian connector가 record를 조용히 버린다.

시운전 시험

운영 인계 전에 store-and-forward를 따로 시험한다.

시험기대 결과
Broker를 짧게 정지Gateway가 정해진 stream을 buffer하고 queue depth를 보고한다.
Broker 복구Data가 원래 source timestamp와 stream 순서로 replay된다.
장애 중 gateway restartPersistent queue가 유지되거나, 문서화된 data loss alarm이 뜬다.
Queue warning threshold 도달Data loss 전에 SCADA 진단 알람이 발생한다.
Queue full threshold 도달Drop policy가 문서화된 priority 규칙을 따른다.
Duplicate replay 모의수신 측이 duplicate를 무시하거나 표시하고 event를 중복 계산하지 않는다.
Gateway clock 변경수신 측이 bad time을 표시하거나 quality rule로 history 오염을 막는다.

같은 시험 구간의 broker log, gateway log, historian record를 같이 보관한다. 필요한 증거는 message가 도착했다는 사실만이 아니다. 맞는 시간, 순서, quality, count로 들어왔는지가 중요하다.

프로젝트에 남길 규칙

MQTT edge pipeline마다 buffer health를 나타내는 diagnostic topic이나 tag set을 만든다. 최소한 connection state, queue depth, oldest queued timestamp, dropped message count, replay active flag, last successful publish time은 있어야 한다. SCADA 화면에서 buffer 상태를 볼 수 없으면 store-and-forward 문제는 누군가 historian을 열어 누락 데이터를 발견한 뒤에야 드러난다.