← 전체 글
OPC UA/약 11분 읽기/— 조회

OPC UA Browse에서 태그가 조용히 빠진다면 continuation point를 의심하라

Browse는 성공했는데 태그 일부만 들어온다. Part 4의 continuation point 규칙과 MaxBrowseContinuationPoints로 원인을 잡는다.

OPC UASCADA태그문제 해결

에러는 없었다. 태그만 없었다

게이트웨이에서 태그를 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를 돌려준다.

그래서 이런 클라이언트가 깨진다.

  1. 폴더 A를 Browse한다. 잘려서 continuation point가 온다. 일단 큐에 넣어 둔다.
  2. 폴더 B를 Browse한다. 역시 잘린다. 또 큐에 넣는다.
  3. 폴더 C, D, E … 트리 전체를 breadth-first로 훑는다.
  4. 다 돈 뒤에 큐를 꺼내 BrowseNext를 부른다.
  5. 서버의 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 클라이언트 결과만 보면 안 된다.

  1. 장비 업체 툴로 대상 설비 폴더를 Browse한다.
  2. 노드가 많은 하위 폴더 몇 곳의 child 개수를 센다.
  3. 같은 branch를 SCADA 클라이언트로 import한다.
  4. 폴더별 개수와 NodeId 몇 개를 대조한다.
  5. Server/ServerCapabilities/MaxBrowseContinuationPoints를 읽어서 그대로 적어 둔다.
  6. namespace index가 바뀌는 서버라면 서버 재시작 후 한 번 더 확인한다.

5번은 30초면 끝나는데 나중에 논쟁 하나를 통째로 없애 준다. 그 값이 1이나 2로 나오면 클라이언트를 depth-first로 고치기 전에는 import를 승인하지 마라.

운영팀에 남길 것

운영팀이 Browse trace를 볼 필요는 없다. 태그 갱신을 다시 돌려도 되는지, 정상 결과가 몇 개인지만 알면 된다. 검증된 시작 폴더, 주요 설비 branch별 예상 노드 수, 일부러 제외한 branch, 서버가 보고한 제한값, 성공한 클라이언트 버전, PLC나 게이트웨이 업데이트 후 태그를 갱신하는 절차.

여기서 제일 아까운 실패는 조용한 누락이다. 알람도 없고 exception도 없는데 태그만 없다. 개수를 안 세면 아무도 모른다.