[{"content":"這篇文章記錄一次 IIoT 接入路徑部署故障的排查與修復過程。目標是在雲端透過 Podman Rootless 容器執行 EMQX 5.8.9，完成設備 MQTT 接入、HTTP 動態鑑權、Webhook 轉發與時序資料寫入。\n實際推進中，這條路徑先後暴露出四類問題：emqx ctl 在容器內無法穩定連回主節點、改用預寫 cluster.hocon 後 EMQX 在啟動階段因 Schema 驗證失敗而崩潰、外部設備連線持續逾時，以及後端對 EMQX 發起的 M2M HTTP 請求返回 403 Forbidden。這些現象分散在執行期、配置、網路與安全中介軟體四個層面，如果只盯著單一報錯，很容易誤判為某一處設定寫錯。\n說明：本文中的專案名、服務名、網域、IP、路徑、帳號、資料表名、請求標頭、日誌片段與命令輸出均已脫敏、抽象或重寫，只保留與排查路徑相關的技術資訊。\n背景與目標 容器執行環境：Podman Rootless Broker：EMQX 5.8.9 鑑權方式：HTTP AuthN 資料路徑：MQTT -\u0026gt; Rule Engine -\u0026gt; Webhook -\u0026gt; TimescaleDB 後端安全：瀏覽器側 CSRF 防護 + M2M Token 驗證 目標本身並不複雜：設備透過 MQTT 接入，EMQX 呼叫後端完成動態認證，再把遙測資料透過 Webhook 轉發給後端，最後寫入時序資料庫。\n故障摘要 這次故障最後確認並不是單點問題，而是四條故障路徑疊加：\nPodman Rootless 的隔離機制導致 Erlang EPMD IPC 不穩定，emqx ctl 無法作為可靠的動態載入入口 EMQX 5.8.9 對 Webhook Bridge 配置執行更嚴格的 HOCON Schema 驗證，歷史欄位觸發 unknown_fields 雲端安全性群組未開放 1883 入站流量，外部 MQTT 連線直接被阻斷 後端全域 CSRF 中介軟體預設保護所有 POST 請求，誤傷 EMQX 發起的 AuthN 與 Webhook 呼叫 最終修復也分別對應這四條路徑：\n放棄依賴 emqx ctl 的動態載入，改為在 Entrypoint 前置階段渲染完整 cluster.hocon 移除不再相容的 resource_opts 歷史欄位 開放安全性群組 1883 對 /api/v1/iot/* 路徑繞過 CSRF，同時在 /iot/ingest 上增加常數時間 X-EMQX-Token 驗證 排查時間線 第一階段：先確認為什麼動態配置不可靠 最初部署方案是在 EMQX 主行程啟動後，透過腳本執行：\nemqx ctl conf load --merge\n把 HTTP AuthN 與 Webhook 規則動態注入 Broker。實際執行時，容器內持續出現：\nNode emqx@\u0026lt;service\u0026gt;-emqx not responding to pings 這一步的關鍵結論是：問題暫時還不在業務配置，而是在執行期控制層。只要 emqx ctl 不能穩定透過 Erlang 分散式節點協議連回主節點，後續的動態配置方案就沒有可靠性基礎。\n假設 A：節點 Hostname 綁定錯誤 第一反應是節點名 emqx@\u0026lt;service\u0026gt;-emqx 的解析異常，因此在 Compose 中顯式宣告 hostname。這一步修正後，節點命名確實更一致，但 emqx ctl 仍然時好時壞，說明 Hostname 只是干擾項，不是根因。\n假設 B：sed 佔位符替換被 Compose 提前展開 另一個已確認的問題是命令裡的 ${VAR} 會被 podman-compose 提前展開，導致 sed 的替換目標被吃掉，Token 注入失效。將替換目標改為靜態佔位字串後，Token 渲染恢復正常，但 emqx ctl 依然不可靠。這說明模板替換確實有缺陷，但仍不是整條部署路徑失敗的主因。\n第二階段：繞開 emqx ctl，改為啟動前寫配置 既然問題集中在執行期 IPC，排查方向自然收斂到「是否能在主行程啟動前就完成配置注入」。後續做法是把渲染後的配置直接寫入：\n/opt/emqx/data/configs/cluster.hocon\n再由 Entrypoint exec 啟動 EMQX。這個方向繞開了 emqx ctl，但立刻暴露出第二個阻塞點：EMQX 在啟動階段直接崩潰。\n日誌中的關鍵訊息為：\n[error] failed_to_check_schema: emqx_conf_schema [error] #{reason =\u0026gt; unknown_fields, path =\u0026gt; \u0026#34;bridges.webhook.\u0026lt;service\u0026gt;_backend_ingest_bridge.resource_opts\u0026#34;, unknown =\u0026gt; \u0026#34;pool_size,buffer_type,max_retries,...\u0026#34;} 這一步的結論也很清楚：預寫配置本身是正確方向，但模板中仍帶有舊版本可接受、當前版本已不再相容的歷史欄位。\n假設 C：只精簡部分 resource_opts 欄位即可恢復 初始做法是先刪除 buffer_type、max_retries 等欄位，保留看起來仍合理的 pool_size。結果 EMQX 仍然在 Schema 驗證階段失敗，說明這裡不是「個別欄位值不合法」，而是整個 resource_opts 子樹都已不再適配當前版本。最終將整塊完全移除後，EMQX 才恢復正常啟動。\n同一階段暴露出的配置覆蓋風險 這一階段還暴露出另一個隱患：cluster.hocon 改成覆蓋寫入後，如果模板只包含 Webhook 規則，就可能把先前透過 Dashboard 設定的 HTTP AuthN 一併覆蓋掉，導致 Broker 雖然能啟動，但設備接入鑑權路徑反而被切斷。\n因此最終模板調整為一次性宣告三類配置：\nHTTP AuthN Rule Engine 規則 Webhook Bridge 這一步不是效能或可維護性優化，而是為了讓 Broker 啟動配置保持原子性，避免不同入口寫同一份生效配置時互相覆蓋。\n第三階段：Broker 啟動後，外部設備仍然連不上 當 EMQX 已能正常啟動，且容器日誌確認：\nListener tcp:default on 0.0.0.0:1883 started 外部客戶端連線仍然持續逾時。此時排查對象已從 Broker 內部轉移到外圍網路層。\n本地使用 mosquitto_pub 直接測試 \u0026lt;IP\u0026gt;:1883 後依然逾時，這一步將問題範圍縮小到 IaaS 層。最終確認是雲端安全性群組沒有開放 1883 入站流量。補齊規則後，設備到 Broker 的 MQTT 連通性恢復。\n這一步非常典型：容器內連接埠監聽正常，不代表外部存取路徑已經打通。只要安全性群組或外層防火牆未放行，應用側日誌就會呈現「服務正常、客戶端逾時」的假象。\n第四階段：M2M HTTP 請求被後端安全中介軟體攔截 MQTT 連線打通後，EMQX 發起的 HTTP AuthN 與 Webhook 請求又返回 403 Forbidden。繼續檢查後端邏輯後，問題定位到全域 CSRF 中介軟體。\n後端原本面向瀏覽器流量啟用了嚴格的 Origin / Referer 驗證，這對 Web 管理端是合理的，但對 EMQX 發起的機器對機器請求並不適用。問題不在於這類請求「不能攜帶」相關標頭，而在於它們並不處於瀏覽器信任模型內，無法穩定滿足基於 Origin / Referer 的 CSRF 驗證前提，因此會被誤攔截。\n這裡的處理不是簡單關掉 CSRF，而是做安全分流：\n/api/v1/iot/* 前綴繞過 CSRF 資料接收端點 /iot/ingest 額外驗證 X-EMQX-Token Token 比對使用 crypto/hmac.Equal，避免時序側信道 如此處理後，Web 瀏覽器端仍保留原有 CSRF 防護，而 IoT M2M 路徑則改用更適合機器呼叫場景的預共享密鑰驗證。\n根因分析 1. Podman Rootless 與 Erlang EPMD 的執行期阻抗 emqx ctl 本質上依賴 Erlang Distribution Protocol 與 EPMD 進行節點發現與控制。在 Rootless 場景下，User Namespace 與虛擬網路堆疊改變了容器內回環與本地 IPC 的行為，使這條控制面路徑無法穩定建立。因此問題不在於 emqx ctl 命令本身寫錯，而在於它不適合作為目前執行環境中的核心配置入口。\n2. EMQX 5.8.x 對歷史 HOCON 欄位收緊 舊版本 Bridge 配置中常見的 resource_opts.pool_size、buffer_type、max_retries 等欄位，在 5.8.x 中已不再按原路徑生效。當前版本在啟動階段就會對未知欄位執行強驗證，因此這類歷史配置不會被忽略，而是直接阻斷 Broker 啟動。\n3. 外圍網路策略與應用日誌之間存在錯位 EMQX 容器內成功監聽 1883，只能證明 Broker 完成本地綁定；它並不能證明外部設備到 Broker 的接入路徑已經打通。當客戶端逾時、而應用層沒有明顯異常日誌時，排查就必須繼續向外走到安全性群組、宿主機防火牆與入口轉發層。\n4. 瀏覽器安全策略不能直接套用到 M2M 路徑 CSRF 是為瀏覽器上下文設計的防護模型，而 EMQX 與後端之間的 AuthN / Webhook 呼叫屬於 M2M 通信。若直接將同一套瀏覽器導向的驗證套在兩類流量上，就會把不符合瀏覽器來源判定前提的合法 Broker 請求誤判為可疑流量。因此正確做法不是「少做安全」，而是讓不同呼叫者類型使用不同的安全機制。\n修復方案 最終落地方案包含四個部分。\n1. 啟動前渲染完整 cluster.hocon 放棄執行期動態載入，在容器 Entrypoint 前置階段渲染最終配置並寫入，之後再啟動 EMQX。這樣做的目的，是把配置生效時機前移到 Broker 啟動前，避免繼續依賴不穩定的執行期 IPC。\n2. 合併 AuthN、Rule Engine 與 Webhook 模板 最終模板統一包含：\nHTTP AuthN 遙測轉發規則 Webhook Bridge 同時完全刪除不再相容的 resource_opts 區塊。\n3. 打通外部網路入口 在確認 EMQX 已監聽 0.0.0.0:1883 後，補齊雲端安全性群組入站規則，恢復設備從外部存取 Broker 的通路。\n4. 重構 M2M 安全策略 後端現在對 /api/v1/iot/* 繞過瀏覽器導向的 CSRF 驗證，同時在 /iot/ingest 上啟用共享密鑰驗證。最終形成兩套並行防護：\n瀏覽器端：繼續使用 CSRF IoT M2M 端：使用 X-EMQX-Token 這讓後端安全邊界更清楚，也避免後續再把瀏覽器流量與設備流量混用同一套驗證模型。\n驗證結果 修復後，驗證分三層完成。\n1. 後端中介軟體驗證 針對 CSRF 與 Token 中介軟體的測試確認：\n普通 Web/API 請求在缺失或偽造 Origin / Referer 時仍返回 403 /api/v1/iot/* 前綴請求可繞過 CSRF /iot/ingest 在缺失或錯誤 Token 時被拒絕 2. MQTT 端到端驗證 使用外部客戶端執行發布測試：\nmosquitto_pub -d -h \u0026lt;Domain\u0026gt; -p 1883 \\ -i \u0026#34;\u0026lt;Device_SN\u0026gt;\u0026#34; -u \u0026#34;\u0026lt;Device_SN\u0026gt;\u0026#34; -P \u0026#34;\u0026lt;Token\u0026gt;\u0026#34; \\ -t \u0026#34;telemetry/fms/\u0026lt;Device_SN\u0026gt;\u0026#34; \\ -m \u0026#39;{\u0026#34;fcs_speed_rpm\u0026#34;: 7766, \u0026#34;fcs_soc\u0026#34;: 88.9, \u0026#34;fms_fault_code\u0026#34;: 0}\u0026#39; 驗證結果表明：\n設備連線成功 HTTP AuthN 返回通過 訊息被 Broker 接收並進入 Rule Engine 3. 資料寫入驗證 時序資料庫查詢確認資料寫入成功，_iot 資料面角色權限與控制面角色保持隔離，整條路徑形成閉環。\n經驗與後續 這次排查沉澱出幾條比較明確的工程經驗：\n在 Rootless Podman 下，凡是可以透過宣告式配置解決的問題，優先不要依賴啟動後的 Erlang IPC 控制路徑 中介軟體跨版本升級時，高級調優欄位比基礎功能配置更容易失效，配置應盡量從最小集開始驗證 「服務已監聽」不等於「外部可達」，網路連通性必須一路驗證到安全性群組或防火牆 瀏覽器安全機制與 M2M 安全機制應明確分流，避免彼此誤傷 從目前階段來看，單節點模板渲染方案已滿足 MVP 驗證需求，但若繼續演進到生產高可用架構，後續仍需要關注：\nBroker 叢集化與配置中心化 Webhook 非同步化，避免後端被同步寫入流量放大 從共享密鑰逐步演進到雙向 TLS 或更強的設備身分體系 這次修復的價值，不在於消除了某一條啟動報錯，而在於把原本分散在執行期、配置、網路與安全層的故障路徑，重新整理成一條可解釋、可驗證、可重複執行的接入流程。\n","permalink":"https://intent.me/zh-tw/blog/tech/iiot-emqx-rootless-deployment-postmortem/","summary":"記錄一次 IIoT 接入路徑部署故障的完整排查：在 Podman Rootless 環境下，EMQX 5.8 同時暴露出 Erlang IPC 失效、HOCON Schema 驗證失敗、安全性群組未開放與 M2M 請求被 CSRF 誤攔截等問題，並整理最終修復方案與驗證結果。","title":"IIoT 接入路徑排查與修復紀錄：Podman Rootless 下 EMQX 5.8 部署故障復盤"},{"content":"在一次 100ms 輪詢場景的效能排查中，TrafficMonitor 所在程序的 CPU 使用率長期穩定在 3.6%~4.2%。除資源佔用偏高外，介面在高頻收發期間也會出現可感知的刷新遲滯與滾動不跟手。對比兩個版本實作後，優化版本最終將該指標降到 0.1%~0.6%，同時高頻輪詢下的 UI 卡頓現象也明顯緩解。\n這次收益並非來自單點熱點函式優化，而是一次典型的工程化收縮：減少高頻路徑上的中間層、狀態機與無效喚醒，讓系統回到更符合其職責邊界的實作方式。\n本文復盤的優化對象來自開源工具 Modbus-Tools。如果想先了解工具的整體定位與核心能力，可先閱讀該介紹文章。\n背景與結論 這次優化的結論可以概括為兩點：\nTrafficMonitor 回歸輕量流量展示元件，不再承擔複雜日誌系統職責 工作執行緒在無事件時真正阻塞，而不是以固定時間片持續輪詢 對應到程式碼層，主要改動集中在四個方向：\nTrafficMonitorWidget 從事件模型回退到直接文字追加 ModbusTcpView 與 ModbusRtuView 刪除輪詢彙總與抑制邏輯 ModbusClient 從 wait_for + 5ms slice 改為 wait_until SerialChannel 與 TcpChannel 移除高頻路徑上的執行緒診斷日誌 關鍵改動 1. UI 日誌鏈路收縮 複雜版本中，TrafficMonitor 引入了完整事件抽象，日誌寫入路徑為：\n調用方 -\u0026gt; 構造事件物件 -\u0026gt; 元件判定級別/模式 -\u0026gt; 渲染文字 -\u0026gt; 寫入列表\n優化版本刪除這套中間層，保留 appendTx()、appendRx()、appendInfo()、appendWarning() 等直接入口，路徑收縮為：\n調用方 -\u0026gt; 直接格式化文字 -\u0026gt; 追加到 QListWidget\n這一步減少了物件構造、事件分類、二次渲染與狀態同步開銷，是本次 CPU 下降的主要來源之一。\n2. 元件實作從 Model/View 回退到直接列表 複雜版本的 TrafficMonitorWidget 不只是顯示控制項，還維護了完整的資料層，包括：\nQListView + QAbstractListModel pendingEvents_ eventHistory_ flushTimer_ rebuildScheduled_ pausedEventCount_ 這類設計在功能上更完整，但在 100ms 高頻刷新下，會持續放大以下成本：\n事件入隊與批量刷新 文字格式化與過濾判斷 歷史重建與滾動同步 優化版本改回 QListWidget 直接追加，本質上是刪除一層常駐的資料管理系統。對高頻日誌場景，這種收縮帶來的收益非常直接。\n3. 刪除高頻附加能力與輪詢狀態機 複雜版本圍繞「更強可觀察性」增加了多項能力，包括：\nPause View Raw Frames Level Filter 暫停計數 可見列表重建 Poll Summary 彙總狀態機 這些能力單獨看都合理，但在高頻場景下，會把每條日誌都變成「需要判斷和調度的事件」，而不是「直接展示的文字」。\n尤其是 ModbusTcpView 與 ModbusRtuView 中的 Poll Summary 邏輯，雖然減少了可見日誌條數，但並未減少系統總工作量。每次輪詢仍需更新計數、維護狀態、判斷窗口並格式化摘要。優化版本刪除這套邏輯後，鏈路明顯縮短。\n4. 等待模型改為真正阻塞 除 UI 外，另一處關鍵改動在 ModbusClient 的等待策略。\n複雜版本使用 cv_.wait_for(lock, 5ms, ...) 配合事件泵機制，意味著執行緒即使沒有真實事件，也會以 5ms 週期被喚醒一次。對 100ms 輪詢場景，這會製造大量無效 wakeup。\n優化版本統一改為：\ncv_.wait_until(lock, deadline, predicate)\n改造後的效果是：\n無事件時執行緒真正休眠 僅在超時或條件滿足時喚醒 不再週期性處理當前執行緒事件 這部分改動直接降低了工作執行緒空轉，是本次優化的另一主要來源。\n5. 清理高頻調試輸出 SerialChannel.cpp 和 TcpChannel.cpp 中，複雜版本保留了執行緒上下文診斷邏輯，例如：\nthreadToken(...) logThreadContextOnce(...) open()、onReadyRead()、onConnected() 中的執行緒日誌 單條日誌成本不高，但它們位於高頻收發路徑，累積後會放大格式化與分支判斷開銷。優化版本將其移除，是一次典型的高頻路徑減負。\n為什麼收益這麼明顯 這次優化的價值，不在於把某個函式從 10ms 優化到 2ms，而在於刪除了高頻鏈路上的多項乘法項。\n複雜版本的一次輪詢收發，通常會經過：\nIO 回調 事件物件構造 級別與模式判斷 佇列操作 定時器喚醒 批量渲染 過濾判斷 歷史重建 自動滾動 優化版本則更接近：\nIO 回調 文字格式化 直接追加到列表 當調用頻率固定為 100ms 時，鏈路長度差異會持續放大，最終直接反映為 CPU 使用率從 3.6%~4.2% 降到 0.1%~0.6%。由於主執行緒不再持續承受事件分發、批量刷新與列表重建壓力，介面互動也恢復到更穩定的狀態。\n工程經驗 這次復盤沉澱出幾條比較明確的工程經驗：\n高頻場景下，功能複雜度本身就是效能成本 GUI 效能問題很多時候不在繪製，而在繪製前的事件系統與狀態機 對等待模型來說，能阻塞就不要輪詢，能按條件喚醒就不要按時間片喚醒 日誌元件職責越清晰，實作越容易穩定，效能也越可控 從結果看，這次優化並不是「做了很多技巧性調優」，而是重新收縮了系統邊界。當 TrafficMonitor 回歸輕量視圖、ModbusClient 改回真正阻塞等待後，系統不再為低頻附加能力支付高頻成本，整體負載自然回落。\n如需進一步了解工具能力全貌，可參考 Modbus-Tools 介紹文章。專案原始碼見 GitHub 倉庫：mingyucheng692/Modbus-Tools。\n","permalink":"https://intent.me/zh-tw/blog/tech/trafficmonitor-cpu-optimization-postmortem/","summary":"記錄一次高頻輪詢場景下的 CPU 優化復盤：透過收縮 UI 日誌鏈路、刪除高頻狀態機、改造執行緒等待模型，將程序 CPU 使用率從 3.6%~4.2% 降到 0.1%~0.6%。","title":"TrafficMonitor CPU 佔用優化復盤：從 4% 降到 0.6% 的工程化收縮"},{"content":"這篇文章記錄一次容器恢復鏈路失效後的排查與修復過程。在一套由 Rootless Podman 託管的服務中，資料庫容器異常退出後沒有如預期被自動拉起，隨後又連帶暴露出啟動逾時、容器仍在線但服務顯示為 inactive，以及重啟多次後被頻率限制鎖死等問題。\n說明：本文中的專案名、服務名、路徑、網域、使用者名稱、主機識別、時間點、PID、命令輸出與日誌片段皆為脫敏、抽象或重寫示例，只保留與排障鏈路相關的技術資訊，不對應任何真實生產識別。\n執行環境：Rootless Podman 4.9.x 行程託管：Systemd User Units 業務元件：TimescaleDB、Redis、Go Backend、Nginx Frontend 目標能力：容器異常退出後由 Systemd 自動恢復 事故摘要 觸發背景：資料庫容器異常退出後，現場最先看到的是資料庫對應 Unit 變成 inactive，其餘服務對應 Unit 仍維持 active 過程表現：進入修復後，資料庫服務先恢復為 active，但其餘服務一度未被 Systemd 正確接管，於是陸續出現啟動逾時、容器狀態與 Unit 狀態不一致，以及部分服務因頻率限制被鎖死 已確認的致因因素：主 Unit 曾透過 sed -i 被腳本直接改寫，並在 PowerShell 跳脫截斷後被寫壞；Compose 與 Systemd 並存造成控制權分離；自動生成的 Unit 仍需後處理；恢復腳本對依賴清理與重啟失敗覆蓋不足 修復方向：停止用 sed -i 直接改主 Unit，改成重新生成 .service 後交由 Python 腳本修補，並顯式把執行期控制權交回 Systemd，再補上狀態輪詢與最終健康檢查 當前結果：在目前環境中，恢復鏈路已能按固定順序執行，失敗時也有明確診斷入口；但 readiness 判定與觀測能力仍待補強 現象與影響 最初現場並不是「所有服務一起掉線」，而是資料庫對應 Unit 先變成 inactive，其餘服務仍維持 active。真正把問題放大的，是進入修復與接管階段後又暴露出另一組現象：\n有些服務在 systemctl --user status 中顯示逾時後重啟 有些容器已經 Up，但對應的 Systemd 服務仍是 inactive 有些服務因為連續失敗，被 Systemd 直接停止重試 這表示問題不是某一個容器的單點故障，而是「容器實際狀態、Systemd 託管狀態、部署腳本預設假設」三者之間已經出現偏差。本文聚焦在「恢復鏈路為什麼失效、腳本後來如何調整」，不展開資料庫掉線本身的業務觸發原因。\n排查思路：先確認誰真正持有行程控制權 這次排查不是從單條報錯開始，而是先回答一個更基礎的問題：容器退出後，究竟是誰負責發現它失敗、判定它異常，並把它重新拉起。沿著這條線索，最後定位到四類彼此疊加的問題。\n問題一：主 Unit 被直接修改，最終損壞 最先暴露的問題，是資料庫對應的主 Unit 被破壞成空檔案：\nsystemd[\u0026lt;pid\u0026gt;]: /home/\u0026lt;deploy-user\u0026gt;/.config/systemd/user/container-\u0026lt;db-service\u0026gt;.service:1: Missing \u0026#39;=\u0026#39;. 進一步檢查檔案本體時，可以直接看到它已被截斷為 0 位元組：\nls -l /home/\u0026lt;deploy-user\u0026gt;/.config/systemd/user/container-\u0026lt;db-service\u0026gt;.service # -rw-rw-r-- 1 \u0026lt;deploy-user\u0026gt; \u0026lt;deploy-user\u0026gt; 0 \u0026lt;timestamp\u0026gt; container-\u0026lt;db-service\u0026gt;.service file /home/\u0026lt;deploy-user\u0026gt;/.config/systemd/user/container-\u0026lt;db-service\u0026gt;.service # container-\u0026lt;db-service\u0026gt;.service: empty 這次對觸發鏈已經能描述得更具體：舊版腳本確實會直接以 sed -i 修改主 Unit；而在透過 PowerShell 下發這類命令時，跳脫處理出現偏差，導致目標檔沒有被正確寫回，最終把主 Unit 截斷成 0 位元組，使 Systemd 無法再解析。問題不只是「腳本用了 sed -i」，而是「把主 Unit 當成可原地修改的對象」，再疊加跨 Shell 跳脫差異，共同放大了損壞風險。\n舊版腳本中的關鍵路徑大致如下：\n# 舊版：直接修改主 Unit sed -i \u0026#39;s/Restart=always/Restart=no/g\u0026#39; \u0026#34;$service\u0026#34; 問題本身已經足夠清楚：主服務檔同時承擔「執行定義」與「運維開關」兩種角色，本身就是高風險設計。一旦主 Unit 損壞，解析、啟動與重啟策略會一起失效。\n後來的調整比較直接：\n主服務檔只保留生成產物屬性，不再由 sed -i 直接修改 將重啟策略等開關下沉到 .service.d/watchdog.conf 生成後的 .service 相容性修補交給 Python 處理，Drop-in 則由腳本覆寫寫入 目前腳本中，Drop-in 的寫入方式大致如下：\ncat \u0026gt; \u0026#34;$DROP_IN_DIR/watchdog.conf\u0026#34; \u0026lt;\u0026lt; EOF [Service] Restart=always RestartSec=10s ExecStartPre=-/usr/bin/podman rm -f \u0026lt;db-service\u0026gt; EOF 至少這樣把邊界拆清楚了：主 Unit 視為可重建產物，執行期開關統一放在 Drop-in 管理。從目前腳本與後續現場結果看，也沒有再重現「因原地修改主 Unit 而導致服務檔損壞」的問題。\n問題二：Systemd 判定失敗，但容器其實已經啟動 第二類問題更隱蔽。故障階段，Systemd 會把某些服務標記為啟動逾時：\nActive: activating (auto-restart) (Result: timeout) Main PID: \u0026lt;pid\u0026gt; (code=exited, status=0/SUCCESS) 但同時 podman ps 又能看到容器確實已經起來，本質上是行程契約出現分裂。\npodman generate systemd 生成的 Unit 預設使用 Type=notify。這代表 Systemd 不會只因為行程存在就判定服務啟動成功，而是要求執行中的容器向宿主送出 sd_notify 就緒訊號。\n故障階段尚未修補時，生成產物中的關鍵欄位如下：\nType=notify NotifyAccess=all 在恢復後重新採集目前環境時，重新生成的產物仍保留這個預設值；而成功上線後實際生效的 Unit 已經改為 Type=simple：\n# 故障階段對應的預設生成結果 Type=notify # 修復上線後的生效 Unit Type=simple 這次現場能確認的現象是：當服務仍使用預設生成的 Type=notify Unit 時，容器雖然已經 Up，對應 Unit 仍會在逾時後被 Systemd 判為失敗並重啟。後來的修復做法，是把生成產物中的 Type=notify 改成 Type=simple，把狀態判定切回到「由 Systemd 直接追蹤前景行程」。\n這裡的邊界仍要說清楚：這並不等於已經證明 Rootless 場景下 notify 一定不可用，而是目前這套環境中，notify 沒有表現出穩定、可依賴的就緒語義。對這類長駐前景行程來說，Type=simple 是更保守、也更容易排障的選擇。\n問題三：一部分容器其實不在 Systemd 控制之下 更準確地說，這個危險狀態出現在修復過程裡，而不是最初故障現場：資料庫對應 Unit 已恢復為 active，但其餘服務對應 Unit 仍顯示 inactive (dead)。\n當時現場狀態大致如下：\npodman ps # ... 部分業務容器可能已重新起來 systemctl --user list-units \u0026#39;container-*.service\u0026#39; # ... container-\u0026lt;db-service\u0026gt;.service 為 active # ... 其餘服務對應 Unit 為 inactive (dead) 原因是這些容器一開始並不是由 Systemd 拉起，而是更早以前透過 podman-compose up -d 直接啟動。對這類非託管容器來說，即使容器行程已經存在，Systemd 仍未真正持有控制權，自然也談不上穩定接管、失敗感知與自動恢復。\n這暴露出一個很容易被忽略的前提：託管能力不只取決於 Unit 檔是否存在，更取決於行程是不是由 Systemd 真正持有。如果執行期控制權仍由 Compose 持有，那麼「已經寫了重啟設定」和「恢復能力真的生效」就不是同一回事。\n這裡也需要補充實作細節：目前部署腳本並不是完全不用 Compose。它仍會先用 podman-compose 完成建置與冷啟動，再呼叫 watchdog-enable.sh 生成並修補 Unit，隨後顯式停止這些由 Compose 拉起的容器，最後才透過有順序的 systemctl --user start 將控制權交回 Systemd。問題不在於「腳本裡出現過 podman-compose」，而在於恢復能力應生效時，執行期控制權究竟是不是還握在 Compose 手上。後來的調整，就是把這個交接動作固定下來：\n停掉現有非託管容器，重新由 systemctl --user start 接管 在交接前後做狀態檢查，避免再次留下「容器在跑，但 Systemd 不知情」的狀態 問題四：自動生成與恢復腳本仍需要補足細節 1）自動生成的 Unit 仍需後處理 重新回到 podman generate systemd --new 後，至少暴露出三類問題：\n透過 podman-compose 等工具初始化的參數會污染生成結果。例如原容器若帶有 -d，podman generate systemd --new 也會原樣帶出，導致 Systemd 執行 podman run -d 後，從託管視角看行程立刻退出，無法追蹤真正的容器主行程 Redis 是另一個典型案例：若原始設定透過空參數禁用危險命令，例如 rename-command \u0026quot;\u0026quot;，生成工具會靜默丟掉空字串，最後變成 redis-server --rename-command FLUSHALL，直接造成啟動參數錯誤 預設值 Type=notify 會再次把 Rootless 場景的逾時問題帶回來 Redis 這個問題，從原始容器參數和生成結果之間就能直接對照出來：\n# 原始容器參數 command: - --rename-command - \u0026#34;FLUSHALL\u0026#34; - \u0026#34;\u0026#34; # 未修補的 generate 結果 redis-server --rename-command FLUSHALL 即使在恢復後，重新執行 podman generate systemd --new --name \u0026lt;redis-service\u0026gt;，仍能看到空字串被吞掉；而實際生效的 Unit 已經補回：\n# 恢復後重新生成的結果 --rename-command FLUSHALL # 修復上線後的生效 Unit --rename-command FLUSHALL \u0026#34;\u0026#34; -d 也是同類問題。對照恢復後重新生成的產物與修復上線後的生效 Unit，可以直接看到預設生成結果仍包含 -d，而接管用的 Unit 已經把它移除：\n# 恢復後重新生成的結果 ExecStart=/usr/bin/podman run \\ ... -d \\ --sdnotify=conmon \\ # 修復上線後的生效 Unit ExecStart=/usr/bin/podman run \\ ... --sdnotify=conmon \\ 因此目前 watchdog-enable.sh 的流程是：先重新執行 podman generate systemd --new --name ...，再用 Python 補丁腳本修補生成產物。實際落地包含三件事：\n將 Type=notify 改成 Type=simple 移除 ExecStart 中殘留的 -d 對 Redis 的 rename-command \u0026quot;\u0026quot; 做回填，避免生成階段丟失空參數 這裡選擇 Python 也有很實際的理由：它更適合處理由 \\ 續行的多行命令，能讓生成產物的修補行為更可控。\n2）依賴關係會影響失敗容器的清理 資料庫服務恢復前，需要先執行 podman rm -f 清理舊容器；但因為上層服務仍持有依賴引用，刪除動作回傳 exit 125。這代表單容器恢復不是孤立操作，依賴圖會反向影響清理過程。\n直接報錯大致如下：\nProcess: ExecStartPre=/usr/bin/podman rm -f \u0026lt;db-service\u0026gt; (code=exited, status=125) 目前腳本的處理方式，是把依賴清理邏輯寫進 Drop-in：\n資料庫服務在 ExecStartPre 中會先停掉依賴它的上層服務，再執行 podman rm -f \u0026lt;db-service\u0026gt; 其他幾個服務只清理各自容器 這不是一套通用模板，而是針對這次這組服務依賴關係做的腳本化處理。\n3）恢復鏈路不能只依賴固定 sleep 早期腳本確實存在靠固定 sleep 等待依賴的做法。\n目前部署腳本在 Compose 冷啟動階段仍保留少量固定等待；但在控制權交給 Systemd 之後，已經改成按依賴順序逐個 systemctl --user start，並配合 wait_for_service() 輪詢 is-active / is-failed，在失敗或逾時時輸出對應的 journalctl。這比盲等更容易定位失敗點。\n因此最後把啟動順序改成顯式編排：\n\u0026lt;db-service\u0026gt; -\u0026gt; \u0026lt;redis-service\u0026gt; -\u0026gt; \u0026lt;backend-service\u0026gt; -\u0026gt; \u0026lt;frontend-service\u0026gt; 在全部服務接管完成後，腳本還會再補一次應用層健康檢查，用來確認「服務 active」已進一步接近「對外可用」。這一步是對 Type=simple 取捨的補償，不是每個服務啟動時都執行的深度 readiness 檢查。\n交接給 Systemd 後，等待邏輯大致如下：\nsystemctl --user start container-\u0026lt;db-service\u0026gt;.service for i in $(seq 1 60); do systemctl --user is-active --quiet container-\u0026lt;db-service\u0026gt;.service \u0026amp;\u0026amp; break systemctl --user is-failed --quiet container-\u0026lt;db-service\u0026gt;.service \u0026amp;\u0026amp; exit 1 sleep 1 done 4）Systemd 的頻率限制會把服務鎖死 當依賴尚未就緒時，Frontend 會反覆失敗並重啟。例如 Nginx 在啟動時會強制解析 \u0026lt;backend-service\u0026gt; 的 upstream 名稱。若在設定視窗內重啟次數超過 StartLimitBurst，Systemd 就會停止重試。這時即使後續依賴恢復正常，服務也不會自動起來，除非顯式重設失敗狀態：\ncontainer-\u0026lt;frontend-service\u0026gt;.service: Start request repeated too quickly. container-\u0026lt;frontend-service\u0026gt;.service: Failed with result \u0026#39;exit-code\u0026#39;. 在 Nginx 這一側，應用層證據通常更直接：\n[emerg] 1#1: host not found in upstream \u0026#34;\u0026lt;backend-service\u0026gt;\u0026#34; in default.conf:31 systemctl --user reset-failed container-\u0026lt;frontend-service\u0026gt;.service deploy-all.sh 現在也已經在拉起 Frontend 前補上一個 reset-failed，用來清掉此前可能殘留的鎖死狀態。\n本次故障中已確認的致因因素 從表面看，這次故障像是資料庫異常後，現場狀態與接管狀態一路分裂；回頭看，其實是幾類問題疊加：\n主 Unit 被腳本直接改寫，服務定義本身存在被破壞的風險 podman-compose 直接啟動與 Systemd 託管並存，導致容器狀態與託管狀態分離 podman generate systemd 的產物不能直接拿來用，仍需補齊 Type、-d 與參數修補 恢復腳本最初對依賴清理、頻控恢復與最終健康檢查覆蓋不足 這不是某一個開關寫錯造成的單點問題，而是恢復鏈路在定義、接管與執行三個層面都出現斷點。因此修復也不只是改一條報錯，而是把執行期控制權、生成後修補與失敗後診斷入口重新串成一條可用的鏈路。\n已落地的修復動作 結合 watchdog-enable.sh 與 deploy-all.sh，目前實際落地的動作大致如下：\n部署腳本先檢查執行使用者、HOME、XDG_RUNTIME_DIR 與 DBUS_SESSION_BUS_ADDRESS，降低 Rootless User Units 因執行環境不一致而失效的機率 部署階段仍使用 podman-compose 完成建置與冷啟動，但在交接階段會顯式停掉這些容器，再由 Systemd 按順序接管 watchdog-enable.sh 會先備份舊 Unit，再重新生成 .service 生成後的 .service 改由 Python 腳本做三項修補：改成 Type=simple、刪除 -d、補回 Redis 空參數，同時避開先前 sed -i 路徑裡 PowerShell 跳脫截斷的問題 Drop-in 中統一寫入 Restart=always、RestartSec、StartLimit* 以及各服務對應的 ExecStartPre 交接給 Systemd 後，腳本按 \u0026lt;db-service\u0026gt; -\u0026gt; \u0026lt;redis-service\u0026gt; -\u0026gt; \u0026lt;backend-service\u0026gt; -\u0026gt; \u0026lt;frontend-service\u0026gt; 順序啟動，並用 wait_for_service() 檢查 is-active / is-failed；若失敗或逾時，直接輸出對應 journalctl 在拉起 Frontend 前補一次 reset-failed，避免殘留的頻率限制狀態阻斷恢復 全部服務接管完成後，再追加一次應用層健康檢查，確認系統不只「服務 active」，也更接近「對外可用」 這套做法仍然是在單機、Rootless、User Units 這組限制下的工程化方案，解決的是「誰持有行程、如何恢復、失敗後如何排查」這些具體問題，並不等同於更廣義的高可用設計。\n修復後如何驗證 在交接前後對照 podman ps 與 systemctl --user list-units，確認不再出現「容器在跑，但對應 Unit 為 inactive」的狀態 透過 wait_for_service() 輪詢 is-active / is-failed，讓服務在啟動階段就能暴露失敗點，而不是交給固定 sleep 若服務失敗或逾時，立即輸出對應 journalctl，把排查入口固定到具體 Unit 全部服務接管完成後補一次應用層 /health 檢查，避免只用 Type=simple 的行程在線語義來推斷外部可用性 恢復後再次核對時，四個容器與四個 User Unit 已重新對齊：\npodman ps \u0026lt;db-service\u0026gt; / \u0026lt;redis-service\u0026gt; / \u0026lt;backend-service\u0026gt; / \u0026lt;frontend-service\u0026gt; 均為 Up systemctl --user list-units \u0026#39;container-*.service\u0026#39; 對應四個 Unit 均為 active (running) 這些驗證主要覆蓋「是否由 Systemd 持有行程」「失敗時是否能及時暴露」「接管完成後是否對外可用」，尚未擴展到更細的 readiness 訊號與持續觀測。\n這次排障後明確下來的約束 主 Unit 只保留為生成產物，執行期開關統一放到 Drop-in；這不是形式上的規範，而是為了避免主檔一旦損壞，就同時打斷解析、啟動與重啟策略 podman generate systemd 的產物不能直接視為可執行結果；生成、修補與重載應被視為同一個步驟。就這次腳本而言，Type=notify、-d 與空參數問題都屬於生成後必須處理的相容性項 部署期控制面與執行期控制面需要明確分開；Compose 仍可用於建置與冷啟動，但進入執行期後，控制權必須顯式交回 Systemd，否則「已寫重啟設定」與「恢復能力真的生效」會繼續混在一起 恢復鏈路不能只寫 Restart=always 就算完成，還必須覆蓋依賴清理、頻率限制恢復、狀態輪詢與最終健康檢查，讓故障後的恢復與排查可重複執行 後續演進方向 這次修復解決了恢復鏈路中的幾個確定問題，但仍有幾類風險尚未完全覆蓋，後續仍需要繼續推進：\nType=simple 配合 is-active / is-failed 與最終 /health 檢查，已補上「行程在線」與「實際可用」之間的缺口，但仍缺少更細粒度的 readiness 訊號；若服務短暫變成 active，之後才暴露內部初始化失敗，現有腳本仍可能較晚才發現 目前方案仍依賴 podman generate systemd 加腳本修補；只要生成產物格式、參數展開方式或 Podman 版本行為改變，現有補丁邏輯就可能失效，因此是否改用 Quadlet 或其他宣告式方式，仍值得單獨評估 目前驗證仍偏向腳本內診斷；失敗次數、退出碼、容器狀態與頻率限制命中情況尚未接入採集、上報與告警，代表系統外層仍缺少持續觀測能力，類似問題更可能在故障後才被動暴露 這次排障帶來的直接改善 由誰啟動行程、由誰負責重啟，邊界比之前清楚 自動生成的 Unit 是否可直接使用，如今有了明確判準與後處理步驟 接管失敗時該看哪一層日誌、在哪一步退出，腳本內已有穩定入口 服務 active 與對外可用之間的差異，已補上一層應用層驗證 這些改動並不代表整套方案已成為更廣義的高可用設計，但至少把原本「重啟配置存在、恢復卻不可靠」的狀態，整理成一條更容易驗證、也更容易重複執行的恢復鏈路。\n結語 這次排查最後留下的結論其實很直接：設定裡寫了 Restart=always，不代表恢復鏈路就已經完整。主 Unit 是否穩定、容器是不是由 Systemd 啟動、生成產物有沒有修補、依賴與頻率限制有沒有被腳本覆蓋，這些細節都會影響最終結果。\n至少在這次這套單機 Rootless Podman 場景裡，把這些斷點補進腳本之後，行程控制權、排查入口與故障處置路徑都比之前清楚很多。這未必表示方案已經「完美」，但至少把一次原本高度依賴經驗判斷的故障恢復，整理成一條更可解釋、也更容易驗證的工程鏈路。\n附錄：關於 Linger 機制的補充說明 若 Rootless Podman 要搭配 systemd --user 託管服務，通常需要先為目標使用者啟用 linger。否則一旦該使用者登出，使用者層級的 Systemd 實例可能被回收，相關服務也就無法在「無人登入」的情況下持續運行。本次事故發生時，環境已滿足 Linger=yes，因此 linger 不是本次故障根因，這裡僅作為前置檢查項補充。\n# 檢查目前狀態 loginctl show-user \u0026lt;deploy-user\u0026gt; --property=Linger # 若尚未啟用，請由具 sudo 權限的使用者執行： sudo loginctl enable-linger \u0026lt;deploy-user\u0026gt; ","permalink":"https://intent.me/zh-tw/blog/tech/rootless-podman-systemd-watchdog-postmortem/","summary":"一次 Rootless Podman + Systemd 使用者單元託管失效的復盤，梳理已確認的致因因素、實際修復動作、驗證方式與尚未覆蓋的風險。","title":"Rootless Podman + Systemd 託管失效復盤：一次恢復鏈路排查與修復紀錄"},{"content":"這篇文章記錄一次 JWT 雙票（AT/RT）治理的實際經驗。\n我們想回答的問題很簡單：當刷新令牌在異常情境下被重放時，系統能不能快速發現並收口。\n說明：本文的網域、IP、帳號識別、會話識別與日誌內容皆為脫敏示例，不對應任何真實生產資料。\n系統邊界如下：\n前端：Web SPA（AT 僅存於記憶體） 後端：Go + Gin（鑑權與刷新） 會話層：Redis（令牌狀態索引） 閘道：HTTPS 反向代理 現象與觸發條件 在儲能雲平台的日常安全演練中，我們針對大量邊緣閘道與前端看板流量做了認證鏈路滲透測試，其中包含「舊 RT 被攔截後重複提交」的極端情境。\n舊方案雖然有 AT/RT 分層，但刷新路徑缺少完整狀態約束，舊 RT 在短時間並發下仍可能被利用。\n脫敏事件樣式如下：\n{ \u0026#34;event\u0026#34;: \u0026#34;auth.refresh.reuse_detected\u0026#34;, \u0026#34;user_id_masked\u0026#34;: \u0026#34;u-***39\u0026#34;, \u0026#34;session_id\u0026#34;: \u0026#34;s-***b1\u0026#34;, \u0026#34;ip_masked\u0026#34;: \u0026#34;10.**.**.21\u0026#34;, \u0026#34;action\u0026#34;: \u0026#34;session_revoked\u0026#34; } 排查與改造策略：保留 AT 無狀態，強化 RT 治理 我們沒有改動雙票結構，而是針對 RT 刷新主路徑補齊治理能力，目標是可撤銷、可審計、可回收。\n第一層：維持清楚的令牌分工 AT 負責高頻請求，走 Authorization: Bearer RT 僅透過 HttpOnly Cookie 傳輸 AT 校驗維持無狀態，確保效能與擴展性 這樣可以把控制面集中在 RT，不影響 AT 的輕量存取。\n第二層：把 RT 刷新收斂為單次可追蹤流程 刷新路徑遵循固定順序：校驗舊 RT -\u0026gt; 簽發新 RT -\u0026gt; 更新 Redis 狀態 -\u0026gt; 失效舊 RT。\nRedis Key 模型（脫敏示例）：\nrt:active:{jti} -\u0026gt; { user_id, session_id, exp } // 核心狀態，TTL 跟隨 RT 到期 rt:session:{session_id} -\u0026gt; Set\u0026lt;jti\u0026gt; // 冗餘索引，用於會話級一鍵清理 rt:deny:{jti} -\u0026gt; 1 (TTL=剩餘有效期) // 黑名單，攔截被重放的舊票據 user:sessions:{user_id} -\u0026gt; Set\u0026lt;session_id\u0026gt; // 使用者級管控，支援「踢出其他裝置」 對應約束：\n同一 RT 僅允許成功刷新一次 新 RT 生效後，舊 RT 立即進入 deny 清單 刷新流程加入短時鎖，避免並發競態 並發防抖設計（結果復用視窗） 現代 SPA 常會同時送出多個請求，若沒有防抖協調，舊 RT 可能在同一時間觸發多次刷新，進而讓合法請求被誤判為重放。為此我們設計了 5 秒復用視窗：\n第一個請求先拿鎖並完成 RT 輪換 輪換後的 AT/RT 會快取 5 秒 同 key 的併發請求在 5 秒內直接復用同一組 AT/RT，不再二次輪換 視窗到期後，恢復嚴格單次輪換策略 第三層：重放事件的處置方式 一旦識別 RT 重放，系統會對相關會話執行失效、寫入審計事件，並引導前端回到登入流程。\n重點不是硬撐原會話，而是盡快終止風險會話。\n第四層：讓登出與高風險操作觸發服務端吊銷 以下操作統一接入會話失效流程：\n使用者主動登出 管理員重設密碼 使用者狀態切換為鎖定或停用 也就是說，登出不再只是刪除瀏覽器 Cookie，而是服務端狀態同步失效。\n根因總結 這次問題本質上是治理深度不足，而非單一程式錯誤：\n早期實作以簽發與驗簽為主，對令牌生命週期覆蓋不足。 會話狀態能力雖已預留，但未完整接入刷新主鏈路。 改造後的收益 這次治理後，鏈路上的變化主要有三點：\n會話可在服務端快速撤銷 RT 重放可被識別並追蹤 安全事件可按使用者與會話維度快速定位 對使用者體驗影響可控：正常路徑維持無感刷新，異常路徑回到重新登入。\n過程改進與行動項 1）把令牌生命週期校驗納入發版檢查 發版前固定校驗：\nRT 單次使用驗證 刷新並發一致性驗證 登出/重設密碼後的會話失效驗證 2）統一安全審計欄位 審計日誌統一採用脫敏欄位：user_id_masked、session_id、jti_prefix、ip_masked、ua_hash、risk_level。\n3）固定進行重放演練 每月執行一次 RT 重放演練，持續驗證告警、失效與回收流程是否正常。\nPhase 2 演進路線（Roadmap）：從會話可控到上下文零信任 經過這次改造，Phase 1 已完成核心目標：建立 Redis 會話撤銷鏈路與並發防重放機制。Refresh 流程已能即時校驗使用者狀態，帳號一旦鎖定或停用，會立即阻斷新票據簽發。\n下一階段將針對高階 Token 劫持場景持續演進：\n上下文感知風控：規劃引入環境指紋（IP 段 + UA Hash）比對。當 RT 出現異常地域跳變時，自動執行 AT/RT 全量失效，並聯動帳號凍結與第三方二次身分驗證。 誤判治理：考量工業現場常見的 4G/5G 頻繁切網與出口漂移，相關策略會先完成風控模型調優，再以灰度方式逐步切入主鏈路。 結語 認證安全真正的考驗，不在簽發速度，而在異常發生後的收口速度。\n這次改造最有價值的地方，是讓 AT/RT 架構從「可用」進一步走向「可治理」。\n","permalink":"https://intent.me/zh-tw/blog/tech/jwt-at-rt-redis-hardening-postmortem/","summary":"一次 JWT AT/RT 架構安全加固復盤：將 Redis 預留能力視為已落地，實現 RT 輪換、重放檢測與會話可撤銷。","title":"JWT 雙票體系加固復盤：從無狀態刷新到 Redis 可撤銷會話"},{"content":"近期在推進核心業務線基礎架構安全演進時，我們將全域 HTTP 協議升級為 HTTPS。Nginx 閘道層完成 SSL 配置並啟用 443 強制跳轉後，回歸測試穩定復現登入介面的 403 Forbidden。\n返回報文如下：\n{\u0026#34;code\u0026#34;:403,\u0026#34;msg\u0026#34;:\u0026#34;forbidden: invalid origin\u0026#34;} 系統架構：\n前端：Vue 後端：Go + Gin 閘道：Nginx 反向代理 執行環境：Podman（Rootless 模式） 排查策略：自頂向下，逐層剝離 本次排查遵循「邊界清晰、逐層驗證」原則，依序穿透閘道層、應用層與容器執行時層。\n第一層：閘道層（Nginx） 第一反應是 Nginx 未正確透傳請求標頭。檢查配置後確認 Origin 等關鍵標頭已正常透傳。\n除錯過程中發現，只要在閘道中強行注入：\nproxy_set_header Origin \u0026#34;http://業務域名\u0026#34;; 介面就能「恢復正常」。\n但這個方案本質是在偽造請求來源，用 HTTP 值去冒充 HTTPS 請求，等同削弱後端 CSRF 防線。該 workaround 被明確廢棄，繼續向應用層深挖。\n第二層：應用層（CORS 與 CSRF 的錯位） 抓包確認回應標頭中已返回：\nAccess-Control-Allow-Origin: https://業務域名 說明 CORS 中介軟體對 HTTPS 來源已放行。問題在於：\nCORS 負責「瀏覽器能否讀取跨域回應」 CSRF 負責「後端是否信任請求來源」 兩者並行但獨立。\n最終在程式碼中定位到核心遺漏：協議升級時只更新了 CORS 白名單，CSRF 中介軟體仍保留舊的 http:// 信任列表。請求通過了 CORS，卻被 CSRF 攔截並返回 invalid origin。\n修復動作：同步補齊 CSRF 中介軟體中的 https:// 域名白名單。\n第三層：執行時層（Podman Rootless 隔離） 程式碼修復並觸發建置後，線上現象仍未消失。進一步透過 podman inspect 比對映像雜湊，發現一個工程化陷阱：\n生產服務運行在 deploy 帳號的 Rootless 命名空間 部分映像曾在 Root 命名空間建置 Rootless 隔離下，deploy 無法感知 Root 空間中的新映像 結果是部署腳本反覆重啟舊映像，形成「程式碼已修復、環境未生效」的假象。\n為了從機制上杜絕這類問題，我們在生產機落地強約束：以 /usr/local/bin/podman 與 /usr/local/bin/podman-compose 包裝腳本攔截 uid=0，一律拒絕執行。當 which podman 指向包裝器後，即使 root 誤操作也會立即失敗，避免再次污染錯誤命名空間的映像。\n根因總結 這次故障不是單點 Bug，而是兩類系統性遺漏疊加：\n架構規範缺口：CORS 與 CSRF 白名單分散配置，協議升級時缺乏統一治理入口。 工程閉環缺失：生產映像建置權限未完全收斂到標準化 CI/CD，導致跨命名空間髒狀態。 流程改進與行動項 1）完善安全發版 SOP 與 CheckList 將「CORS 與 CSRF 策略一致性校驗」納入強制發布檢查項，新專案上線與協議升級必須通過該關卡。\n2）收斂生產環境運維準入 明確應用容器僅允許透過 CI/CD 流水線建置與發布，禁止人工跨權限命名空間干預線上服務。\n3）推進可觀測性規範 將中介軟體載入與路由註冊順序納入架構規範，確保日誌層前置於安全攔截層，避免「被攔截但無日誌」的幽靈請求。\n結語 這次 403 排查的價值不在於「把介面調通」，而在於暴露了安全配置治理與交付鏈路上的薄弱點。技術修復解決當下，流程與制度收斂才是避免同類問題重演的關鍵。\n","permalink":"https://intent.me/zh-tw/blog/tech/https-upgrade-403-postmortem/","summary":"一次 HTTPS 協議升級後穩定復現 403 的排障復盤，最終定位為 CSRF 白名單遺漏與 Podman Rootless 映像隔離的疊加問題。","title":"記一次 HTTPS 升級引發的 403 排查：從安全中介軟體到容器隔離機制的深度復盤"},{"content":"在 Modbus-Tools 的迭代過程中，遇到過一次典型的並發問題：視窗已關閉，但程序未退出，調試會話也無法正常結束。後續修復中，又暴露出通信超時誤判與重入競爭問題。本文記錄排查過程與最終方案。\n現象一：退出階段卡住 主執行緒銷毀 MainWindow 時會等待工作執行緒退出 工作執行緒可能仍阻塞於串口或網路讀寫流程 UI 已消失，但程序保持存活 原因一：執行緒依附關係不一致 QSerialPort 與 QTcpSocket 的事件處理依賴其所屬執行緒事件循環。若物件建立於主執行緒，但調用路徑在工作執行緒，退出階段容易出現互相等待。\n處理一：明確物件歸屬執行緒 在通道建立後顯式遷移至工作執行緒：\nstack.thread = std::make_shared\u0026lt;QThread\u0026gt;(); stack.channel-\u0026gt;moveToThread(stack.thread.get()); 同時確保物件建立時不綁定 parent，避免遷移失敗。\n現象二：連線後偶發 Timeout 監控抓包可見設備有回應 業務層仍返回超時 原因二：阻塞等待餓死事件循環 請求送出後使用 condition_variable::wait_until 阻塞等待，導致承載 IO 的執行緒無法即時處理 readyRead 事件，回包回調被延後。\n處理二：改為可讓渡事件的等待循環 while (true) { QCoreApplication::processEvents(QEventLoop::AllEvents); if (signalReceived) break; if (timeout) return Error; std::this_thread::sleep_for(std::chrono::milliseconds(1)); } 該方案在可控範圍內維持回應性，同時避免忙等佔滿 CPU。\n現象三：高頻操作下偶發自鎖 引入 processEvents 後，等待窗口期內可能觸發同執行緒重入，導致同一把互斥鎖被重複申請。\n原因三：鎖模型與重入路徑不匹配 原有 std::mutex 無法支持同執行緒重複進入臨界區。\n處理三：改用遞歸鎖保護請求序列 std::recursive_mutex requestMutex_; 復盤結論 IO 物件所屬執行緒必須在設計階段固定並貫穿生命週期管理。 IO 執行緒中應避免長時間阻塞等待，必要時採用可處理中斷事件的等待策略。 引入事件讓渡後，要同步評估重入路徑與鎖策略。 這次修復不僅解決了退出掛起，也提升了 Modbus 通信超時判定的一致性。對工業軟體而言，穩定退出與時序可解釋性與功能本身同等重要。\n","permalink":"https://intent.me/zh-tw/blog/tech/modbus-deadlock-fix-notes/","summary":"記錄一次 Qt 多執行緒 Modbus 場景中的退出死鎖與超時誤判問題，以及對應的工程化修復方案。","title":"踩坑日記：Modbus 多執行緒死鎖修復記錄"},{"content":"Modbus-Tools C++20 | Qt6 | Industrial Protocols\n本文基於 Modbus-Tools 最新版本撰寫，更新於 2026-04-21\n在工業自動化與嵌入式開發中，Modbus 幾乎是日常的一部分。真正進入現場聯調後，時間通常花在重複操作：手動組幀、反覆核對日誌、逐筆做倍率換算。\n開發 Modbus-Tools 的目標很明確：把「發幀、看日誌、解幀」三件高頻操作做順手。它不追求功能堆疊，而是以調試效率和可用性為優先。實作上採用 channel / transport / session / parser 分層設計，並配套 CI/CD、多語言與自動更新能力，方便工具持續迭代。\n以下依照實際使用順序，快速介紹核心操作方式。\n1. 快速連線與報文建立 打開軟體後，不論使用 Modbus RTU（串口）或 Modbus TCP（網路），連線參數都集中在左側面板，設定很直觀。\n在聯調時，最需要的是快速驗證。Modbus-Tools 將參數與功能碼拆分得清楚易用：\n一鍵發送常用功能碼：填入 從站地址 (Slave ID)、起始地址、數量/資料，點選 01/02/03/04/05/06/0F/10 等按鈕即可發送。底層自動組幫並計算 CRC/LRC。功能碼涵蓋 0x01–0x04（讀）、0x05–0x06（單寫）、0x0F–0x10（多寫）。 HEX / DEC 智慧識別：Slave ID 與 起始地址 支援 HEX（如 0x10、10H）與 DEC（如 16）兩種格式輸入，由 parseSmartInt() 統一解析並做範圍校驗。 寫入格式可切換（HEX / DEC / Binary）：在 Write Data 旁透過 Format 下拉選單切換 Hex、Decimal 或 Binary，可依專案習慣輸入。 Raw 模式增強：需要發送自定義 Hex 報文時，可直接切換 Raw 輸入並發送，適合非標流程測試。Raw 模式內建兩個輔助按鈕： Append CRC (RTU)：自動計算並追加 CRC16 校驗值至輸入框。 Add MBAP (TCP)：自動封裝 Modbus TCP 主站報頭（Transaction ID / Protocol ID / Length / Unit ID）至輸入框。 線圈 (Coils) 二進位下發互動 針對位元操作場景，工具提供了直覺的 Binary 輸入模式：\nBinary 輸入：支援直接輸入位元串（如 1 0 1 1），系統自動編碼並配合 0x05（單線圈寫入）或 0x0F（多線圈寫入）功能碼下發。 位元級讀取：配合 0x01/0x02 讀取指令，實現對遠端設備線圈與離散輸入狀態的高效驗證。 2. 看日誌與一鍵複製 (Traffic Monitor) 問題定位通常從日誌開始。Traffic Monitor 的設計重點是可讀性與可分享性。\nTX/RX 分離顯示：發送與接收資料分色呈現，並附毫秒級時間戳，時序關係更清楚。 一鍵複製：可快速複製關鍵報文，用於問題回報、團隊溝通與測試記錄。 方向過濾：支援僅顯示 TX 或 RX，面對高頻輪詢時更容易聚焦。 日誌可匯出保存：可將當前通訊記錄保存，方便現場問題留痕與測試歸檔。 3. 幀解析器 (Frame Analyzer)：把 Hex 變成可讀資料 Frame Analyzer 是日常使用頻率很高的模組。將 Hex 報文貼上後點擊解析，即可看到結構化欄位與可讀表格。\n在現場最實用的是以下幾個能力：\n解碼模式切換 (Unsigned / Signed) 解析器工具列提供 Decode Mode，可在 Unsigned 與 Signed 間切換。切換後會立即刷新解析結果，十進位、Hex、二進位與換算值都會同步更新，查看有符號量更直觀。\n倍率換算 (Multiplier Scaling) 許多設備會將浮點值放大後再傳輸（例如 220.5V -\u0026gt; 2205）。\n在表格中可針對寄存器設定 Scale（如 0.1、0.01），系統會即時顯示換算後的工程值。\n多維位元序分析 (Byte Order) 不同廠商的 PLC 和儀表可能採用不同的資料排列方式。解析器支援 四種位元組/字序模式：\nABCD (Big Endian)：大端模式，高位在前。 CDAB (Little Endian Byte Swap)：小端位元組交換。 BADC (Big Endian Byte Swap)：大端位元組交換。 DCBA (Little Endian)：小端模式，低位在前。 切換位元序後，寄存器值會重新計算並顯示。\n寄存器功能註解 (Description) 可為寄存器地址補上說明，例如「A 相電壓」「電機轉速」。\n數值與語意同時呈現，減少來回查表時間。\n配置持久化與 JSON / CSV 模板 配置可保存並重複利用。\n自動保存：保留最近使用的倍率與註解。 JSON / CSV 匯入/匯出：可依設備建立專屬模板，切換機型時直接套用。 其他實用細節 Format Hex 按鈕：可一鍵清理並規範 Hex 輸入格式，貼上長報文後更易閱讀。 回應起始位址可設定：可透過 Start Address 配合回應幀解析，對齊寄存器點位表。 協議模式可選：支援 Auto Detect / Modbus TCP / Modbus RTU，方便混合抓包場景快速判斷。 強制解析 (Force Parse) 現場抓包時，報文可能因截斷、中間設備修改等原因導致校驗不通過。當使用者在 Protocol 下拉選單中手動指定 TCP 或 RTU（而非 Auto Detect）時，解析器進入強制模式：\nRTU 強制模式：CRC 不匹配時不再直接報錯終止，而是標記 checksumValid = false 並在 warnings 中記錄 \u0026quot;CRC Mismatch (Forced)\u0026quot;，同時繼續解析 PDU 資料欄位。 TCP 強制模式：MBAP length 欄位異常或幀尾有多餘位元組時，記錄對應 warning 後仍按實際位元組長度提取 PDU。 此機制適用於分析被閘道器/中繼修改過、或從串口抓包工具截取的不完整報文。Auto Detect 模式下校驗嚴格，適合正常通訊場景的精確驗證。\nLink to Analyzer (即時聯動) 除手動貼上 Hex 報文外，Frame Analyzer 也支援從 Traffic Monitor 接收即時資料：\n自動推送：在 Modbus TCP / RTU 視圖中開啟 Linkage 開關後，RX 回應報文的 PDU 會自動送入解析器，無需手動複製。 暫停 / 恢復：點擊 Pause Refresh 可駐留當前幀，方便編輯 Scale 或 Description；再次點擊 Resume Refresh 恢復自動重新整理。 停止聯動：點擊 Stop Link 中斷資料流，解析器恢復為手動模式。 非同步執行：解析邏輯運行於 QThread 背景執行緒，不阻塞 Traffic Monitor 的列表捲動。 如果你同時維護多種型號設備，依設備或專案儲存獨立模板後，切換調試物件時可直接匯入，減少重複配置。\n4. 補充工具 除 Modbus 主流程外，也內建兩個輕量模組，方便臨時調試：\nTCP Client：快速驗證自定義網路報文。 Serial Port：基本串口收發測試（ASCII/Hex）。 5. 工程品質與測試保障 作為持續迭代的開源專案，Modbus-Tools 在程式碼品質方面投入了相當精力：\n自動化測試 專案採用 Google Test (GTest) 與 Google Mock (GMock) 框架進行自動化品質檢測，覆蓋以下核心模組：\n會話管理：連線/斷線邏輯、請求逾時重試及異常狀態恢復。 協議傳輸：TCP/RTU 報文封裝/解包、校驗和計算及完整性驗證。 解析邏輯：針對多種有效指令及畸形報文的魯棒性驗證。 資料處理：位元序轉換、工程量縮放及格式化演算法的計算準確性。 目前全量 42 個自動化測試案例（TEST + TEST_F），覆蓋會話管理、協議傳輸、解析邏輯、資料處理及格式化等模組，每次 Release 發布均執行迴歸測試。\nCI/CD 整合 GitHub Actions 流水線整合 MSVC AddressSanitizer (ASan)，用於記憶體損壞與洩漏的自動化監測。 支援自動建置、測試及 Release 解析包分發。 自動更新 (OTA) 工具整合了基於 GitHub Releases 的自動更新機制：啟動時靜默偵測新版本 + 功能表列手動檢查。更新包透過 SHA256 校驗後執行替換，支援 UpdateOnly（增量）和 Full Package 兩種模式。\n結語 Modbus-Tools 的設計目標是將高頻調試操作（發幀、看日誌、解幀）封裝為可複用的工作流，使開發者能更專注於業務邏輯與問題定位。\n功能概要：\n快速組幀：HEX/DEC 智慧識別（parseSmartInt）+ Raw 模式 CRC/MBAP 輔助計算。 即時聯動：Link to Analyzer 支援 RX 報文自動推送至解析器。 深度分析：倍率換算（Scale Factor）+ 四種位元序（ABCD/BADC/CDAB/DCBA）+ 寄存器描述。 位元級控制：線圈 Binary 輸入模式，支援 0x05/0x0F 功能碼。 品質保障：42 個自動化測試 + CI/CD 整合 MSVC AddressSanitizer。 若你正在做嵌入式或上位機開發，歡迎前往 GitHub 倉庫 體驗並提出建議。\n","permalink":"https://intent.me/zh-tw/blog/tech/modbus-tools-intro/","summary":"面向嵌入式開發與現場聯調的輕量級 Modbus 工具。介紹快速發幀、日誌查看、幀解析（倍率換算、寄存器註解、JSON / CSV 配置持久化）、Link to Analyzer 即時聯動分析等功能的實際用法。","title":"Modbus-Tools 深度解析：高效輕量的工業協議調試利器"},{"content":"package main import \u0026#34;fmt\u0026#34; func main() { fmt.Println(\u0026#34;你好，Intent\u0026#34;) } 關於我 我是一名遊走在 嵌入式系統 與 雲原生 技術邊界的軟體工程師。\n擁有 電力系統 (EMS/PCS) 行業背景，致力於構建連接物理世界與數位世界的可靠系統。\n核心: C/C++, Golang, Qt, Modbus, CAN, MQTT, IEC-104, IEC-61850, 協議轉換 關注: 物聯網協議, 實時控制, 分佈式架構 \u0026ldquo;Stay hungry, stay foolish.\u0026rdquo;\n這裡是紀錄我技術探索與開源專案的數位花園。\n","permalink":"https://intent.me/zh-tw/blog/hello-world/","summary":"fmt.Println(\u0026ldquo;你好，Intent\u0026rdquo;)","title":"Hello World"},{"content":"🏗️ 開源專案 (Open Source) 這裡展示了我個人開發或參與的開源專案。對於我在企業工作期間負責的商業專案（如儲能監控系統、工業雲平台等），請移步至 履歷 查看詳細介紹。\n專案名稱 簡介 技術棧 傳送門 Modbus-Tools 一個面向嵌入式聯調的輕量級 Modbus 調試工具，支援 RTU/TCP、可視化發幀、Frame Analyzer 解析，以及可重用的 JSON / CSV 模板配置。 C++20 Qt6 Industrial Protocols 📖 深度解析\n🏗 源碼倉庫 更多個人實驗性專案代碼請訪問我的 GitHub 主頁.\n","permalink":"https://intent.me/zh-tw/projects/","summary":"個人開源專案與技術實踐","title":"專案"},{"content":" 明裕成 嵌入式 / 上位機軟體工程師 ｜ 3年經驗 ｜ 聚焦儲能系統、工業通信與雲邊協同\n學歷：本科 ｜ 城市：中山（意向廣州/深圳） ｜ 郵箱：點擊獲取 ｜ GitHub：mingyucheng692\n核心能力 程式語言：C / C++ ｜ Golang ｜ Python ｜ Shell 工業協議：Modbus RTU / TCP ｜ MQTT ｜ IEC-104 ｜ CAN 框架與工具：Qt6 ｜ CMake ｜ Docker ｜ Redis ｜ PostgreSQL ｜ Git 工作經歷 泓慧能源·南方總部 / 廣東睿來華控科技有限公司 軟體工程師 ｜ 2025.06 - 至今 ｜ 廣東中山\n▸ 所在公司為泓慧能源（飛輪儲能頭部企業）全資子公司，承擔南方研發與量產基地核心系統開發\n參與南方基地儲能軟體體系（端-邊-雲）從 0 到 1 建設，負責銜接集團技術規範，交付的軟體模組直接服務於本地产線設備的整機測試與現場出廠交付。 深度對接硬體與電氣規約團隊，解決複雜工業現場的通信擁塞與信號抖動等痛點，保障底層電力電子設備與上層系統的可靠交互。 協助推行團隊 Git 協同與代碼審查機制；針對跨部門設備聯調耗時長的痛點，主導開發多套通用聯調工具鏈，以可視化解析取代手工查表與拼接，顯著縮短現場排障週期。 代表項目 飛輪儲能監控系統上位機（FMS） 核心開發 ｜ C++ / Qt6 / Modbus-TCP / CAN / SQLite / IOCP ｜ 2025.06 - 至今\n重構 Modbus-TCP 通信鏈路，引入 Windows IOCP 與連接池機制，解決高頻數據採集導致的界面阻塞問題，CPU 占用率降低約 15%。 開發底層 DSP 控制板專屬調試模組（基於 ZLG CAN SDK），實現 IEEE 754 浮點數與 HEX 報文的雙向動態解析，支援多位元組序（CDAB/ABCD）自動轉換與暫存器語義映射，替代 CANTest 手動抓包換算流程，顯著提升軟硬體聯調效率。 設計滑動窗口算法實現報警信號防抖；基於 SQLite WAL 模式實現高頻報文落盤，支援現場歷史數據的高效本地檢索與追溯。 推進 CMake 模組化構建，並整合 Crash Dump 異常捕獲機制，將全量編譯耗時從 5 分鐘壓縮至 50 秒內，顯著提升日常開發迭代與現場排障效率。 飛輪儲能邊緣通信網關 核心開發 ｜ C / STM32F407 / FreeRTOS / MQTT / Modbus / IEC-104 ｜ 2025.12 - 至今\n負責基於 STM32F407 與 FreeRTOS 的邊緣網關核心業務邏輯，向下通過 Modbus/RS485 輪詢底層設備，向上通過 MQTT 建立與雲端的時序數據通道。 實現 Modbus、IEC-104 與 MQTT 之間的協議解析與映射轉換，打通設備端、電力規約側與平台側的數據交互閉環。 針對現場弱網工況，設計並實現時序數據緩存與斷點續傳機制，保障網絡波動下的數據完整性。 飛輪儲能智慧雲平台 後端開發 ｜ Golang / Docker / Podman / Redpanda / PostgreSQL / Redis ｜ 2025.06 - 至今\n參與儲能雲平台（基於 Alibaba Cloud Linux）核心後端服務開發，承接設備數據的高頻接入、協議解析與時序入庫。 主導部署環境配置，採用 Docker 兼顧本地開發，生產環境應用 Podman rootless 結合 Shell 腳本，實現 Rootless 帳號的安全隔離部署。 引入 Redpanda 消息隊列解耦數據採集與存儲層，平抑並發寫入峰值；獨立設計認證中間件，基於 JWT (AT/RT) 與 Redis 維護安全的會話狀態。 Modbus-Tools（個人開源項目） 獨立開發者 ｜ C++20 / Qt6 / CMake / CI-CD ｜ 2025.12 - 至今\n採用 channel / transport / session / parser 分層架構研發跨平台調試工具，內建可視化報文構建器，徹底免除繁瑣的手動查表與十六進制幀拼接。 開發 Frame Analyzer 核心解析器，支援協議自動識別、自定義倍率換算與暫存器語義標註；支援 JSON / CSV 配置導入導出，將現場排障耗時從分鐘級壓縮至秒級，聯調提效 5 倍以上。 基於 GitHub Actions 跑通全自動 CI/CD 流水線，實現代碼推送後的自動構建與 Release 發布；客戶端內建多語言切換與自動更新（Auto-Updater）機制，保障工具在現場的敏捷迭代。 教育背景 重慶對外經貿學院 ｜ 物聯網工程 ｜ 本科 / 工學學士\n","permalink":"https://intent.me/zh-tw/resume/","summary":"明裕成 - 嵌入式 / 上位機工程師，聚焦儲能系統、工業通信與雲邊協同","title":"履歷"}]