에러는 없었다. 태그만 없었다
게이트웨이에서 태그를 import했다. 로그는 깨끗하고 exception도 없다. 그런데 챔버 3번부터 뒤로 태그가 하나도 안 들어왔다. 앞쪽 설비는 멀쩡하다.
권한 문제처럼 보인다. 아니다.
Browse는 한 번에 끝나는 서비스가 아니다. OPC UA Part 4 v1.05 §5.9.2(Browse)에서 서버가 시작 노드의 reference를 한 응답에 다 담지 못하면 결과에 continuationPoint를 같이 돌려준다. 나머지는 §5.9.3의 BrowseNext로 가져가라는 뜻이다. 클라이언트가 이걸 안 부르면 import는 "성공"으로 끝난다. 조용히, 일부만.
개수를 자르는 값이 무엇인지부터 정확히 알자
여기서 이름을 헷갈리면 엉뚱한 설정을 찾게 된다. 서로 다른 세 값이다.
requestedMaxReferencesPerNode — Browse 요청 파라미터다. 시작 노드 하나당 돌려받을 reference 최대 개수다. Part 4 §5.9.2는 값 0을 "클라이언트가 제한을 두지 않는다"로 정의한다. 클라이언트가 0을 보내도 서버는 자기 한계로 여전히 자를 수 있다. 0은 "전부 준다"는 보장이 아니다.
MaxNodesPerBrowse — Part 5 v1.05 §6.3.11 OperationLimitsType의 optional property(UInt32)다. reference 개수와 관계없다. Browse 호출의 nodesToBrowse 배열 크기, 그리고 BrowseNext 호출의 continuationPoints 배열 크기를 제한한다. 한 번에 폴더 500개를 밀어 넣는 클라이언트가 걸리는 값이지, 폴더 하나가 잘리는 원인이 아니다.
MaxBrowseContinuationPoints — Part 5 v1.05 §6.3.2 ServerCapabilitiesType의 mandatory property(UInt16)다. session당 동시에 열어 둘 수 있는 Browse continuation point 개수다. 0이면 서버가 제한하지 않는다는 뜻이다.
시운전에서 실제로 문제를 일으키는 건 세 번째다. 그런데 대부분 첫 번째만 본다.
Continuation point는 timeout으로 죽지 않는다
이 글에서 하나만 가져간다면 이것이다.
Part 4 v1.05 §7.9는 이렇게 규정한다. Continuation point는 클라이언트가 남은 결과를 다 받거나, 클라이언트가 직접 해제하거나, session이 닫힐 때까지 살아 있다. 유효 시간 같은 건 없다.
같은 절에 이어지는 문장이 진짜 원인이다. 서버는 같은 session의 새 요청을 처리하는 데 필요하면 그 session의 이전 요청에서 만든 continuation point를 자동으로 해제해야 한다. 그리고 이미 해제된 continuation point를 클라이언트가 다시 쓰면 Bad_ContinuationPointInvalid를 돌려준다.
그래서 이런 클라이언트가 깨진다.
- 폴더 A를 Browse한다. 잘려서 continuation point가 온다. 일단 큐에 넣어 둔다.
- 폴더 B를 Browse한다. 역시 잘린다. 또 큐에 넣는다.
- 폴더 C, D, E … 트리 전체를 breadth-first로 훑는다.
- 다 돈 뒤에 큐를 꺼내
BrowseNext를 부른다. - 서버의
MaxBrowseContinuationPoints가 2였다. A와 B는 벌써 밀려서 해제됐다.Bad_ContinuationPointInvalid.
"오래 놔뒀더니 만료됐다"가 아니다. 우리가 새 Browse를 계속 보내서 우리 것을 밀어낸 것이다. 세 시간짜리 import든 3초짜리든 똑같이 난다. 실제로 로컬에서는 되고 VPN에서 깨지는 이유도 timeout이 아니라 큐 깊이가 달라져서인 경우가 많다.
고치는 방법은 설정이 아니라 순서다. 폴더 하나를 끝까지 비운 다음 다음 폴더로 넘어간다. continuation point가 사라질 때까지 BrowseNext를 돌리고, 그때 재귀한다. depth-first로 하면 열려 있는 continuation point가 항상 한 개다.
중간에 import를 취소한다면 BrowseNext를 releaseContinuationPoints = TRUE로 호출해서 정리한다. 결과는 안 받고 서버 자원만 풀어 준다. 안 해도 session 닫을 때 풀리기는 하지만, session을 오래 붙잡는 SCADA 클라이언트에서는 다음 import가 남은 자리를 못 찾는다.
빈 결과에 continuation point가 붙어 올 수 있다
Part 4 §5.9.2에는 잘 안 읽히는 예외가 하나 있다. 노드를 다 처리하는 데 클라이언트의 timeout hint보다 오래 걸릴 것 같으면, 서버는 결과 0개에 continuation point만 붙여서 먼저 돌려줄 수 있다.
references.length == 0을 "이 폴더는 비었다"로 해석하는 클라이언트는 여기서 멀쩡한 설비 폴더를 통째로 빈 폴더로 기록한다. 끝났는지 여부는 개수가 아니라 continuation point의 유무로 판단해야 한다.
Bad_NoContinuationPoints도 오해가 많다. 이건 이미 시작한 작업을 이어받다가 나는 에러가 아니다. §7.9는 서버가 이미 중단된 작업을 이어갈 때는 이 코드를 절대 돌려주면 안 된다고 못 박는다. 한 요청에 노드를 여러 개 넣었을 때, 서버가 continuation point를 다 쓴 뒤 남은 노드들에 붙는 코드다. 이게 보인다면 한 Browse에 노드를 너무 많이 넣고 있다는 신호다.
전체 트리를 무작정 긁지 않는다
큰 주소 공간을 매번 통째로 Browse하면 서버와 클라이언트 둘 다 힘들다. HMI에서 쓰지도 않을 type 정의, 진단 노드, method까지 끌고 와 화면 태그 목록을 더럽힌다.
시작점을 좁히는 편이 낫다. Objects 루트 대신 장비 업체가 지정한 설비 폴더에서 시작한다. 공정 태그 import라면 nodeClassMask로 Variable만 거른다. method, type, 진단 branch는 필요할 때 따로 본다. 이력, 레시피, 이벤트 모델은 일반 태그 import와 분리한다.
데이터를 숨기자는 게 아니다. FAT에서 한 번, SAT에서 또 한 번, 서버를 죽이지 않고 같은 결과가 나오는 절차를 만들자는 것이다.
DisplayName만 믿고 태그를 만들지 않는다
큰 주소 공간에는 같은 이름이 반복된다. Status, Mode, Start, Fault, Temperature는 설비마다 계속 나온다.
import 때 같이 저장해야 할 것: namespace URI와 import 당시 namespace index, NodeId, BrowseName, DisplayName, 전체 browse path, data type과 engineering unit, 서버 이름과 endpoint.
namespace URI를 꼭 넣어라. namespace index는 서버 재시작이나 펌웨어 업데이트로 바뀔 수 있고, index만 저장한 태그는 그때 조용히 다른 노드를 가리킨다. URI가 있으면 NamespaceArray를 다시 읽어 index를 재계산할 수 있다.
DisplayName은 화면 라벨로는 좋다. 태그 식별 키로는 약하다.
인수 전에 개수를 대조한다
태그 import를 승인하기 전에 노드가 많은 폴더 몇 개를 골라 비교한다. SCADA 클라이언트 결과만 보면 안 된다.
- 장비 업체 툴로 대상 설비 폴더를 Browse한다.
- 노드가 많은 하위 폴더 몇 곳의 child 개수를 센다.
- 같은 branch를 SCADA 클라이언트로 import한다.
- 폴더별 개수와 NodeId 몇 개를 대조한다.
Server/ServerCapabilities/MaxBrowseContinuationPoints를 읽어서 그대로 적어 둔다.- namespace index가 바뀌는 서버라면 서버 재시작 후 한 번 더 확인한다.
5번은 30초면 끝나는데 나중에 논쟁 하나를 통째로 없애 준다. 그 값이 1이나 2로 나오면 클라이언트를 depth-first로 고치기 전에는 import를 승인하지 마라.
운영팀에 남길 것
운영팀이 Browse trace를 볼 필요는 없다. 태그 갱신을 다시 돌려도 되는지, 정상 결과가 몇 개인지만 알면 된다. 검증된 시작 폴더, 주요 설비 branch별 예상 노드 수, 일부러 제외한 branch, 서버가 보고한 제한값, 성공한 클라이언트 버전, PLC나 게이트웨이 업데이트 후 태그를 갱신하는 절차.
여기서 제일 아까운 실패는 조용한 누락이다. 알람도 없고 exception도 없는데 태그만 없다. 개수를 안 세면 아무도 모른다.