溫濕度采集的斷線重連與斷點續(xù)傳機(jī)制解析)
先講一件真事。前年我負(fù)責(zé)一個藥品陰涼庫的溫濕度監(jiān)控改造采集器用的是帶以太網(wǎng)口的嵌入式設(shè)備上報頻率30秒一次。上線當(dāng)天一切正常結(jié)果第二天凌晨三點被值班電話吵醒——庫房溫濕度曲線從零點開始出現(xiàn)一整段空洞。排查到最后原因是庫房配電柜旁邊的一根網(wǎng)線被叉車壓斷設(shè)備斷線后又沒有重連和數(shù)據(jù)補(bǔ)傳邏輯網(wǎng)絡(luò)恢復(fù)后只能干瞪眼監(jiān)控大屏上永遠(yuǎn)是“離線”。這個項目之后我徹底想明白一件事以太網(wǎng)溫濕度采集通訊難點從來不在“讀取傳感器”和“把數(shù)據(jù)塞進(jìn)TCP包”而在網(wǎng)絡(luò)鏈路不可靠時如何保證數(shù)據(jù)不丟、鏈路自愈、歷史可補(bǔ)。本文要聊的是一套實際落地在溫濕度采集器上的通訊機(jī)制設(shè)計涵蓋多協(xié)議支持Modbus TCP、MQTT、HTTP、斷線重連狀態(tài)機(jī)、斷點續(xù)傳緩存與確認(rèn)機(jī)制以及現(xiàn)場踩過的那些文檔里不會寫的坑。適合正在做環(huán)境監(jiān)控采集器、IoT網(wǎng)關(guān)、工業(yè)數(shù)據(jù)上云的嵌入式或后端工程師參考。1. 溫濕度采集通訊的需求拆解為什么“低頻數(shù)據(jù)”反而最怕斷線1.1 數(shù)據(jù)的“低頻高價值”特性決定了通訊設(shè)計方向溫濕度數(shù)據(jù)和視頻流、高頻振動數(shù)據(jù)完全不同它的采集頻率通常很低短則1秒一條長則5分鐘一條。單條數(shù)據(jù)本身只有幾十字節(jié)一年滿打滿算也就幾百萬條算下來占用的帶寬和存儲幾乎可以忽略不計。但恰恰是這種低頻數(shù)據(jù)對“連續(xù)性”的要求高得嚇人。一個醫(yī)藥冷庫的溫度標(biāo)準(zhǔn)可能是2℃到8℃一旦溫度越界監(jiān)管要求你必須能證明“從什么時候開始越界、持續(xù)了多久”。如果中間斷了一小時數(shù)據(jù)審計人員根本不會認(rèn)可“這段時間大概率沒問題”這種解釋。數(shù)據(jù)缺失記錄缺失管理事故。所以溫濕度采集通訊系統(tǒng)的第一設(shè)計原則從來不是“把單條數(shù)據(jù)發(fā)出去”而是“保證端到端的數(shù)據(jù)最終連續(xù)、完整、可追溯”。這一點和常見的文件傳輸有本質(zhì)區(qū)別文件傳丟了可以重新傳整個文件但溫濕度數(shù)據(jù)如果斷了幾小時重新生成這幾小時的“假數(shù)據(jù)”根本不可能。唯一的辦法是在源頭把它緩存下來等待鏈路恢復(fù)后補(bǔ)傳。1.2 一條完整鏈路中哪些環(huán)節(jié)最容易斷一個典型的以太網(wǎng)溫濕度采集系統(tǒng)設(shè)備端到服務(wù)器之間隔著好幾層采集器自身的以太網(wǎng)PHY和協(xié)議棧現(xiàn)場的交換機(jī)或路由器跨網(wǎng)段時的防火墻和NAT設(shè)備服務(wù)器端的監(jiān)聽服務(wù)進(jìn)程每一層都可能出問題。我在多個現(xiàn)場踩到過這幾類典型的斷鏈交換機(jī)和設(shè)備之間自協(xié)商失敗網(wǎng)口指示燈亮著物理層顯示link up但設(shè)備收不到任何ARP響應(yīng)路由器NAT會話超時默認(rèn)空閑超時一般是300秒。設(shè)備保持TCP連接不發(fā)數(shù)據(jù)連接被靜默拆除設(shè)備端還以為自己在線服務(wù)器端服務(wù)重啟或部署更新舊連接沒有正常發(fā)FIN包設(shè)備端半開連接直到自己發(fā)數(shù)據(jù)收到RST才反應(yīng)過來供電不穩(wěn)導(dǎo)致采集器反復(fù)重啟尤其是現(xiàn)場用POE供電但交換機(jī)POE預(yù)算不足的情況這些斷鏈場景有一個共同特點它不是你關(guān)掉設(shè)備電源那種明確的“下線”而是鏈路狀態(tài)和設(shè)備本地狀態(tài)不一致。處理這種“半開連接”和“靜默失效”正是斷線重連機(jī)制要解決的問題。1.3 “重連”和“續(xù)傳”必須配套設(shè)計缺一個都白搭我最初接手這個項目時只想著加一個斷線重連。后來發(fā)現(xiàn)只加重連根本沒用。鏈路恢復(fù)后設(shè)備確實能重新連上服務(wù)器但它只會把“當(dāng)前這一條”數(shù)據(jù)發(fā)上去。斷線期間積累的數(shù)據(jù)全部留在本地如果不做續(xù)傳歷史空洞照樣存在。反過來如果只做續(xù)傳不做重連那緩存里的數(shù)據(jù)永遠(yuǎn)沒有機(jī)會傳出去。所以這兩者是一對閉環(huán)設(shè)計斷線重連負(fù)責(zé)“把通道重新建立起來”斷點續(xù)傳負(fù)責(zé)“把通道斷開期間欠下的數(shù)據(jù)補(bǔ)上去”。沒有續(xù)傳重連只是表面在線沒有重連續(xù)傳連觸發(fā)的機(jī)會都沒有。這套設(shè)計還有一個隱含要求斷線期間設(shè)備必須持續(xù)在本地采集并緩存數(shù)據(jù)而不是停下來等待。設(shè)備的本職是“采集”通訊只是“運(yùn)輸”。運(yùn)輸斷了采集不能斷。2. 多協(xié)議選型Modbus TCP、MQTT、HTTP不是排他關(guān)系2.1 為什么一個采集器要支持多協(xié)議有的工程師會問直接定一種協(xié)議不就行了為什么要做多協(xié)議答案在兩個場景里很清楚。第一個場景是存量設(shè)備集成。很多現(xiàn)場的PLC或者老式環(huán)境監(jiān)控主機(jī)只支持Modbus TCP你新上的溫濕度采集器如果不同時支持這個協(xié)議就只能另配協(xié)議轉(zhuǎn)換網(wǎng)關(guān)成本和故障點都增加。第二個場景是服務(wù)器端不固定。同一個采集器可能今天接的是客戶自己的SCADA系統(tǒng)明天被接到云端IoT平臺兩邊的協(xié)議棧完全不一樣。與其給每個項目都定制固件不如在設(shè)備端做一個可配置的協(xié)議層。這里要強(qiáng)調(diào)一個原則多協(xié)議指的是“同一份數(shù)據(jù)按配置選擇通道和格式”而不是每個協(xié)議維護(hù)一套獨立的數(shù)據(jù)邏輯。協(xié)議只是編碼和傳輸方式數(shù)據(jù)的來源、緩存、補(bǔ)傳邏輯必須統(tǒng)一。2.2 Modbus TCP兼容存量工業(yè)設(shè)備的最穩(wěn)選項Modbus TCP在工業(yè)場景里的地位不用多說結(jié)構(gòu)簡單報文是純粹的寄存器讀寫。溫濕度傳感器掛到Modbus TCP上通常就是把溫度和濕度映射到兩個保持寄存器比如溫度寄存器地址0x0001濕度0x0002單位精確到0.1。從通訊機(jī)制設(shè)計來看Modbus TCP有幾個特點需要注意它是一種“請求-響應(yīng)”協(xié)議服務(wù)器輪詢設(shè)備。設(shè)備端不能主動推數(shù)據(jù)只能等PLC或上位機(jī)來讀默認(rèn)端口502報文有MBAP頭功能碼數(shù)據(jù)CRC在TCP模式下是不需要的它沒有應(yīng)用層心跳機(jī)制連接斷開只能靠TCP本身去判斷這帶來一個設(shè)計上的麻煩如果采集器作為Modbus TCP的Server斷線重連邏輯幾乎沒法做因為主動發(fā)起連接的是對方。所以在多協(xié)議架構(gòu)里Modbus TCP更適合作為“被動服務(wù)”存在PLC周期性來讀寄存器我們保證寄存器的值永遠(yuǎn)是最新的同時把每次被讀走的記錄標(biāo)記為已同步。真正需要主動上報和斷點續(xù)傳的業(yè)務(wù)走M(jìn)QTT或HTTP通道。2.3 MQTT給云端平臺準(zhǔn)備的默認(rèn)通道MQTT是這批溫濕度采集器里我最推薦的上報協(xié)議主要原因有三個一是始終保持長連接但極其輕量。一條溫濕度數(shù)據(jù)轉(zhuǎn)成JSON后可能才100字節(jié)不到加上MQTT的固定頭開銷也就幾個字節(jié)非常適合小帶寬場景。二是QoS等級可以匹配不同的可靠性要求。QoS 0最多一次QoS 1至少一次QoS 2恰好一次。對溫濕度數(shù)據(jù)來說用QoS 1很合適允許重復(fù)但絕不能丟重復(fù)由服務(wù)器端去重即可。三是天然適合多個訂閱方。同一個溫濕度主題既可以給監(jiān)控平臺訂閱也可以給告警服務(wù)訂閱互不影響。不過MQTT有一個需要特別注意的點它自帶的心跳機(jī)制是PINGREQ/PINGRESP間隔由KeepAlive參數(shù)控制默認(rèn)可以設(shè)到60秒甚至更長。但實際網(wǎng)絡(luò)里NAT會話超時、交換機(jī)老化時間都可能比這個值短。我的建議是在公網(wǎng)或跨NAT場景下KeepAlive不要超過30秒如果用的是4G或者WiFi這類不穩(wěn)定鏈路甚至可以縮到15秒。代價只是每15秒多幾個字節(jié)的PING包完全值得。2.4 HTTP作為兜底通道簡單但別當(dāng)主力HTTP上報的好處是接入成本極低服務(wù)器端隨便寫個接口就能收。缺點是開銷相對較大而且HTTP是典型的請求-響應(yīng)模型設(shè)備要主動POST服務(wù)器沒法主動下發(fā)控制指令至少得用輪詢配合。在這個項目里我把HTTP設(shè)計成“兜底通道”觸發(fā)條件有兩個MQTT連續(xù)多次連接失敗確認(rèn)當(dāng)前網(wǎng)絡(luò)到MQTT broker不通設(shè)備檢測到服務(wù)器端HTTP接口可達(dá)但MQTT端口被運(yùn)營商或防火墻屏蔽這種場景在真實項目里遇到不止一次。某些客戶的網(wǎng)絡(luò)安全策略非常嚴(yán)格只放行了80/443端口其他端口一律不通。這時候MQTT走不了但HTTP還能用數(shù)據(jù)就不至于完全斷供。HTTP斷點續(xù)傳的實現(xiàn)也比較直白設(shè)備把緩存數(shù)據(jù)分批POST到服務(wù)器的補(bǔ)傳接口每批附帶起始序列號和結(jié)束序列號服務(wù)器返回ACK。這里要特別注意HTTP連接的復(fù)用如果一個一個POST每次都要重新建TCP連接效率太低最好是同一個連接內(nèi)連續(xù)上傳多批數(shù)據(jù)傳完一批再請求下一批。2.5 協(xié)議層之上的統(tǒng)一數(shù)據(jù)抽象多協(xié)議共存最怕的是各寫各的最后邏輯亂成一團(tuán)。我在設(shè)計時把所有協(xié)議收斂到同一個統(tǒng)一數(shù)據(jù)模型上設(shè)備ID全局唯一出廠寫入序列號單調(diào)遞增本地持久化是斷點續(xù)傳的游標(biāo)基礎(chǔ)采集時間設(shè)備本地時間戳單位秒溫度值、濕度值浮點采集質(zhì)量標(biāo)記正常/越界/傳感器異常不管是走M(jìn)QTT、HTTP還是Modbus寄存器的值底層都從這個統(tǒng)一模型取出。緩存和續(xù)傳也只針對這個模型操作和具體協(xié)議無關(guān)。這樣后期再加一個CoAP或者WebSocket完全不需要動緩存層的代碼。3. 斷線重連機(jī)制設(shè)計狀態(tài)機(jī)、心跳與退避策略3.1 重連的本質(zhì)是連接狀態(tài)機(jī)很多初學(xué)TCP編程的人會犯一個錯誤把重連邏輯做成一個while循環(huán)連不上就sleep一秒然后繼續(xù)連。這個寫法在線程里湊合能跑但一旦涉及連接狀態(tài)變化、數(shù)據(jù)緩存、補(bǔ)傳觸發(fā)這些聯(lián)動邏輯就會變得不可維護(hù)。正確做法是把連接生命周期抽象成一個狀態(tài)機(jī)至少包含四個狀態(tài)IDLE初始狀態(tài)當(dāng)前沒有任何連接CONNECTING正在嘗試建立連接CONNECTED連接已建立可以收發(fā)數(shù)據(jù)WAITING_RETRY連接失敗或斷開正在等待下一次重試狀態(tài)轉(zhuǎn)換的決策邏輯大致是IDLE收到啟動指令進(jìn)入CONNECTINGCONNECTING成功進(jìn)入CONNECTED同時復(fù)位連續(xù)失敗計數(shù)CONNECTED檢測到心跳超時或socket異常進(jìn)入WAITING_RETRYWAITING_RETRY的等待計時結(jié)束回到CONNECTINGCONNECTING失敗記錄失敗次數(shù)退出WAITING_RETRY狀態(tài)機(jī)的最大好處是每一個狀態(tài)下的行為都是確定的不會出現(xiàn)“明明斷開了還在發(fā)數(shù)據(jù)”這種尷尬。比如在WAITING_RETRY狀態(tài)下采集線程照常工作緩存照常寫入但發(fā)送線程必須掛起。數(shù)據(jù)緩存和連接狀態(tài)完全解耦。3.2 心跳機(jī)制TCP層的KeepAlive靠不住必須自己做應(yīng)用層心跳TCP有SO_KEEPALIVE選項默認(rèn)空閑2小時才發(fā)一個探測包而且探測失敗后還要等若干次才確認(rèn)連接死亡。對于溫濕度采集這種本來就低頻發(fā)送的場景SO_KEEPALIVE基本起不到及時斷鏈的作用。我采用的方案是應(yīng)用層心跳兩條消息設(shè)備→服務(wù)器PING每30秒一次服務(wù)器→設(shè)備PONG如果設(shè)備連續(xù)3個PING周期也就是90秒沒收到對應(yīng)的PONG就判定連接失效主動關(guān)閉socket并進(jìn)入WAITING_RETRY狀態(tài)。這里有個細(xì)節(jié)很容易踩坑PING和PONG都必須包含一個會話標(biāo)識或遞增序號用來匹配。否則一個遲到的PONG會被誤認(rèn)為是當(dāng)前連接的回應(yīng)導(dǎo)致狀態(tài)判斷混亂。我在實際項目里見過有同事用簡單的PING/PONG字符串結(jié)果服務(wù)器端重連后舊連接的PONG延遲到達(dá)設(shè)備把新連接的PONG和舊的對上號誤判連接正常。加一個32位遞增序號就徹底解決了。心跳周期怎么定我的經(jīng)驗是心跳間隔必須小于網(wǎng)絡(luò)鏈路中最短的空閑超時時間。如果不知道具體值按NAT默認(rèn)300秒的一半來選比較穩(wěn)也就是不超過150秒。再根據(jù)“連續(xù)失敗N次”來判斷一般N取2到3。所以30秒心跳、連續(xù)3次失敗90秒判死這個參數(shù)實測下來在公網(wǎng)和跨網(wǎng)段場景都足夠靈敏。3.3 指數(shù)退避加抖動別讓一堆設(shè)備同時撞車斷線重連最忌諱的是所有設(shè)備在同一個時刻瘋狂重試。如果現(xiàn)場有上百臺采集器同時掉電又同時來電如果用固定5秒重連恢復(fù)供電的瞬間網(wǎng)絡(luò)里全是重連風(fēng)暴交換機(jī)都可能被沖垮。正確做法是指數(shù)退避第1次失敗后等1秒第N次失敗后等2^N秒設(shè)置上限比如最長60秒加上隨機(jī)抖動實際等待時間在計算值基礎(chǔ)上乘以(0.8~1.2)舉個例子第1次失敗等1秒第2次等2秒第3次等4秒第4次等8秒第5次等16秒第6次等30秒之后封頂60秒。同時每次加20%以內(nèi)的隨機(jī)量。這個策略的效果單臺設(shè)備的恢復(fù)時間是秒級但上百臺設(shè)備不會集中在同一毫秒發(fā)起連接而是散布在幾十秒范圍內(nèi)。實測我們102臺設(shè)備同時斷電重啟后第一條MQTT連接在2秒內(nèi)建立最后一條在約90秒內(nèi)完成服務(wù)器負(fù)載完全可控。3.4 重連成功后的行為不只是把socket建起來重連成功容易讓人以為“萬事大吉”實際上重連成功后要做的事情比“建立socket”多得多發(fā)送一條上線通知攜帶設(shè)備當(dāng)前的狀態(tài)信息刷新會話向服務(wù)器端查詢最近一條已確認(rèn)的數(shù)據(jù)序列號用來確定續(xù)傳起點觸發(fā)斷點續(xù)傳把本地緩存里序列號大于服務(wù)器確認(rèn)點的數(shù)據(jù)按序補(bǔ)傳補(bǔ)傳完成后恢復(fù)實時數(shù)據(jù)的正常上報第二步特別重要。如果不查詢服務(wù)器端的已確認(rèn)序列號設(shè)備就只能盲目地把本地緩存全部倒上去既浪費(fèi)帶寬也可能重復(fù)傳大量服務(wù)器早就收到的數(shù)據(jù)。一個簡單的GetLatestAckedSeq請求就能讓續(xù)傳有的放矢。4. 斷點續(xù)傳機(jī)制從緩存設(shè)計到可靠確認(rèn)4.1 斷點續(xù)傳和“重新上傳”的本質(zhì)區(qū)別這里必須把概念掰清楚斷點續(xù)傳不是“把所有數(shù)據(jù)重新上傳一遍”?!爸匦律蟼鳌笔潜哭k法服務(wù)器反正要全量數(shù)據(jù)我不管三七二十一全部重發(fā)。這在數(shù)據(jù)量小時看著可行一旦斷線一天、每30秒一條數(shù)據(jù)就是2880條全部重發(fā)不僅浪費(fèi)帶寬還會和實時數(shù)據(jù)搶占連接造成“越補(bǔ)越亂”。真正的斷點續(xù)傳是精確到序列號的增量補(bǔ)傳設(shè)備端維護(hù)一個“最后推送游標(biāo)”服務(wù)器端維護(hù)一個“最后確認(rèn)游標(biāo)”兩者之差就是需要補(bǔ)傳的數(shù)據(jù)區(qū)間。服務(wù)器已經(jīng)確認(rèn)過的數(shù)據(jù)一條都不多傳沒確認(rèn)的一條都不少傳。這個設(shè)計在溫濕度場景里的效果非常明顯斷線2小時240條數(shù)據(jù)補(bǔ)傳時每個包壓縮后可能10KB就能搞定幾乎是瞬間完成。而且因為只補(bǔ)缺失區(qū)間實時數(shù)據(jù)的延遲幾乎不受影響。4.2 嵌入式設(shè)備上的緩存介質(zhì)選擇別一上來就上SQLite溫濕度采集器大多是MCU級別設(shè)備資源有限。緩存介質(zhì)的選擇直接影響續(xù)傳機(jī)制的復(fù)雜度。我按設(shè)備檔次給三種方案低端MCU無文件系統(tǒng)用Nor Flash或EEPROM按塊寫入原始二進(jìn)制數(shù)據(jù)。容量一般256KB到4MB每條約32字節(jié)可以存幾千到幾萬條中端設(shè)備帶SD卡或文件系統(tǒng)直接寫CSV或二進(jìn)制日志文件容量寬裕得多高端邊緣網(wǎng)關(guān)可以上嵌入式數(shù)據(jù)庫但老實說溫濕度數(shù)據(jù)用不上別把簡單問題復(fù)雜化我在這批采集器上選的是Nor Flash方案存儲結(jié)構(gòu)非常直接固定分塊每塊存一條完整記錄塊頭部寫序列號和寫入時間戳Flash剩余空間不足時最老的未確認(rèn)數(shù)據(jù)可以被覆寫同時告警通知服務(wù)器“緩存溢出”這里有個關(guān)鍵判斷要不要覆蓋舊數(shù)據(jù)我的選擇是“寧丟數(shù)據(jù)保證設(shè)備不死”。傳感器采集不能停緩存如果滿了還在繼續(xù)寫要么設(shè)備死機(jī)要么數(shù)據(jù)全毀。前者更不可接受。所以緩存滿了之后策略是覆蓋最老的未確認(rèn)數(shù)據(jù)并在下一次上線時把溢出區(qū)間標(biāo)記為“已丟失”讓服務(wù)器知道這段數(shù)據(jù)不完整比假裝沒有發(fā)生過強(qiáng)得多。4.3 數(shù)據(jù)幀格式讓斷點可以被精確定位斷點續(xù)傳的實現(xiàn)前提是每條數(shù)據(jù)都有一個可比較的游標(biāo)。我用的是“設(shè)備ID序列號”雙字段定位設(shè)備ID區(qū)分不同的采集器默認(rèn)32位整數(shù)序列號每條采集記錄一個全局單調(diào)遞增重啟不重置序列號有兩個作用一是排序二是去重。服務(wù)器端只要記錄每個設(shè)備ID下“最大已確認(rèn)序列號”就能計算出設(shè)備需要補(bǔ)傳的區(qū)間。一條補(bǔ)傳數(shù)據(jù)幀的格式大致是typedef struct { uint32_t device_id; uint32_t seq; uint32_t timestamp; int16_t temperature; // 單位 0.01℃ int16_t humidity; // 單位 0.01%RH uint16_t flags; // 質(zhì)量標(biāo)記 } sensor_record_t;字段全部定長好處是解析簡單、節(jié)省空間、也不需要JSON解析器占用MCU資源。服務(wù)器端解析后再轉(zhuǎn)換成JSON或數(shù)據(jù)庫記錄即可。4.4 續(xù)傳流程分批上送加ACK游標(biāo)更新別一次全發(fā)續(xù)傳最忌諱一次把幾千條數(shù)據(jù)全部塞進(jìn)一個TCP包或者一大串MQTT報文里。一旦中途斷鏈到底是哪些收到了、哪些沒收到又是一筆糊涂賬。我的做法是分批續(xù)傳每批最多100條每批發(fā)送完成后等待服務(wù)器返回ACKACK里帶上這一批的最大序列號設(shè)備收到ACK后把本地“最后已確認(rèn)序列號”更新到ACK值然后發(fā)送下一批所有批次都確認(rèn)后切回實時模式這個流程雖然保守但每一步都可恢復(fù)。比如傳完第3批、共300條后連接斷了服務(wù)器已確認(rèn)到seq300設(shè)備本地游標(biāo)也更新到300。重連后設(shè)備只問服務(wù)器確認(rèn)線服務(wù)器返回300然后從301繼續(xù)補(bǔ)傳。斷點續(xù)傳的“斷點”就是這么精確容錯粒度控制在100條以內(nèi)通常只有幾十條甚至幾條。4.5 冪等設(shè)計重復(fù)數(shù)據(jù)不可怕缺失數(shù)據(jù)才可怕實現(xiàn)續(xù)傳時為了避免“萬一ACK丟了設(shè)備重傳了已經(jīng)確認(rèn)的數(shù)據(jù)”這種情況服務(wù)器端的接收邏輯必須設(shè)計成冪等。也就是說同一序列號的數(shù)據(jù)傳兩次不會產(chǎn)生兩條記錄而是以第一次到達(dá)的為準(zhǔn)。具體做法是在數(shù)據(jù)庫表里給“設(shè)備ID序列號”建唯一索引重復(fù)插入時直接忽略或者做UPSERT。這樣設(shè)備的ACK丟失重傳也好、服務(wù)器端收到重復(fù)數(shù)據(jù)也好都不會污染數(shù)據(jù)。這個細(xì)節(jié)我在好幾個項目里都被坑過一開始沒做冪等后來發(fā)現(xiàn)服務(wù)器數(shù)據(jù)庫里同一秒出現(xiàn)了兩條溫度記錄而且數(shù)值還不一樣。排查半天發(fā)現(xiàn)是補(bǔ)傳和實時上報在時間窗口內(nèi)重疊了。做了冪等之后這類問題徹底消失。5. 現(xiàn)場最容易翻車的四個環(huán)節(jié)與排查記錄5.1 網(wǎng)線物理斷開時協(xié)議棧的真實表現(xiàn)讓人迷惑很多開發(fā)者在實驗室里用網(wǎng)線斷開測試發(fā)現(xiàn)設(shè)備在幾十毫秒內(nèi)就感知到了連接斷開。真實場景遠(yuǎn)沒有這么理想。在某個倉庫項目里網(wǎng)線被老鼠咬斷了一根芯物理層處于一個“半斷不斷”的詭異狀態(tài)指示燈偶爾亮偶爾滅設(shè)備有時能ping通網(wǎng)關(guān)但一到跨網(wǎng)段通信就丟包。我們的設(shè)備表現(xiàn)是TCP連接長期掛在ESTABLISHED狀態(tài)心跳偶爾能收到PONG但實際數(shù)據(jù)上報成功率只有30%。這種問題靠應(yīng)用層心跳不一定能快速識別因為間歇性通斷會“騙過”超時判斷。最后的排查方法是加了一個“連續(xù)N次數(shù)據(jù)包往返超時”的計數(shù)器。事件概況是如果連續(xù)5次任意類型的網(wǎng)絡(luò)交互PING、上報、ACK都失敗即使心跳沒超時也強(qiáng)制觸發(fā)重連流程。這個方法實測對半斷狀態(tài)很有效。5.2 緩存被寫穿斷線時間太長Flash先扛不住了按30秒一條、每條32字節(jié)算256KB的Flash大概能緩存2700多條也就是22小時左右。聽起來夠用但遇到下面這種情況就崩了某項目停電檢修設(shè)備恢復(fù)供電后運(yùn)維人員發(fā)現(xiàn)設(shè)備一直離線排查發(fā)現(xiàn)是斷線期間緩存滿了主控在Flash擦除和寫入之間反復(fù)循環(huán)導(dǎo)致系統(tǒng)看門狗超時設(shè)備反復(fù)重啟。后續(xù)我把緩存策略改成了“緩存滿后不再覆寫未確認(rèn)數(shù)據(jù)而是停止寫入并保留已有數(shù)據(jù)”。雖然會丟實時數(shù)據(jù)但至少保留了一部分歷史可用數(shù)據(jù)設(shè)備也能正常上線。這個場景的選擇取決于業(yè)務(wù)我傾向于保設(shè)備穩(wěn)定因為溫濕度數(shù)據(jù)本身重復(fù)采集成本很低但歷史數(shù)據(jù)丟了無法挽回。之后我又加了一個措施緩存用量超過80%時如果鏈路仍未恢復(fù)就降低采集頻率從30秒降到60秒極限情況下降到5分鐘。這樣盡量延長緩存的覆蓋時間換取更多的歷史數(shù)據(jù)。5.3 設(shè)備本地時間戳漂移補(bǔ)傳數(shù)據(jù)的時間軸可能錯位斷點續(xù)傳時每條數(shù)據(jù)都帶有設(shè)備本地時間戳。但嵌入式設(shè)備用的晶振便宜溫漂大加上沒有NTP同步跑幾天就可能差出幾分鐘。斷線期間如果一口氣補(bǔ)傳240條數(shù)據(jù)服務(wù)器端按時間戳排序會出現(xiàn)輕微的順序錯亂甚至和實時數(shù)據(jù)交錯。解決方案是服務(wù)器端對補(bǔ)傳數(shù)據(jù)做“時間軸重對齊”用設(shè)備的序列號作為主排序鍵時間戳只作為展示參考。同時在設(shè)備端加入NTP對時邏輯只要鏈路可用每天至少對時一次。對時成功后會修正本地時間但由于序列號是單調(diào)遞增的修正時間戳不會影響數(shù)據(jù)順序。這里我特別想強(qiáng)調(diào)序列號的一個重要優(yōu)勢它讓數(shù)據(jù)排序不再依賴時間戳等于自動免疫了時鐘漂移。這也是為什么我在設(shè)計數(shù)據(jù)幀時堅持把序列號放在時間戳之前的排序優(yōu)先級。5.4 斷電恢復(fù)后的“補(bǔ)傳風(fēng)暴”多臺設(shè)備同時搶線上報項目里102臺設(shè)備同時在線某次市電閃斷又恢復(fù)后幾乎同時涌進(jìn)來全量補(bǔ)傳請求。雖然每批100條、間隔時間不長但在幾十秒內(nèi)服務(wù)器的接收線程還是被打滿了數(shù)據(jù)庫寫入隊列一度積壓到幾萬條。解決辦法分兩頭設(shè)備端補(bǔ)傳時增加一個隨機(jī)的啟動延遲0~30秒避免大家同時涌進(jìn)來服務(wù)器端接收接口前加一層內(nèi)存隊列削峰填谷入庫改為批量寫入每次事務(wù)提交100~500條實測效果很理想補(bǔ)傳風(fēng)暴從幾十分鐘縮短到一兩分鐘服務(wù)器CPU峰值從85%降到35%。設(shè)備端的“隨機(jī)延遲補(bǔ)傳”是成本最低、收益最大的一個改動。6. 這套設(shè)計在真實項目里跑出來的數(shù)據(jù)項目最終交付時的關(guān)鍵指標(biāo)102臺以太網(wǎng)溫濕度采集器30秒上報周期Modbus TCP、MQTT、HTTP三通道按項目配置切換連續(xù)運(yùn)行3個月設(shè)備在線率從最初的96%提升到99.95%月掉線次數(shù)從30多次降到1~2次斷線2小時以內(nèi)的數(shù)據(jù)續(xù)傳完成率100%斷線24小時以內(nèi)完成率大于98%未完成部分主要是緩存溢出導(dǎo)致的數(shù)據(jù)丟失服務(wù)器端接入延遲從補(bǔ)傳風(fēng)暴高峰期的分鐘級恢復(fù)到秒級這些數(shù)字不能說驚艷但確實是在真實現(xiàn)場跑出來的比實驗室里好看的結(jié)果可靠得多。我在實際項目中最大的體會是以太網(wǎng)溫濕度采集的“技術(shù)難度”不高難的是把斷線重連、斷點續(xù)傳、多協(xié)議適配、緩存管理、冪等處理這些細(xì)節(jié)串成一個整體。每一個環(huán)節(jié)單獨看都很簡單但它們之間的狀態(tài)聯(lián)動才是真正容易出錯的地方。建議大家都用狀態(tài)機(jī)把邏輯先畫清楚再動手寫代碼能少走很多彎路。最后分享一個小技巧設(shè)計斷線重連機(jī)制時可以人為構(gòu)造“斷電重啟、網(wǎng)線抽拔、交換機(jī)重啟、服務(wù)器重啟、NAT超時”五種故障每種故障測試一遍。能把這五種故障全部跑通這套通訊機(jī)制在絕大多數(shù)現(xiàn)場就不會有太大問題了。