서류상으로는 통과한 전환 시험
전환에 걸린 시간은 4초 정도였다. Standby가 녹색으로 바뀌고, operator 화면이 다시 그려졌고, 다들 시험 성적서에 서명하고 점심을 먹으러 갔다.
3주 뒤 월간 생산 report에 시험하던 날 밤의 유량 적산값 26분치가 비어 있었다. Standby node의 historian collector는 설치는 되어 있었지만 store-and-forward buffer 경로가 존재하지 않는 폴더로 잡혀 있었다. Service가 기동할 때 warning을 한 줄 남겼고 아무도 읽지 않았다. Service를 재시작할 때까지 수집한 데이터를 전부 버렸다. 그동안 redundancy 진단 화면은 계속 녹색이었다. 그 아이콘은 거짓말을 한 게 아니다. 두 server가 서로의 heartbeat를 본다는 뜻이었고, 원래 그 이상을 의미한 적이 없다.
그래서 전환 시험에서 물어야 할 것은 "전환이 됐는가"가 아니다. "전환에서 무엇이 살아남았는가"다. Operator 가시성, alarm 상태, 데이터 연속성, background job, 그리고 아무도 담당자가 아닌 외부 interface.
지금 설치된 이중화가 어떤 방식인지 먼저 적는다
제품마다 같은 단어를 전혀 다른 구조에 쓴다. 손대기 전에 설치된 구성을 적어 둔다. 전환 시험에서 나오는 놀라움의 절반은 runtime이 아니라 architecture에서 나온다.
| 구분 | 확정해야 할 내용 |
|---|---|
| Server role | Hot / warm / cold standby, active-active, active-passive |
| Client routing | Virtual IP, DNS name, load balancer, client 측 primary/standby 목록 |
| Data source routing | 양쪽 node의 PLC driver, 공용 OPC server, 앞단 gateway |
| Database path | Local replication, 단일 SQL instance, collector pair |
| File path | Shared project folder, 복제된 runtime file, node별 local 복사본 |
| License model | Floating, dongle, node-locked, 별도 redundancy SKU |
여기서 반복되는 실패 유형은 비대칭이다. Standby는 primary보다 6개월 뒤에, 다른 사람이 만들었고, 딱 하나가 빠져 있다. ODBC driver 하나, 신뢰 저장소에 없는 client certificate, MES 서버로 나가는 방화벽 rule, "서비스로 로그온" 권한이 없는 service account. 이런 것은 그 node가 active가 되기 전까지 드러나지 않는다. Service 목록을 눈으로 훑지 말고 설치된 기능, driver version, certificate, 방화벽 rule, 환경 변수, 네트워크 드라이브를 제대로 비교한다.
Clock 동기화도 같이 본다. 두 node가 몇 초만 벌어져 있어도 전환 시점의 event journal이 시간 역전을 일으키고, 일부 historian은 순서가 어긋난 sample을 거부하거나 조용히 덮어쓴다. 양쪽을 같은 NTP source에 묶는다. 도메인에 가입된 서버라면 Kerberos의 기본 skew 허용치가 5분이라는 점도 기억해 둔다. 꽤 어긋나 있어도 인증은 멀쩡히 된다.
Client가 실제로 어디에 다시 붙는가
가장 눈에 띄는 실패는 operator 자리에서 보인다. Rack 앞에만 있지 말고 console 앞에도 엔지니어를 세운다. 일반 operator station 하나와 engineering station 하나를 같이 본다. 로그인 권한 범위도 다르고 물고 있는 connection도 다르기 때문이다.
Virtual IP를 따라가는 구성이면 전환은 gratuitous ARP가 나가고 그것을 스위치와 client가 믿어 주는 데 달려 있다. Cisco 스위치의 기본 ARP cache timeout은 4시간인데 MAC address table aging은 300초다. GARP가 유실되면 일부 client는 이미 옮겨간 MAC으로 계속 unicast를 보낸다. DNS name을 따라가는 구성이면 Windows DNS client가 record TTL을 그대로 지킨다. A record TTL이 3600초면 한 시간 동안 죽은 server와 대화한다. 30~60초로 낮춘다. 둘 다 네트워크 팀 영역이고, SCADA 전환 시험 때만 드러나는 문제다.
OPC UA는 redundancy mode를 암묵적으로 두면 안 된다. IEC 62541-4는 transparent와 non-transparent redundancy를 구분하고, non-transparent에서는 server를 고르는 책임이 client에 있다. Client가 ServerRedundancy object의 ServiceLevel과 ServerUriArray를 읽고 가장 건강한 server로 옮겨야 한다. Client가 이걸 구현하지 않았다면 "이중화된 OPC UA"의 실제 의미는 "누군가 client를 재시작한다"이다. 협상된 session timeout 값도 확인한다. RevisedSessionTimeout 안에 다시 붙는 client는 session을 재활성화해서 subscription을 유지할 수 있지만, 그 시간을 넘기면 처음부터 다시 browse한다.
"빠르다"로 적지 말고 초시계로 재야 할 항목들이다.
- 대표 화면에서 tag quality가 bad/uncertain을 벗어나기까지 걸린 초.
- Quality가 불확실한 동안 command가 막히는지. 막혀야 정상이다.
- Login session이 유지되는지, upset 한가운데서 재인증을 요구하는지.
- Operator가 진단 화면을 열지 않고도 지금 어느 server가 control 중인지 알 수 있는지.
합격 기준은 시험이 끝난 뒤가 아니라 시작 전에 숫자로 적는다. 내가 쓰는 기준은 보통 이렇다. Operator 화면에 30초 안에 live 데이터 복귀, operator 조치 없음, 재로그인 없음.
조용히 깨지는 쪽은 alarm 상태다
Alarm 상태는 on/off가 전부가 아니다. IEC 62682(ISA-18.2)는 unacknowledged, acknowledged, shelved, suppressed-by-design, out-of-service를 포함하는 state machine을 정의한다. 이 모든 상태가 복제 대상이고, standby가 control을 가져가기 전에 최신 상태를 받았는지는 별개 문제다.
Operator가 보고하는 증상은 "지나간 alarm이 다시 떴다"이다. 실제로 벌어진 일은 마지막 몇 초 동안의 acknowledge가, 복제가 끊겨 있었다면 지난 몇 시간치가, 넘어가지 못한 것이다. Standby의 alarm 목록은 오래된 snapshot이다. Shelve 상태는 이 문제의 최악 버전이다. 정비 작업 때문에 shelve 해 둔 alarm이 하필 upset 한가운데 다시 올라온다.
그래서 전후로 기록한다. Active alarm count, 이름을 정해 둔 몇 개의 acknowledgement 상태, shelved와 suppressed 전체 목록, 전환 시점 앞뒤의 timestamp와 순서. 그리고 전환 자체가 event journal에 남는지 확인한다. 남지 않으면 새벽 3시에 사고를 조사하는 사람이 상관관계를 잡을 방법이 없다.
Alarm history를 SQL에 쓰는 구성이라면 standby 쪽 connection string과 credential을 따로 본다. Standby에 database driver가 빠져 있어도 그 node가 active가 되어 모든 event를 버리기 시작하기 전까지는 완벽하게 건강해 보인다.
Historian의 구멍은 다음 달 report에서 발견된다
전환 후 live trend가 멀쩡해 보이는 경우가, 저장된 데이터가 멀쩡한 경우보다 훨씬 많다. Pen을 믿지 말고 시험 후에 historian을 query한다.
빠른 tag는 시험 구간의 예상 sample 수와 저장된 수를 비교하고, 느린 tag는 전환 전후 raw timestamp를 직접 본다. 볼 것은 네 가지다. Stale cache 값이 만든 flatline, 같은 timestamp에 다른 값이 들어간 duplicate, 끝내 비워지지 않은 store-and-forward buffer, 그리고 잘못된 context나 반쪽짜리 입력 구간으로 다시 시작한 calculated tag. Batch·lot identifier는 따로 확인할 값어치가 있다. Context가 active node에서만 붙는 구조라면 gap 구간의 sample이 미아가 되거나, 더 나쁘게는 직전 lot에 붙는다.
건물 utility tag의 60초 gap은 문제가 아니다. 같은 60초가 거래용 유량이나 규제 대상 process variable에서는 deviation 보고서가 된다. 어떤 tag가 어느 쪽인지는 시험 전에 정해 둔다. 그래야 끝난 뒤에 논쟁하지 않고 기준으로 판정한다.
제일 무서운 건 두 번 도는 job이다
이중화 node에는 background logic이 붙어 있다. Report 생성, shift summary, email notification, cleanup job, interface export, custom script. Failover는 이것들을 양방향으로 깨뜨린다. 아무 데서도 안 돌거나, 양쪽에서 돌거나.
안 도는 쪽은 월요일 아침에 불편한 정도로 끝난다. 양쪽에서 도는 쪽은 그 job이 recipe download나 setpoint write, MES transaction이면 생산 사고다. 각 background task마다 무엇이 단일 실행을 보장하는지 구체적으로 확인한다. 제품이 제공하는 "내가 primary인가" flag인가, database semaphore인가, file lock인가, 아니면 아무것도 없는가. "Primary에서만 돈다"는 그 gate를 눈으로 보기 전까지는 가정이다.
중복 실행은 시험으로 피하기보다 설계로 막는 게 낫다. 받는 쪽을 idempotent하게 만들면 — 예를 들어 MES가 중복 transaction key를 거부하게 하면 — failover 질문 자체가 의미를 잃는다.
버튼만 누르지 말고 선도 뽑아 본다
Graceful role switch는 happy path만 확인한다. 현장 risk가 허용하면 commissioning 때 아래는 넣는다.
| Scenario | 확인되는 것 |
|---|---|
| Manual role switch | 제품 logic과 문서화된 operator 절차 |
| Primary의 application service 정지 | Host 사망이 아니라 application 사망을 standby가 감지하는지 |
| Primary 재부팅 | Client reconnect와 service 기동 순서·의존성 |
| Heartbeat 경로 상실 | Heartbeat 설계와 split-brain 방지 |
| 죽었던 node 복귀 | 복구된 node가 엉뚱한 시점에 control을 가져가지 않는지 |
특히 network isolation 시험은 조심한다. Heartbeat가 단일 경로에 quorum이나 witness 없이 얹혀 있으면 격리 즉시 split brain이 된다. 두 node가 모두 자기가 active라고 믿고, 둘 다 historian에 쓰고, 둘 다 PLC와 통신한다. 케이블을 뽑기 전에 제품이 이 상황에서 무엇을 하는지 확인한다. 답이 애매하면 PLC link를 먼저 분리한 상태에서 시험한다.
증거는 남긴다. 시작·종료 시간, 각 step의 active node, 실측한 client reconnect 시간, alarm count와 상태, historian query 결과, 진단 화면 캡처, 발견된 이슈마다 담당자와 재시험 결과. 감사 파일용이 아니다. 2년 뒤에 같은 작업을 하면서 "정상"이 어떤 모습이었는지 알아야 하는 사람을 위한 것이다.
현장에서 가장 자주 건너뛰는 단계는 저 표의 마지막 줄이다. 넘기는 것은 쉬운 절반이다. 복구된 node가 다시 합류하고, alarm과 historian 상태를 재동기화하고, 나쁜 타이밍에 control을 도로 가져가지 않는 부분에서 실제 정전 사고를 더 많이 봤다. 시험 시간이 한 번뿐이라면 그 끝자락을 여기에 쓴다.