소켓은 붙었는데 응답이 틀릴 수 있다
Modbus TCP는 겉으로 보면 단순하다. 502번 포트에 붙고, 요청을 보내고, 응답을 읽는다. 하지만 현장에서 까다로운 문제는 소켓 연결이 아니다. 드라이버가 받은 응답이 방금 보낸 요청의 응답인지 증명하는 일이다.
그 증명은 딱 2바이트에 들어 있다. 모든 Modbus TCP PDU 앞에 붙는 7바이트 MBAP header는 offset 0–1, big-endian의 transaction identifier로 시작한다. 값은 client가 정하고, server는 그 값을 바꾸지 않고 그대로 되돌려줘야 한다. MODBUS Messaging on TCP/IP Implementation Guide v1.0b는 이 점을 명확히 적어 둔다. 한 connection에 여러 요청이 outstanding 상태일 때 client가 짝을 맞출 수 있도록, server는 request의 transaction identifier를 response에 그대로 "recopy" 한다.
정상 장비는 이 규칙을 지킨다. 그런데 지키지 않는 장비도 있다. 오래된 PLC Ethernet module, 임베디드 serial-to-TCP 게이트웨이, 값싼 프로토콜 컨버터, 벤치 테스트용 simulator는 요청이 항상 하나씩만 온다고 가정한다. Client 하나를 느리게 붙이면 완벽해 보인다. 부하를 주는 순간 transaction ID는 아무 의미가 없어진다.
이때 증상은 꽤 이상하게 보인다.
- Trend 값이 순간적으로 옆 register block 값처럼 보였다가 다음 scan에서 돌아온다.
- 같은 장비인데 한 polling group은 timeout이고 다른 group은 정상이다.
- Packet capture에는 response가 들어오는데 driver는 mismatch나 stale data를 낸다.
- Tag를 추가하거나, HMI를 하나 더 붙이거나, 장비를 더 빠른 scan class로 옮긴 뒤부터 문제가 나온다.
"502번 포트 열림"은 진단의 시작이지 끝이 아니다.
Register 값 말고 MBAP header를 capture한다
대부분은 decode된 register 값만 잡고 끝낸다. 값은 제일 나중에 봐야 할 것이다. 값이 틀렸을 때쯤이면 증거는 이미 header에 다 있다. 요청/응답 쌍마다 아래 필드를 뽑는다.
| 항목 | 보는 이유 |
|---|---|
| Transaction ID | 어떤 request의 response인지 확인 |
| Unit ID | TCP endpoint 뒤 gateway target 또는 논리 slave 확인 |
| Function code | FC 03/04 read, FC 06/16 write, diagnostic, 0x80+ exception 구분 |
| 시작 주소 + 개수 | Block optimization이 요청 형태를 바꿨는지 확인 |
| TCP stream | 어느 client socket의 통신인지 확인 |
| Response time | Timeout 되기 전에 느린 장비를 찾기 위함 |
판단 기준은 간단하다. Response의 transaction ID가 request와 다르면 정상 client는 버린다. 약한 server는 세 가지로 정체를 드러낸다. 항상 0을 돌려주거나, 이전 교신의 오래된 ID를 반복하거나, socket 두 개 사이에서 응답을 섞는다. 제일 위험한 건 마지막이다. 돌아온 register 값이 진짜 데이터이기 때문이다. 단지 다른 read의 값일 뿐이다. Driver는 이걸 의심할 근거가 없어서 good quality로 화면에 올린다.
Wireshark는 MBAP 필드를 바로 decode한다. mbtcp로 filter하고 mbtcp.trans_id를 column으로 추가하면 된다. 전체 교대 근무 capture보다 장애가 담긴 10초짜리가 낫다. 문제가 들어 있는 짧은 capture가 아무도 안 여는 1GB짜리보다 쓸모 있다.
Driver가 요청을 pipeline 하는가?
이 설정이 transaction ID가 문제가 되느냐 마느냐를 결정한다. 요청 하나 보내고 응답을 기다린 뒤 다음을 보내는 driver는 ID가 동시에 둘 이상 떠 있을 일이 없다. 그래서 망가진 server도 섞을 기회를 못 얻는다. 처리량을 올리려고 한 connection에 여러 요청을 먼저 던지는 driver는 그 약점을 바로 드러낸다.
Pipelining 자체는 정당한 Modbus TCP다. 단, server가 각 transaction을 실제로 추적할 때만 안전하고, 많은 gateway는 그러지 못한다. 아직 못 믿는 장비에 붙일 때는 이렇게 한다.
- Driver 설정에서 maximum outstanding request, concurrent transaction, parallel poll 항목을 찾는다.
- 장비(또는 socket)당 outstanding request를 1로 고정하고 증상이 사라지는지 확인한다.
- 장비 manual이 지원을 명시하고 동시에 capture로 ID가 계속 맞는 게 확인될 때만 늘린다.
- Block optimization을 켠 뒤 다시 시험한다. 요청 크기와 타이밍이 둘 다 바뀐다.
- 운영에 들어갈 VPN, firewall, cellular link 경로에서도 다시 본다. 벤치에서 안 보이던 reordering과 지연이 생긴다.
함정 하나. 느린 RS-485 구간을 앞단에서 감싸는 gateway라면, TCP 쪽 pipelining은 gateway 내부 queue만 키운다. 아래쪽 9600 baud serial 구간이 여전히 병목이다. Outstanding request만 늘고 처리량은 그대로인데 latency만 나빠진다.
Client가 늘면 server가 약해진다
SCADA server 하나에는 멀쩡하던 장비가, engineering tool·redundant server·historian·local HMI가 한꺼번에 붙는 순간 무너질 수 있다. Implementation Guide는 동시 connection 수를 구현체 재량으로 두는데, 값싼 gateway는 그 수를 낮게 잡는다. 깨끗한 session 하나만 지원하면서 나머지 socket은 조용히 받아 놓고 쓰레기를 돌려주기도 한다.
배제해 볼 패턴들.
- 각 client는 단독으로 정상인데 합치면 gateway serial side가 포화된다.
- Vendor tool이 commissioning 때부터 diagnostic poll을 돌리고 있는데 아무도 안 껐다.
- Redundant SCADA node 둘이 active/standby가 아니라 둘 다 active polling을 한다.
- NAT나 firewall이 idle socket을 끊어서 client가 reconnect를 반복하고, gateway는 옛 session을 회수하지 못한다.
장비를 탓하기 전에 client 목록을 만든다. 502번 포트에 붙는 모든 IP에 대해 polling interval, 쓰는 function code, write 가능 여부를 적는다. 502번 포트에 붙은 모르는 client는 설명이 될 때까지 용의선상에 남긴다.
Timeout은 응답 시간을 재고 정한다
다른 현장 값을 복사해 온 timeout은 진단을 흐린다. 너무 짧으면 느리지만 정상인 response를 버리고, 너무 길면 죽은 장비가 scan group을 몇 초씩 잡는 동안 운전자는 stale data를 본다.
링크가 실제로 겪을 부하에서 측정한다. 기준이 되는 정상 생산 scan, 주요 화면을 전부 연 상태, outage 후 catch-up 중인 historian, serial gateway 뒤 가장 느린 slave, 그리고 network를 강제로 끊어 reconnect 후 첫 정상 response 시간까지. 그런 다음 timeout은 측정한 최악의 정상 응답보다 길게, 하지만 bad quality가 운전자에게 빨리 닿을 만큼 짧게 둔다. Retry도 같은 논리다. 패킷 하나 빠졌다고 알람을 내면 안 되지만, 2초 timeout에 retry 3번이면 죽은 장비를 6초 동안 숨긴다.
외워 둘 만한 고장 형태
| 증상 | 가능성 높은 원인 | 확인 방법 |
|---|---|---|
| 특정 block 값이 가끔 엉뚱함 | 잘못된 request의 response를 받아들임 | Capture에서 transaction ID와 address 비교 |
| Scan rate를 올리면 timeout 증가 | 장비/gateway가 concurrent request를 못 처리 | Outstanding request를 1로 줄이고 polling을 늦춤 |
| 두 번째 client 접속 뒤 오류 발생 | Server connection limit 또는 serial side 과부하 | Engineering tool·redundant client를 하나씩 끊어 봄 |
| Reconnect 직후 첫 값이 이상함 | Gateway가 오래된 buffered response를 돌려줌 | Socket을 비우고 재사용을 끄고 첫 transaction ID 확인 |
| Write는 되는데 read만 timeout | Read block이 가벼운 write 경로보다 무거움 | Read stream과 write stream을 따로 capture |
모든 timeout을 올려서 알람이 멈출 때까지 밀어붙이는 건 여기서 정확히 반대 수다. 알람은 조용해지고 틀린 값은 화면에 남는다. 고장을 고친 게 아니라 숨긴 것이다.
손대는 장비마다 두 줄짜리 통신 note를 남긴다. IP·port·unit ID 규칙, 실제로 견디는 client 수, concurrent transaction이 안전한지 여부, 최종적으로 정한 timeout과 retry 값, baseline capture 파일명. 다음 프로젝트가 historian이나 MES connector를 얹을 때 PLC logic은 하나도 안 건드려도 된다. 그래도 장비가 받는 Modbus transaction 부하를 바꾸는 것만으로 통신은 깨질 수 있다.