일요일 오전에 제어 담당자가 데이터 블록 구조를 조금 정리해서 PLC를 다운로드했습니다. 월요일 아침, 믹서 라인 페이스플레이트 여섯 개가 점선으로 떠 있습니다. SCADA 서버는 OPC UA 연결 상태를 good으로 표시합니다. 인증서도 신뢰됐고, 세션도 살아 있고, browse 트리에는 Line1/Mixer가 그대로 있습니다. 그런데 히스토리언은 04시부터 평평하고, 운전원은 로컬 판넬로 라인을 돌리고 있습니다.
제가 가장 자주 불려 가는 OPC UA 장애가 이겁니다. 그리고 네트워크 문제인 경우는 거의 없습니다. 세션은 태그를 신경 쓰지 않으니 당연히 정상입니다. 실패는 한 층 아래 CreateMonitoredItems에서 일어나고, 죽은 항목마다 붙어 있는 status code는 보통 Bad_NodeIdUnknown입니다. 요청한 주소가 이제 존재하지 않는다고 서버가 분명히 말해 주는 겁니다.
손대기 전에 항목별 status code부터 읽는다
이 문제로 반나절을 날리는 이유는 사람들이 이걸 통신 장애로 접근하기 때문입니다. 계층을 나누면 10분 안에 답이 나옵니다. 순서는 이렇습니다.
- Endpoint 도달과 인증서 trust → 여기서 막히면 태그가 bad가 아니라 채널 에러가 납니다.
- Session 생성 → 클라이언트가 connected로 보이면 이미 통과한 단계입니다.
Server_NamespaceArray(NodeIdi=2255) 읽기 → namespace table이고, 이 단계에서 뽑아 두는 값이 나중에 가장 요긴합니다.- 설비 폴더 browse →
Line1/Mixer/Speed가 아직 있는지, 지금 NodeId는 무엇인지. CreateMonitoredItems→ 구독이 아니라 항목별 StatusCode를 봅니다.
진단은 5번에서 끝납니다. Bad_NodeIdUnknown은 형식은 맞는데 주소 공간에 해당 노드가 없다는 뜻이고, 곧 identifier가 옮겨졌거나 재생성됐다는 뜻입니다. Bad_NodeIdInvalid는 NodeId 자체가 잘못된 형식이라는 뜻인데, 현장에서는 누가 손으로 타이핑했거나 스크립트가 문자열을 잘못 붙인 경우입니다. 원인도 조치도 다른 두 문제인데, 대부분의 클라이언트는 둘 다 HMI 위의 회색 사각형 하나로 뭉개 버립니다.
namespace index는 식별자가 아니다
OPC UA Part 3에서 NodeId는 두 부분입니다. namespace index와 identifier이고, identifier는 Numeric, String, GUID, Opaque 중 하나입니다. 태그 설정에 ns=4;s=Line1.Mixer.Speed를 적었다면, 여러분이 적어 둔 것은 테이블의 몇 번째 자리 하나와 이름 하나입니다.
Index 0은 항상 OPC UA namespace(http://opcfoundation.org/UA/)입니다. Index 1은 관례적으로 서버 자신의 application URI입니다. 2번부터는 그 서버가 그 부팅에서 namespace를 만든 순서일 뿐입니다. 내일도 4번이 같은 것을 가리킨다고 규격이 보장하지 않습니다. 게이트웨이에 companion spec namespace를 하나 추가하거나, 펌웨어 업데이트로 디바이스 모델 두 개의 로드 순서가 바뀌면, 태그 DB에 있는 모든 ns= 값이 한 칸씩 밀립니다.
안정적인 쪽은 URI입니다. 그래서 인수인계 때 제가 고집하는 규칙은 하나입니다. 인터페이스 문서에는 URI를 적고, index는 적지 않는다. index는 "FAT 당시 관측값, 변경될 수 있음"으로만 남깁니다.
서버가 NamespaceMetadataType 객체로 NamespaceVersion과 NamespacePublicationDate를 노출하는 경우도 있습니다. 그렇다면 교체 전 export에 꼭 포함하세요. publication date가 바뀐 것을 보는 게 browse name 2천 개를 비교하는 것보다 훨씬 빠릅니다.
identifier가 움직이는 세 가지 경로, HMI에서는 똑같이 보인다
심볼에서 파생된 string NodeId. PLC 내장 서버는 대부분 프로그램의 심볼 경로로 identifier를 만듭니다. Siemens S7-1500은 ns=3;s="MixerData"."Speed" 같은 형태로, KEPServerEX 계열 게이트웨이는 ns=2;s=Channel1.Device1.Speed 형태로 내보냅니다. 데이터 블록 이름을 바꾸거나, 채널 이름을 바꾸거나, 변수를 중첩 struct 안으로 옮기면 NodeId가 바뀝니다. 원래부터 프로그램과 독립이 아니었으니까요. 일요일 다운로드 사례가 여기에 해당합니다.
재생성된 numeric NodeId. 주소 공간을 만들 때 숫자를 부여하는 서버는 모델을 다시 만들거나 프로젝트를 재import하면 다른 숫자를 아무렇지 않게 부여합니다. ns=2;i=6104는 틀린 값이 아닙니다. 다만 지금은 다른 변수의 것입니다. 이게 가장 위험합니다. 삭제가 아니라 재배정된 numeric NodeId는 품질이 good으로 읽히면서 엉뚱한 값을 돌려줍니다. bad도 없고 알람도 없고, 현장과 맞지 않는 유량 값만 화면에 떠 있습니다.
update 대신 import. 기존 인스턴스를 갱신하는 대신 새 인스턴스로 프로젝트를 import하면 주소 공간 전체가 다시 만들어집니다. 태그도 폴더도 그대로인데, 모든 것의 신원이 새로 발급됩니다.
증상은 겹쳐서 구분에 쓸 수 없습니다. 값이 멈춘 것처럼 보이거나, bad로 떨어지거나, 옆에 있는 동일 skid는 정상인데 한 대만 죽습니다. 특정 namespace만 재생성된 경우가 그렇습니다. 증상이 아니라 NodeId를 보세요.
서버가 지원하면 인터페이스를 고정한다
서버가 지원한다면, 노출하는 주소와 프로그램 작성 방식을 분리하세요. S7-1500 펌웨어는 server interface를 정의해서 내부 DB 구조를 리팩터링해도 공개되는 browse name과 NodeId는 그대로 유지할 수 있습니다. 대부분의 OPC UA 게이트웨이에도 alias나 노출 태그 계층에 해당하는 기능이 있습니다. SCADA에 물리는 신호는 그 기능을 쓰고, 그 인터페이스를 Modbus 레지스터 맵과 똑같이 변경 관리 대상으로 취급하세요.
살아남는 NodeId의 조건을, 대략 중요한 순서로 정리하면 이렇습니다.
| 조건 | 이유 |
|---|---|
| compile/download와 무관 | 아니면 프로그램 변경마다 SCADA 변경이 따라옵니다 |
| 임시·테스트 프로젝트명 없음 | ns=2;s=FAT_Rig.Mixer.Speed는 현장까지 따라옵니다 |
| 반복 skid 간 유일성 | 여기가 애매하면 write가 다른 설비로 들어갑니다 |
| 표시 라벨 수정에 영향 없음 | 설명문 번역이 히스토리언을 깨면 안 됩니다 |
| 텍스트로 diff 가능 | 절체 당일 밤에 diff old.csv new.csv가 필요합니다 |
읽기 쉬운 string NodeId가 numeric보다 무조건 낫지는 않습니다. string identifier는 전송량이 크고, 서버에 따라 resolve가 더 느립니다. 나은 이유는 하나, 검토할 수 있다는 것이고 그건 변경 작업 때만 의미가 있습니다. 서버가 자동 생성 identifier만 지원한다면, 확인할 것은 그 서버의 백업·복원이 실제로 identifier를 보존하는지입니다. 필요해지기 전에 시험해 두세요.
Registered NodeId는 세션 한정이다 — 저장하면 안 된다
이건 특히 자체 제작 클라이언트와 스크립트를 물어뜯습니다. RegisterNodes는 반복해서 읽을 NodeId 목록을 서버에 미리 알려 주고, 서버가 더 빨리 resolve할 수 있는 핸들을 받아 오는 서비스입니다. 그 핸들은 그 핸들을 만든 세션 안에서만 유효하고, Part 4에 명시되어 있습니다. 이걸 디스크에 저장하거나 재접속 후에 재사용하는 클라이언트는 2분 전까지 정상이던 태그에서 Bad_NodeIdUnknown을 받습니다. 그리고 서버 쪽 주소 변경과 증상이 똑같이 보입니다.
서비스 재시작하면 풀리는 간헐적 버전의 이 증상을 쫓고 있다면, PLC 담당자를 탓하기 전에 registered node 캐싱을 먼저 확인하세요.
browse path는 복구 수단이지 보험이 아니다
TranslateBrowsePathsToNodeIds는 /Line1/Mixer/Speed 같은 경로를 지금 그 자리에 있는 NodeId로 다시 풀어 줍니다. 실패 시 이걸 시도하는 클라이언트는 이런 사고를 알아서 넘기는 경우가 많고, 돈 주고 살 만한 기능입니다.
다만 인터페이스 관리를 대체하지는 못합니다. browse name도 NodeId와 같은 이유로 움직입니다. 반복 설비에서는 경로가 애매할 수 있고, 폴더 이름을 로케일에 따라 번역해 주는 벤더도 있어서, 클라이언트가 영어로 저장해 둔 경로가 한국어 로케일 서버에서 풀리지 않을 수 있습니다.
클라이언트를 아직 바꿀 수 있는 FAT 단계에서 답을 받아 둘 질문들입니다.
- namespace URI를 기준으로 다시 붙는가, index를 저장하는가?
Bad_NodeIdUnknown에서 자동으로 rebrowse하는가, 사람을 기다리는가?- 주소 캐시를 지우려면 서비스 재시작이 필요한가? (보통 필요합니다. 정지 시간에 포함하세요.)
- 다시 붙은 뒤 sampling interval과 deadband 설정이 유지되는가?
- old → new NodeId 쌍을 로그로 남기는가? 그 로그가 없으면 무엇이 조용히 remap됐는지 감사할 수 없습니다.
같이 확인할 것이 하나 더 있습니다. 서버가 주소 공간이 바뀔 때 GeneralModelChangeEventType을 올려 주는지 보세요. 구현한 서버가 매우 적고 그걸 구독하는 클라이언트는 더 적습니다. 이 부류의 장애를 소프트웨어가 아니라 운전원이 먼저 발견하는 이유가 그것입니다.
절체: 나중에 있었으면 하는 export
SCADA에 물린 서버를 건드리기 전에 다섯 가지만 뽑아 두세요. 20분이면 되고, 밤새 추측하는 작업이 diff 한 번으로 바뀝니다.
- URI가 포함된 namespace array, 노출된다면
NamespaceVersion까지. - 클라이언트 전체 태그 목록과 NodeId, 텍스트 형식으로.
- 중요 설비 폴더의 browse export.
- 어떤 태그가 write인지, 알람 원천인지, 히스토리언 수집 대상인지 표시한 목록.
- 서버 제품명과 펌웨어/빌드 버전.
변경 후에는 HMI 전체를 훑지 마세요. 영향받은 namespace마다 아날로그 하나, 디지털 상태 하나, 알람 원천 하나, 명령 write 하나, 히스토리언 핵심 값 하나를 봅니다. 클라이언트의 서로 다른 처리 경로 다섯 개를 건드리는 다섯 지점입니다.
그리고 반복 설비는 Line1이 아니라 마지막 인스턴스를 시험하세요. 템플릿, 배열, 자동 생성 인스턴스의 off-by-one은 거의 항상 범위 끝에서 드러납니다.
절반만 고치면 남는 위험
급할 때 실제로 적용되는 조치는 화면 태그 remap입니다. 운전원이 그걸 얘기하니까요. 그리고 라인이 돌기 시작하면 다들 퇴근하고, 다음이 남습니다.
- 알람 정의는 여전히 old NodeId를 보고 있고, 그 알람들은 이제 영원히 발생하지 않습니다.
- 히스토리언 collector는 별도 태그 DB를 쓰는데 remap 범위에 없었습니다.
- 이중화 대기 노드는 예전 캐시를 들고 있고, 절체되면 죽은 화면으로 넘어갑니다.
- 리포트 스크립트 문자열 안에
ns=2가 하드코딩되어 있습니다. - 반복 skid의 다른 인스턴스로 write가 들어가고 있습니다. 이 목록에서 가장 비싸고, 증상이 전혀 없는 유일한 항목입니다.
내일이 아니라 오늘 밤에 다시 볼 것은 알람과 히스토리언입니다. 눈에 보이게 깨진 화면은 결국 고쳐집니다. 조용히 사라진 알람은 고쳐지지 않고, 그 알람이 잡아야 했던 사건이 터질 때 알게 됩니다.