近參與了一個設備狀態上報模塊的開發和測試,結果在壓力測試時遇到了一個讓我煩人的問題:明明文件上傳成功了,但網管系統卻偶爾收不到“我已更新”的通知。
這個問題復現率不高,大概跑幾百次才漏一次
我們的上報流程很簡單:
設備本地狀態或資源配置變更;
觸發後台任務,將 ig_resource_{id}.xml 和 ig_state_{id}.xml 通過 FTP 上傳;
同時通過 DDS 發佈一條輕量級消息:“XX設備的資源/狀態已更新,版本號XXX”
壓力測試時發現:FTP 文件總能成功上傳,但 DDS 消息偶爾沒收到。更詭異的是,日誌裏沒有任何錯誤——既沒有“DDS send failed”,也沒有“FTP upload error”。彷彿那條消息憑空蒸發了
初步推斷:是否DDS丟包了
第一反應是網絡問題。畢竟 DDS 默認走 UDP,高負載下可能丟包。但查了 QoS 配置,我們用的是 RELIABLE 模式,理論上會重傳。而且如果是網絡丟包,應該隨機影響所有 Topic,而不是隻“精準”漏掉中繼線資源。 再看日誌:每次 FTP 上傳後,都會打印一行 "Send TK report via DDS",説明 xdds_SendOutData() 被調用了。但網管側就是沒收到。
難道是DDS寫入成功 ,但是底層沒發出去?
查看代碼
void xdds_FtpTask(void *arg)
{
while (1)
{
VOS_SemSynP(xdds_ftp_sem, 0);
if (xdds_ftp_pending <= 0) {
continue;
}
xdds_ftp_pending = 0; // 關鍵在這
xdds_IomTKReportMsgProc(NULL);
}
}
而觸發邏輯是這樣的(在多個模塊中)
xdds_ftp_pending++;
VOS_SemPost(xdds_ftp_sem);
發現問題所在:如果短時間內連續觸發兩次
第一次:pending = 1,SemPost
第二次:pending = 2,SemPost
xdds_FtpTask 被喚醒一次,執行上報,然後 pending = 0
第二次變化被吃掉了
雖然 xdds_IomTKReportMsgProc 會全量上傳文件(靠 MD5 判斷是否真變化),但如果兩次變更間隔極短,且內容恰好相同(比如快速開關同一個功能),就可能出現“文件沒變,所以沒上傳,但其實中間狀態丟了”的情況。 而在壓力測試中,中繼線資源恰恰是高頻變更項——這解釋了為什麼它“特別容易丟”
修改思路:不讓事件合併
最直接的想法是:別用計數器,改用“有事就喚醒”。
但項目處於穩定期,不能大改架構。於是琢磨出一個最小改動方案:
把 xdds_ftp_pending++ 全部改成 xdds_ftp_pending = 1
為什麼可行?
只要需要上報,就把 pending 置為 1;
任務被喚醒後,發現 pending > 0,就執行上報;
即使多次觸發,每次 SemPost 都會喚醒任務一次;
雖然 pending 始終是 1,但信號量的次數保證了消費次數。
換句話説:用信號量的“計數”特性,代替了變量的“計數”。
修改後,觸發代碼變成:
xdds_ftp_pending = 1; // 不再 ++
VOS_SemPost(xdds_ftp_sem);
而 xdds_FtpTask 一行都不用動。
驗證效果
改完後,重新跑 10 輪壓力測試(每輪 5000 次高頻變更),結果:
FTP 文件更新次數 ≈ DDS 消息接收次數;
中繼線資源再未出現“有文件無通知”的情況;
CPU 和網絡負載無明顯增加(因為內部 MD5 去重,無效上傳極少)。
一點反思
這個 Bug 本質上是一個經典的生產者-消費者模型誤用:
我們用一個整型變量當“緩衝區”,但它只能表示“有”或“無”,無法承載“有多少”;
當生產速度 > 消費速度時,事件就被合併甚至丟棄。
在實時系統中,“通知有事發生”不如“確保每件事都被處理”可靠。下次設計類似機制,我會直接用隊列,或者至少用原子信號量計數。