 SCADA 上位機:整體架構(gòu)怎么分層)
用 Qt 做工業(yè) SCADA 上位機整體架構(gòu)怎么分層做工業(yè)上位機這些年見過不少項目栽在同一個地方一開始沒想清楚分層寫到后面數(shù)據(jù)流全是亂的。采集線程直接去改界面控件策略計算里順手連數(shù)據(jù)庫歷史查詢把主界面卡死幾秒……這些問題單看都是小毛病根子卻在架構(gòu)上——沒有把誰負責什么劃清楚。這篇講一套我們實際用過的分層思路基于 Qt C 實現(xiàn)行業(yè)上通用于各類 SCADA 場景。以講分層與取舍為主關(guān)鍵處配少量示意代碼——示意代碼只表達設(shè)計形狀不是可以直接編譯的完整實現(xiàn)。一、先理清上位機到底要干幾件事在動手分層之前先把職能列全。一套工業(yè)上位機通常要做四件事職能做什么采集通過 TCP 或串口連現(xiàn)場裝置周期讀取數(shù)據(jù)監(jiān)控把實時數(shù)據(jù)畫成畫面給人看并支持下發(fā)命令存儲把需要留痕的數(shù)據(jù)寫進數(shù)據(jù)庫支持歷史回看轉(zhuǎn)發(fā)把數(shù)據(jù)按映射關(guān)系轉(zhuǎn)發(fā)給上層主站很多項目的問題在于把這四件事揉在一個模塊里。結(jié)果是采集代碼里混著界面刷新存儲邏輯里夾著業(yè)務(wù)判斷改一處牽動全身。分層的第一個動作就是讓這四件事各歸各位。二、數(shù)據(jù)模型裝置—點分層的起點是數(shù)據(jù)結(jié)構(gòu)。工業(yè)場景的數(shù)據(jù)模型基本都長這樣裝置一臺現(xiàn)場設(shè)備編號唯一點裝置上的一路數(shù)據(jù)點按用途分四類也就是電力行業(yè)說的「四遙」類別含義數(shù)據(jù)形態(tài)遙測連續(xù)變化的測量值浮點遙信兩種狀態(tài)的位置量整型 0/1遙控數(shù)字量輸出命令命令遙調(diào)模擬量輸出命令命令這個模型看起來簡單但有兩個坑必須一開始就定好坑一編號從幾開始。點的編號往往允許空檔配置里是人指定的業(yè)務(wù)編號裝置編號則通常是連續(xù)的。兩者混用后面做映射時會很痛苦。我們的做法是明確約定并寫進注釋點號從 0 開始裝置號從 1 開始判斷未選擇不能用 0 當哨兵。坑二業(yè)務(wù)編號不能直接當數(shù)組下標。點號允許空檔而內(nèi)存里的點表數(shù)組是按配置行序填的。中間缺幾個號用點號當下標就會整段讀錯數(shù)據(jù)——而且不報錯。所以系統(tǒng)里必須統(tǒng)一走一層映射業(yè)務(wù)編號 → 數(shù)組行號。這個映射表是很多 bug 的源頭值得單獨封裝。三、分層設(shè)計寫值取數(shù)存庫讀值讀值采集層設(shè)備接口 協(xié)議解析數(shù)據(jù)層內(nèi)存實時庫所有數(shù)據(jù)流的中轉(zhuǎn)站存儲層歷史數(shù)據(jù)入庫業(yè)務(wù)層策略與報警展示層畫面引擎注意箭頭方向采集層只往里寫其余三層只從里讀沒有任何一條邊是跨過數(shù)據(jù)層直連的。這就是下面要說的分層規(guī)矩。五層的職責依次展開如下。3.1 數(shù)據(jù)層內(nèi)存實時庫實時數(shù)據(jù)放內(nèi)存不放數(shù)據(jù)庫。這是工業(yè)上位機與普通信息系統(tǒng)的最大區(qū)別。界面刷新、策略求值、存庫采樣、轉(zhuǎn)發(fā)取數(shù)全都要高頻讀實時值。走數(shù)據(jù)庫根本扛不住。所以有一份常駐內(nèi)存的實時庫結(jié)構(gòu)就是上面說的裝置-點表。它有幾個特征被多個模塊共享界面、策略、采集、轉(zhuǎn)發(fā)都讀它采集線程、策略引擎、界面都會寫它因此必須加鎖而且要劃清加鎖的粒度我們的做法是每臺裝置對象自帶一把鎖而不是全局一把大鎖。理由是不同裝置之間互不干擾全局鎖會讓采集線程互相排隊。代價是跨裝置的操作比如統(tǒng)計全站數(shù)據(jù)需要多把鎖實現(xiàn)時要小心死鎖。這個取舍在后面第五節(jié)展開。對外暴露的接口大致長這樣——只給讀某個點“寫某個點”不給內(nèi)部數(shù)組理由見第四節(jié)規(guī)則一classRealtimeStore{public:staticRealtimeStoreinstance();// 按裝置 點號讀寫。點號允許空檔內(nèi)部映射到數(shù)組行號boolwrite(constQStringdevice,constQStringpoint,doublevalue);doubleread(constQStringdevice,constQStringpoint,bool*oknullptr);// 批量讀供畫面刷新、策略求值使用內(nèi)部按裝置分批加鎖voidreadBatch(constQVectorPointRefrefs,QVectordoubleout);};注意readBatch這一條批量接口必須提供否則調(diào)用方一定會自己去遍歷。上層一旦寫成循環(huán)調(diào)read就得反復進出鎖——批量接口既是為了效率也是為了把調(diào)用方擋在鎖的外面。3.2 采集層設(shè)備接口與協(xié)議解析采集層再拆成兩半這是很多人容易忽略的一步子層職責設(shè)備接口只管收發(fā)字節(jié)、維護連接、斷線重連協(xié)議解析只管組幀解幀、把值寫進實時庫分開的好處很直接換通訊方式不影響協(xié)議代碼。串口改 TCP只動設(shè)備接口Modbus 改別的規(guī)約只動協(xié)議層。線程模型上我們用的是每個通道一個接口線程 每個協(xié)議一個解析線程接口線程阻塞在 socket 上收發(fā)天然一對一解析線程從通道緩沖區(qū)取數(shù)據(jù)組幀、解幀、寫實時庫為什么不用線程池因為采集是長連接 持續(xù)阻塞的場景線程池的復用優(yōu)勢體現(xiàn)不出來反而增加了哪個任務(wù)在哪個線程上的排查成本。通道數(shù)量由現(xiàn)場規(guī)模決定通常幾十個以內(nèi)線程數(shù)完全可控。這里有個必須注意的約束協(xié)議解析線程內(nèi)部往往是死循環(huán)輪詢因為要不停地讀緩沖區(qū)不能用 Qt 的信號槽或 QTimer——那些依賴事件循環(huán)。正確做法是用一個運行標志位配合帶超時的休眠退出時能及時響應(yīng)。3.3 存儲層實時與歷史分離實時數(shù)據(jù)在內(nèi)存歷史數(shù)據(jù)進數(shù)據(jù)庫兩者物理隔離。存儲層的設(shè)計要點第一寫庫不是全量輪詢。每秒掃一遍所有點但每個點按自己配置的存儲間隔決定這次要不要存。全站幾百個點實際每秒落庫的可能只有幾個。第二用隊列解耦。采樣線程挑出該存的數(shù)據(jù)塞進隊列寫庫線程消費。這樣即使數(shù)據(jù)庫偶發(fā)卡頓也不會阻塞采集。第三按時間分表。工業(yè)歷史數(shù)據(jù)量隨時間是線性增長的單表遲早扛不住。按天建表his_xxx_20260929這類命名是最常見也最好維護的方案清理過期數(shù)據(jù)只需要刪表不用做代價極高的 DELETE。代價是跨天查詢要拼多張表查詢邏輯會復雜一些。3.4 業(yè)務(wù)層策略與報警策略層負責條件成立時執(zhí)行動作報警層負責值異常時記錄。這一層最容易犯的錯是與數(shù)據(jù)層耦合過深——策略代碼里直接操作點表數(shù)組、直接拼 SQL。結(jié)果就是策略改一個條件存儲層要跟著動。我們的邊界是策略只通過實時庫的讀寫接口操作數(shù)據(jù)不碰底層結(jié)構(gòu)。這樣策略引擎可以獨立測試也便于后續(xù)換成腳本引擎。報警同理越限判斷發(fā)生在寫值的那一刻在數(shù)據(jù)層內(nèi)部但報警的記錄與推送交給業(yè)務(wù)層。判斷和執(zhí)行分開后面做報警分級、推送渠道擴展都不用動數(shù)據(jù)層。3.5 展示層畫面引擎展示層要做的事把配置文件里的畫面在運行時重建出來并按數(shù)據(jù)源綁定實時值。關(guān)鍵在于畫面與數(shù)據(jù)解耦畫面文件里描述的是有哪些控件、什么位置、綁哪個點運行時解析后按綁定關(guān)系周期性去實時庫取值這樣改畫面不用改代碼甚至能做到畫面文件熱加載——編輯器里存盤運行態(tài)自動重新加載不用重啟。調(diào)畫面的時候這一點能省大量時間。四、分層的邊界怎么劃幾條我們實際踩過之后總結(jié)的規(guī)則規(guī)則一數(shù)據(jù)層的接口要粗不要細。暴露讀某個點寫某個點就夠了不要暴露內(nèi)部數(shù)組。否則上層遲早會繞過接口直接操作數(shù)據(jù)。規(guī)則二跨層調(diào)用只允許相鄰層。展示層不能直接調(diào)采集層采集層也不能直接刷界面。所有跨層的數(shù)據(jù)流都經(jīng)過數(shù)據(jù)層中轉(zhuǎn)。規(guī)則三加鎖在數(shù)據(jù)層內(nèi)部完成。上層調(diào)用寫值接口時不應(yīng)該關(guān)心鎖——如果調(diào)用方需要自己加鎖說明接口設(shè)計有問題早晚會漏加。規(guī)則四配置讀取集中在一處。各個模塊各讀各的配置文件后期排查配置問題會很痛苦。統(tǒng)一由數(shù)據(jù)層在啟動時加載其他模塊從內(nèi)存取。五、幾個真實的取舍取舍一配置用文件還是數(shù)據(jù)庫我們用 CSV 文件。理由是現(xiàn)場工程師能直接用 Excel 打開查看和批量編輯出問題能肉眼比對不依賴數(shù)據(jù)庫也方便隨包發(fā)布和版本管理。代價是編碼、引號、字段內(nèi)逗號這些都要自己處理跨文件的一致性校驗要額外寫代碼。如果你的項目配置量極大上萬點且頻繁變更數(shù)據(jù)庫可能更合適。取舍二單進程還是多進程我們用單進程多線程采集、存儲、策略、界面全在一個進程內(nèi)。好處是數(shù)據(jù)交換走內(nèi)存沒有進程間通信開銷部署也簡單一個可執(zhí)行文件。代價是一個線程出問題會影響全局共享實時庫的加鎖面較大。如果現(xiàn)場對穩(wěn)定性要求極高比如要求采集模塊崩潰不影響界面多進程通過共享內(nèi)存或消息隊列通信會更穩(wěn)但復雜度顯著上升。取舍三實時庫加鎖粒度前面提過我們用每裝置一把鎖。這個選擇在裝置數(shù)量多、訪問分散時優(yōu)勢明顯但如果你的系統(tǒng)里存在大量遍歷所有裝置的操作多把鎖會帶來死鎖風險與實現(xiàn)復雜度此時全局鎖反而更簡單可靠。沒有普適的最優(yōu)解只有與你訪問模式匹配的解。小結(jié)工業(yè)上位機的分層說到底是為了回答一個問題當現(xiàn)場規(guī)模擴大、需求變更時改動會蔓延到哪里分層清晰的項目加一種協(xié)議只動協(xié)議層加一種報警只動業(yè)務(wù)層。分層混亂的項目改任何一處都要通讀全部代碼。幾個關(guān)鍵決策回顧數(shù)據(jù)模型先定死裝置—點、編號規(guī)則、映射層實時庫放內(nèi)存采集與存儲解耦采集層拆成設(shè)備接口與協(xié)議解析兩層加鎖封在數(shù)據(jù)層內(nèi)部上層無感畫面與數(shù)據(jù)解耦配置驅(qū)動下一篇講配置化設(shè)計為什么工業(yè)軟件偏愛文件配置而不是數(shù)據(jù)庫以及怎么把這條路走穩(wěn)。