녹색 아이콘 두 개는 아무것도 증명하지 않는다
시운전 walkdown에서 서버 두 대가 모두 online으로 보여서 사인한 이중화 쌍이 여럿이었는데, 한 달 뒤에 다시 불려간 적도 있다. HMI는 세 시간 전 유량값을 붙잡고 있고 standby가 조용히 공정을 돌리고 있었다. 그동안 두 서버 다 살아 있었고, 아무도 케이블을 뽑은 적이 없었다.
이중화 시운전의 함정이 이거다. 정상 상태 그림은 만들기 쉽고, 그 그림이 사람을 속인다. 실제로 증명해야 하는 건 한 서버가 응답을 멈춘 뒤 15초 동안 벌어지는 일이다. Client가 다른 서버를 찾는지, subscription이 진짜 timestamp로 돌아오는지, 운전자가 "현재값"과 "멈춘 값"을 구분할 수 있는지. 이 중 어느 것도 상태 아이콘에는 나오지 않는다.
지금 내가 돌리는 절차와, 각 단계가 왜 거기 있는지 정리한다.
먼저 실제 이중화 방식을 적는다
OPC UA Part 4는 이중화를 정보 모델의 Server.ServerRedundancy.RedundancySupport로 정의하고, 이 값에 따라 시험 대상이 달라진다. enum(RedundancySupportType)은 None, Cold, Warm, Hot, Transparent, HotAndMirrored(마지막은 1.04에 추가)다. 시운전에서 중요한 건 누가 failover를 하느냐다.
- Transparent — 서버 쌍이 virtual endpoint/IP를 옮기고 client는 아무것도 못 느끼도록 되어 있다. 시험 대상은 네트워크 계층이다. ARP/DNS 갱신 시간, 그리고 이동 후 제시되는 인증서가 여전히 유효한지.
- Non-transparent(Cold/Warm/Hot) — failover는 client가 한다. Client가
ServerUriArray의 각 서버에서Server.ServiceLevel(Byte, NodeIdi=2267)을 읽어 가장 건강한 쪽에 붙는다. 여기서 시험 대상은 서버가 아니라 client의 로직이다.
RedundancySupport는 datasheet를 믿지 말고 범용 client(UaExpert, opcua-client)로 직접 읽어라. "Hot"이라 광고해 놓고 Warm을 보고하는 제품을 봤는데, 그러면 standby가 subscription 상태를 추적하지 않아서 매번 전체 resubscribe가 된다. 들어간 김에 두 노드의 endpoint URL, hostname, IP, SecurityPolicy, MessageSecurityMode, 인증서 thumbprint를 다 기록한다. 벤더 화면의 "primary/secondary" 표시는 네트워크 사실이 아니다.
트렌드로 봐야 할 숫자는 ServiceLevel이다
Non-transparent 쌍에서는 ServiceLevel이 전부다. 0–255 byte이고 값이 클수록 "데이터를 더 잘 낼 수 있다"는 뜻이다. 제대로 만든 client는 이 값을 지켜보다가 active 서버 값이 standby보다 낮아지면 넘어간다. 유용한 시험은 "failover가 되느냐"가 아니라 무엇이 ServiceLevel을 움직이느냐다.
두 서버의 ServiceLevel을 트렌드에 올린 다음, 하드 stop보다 약한 방식으로 active를 degrade시켜 본다. 상위 PLC 링크를 끊거나, session table을 채우거나, historian feed를 막는 식이다. 자기 소스 데이터가 죽었는데도 계속 255를 보고하는 서버는 client failover를 절대 못 일으키고, 네트워크 시험을 아무리 해도 이건 안 잡힌다. 벤더가 ServiceLevel 계산을 설정할 수 있게 해 뒀다면, 그게 실제 무언가에 연결돼 있는지 여기서 확인한다.
Subscription으로 시험한다, 운영 client가 그걸 쓰니까
Browse와 read 시험은 거의 아무 데서나 통과한다. SCADA driver, HMI, historian collector는 browse하지 않는다. subscription을 잡고, subscription에는 failover가 조용히 날려버릴 수 있는 상태가 있다.
Subscription이 끊김을 견디는지는 두 숫자가 결정한다. publishing interval과 lifetime count다. 서버는 LifetimeCount × PublishingInterval 동안 성공한 Publish가 없으면 subscription을 삭제한다. 즉 publishing interval 250 ms에 lifetime count 60이면 15초 창이 나오고, 그보다 느리게 reconnect하면 subscription은 서버 쪽에서 monitored item까지 통째로 사라진다. 요청값 말고 client가 실제로 협상한 revised 값을 확인하라. 서버가 clamp한다.
Failover 순간에는 네 점을 같이 트렌드로 본다. 빠르게 변하는 test tag, 느린 analog, alarm/event source, 서버 쪽 heartbeat. 그리고 확인한다.
- monitored value가 몇 초 멈추는지,
- 끊김 동안 client가 quality를 Bad로 바꾸는지 아니면 마지막 good 값을 붙잡는지,
TransferSubscriptions를 시도라도 하는지(non-transparent 쌍에서는 서버 간 이전이 거의 안 되므로 client는 subscription을 처음부터 다시 만들어야 한다),- event subscription이 아예 복구되는지 — client가 잊는 게 바로 이거다. 데이터는 다시 subscribe되는데 alarm은 안 되고, 운전자에게 이상을 알렸을 CommunicationFault 이벤트를 잃는다.
정적인 태그 하나가 "복구"되는 건 아무것도 증명하지 않는다. 변한 게 없으니 복구할 것도 없었던 거다.
아픈 건 stale 데이터지 빨간 X가 아니다
정직한 disconnect는 좋은 결과다. 운전자가 보고 사람을 부른다. 비싼 건 HMI가 세 시간 된 값을 good quality로 붙잡고 있고, alarm logic이 그걸로 계속 판단하고, historian이 매 scan마다 같은 숫자를 착실히 기록하는 경우다.
그래서 서버 쪽 heartbeat가 필수다. 서버가 1초마다 counter tag를 올리게 한다. quality가 Good인데 그 숫자가 멈춘다면, 돈이 드는 고장 형태를 찾은 것이고, 그걸 사고 중이 아니라 벤치에서 찾은 것이다. 특히 quality 세탁 경로를 본다. 원본 OPC UA item은 Bad인데 그 위에 만든 derived HMI/계산 tag가 마지막 계산값을 유지하며 Good을 보이는 경우. 그 derived tag가 운전자에게 거짓말을 하고 있고, heartbeat 없는 시험은 전부 통과시킨다.
깔끔한 service stop 말고 거친 방식으로 깨뜨린다
우아한 Stop-Service는 한 가지 경우이고 가장 점잖은 경우다. TCP가 깨끗한 RST를 받고 대부분 stack이 빨리 reconnect한다. 실제 장애는 RST를 안 보낸다.
| 하는 것 | client가 보는 것 | 왜 다른가 |
|---|---|---|
| OPC UA service 정지 | TCP RST | 빠르고 깨끗함 — 쉬운 경우 |
| Active node reboot | RST 후 죽은 포트 | standby 기동 race 추가 |
| 케이블/switch port 차단 | 아무것도 없음 — 조용한 timeout | session timeout까지 기다림, 가장 느린 복구 |
| Firewall로 endpoint port 차단 | 응답 없는 SYN | 죽은 게 아니라 멈춘 서버처럼 보임 |
| Active endpoint DNS 차단 | reconnect 시 resolution 실패 | client가 IP를 캐시했는지 드러남 |
버그를 찾아내는 건 케이블 뽑기다. RST가 없으니 client는 자기 session timeout이 만료돼야 서버가 사라진 걸 안다. RevisedSessionTimeout이 120초로 돌아왔다면, standby가 아무리 빨라도 그게 최악의 감지 시간이다. 여기서 합격 숫자를 정한다. "X초 안에 active 서버 장애 감지, Y초 안에 standby에서 값 갱신." "failover 정상"은 합격 기준이 아니다.
인증서, 조용한 failover 킬러
인증서 문제는 서버를 넘기는 바로 그 순간까지 숨어 있기를 좋아한다. Primary는 trusted고 다 괜찮은데, standby가 client가 본 적 없는 인증서를 내밀거나, 맞는 인증서인데 안에 이름이 틀린다.
사람들이 걸리는 하나: OPC UA에서 인증서의 ApplicationUri(SAN의 uniformResourceIdentifier 항목)는 서버 ApplicationDescription의 applicationUri와 일치해야 한다. Standby를 복제 config에서 만들었는데 자기 인증서를 따로 발급했다면 둘이 안 맞을 수 있고, client는 실제로 붙으려는 순간에야 BadCertificateUriInvalid로 거부한다. 두 노드에서 목록을 훑는다.
- 두 server cert가 client trust list에 있다,
- client cert를 두 서버가 모두 trust한다,
- 제품이 검사한다면 endpoint hostname이 cert SAN과 맞다,
- 각 cert의
ApplicationUri가 그 서버의 ApplicationDescription과 맞다, - 두 노드의 유효 기간이 현재이고, 시계가 맞다 — 몇 분 빠른 노드는 멀쩡한 인증서를
not yet valid로 보이게 한다, - 격리된 제어망에서 각 stack이 CRL/revocation 검사를 어떻게 처리하는지 안다(라우팅도 안 되는 CRL endpoint에 닿으려고 몇 초씩 block하는 stack이 많다).
Virtual hostname을 쓴다면 cert에 virtual name이 있는지, 물리 이름이 있는지, 둘 다인지 확인하고, client가 reconnect 때 어떤 이름을 쓰는지 적어 둔다.
새벽 3시에 읽을 무언가를 현장에 남긴다
시운전 기록은 너를 위한 게 아니라, 밤에 첫 실제 failover 때 HMI 앞에 서 있을 사람을 위한 것이다. 복구를 결정하는 client 쪽 설정을 남긴다. endpoint 우선순위, SecurityPolicy와 MessageSecurityMode, session timeout과 reconnect interval, subscription publishing interval과 lifetime count, monitored item queue size와 discard policy, failback이 자동인지 수동인지. Config 파일을 export해 둬라. 2년 뒤 upgrade 때 누구 기억보다 diff가 낫다.
그리고 운전자가 행동할 수 있는 네 줄을 쓴다. 정상 active/standby 표시가 어떻게 보이는지, failover 때 예상되는 알람이 무엇인지, 시운전에서 끊김이 대략 얼마였는지, 어느 노드가 살아 있는지 확인하는 방법. 이중화는 현장이 정상 전환과 실제 장애를 구분할 수 있어야 하드웨어 값을 한다. 그 노트가 없으면 계획된 failover마다 원인 불명으로 기록되고, 그 원인 불명들은 무시당한다.