청구서에서 시작된 이야기
원격 펌프장 12개소, 각각 월 500 MB M2M SIM, 각각 holding register 100개를 1초 주기 Modbus TCP로 poll. 첫 청구서에서 11개소가 초과 요금이었다. 아무도 계산을 해보지 않았는데, 사실 그 계산은 그리 미묘하지 않다.
Holding register 100개를 읽는 Modbus TCP 요청은 MBAP header 12바이트에 PDU, 응답은 209바이트다. 각각을 IPv4와 TCP로 감싸면 세그먼트당 40바이트가 더 붙는다. 스택이 돌려보내는 ACK도 있다. 재전송을 빼고도 poll 한 번에 대략 300바이트의 과금 대상 IP traffic이 나간다.
300 bytes/poll × 1 poll/s × 86,400 s = 25.9 MB/일
25.9 MB × 30 = 사이트당 월 약 780 MB
엔지니어링 관점의 답은 요금제를 키우는 것이 아니었다. 펌프장 wet well 수위는 하루에 86,400번 바뀌지 않는데, 같은 값을 반복해서 듣는 데 돈을 내고 있었다는 것이다.
WAN 링크에서 poll 주기는 비용 결정이다
공장 LAN에서는 1초 scan class가 측정 가능한 비용을 만들지 않으니 아무도 신경 쓰지 않는다. 셀룰러에서는 그것이 청구 항목이고 동시에 신뢰성 결정이다. Poll 한 번은 timeout이 날 기회 한 번이고, 재시도는 예산에 없던 traffic이다.
다른 것을 손대기 전에 원격 tag 중 정말로 1분 미만의 신선도가 필요한 것을 골라낸다. 보통 펌프장에서 그 목록은 짧다. 운전/고장 상태, high-high 수위, 그리고 토출 중 토출압 정도다. 운전시간 누적, 외기 온도, 일일 적산 유량은 5분이나 15분이면 충분하다. Scan class 하나를 셋으로 나누는 것만으로 traffic이 한 자릿수 줄어드는 경우가 흔하고, 운영 쪽에서 반대할 이유도 없다.
더 큰 차이는 protocol에서 난다. Modbus에는 report-by-exception이 없다. Master는 거의 변하지 않는 값을 매번 물어봐야 한다. 셀룰러 사이트에서 protocol을 고를 수 있다면 push 방식을 고른다.
| 방식 | Traffic 특성 | 적합한 곳 |
|---|---|---|
| Modbus TCP polling | Poll 주기에 비례해 일정하게 발생. | 교체 불가능한 legacy 장치. Edge concentrator를 붙인다. |
| DNP3 unsolicited response (IEEE 1815) | 공정이 조용하면 거의 0, 변화 시 버스트. | 상하수도, 배전 — 전통적인 정답. |
| MQTT / Sparkplug B publish-on-change | 조용하면 거의 0, 작은 keepalive만. | 최신 edge gateway, 고정 IP가 없는 사이트. |
| Deadband 적용한 OPC UA subscription | 낮지만 session과 keepalive overhead가 MQTT보다 무겁다. | 이미 UA로 표준화된 사이트. |
RTU가 Modbus만 한다면 판넬 안에 작은 gateway를 둔다. RS-485든 로컬 Ethernet이든 현장에서는 원하는 주기로 poll하고, 상위로는 변화분만 publish한다. 그러면 링크 비용이 scan class가 아니라 공정을 따라간다. 그 gateway는 장애 중 buffer를 두는 자리이기도 한데, 어차피 필요한 기능이다.
실제로 일을 하는 것은 deadband다
Deadband 없는 report-by-exception은 polling보다 나쁘다. 최하위 비트가 흔들리는 4–20 mA 수위 전송기는 scan마다 change event를 만들어낸다. 1초 poll을 sample당 overhead만 더 얹어서 재발명한 셈이다.
Deadband는 계측기 노이즈와 운전원이 대응할 수 있는 폭을 기준으로 정한다. Wet well이면 span의 2%를 쓴다. 유입 중에는 수위가 몇 초 만에 그 폭을 지나가므로 trend는 여전히 trend로 보이고, 펌프 사이클 사이의 부유 노이즈는 publish를 멈춘다. 느린 탱크 수위라면 0.5%도 괜찮다. 여기에 몇 시간에 한 번 integrity poll — DNP3 Class 0나 주기적 전체 읽기 — 를 넣어 event 하나를 놓쳤다고 화면에 낡은 값이 영원히 남지 않게 한다. 1분마다가 아니다. Class 0는 static database 전체를 끌어오고, 그것을 poll 주기로 돌리면 unsolicited response로 얻은 것을 전부 되돌려주는 꼴이 된다.
누가 누구에게 접속하는가
대부분의 M2M SIM은 통신사 CGNAT 뒤에서 동적 주소를 받는다. RTU에 라우팅 가능한 주소가 없으니 master가 TCP 연결을 열 수 없다. 방법은 둘이다.
- 고정 IP를 주는 사설 APN을 산다. 동작은 한다. SIM당 비용이 더 들고, SCADA 망까지 터널이나 통신사 연동이 필요하다. Modbus/DNP3-over-IP를 쓰는 기존 사이트는 대개 여기로 간다.
- 현장 장치가 연결을 먼저 연다. MQTT는 원래 이 방식이다. DNP3 outstation도 outstation-initiated TCP로 설정할 수 있다. OPC UA에는 reverse connect가 있어서 server가 client 쪽으로 socket을 열고, transport 계층에서만 역할이 뒤집힌다.
관성을 빼면 두 번째가 모든 면에서 낫다. 인바운드 방화벽 규칙이 없고, 갱신할 고정 IP가 없고, 통신사 망을 향해 열린 포트가 없고, 사이트를 추가해도 전사 방화벽을 건드리지 않는다. Conduit이 아웃바운드 전용이면 IEC 62443 zone 설계도 단순해진다. 2026년에 새 원격 사이트를 사양화하면서 누군가 고정 IP APN에 인바운드 polling을 제안한다면, 공장 LAN처럼 보인다는 것 말고 무엇을 얻는지 물어보는 게 좋다.
유휴 연결은 NAT timeout이 잡아먹는다
Push 구조에는 고유한 실패 모드가 있다. 링크가 조용해지면 통신사 NAT가 매핑을 조용히 버린다. RTU는 session이 살아 있다고 믿고, master도 그렇게 믿는다. 무언가 보내려 하기 전까지는 아무도 모르고, 그때는 TCP 재전송 timeout을 전부 소진한 뒤 — 몇 분 뒤에 — 실패한다.
통신사 NAT의 idle timeout은 제각각이고 보장해주는 사업자도 없다. TCP 매핑은 5~30분 사이 어딘가에서, UDP 매핑은 훨씬 빨리, 종종 1분 안에 만료될 수 있다고 가정하고 그보다 자주 traffic을 흘린다.
함정은 OS 기본값이다. RFC 1122는 TCP keepalive를 구현한다면 기본값이 2시간 미만이면 안 된다고 못박았고, Linux는 그대로 tcp_keepalive_time을 7200초로 출고한다. 이 기본값이 셀룰러 session을 구해주는 일은 없다. Application 계층 keepalive를 설정한다.
- MQTT keepalive: 셀룰러에서는 60~120초가 보통이다. PINGREQ/PINGRESP 쌍의 payload는 4바이트 남짓이지만 header까지 합치면 왕복당 120바이트 정도가 과금된다. 60초 주기면 월 5 MB쯤이다. 예산에 넣어두고 놀라지 않으면 된다.
- DNP3: data link keepalive(link status request) 주기를 가정한 NAT timeout보다 짧게 잡는다.
- OPC UA: subscription keepalive는
PublishingInterval × MaxKeepAliveCount로 결정된다. 제품 기본값이 몇 분씩 나오는 경우가 많으니 직접 확인한다.
그리고 실측한다. 다른 traffic을 끊고 하룻밤 사이트를 유휴 상태로 둔 뒤 첫 write가 언제 실패하는지 본다. 그 측정값이 통신사 상담 창구에서 받은 숫자보다 쓸모 있다.
실제 링크에 맞춘 timeout과 재시도
정상적인 LTE 링크의 왕복 지연은 40~100 ms라서 master의 기본 1000 ms timeout을 그냥 두고 싶어진다. 그러다 교대 시간에 기지국이 붐비거나 모뎀이 LTE-M으로 떨어지면 p99 지연이 초 단위로 올라간다. Master는 timeout을 선언하고 재시도하는데, 그 재시도는 아직 날아가고 있는 원래 요청 뒤에 줄을 서고, 문제를 일으킨 바로 그 부하 아래서 링크는 더 나빠진다.
새 셀룰러 사이트에서 하는 일은 이렇다.
- SCADA 서버에서 RTU로 24시간 ping을 돌리고 평균이 아니라 분포를 기록한다. p99를 기준으로 timeout을 잡고 두 배로 둔다.
- 재시도는 5회가 아니라 2회로 둔다. 셀룰러 링크에서 두 번 실패했다면 500 ms 뒤의 세 번째 시도가 다른 망을 만날 리 없다.
- 재접속에 backoff를 넣는다. 5초, 15초, 60초, 몇 분에서 상한. 광역 장애 중 12개소가 동시에 촘촘한 재접속 루프를 돌면 과금 traffic과 attach 시그널링이 상당히 인상적인 양으로 쌓인다.
- 통신 실패 alarm에는 정상 복구 최악값보다 긴 지연을 준다. 그렇지 않으면 운전원이 그 alarm을 무시하는 법을 배운다.
재시도는 공짜 바이트가 아니다. 이미 300바이트인 poll에 재시도 세 번이 붙으면 나쁜 한 시간의 traffic이 좋은 한 시간의 네 배가 되는데, 나쁜 시간은 하필 모뎀이 재attach를 반복하는 시간이다.
끊겼다 붙었다 하는 링크 위의 TLS
몇 분마다 링크가 끊기고 그때마다 client가 full TLS handshake를 한다면 handshake가 데이터보다 무거워질 수 있다. 인증서 체인이 오가는 상호 인증 full handshake는 양방향으로 수 KB다. Session resumption — ticket이나 TLS 1.3 PSK — 을 쓰면 수백 바이트로 줄어든다. Gateway가 실제로 resumption을 하는지 확인한다. 임베디드 스택 중에는 ticket이 있어도 매번 full handshake를 하는 것이 있다.
같은 이유로 상태가 나쁜 사이트에서 MQTT session expiry를 너무 짧게 두면 안 된다. 끊김 사이에 session이 만료되면 broker가 subscription 상태를 버리고, client는 socket만이 아니라 전부를 다시 세워야 한다.
현장을 떠나기 전 확인할 것
| 확인 항목 | 기준 |
|---|---|
| 설치된 안테나 위치의 RSRP | −100 dBm보다 좋을 것. −110~−120 dBm은 비 오기 전까지만 동작한다. |
| RSRQ / SINR | 기록해 둔다. RSRP는 좋은데 SINR이 나쁘면 거리가 아니라 간섭이고, 안테나를 키워도 소용없다. |
| 안테나 케이블 | 얇은 동축은 1 m마다 신호를 깎는다. 판넬이 있는 곳이 아니라 신호가 있는 곳에 안테나를 단다. |
| 24시간 지연·손실 로그 | 시운전 증빙으로 보관한다. 첫 분쟁 때 필요해진다. |
| Idle timeout 시험 | 하룻밤 무통신을 session이 견디거나, 견딜 때까지 keepalive를 줄인다. |
| 실측 월 사용량 | 라우터 자체 카운터로 확인해 요금제와 비교한다. 청구서를 받고 나서가 아니라 첫 주에 한다. |
| 모뎀 watchdog | 지속적 손실에 재부팅하되 backoff를 두고, 재부팅 횟수를 tag로 내보낸다. |
| 전원 예산 | 태양광 사이트에서는 모뎀 송신 피크가 최악 부하이고, 하필 신호가 나쁠 때 발생한다. |
마지막 watchdog 항목에는 경고가 붙는다. Ping 실패마다 backoff 없이 라우터를 재부팅하는 watchdog은 통신사 장애가 이어지는 내내 재부팅 루프에 갇힌다. 재부팅할 때마다 attach를 새로 하고 handshake를 새로 하며 데이터는 한 건도 못 보낸다. 카운터를 두고, 그 카운터를 내보내고, backoff를 주자.
마지막에 사람들이 걸리는 것
LTE-M이나 NB-IoT 모듈을 쓰는 사이트라면 Power Saving Mode와 eDRX가 켜져 있는지 확인한다. 둘 다 배터리 장치가 몇 분에서 몇 시간씩 자게 하려고 만든 3GPP Release 13 기능이고, 모듈이 PSM에 들어가 있는 동안에는 도달할 수 없다. 망이 페이징을 하지 않으니 poll은 흥미롭게 실패하지도 않고 그냥 timeout이 난다. 상용 전원을 쓰는 RTU라면 PSM은 거의 항상 꺼야 한다. 배터리 사이트라면 켜야 하고, 그러면 구조는 장치가 먼저 접속하고 정해진 시각에 깨는 방식이어야 하며, "지금 poll해봐"라는 요구는 아무도 할 수 없는 일이 된다.