시스템 아키텍처
하나의 X-Edge AI 박스는 자기 완결적인 3개 서비스 스택을 실행합니다. 모든 것 — 구성, 모델 버전, 이력, 로그 — 이 박스에 존재하며, 런타임에 클라우드 의존성이 없습니다.
flowchart LR
subgraph Box["X-Edge AI Box (Docker)"]
FE["Frontend\nnginx + React"] -->|REST + SignalR| BE["Backend\nASP.NET orchestration"]
BE -->|gRPC :50051| INF["Inference runtime\nPython + ONNX Runtime"]
BE --- DB[("SQLite\nconfig · history · logs")]
PLUG["Datasource plugins\nOPC UA · MQTT · CSV"] --- BE
end
SENSORS["PLCs · brokers · devices"] --> PLUG
PLUG -->|predictions written back| SENSORS
USER["Operator browser"] --> FE
| 서비스 | 역할 | 포트 |
|---|---|---|
| Frontend | 대시보드 UI, nginx로 제공 | 80 (TLS 사용 시 443) |
| Backend | 오케스트레이션: 데이터소스, 모델, 추론 수명 주기, 인증, 영속화 | 5000 |
| Inference | CPU/CUDA/TensorRT에서 ONNX 모델 실행 | 50051 (gRPC) |
데이터소스 플러그인
섹션 제목: “데이터소스 플러그인”산업 프로토콜은 백엔드가 시작 시 로드하는 플러그인으로 구현됩니다. 각 플러그인은 자신의 연결 설정을 타입이 지정된 스키마로 서술하며, UI는 이를 동적 폼으로 렌더링합니다 — 프로토콜을 추가해도 새 UI 빌드가 필요하지 않습니다.
- 입력(읽기 경로): OPC UA(매 틱마다 폴링), MQTT(오래됨 방지 가드가 있는 푸시), CSV 재생.
- 출력(쓰기 경로): OPC UA, MQTT, 회전형 CSV 파일. 출력 태그의 주소는 그대로
사용되며(MQTT 토픽, OPC UA NodeId, CSV 열), 각 값은
value × scale + offset으로 스케일링됩니다.
두 경로는 의도적으로 서로 다르게 실패합니다: 죽은 입력 태그는 추론을 눈에 띄게 멈춥니다(모델에 오래된 데이터를 공급하는 것은 이상 탐지에서 최악의 실패 모드입니다). 반면 실패하는 출력 싱크는 결코 추론을 멈추지 않으며, 대신 합쳐진 Output write failed 알림을 발생시킵니다.
추론 파이프라인
섹션 제목: “추론 파이프라인”활성화된 하나의 입력 데이터소스가 페어링된 하나의 모델에 공급합니다:
- 백엔드는 데이터소스의 샘플링 속도로 매핑된 태그를 샘플링하고, 이를 gRPC를 통해 추론 런타임으로 스트리밍합니다.
- 런타임은
window size × tag count샘플의 슬라이딩 윈도우를 유지합니다. 가득 차면 새 샘플 하나마다 예측(health_score,failure_probability,rul_normalized)을 생성합니다. - 예측은 다시 흘러나와 대시보드(SignalR), 이력 저장소, 구성된 출력 싱크, 그리고 박스 내 SQLite 감사 추적으로 분배됩니다.
따라서 처리량은 발행자의 속도가 아니라 샘플링 속도 × 데이터소스에 따라 확장됩니다 — 100Hz 센서를 10Hz로 샘플링하면 초당 10건의 예측이 산출됩니다.
**배압(Backpressure)**은 소비자별로 적용됩니다: 느린 대시보드가 추론을 스로틀하도록 결코 허용되지 않으며(그들의 이벤트가 먼저 드롭됨), 출력 싱크는 싱크가 따라잡지 못할 때 파이프라인을 엔드투엔드로 늦추는 경계가 있는 무손실 큐를 받고, 비유한(NaN/Inf) 예측은 필터링되고 카운트됩니다. 이 모두는 카운터로 확인할 수 있습니다 — 모니터링을 참조하세요.
모델 수명 주기
섹션 제목: “모델 수명 주기”모델은 modelctl로 박스 밖에서 작성되고(변환 → 검증 → 양자화 → 패키징), 그다음 UI를 통해 업로드됩니다. 업로드는 로드하지 않고 추론 런타임을 통해 ONNX 계약(형상 + SHA-256)을 검증합니다. 모델은 데이터소스가 활성화될 때만 엔진에 진입합니다. 로딩은 무중단 핫스왑이며, 버전은 불변입니다 — 같은 버전으로 변경된 파일을 다시 업로드하면 거부됩니다. 롤백은 이전의 유효한 버전을 재활성화합니다.
실시간 이벤팅
섹션 제목: “실시간 이벤팅”라이브 대시보드 업데이트(센서 배치, 예측, 헬스, 상태 변경, 알림)는 SignalR WebSocket 허브를 통해 이동합니다. 이 스트림은 발사 후 망각(fire-and-forget) 및 인메모리 방식입니다 — 결코 진실의 원천이 아닙니다. 브라우저가 연결을 끊으면 놓친 이벤트는 재전송되지 않으며, 권위 있는 상태는 항상 REST API와 박스 내 데이터베이스에서 옵니다.
로깅 — 세 개의 경계가 있는 계층
섹션 제목: “로깅 — 세 개의 경계가 있는 계층”| 계층 | 목적 | 경계 |
|---|---|---|
컨테이너 stdout(docker logs) | 라이브 디버깅 | 서비스당 10 MB × 3 파일 |
| 영속 볼륨의 회전형 파일(Warning+) | 사후 포렌식 — 컨테이너 재생성 및 데이터베이스 손상에서 살아남음 | 10 MB × 7(백엔드) / × 6(추론) |
| SQLite 로그 저장소 | LogViewer 페이지 및 알림 경보 | 10,000개 항목, 60초마다 정리 |
틱별 파이프라인 라인은 Debug 레벨로 로깅됩니다. 기본 Info 스트림은 수명 주기 이벤트, 하트비트, 경고만 전달합니다.
구성 가져오기 / 내보내기
섹션 제목: “구성 가져오기 / 내보내기”관리자는 전체 추론 구성(태그 매핑을 포함한 입력 및 출력 데이터소스)을 하나의 버전 관리된 JSON 엔벨로프로 내보내고 다른 박스에서 다시 가져올 수 있습니다. 비밀은 내보내기 시 마스킹됩니다. 가져오기는 단일 트랜잭션으로 실행됩니다 — 항목별 실패가 하나라도 있으면 파일 전체를 롤백하고 전체 오류 목록을 반환합니다.