콘텐츠로 이동

MQTT 입력 중지 또는 결함

MQTT 기반 데이터소스가 스트리밍 중이었는데, 무언가 깨집니다:

  • 추론이 스스로 실행 중에서 유휴/장애 상태로 전환됨 — 아무도 Stop을 누르지 않았습니다.
  • 예측값이 NaN으로 돌아옴, 또는 대시보드에 그럴듯한 값이 전혀 보이지 않음.
  • MQTT 출력 싱크가 쓰기 실패를 보고함, 사유가 transport: not connected와 같음.
  • 태그 매핑이 저장 시점에 거부됨, tag_outside_subscription_* 메시지와 함께.
  • Enable 또는 Test Connection이 아예 실패함 — 아직 아무것도 스트리밍되지 않은 상태에서.

MQTT는 푸시(push) 프로토콜입니다 — 추론은 어댑터가 메시지 도착 시 채우는 캐시에서 매 틱마다 샘플 하나를 가져옵니다(pull). 아래의 각 원인은 사실 다른 각도에서 본 같은 질문입니다: 캐시가 충분히 최근에 사용 가능한 값을 받았는가?

이 동일한 증상 집합을 다섯 가지 뚜렷한 장애 모드가 공유합니다. 백엔드 로그를 grep하여 구분하세요 — 아래 예시는 모두 오류 코드/사유만 보여주며, 페이로드 내용이나 자격 증명은 결코 보여주지 않습니다.

Terminal window
docker compose -f docker-compose.release.yml logs backend | grep -iE "MqttAdapter|ChannelReadException|channel .* read failed|tag stale|disconnected|not connected"
  1. 정체된(stale) 토픽 (느리거나 죽은 퍼블리셔). 장애 전에 경고가 발생합니다:

    MqttAdapter tag stale: tag=<topic> age=<age> staleAfter=<window>
    Channel <index> (<tag>) read failed — stopping inference
    Inference fault: <reason>

    캐시 만료 윈도우는 clamp(50 / SamplingHz, 5s, 60s)입니다 — 기본값인 10 Hz에서는 5초입니다. 이 윈도우보다 느리게 발행되는 토픽은 만료되어 ChannelReadException을 일으키며, 런타임은 이를 죽은 센서와 동일하게 취급합니다: 모델에 정지된(고정된) 값을 공급하는 대신 추론을 중지합니다.

  2. NaN / Infinity 센티널. MQTT 페이로드 파서는 오직 유한한(finite) 숫자만 허용합니다 — NaN, Infinity, -Infinity, 그리고 1e400처럼 오버플로하는 리터럴은 캐시에 도달하기 전에 모두 거부됩니다. 이런 방식으로 “측정값 없음” 센티널을 발행하는 장치는 캐시된 값을 결코 갱신하지 못하므로, 위의 원인 1과 정확히 동일한 방식으로 만료되어 실패합니다 — 동일한 로그 줄, 동일한 ChannelReadException. 설계상 “NaN이 모델에 도달했다”는 별도의 장애 모드는 존재하지 않습니다. 그렇게 되면 이후 모든 다운스트림 예측을 조용히 오염시키게 될 것이기 때문입니다.

  3. 클라이언트 ID 축출(eviction). 다음을 찾으세요:

    MqttAdapter disconnected (SessionTakenOver); reconnecting

    또는 출력 측에서, 쓰기가 다음과 함께 실패합니다:

    transport: not connected

    MQTT 브로커는 두 번째 클라이언트가 동일한 클라이언트 ID로 연결하면 기존 연결을 축출합니다. 현재 빌드에서는 다음 경우에만 이런 일이 발생합니다: 동일한 역할의 데이터소스 두 개(입력 두 개, 또는 출력 두 개)가 하나의 명시적 Client ID를 공유하거나, 외부 MQTT 클라이언트가 데이터소스에 구성된 id를 재사용하는 경우입니다.

  4. 구독한 Base Topic 밖의 태그. 태그 매핑을 저장(또는 데이터소스 활성화)하면 즉시 다음과 함께 실패합니다:

    tag_outside_subscription_<tagRef>

    입력 어댑터는 매핑된 태그(또는 구성된 Base Topic 와일드카드)에만 구독합니다 — 그 범위 밖의 토픽 경로는 조용히 결코 값을 생성하지 않는 대신, 저장 시점에 거부됩니다.

  5. 소켓 수준 연결 실패. 스트리밍이 시작되기도 전에 Test Connection 또는 Enable이 다음 중 하나와 함께 실패합니다:

    connect_failed 도달 불가 / 거부됨 / DNS 실패   timeout 연결 시도가 완료되기 전에 취소됨

    백엔드 로그는 경과 시간과 예외 유형으로 이를 뒷받침합니다:

    MqttAdapter connect failed: broker=<broker> elapsedMs=<ms> errorType=<type>
  1. 정체된 토픽 / NaN 센티널 (원인 1–2). 퍼블리셔의 주기를 고치거나, 유한하지 않은 센티널을 발행하지 않도록 한 뒤 데이터소스를 다시 활성화하세요 — 장애는 런타임을 유휴 상태로 무너뜨리므로, 복구는 자동이 아닙니다:

    • 매핑된 토픽의 실제 발행 간격을 확인하고, 데이터소스에 구성된 Sampling Hz의 만료 윈도우와 비교하세요.
    • 토픽이 정당하게 “측정값 없음”을 보고한다면, 장치/게이트웨이가 그 기간 동안 NaN/Infinity를 보내는 대신 단순히 발행하지 않도록 전환하세요 — 둘 다 동일하게(조용한 토픽) 취급되므로, 센티널을 보내는 데는 아무런 이점이 없습니다.
    • 데이터소스를 다시 활성화한 뒤(모델을 다시 로드하고 어댑터를 재연결), Start를 누르세요.
  2. 클라이언트 ID 축출 (원인 3). 명시적 Client ID를 공유하는 동일한 역할의 데이터소스(입력 두 개, 또는 출력 두 개) 각각에 서로 다른 값을 부여하거나, 필드를 비워 MQTTnet이 각 쪽에 고유 id를 자동 생성하도록 하세요. 외부 클라이언트가 여러분의 데이터소스 id를 재사용하고 있다면, 둘 중 하나를 변경하세요. 이는 동일한 구성으로 만들어진 입력/출력 쌍에는 적용되지 않습니다 — v1.28.0의 역할 접미사(-in/-out)가 이미 그 둘이 충돌하지 않도록 하므로 편집이 필요 없습니다.

  3. 구독 밖의 태그 (원인 4). 태그의 토픽 경로를 포함하도록 데이터소스의 Base Topic을 넓히거나, 태그의 토픽 경로를 현재 Base Topic 안으로 들어오도록 수정하세요. 매핑을 다시 저장하세요.

  4. 소켓 실패 (원인 5). 데스크톱이 아니라 백엔드 컨테이너 내부에서 호스트, 포트, 네트워크 도달 가능성을 확인하세요. localhost를 브로커의 컨테이너/서비스 이름으로 교체하거나(또는 컨테이너에서 Docker 호스트로는 host.docker.internal), Test Connection을 재시도하세요.

  • 새 MQTT 입력을 활성화하기 전에 매핑된 모든 토픽의 발행 간격을 만료 윈도우와 대조해서 확인하세요 — 토픽이 정당하게 느리게 발행한다면 Sampling Hz를 낮춰 허용 범위를 넓히세요(1 Hz → 50초).
  • 동일한 역할의 데이터소스 두 개 사이에 하나의 명시적 Client ID를 재사용하지 마세요 (입력 두 개, 또는 출력 두 개), 그리고 데이터소스의 Client ID를 외부 MQTT 클라이언트에 재사용하지 마세요. 동일한 구성의 입력/출력 쌍은 이미 안전합니다 — v1.28.0이 각 쪽에 자동으로 접미사(-in/-out)를 붙입니다.
  • Base Topic을 실제로 매핑하는 태그에 맞게 좁게 설정하고, 이미 그 범위 안에 있는 토픽 경로를 가진 태그만 매핑하세요.
  • “데이터 없음”을 NaN/Infinity로 회선에 인코딩하지 마세요. 보고할 것이 없을 때 조용해지는 장치는 동일하게 동작하며, 센티널 값보다 추론하기 쉽습니다.
  • 워크스테이션이 아니라 박스의 관점에서 Test Connection을 실행하세요 — 데스크톱에서 localhost로 도달 가능한 브로커라도 백엔드 컨테이너에서는 전혀 도달하지 못할 수 있습니다.