這次處理 MQTT 連線問題,讓我印象最深的是,一個地方慢下來,真的會一路影響到其他地方。
一開始是裝置收不到遠端控制命令。我查 EMQX 和 DIS 的紀錄,發現認證請求其實有進來,但很多答案回去時,已經超過 EMQX 的 10 秒 timeout。
當時 DIS 有兩個 task,每個只有 0.25 vCPU。我估算大概每秒能處理 6 次登入,但裝置送來的流量大約是每秒 50 次。每次登入都要跑 bcrypt,CPU 很快就滿了。請求還在處理,broker 卻已經不等了。
CPU 的問題,怎麼拖到資料庫?
原本的註冊流程是在 DB transaction 裡算 bcrypt hash,計算期間一直占著資料庫連線。所以 CPU 忙不過來,hash 越跑越慢,連線也被占得越久。
DIS 當時沒有明確限制 DB 連線數,繼續開新連線,最後 PgBouncer 開始拒絕:
no more connections allowed (max_client_conn)
這裡有兩種連線要分開看。DIS 的 DB pool 管的是程式開了多少條連到 PgBouncer 的連線;PgBouncer 再讓這些 client 共用後面的 PostgreSQL 連線。因此 DIS 開了 40 條,不代表 PostgreSQL 就一定開了 40 條,還要看 PgBouncer 的 pooling mode 和設定。
接著,認證失敗或逾時,裝置就重新連線。當時 Edge 每 12 秒重試,沒有 backoff。前面的請求還沒消化完,新的又進來。
CPU 不夠
→ 認證變慢
→ 註冊交易占用 DB 連線更久
→ PgBouncer 拒絕新連線
→ 裝置登入失敗
→ 裝置重試
→ CPU 更忙
看到這裡,我就知道只把 pool 調大不夠。CPU 能做的工作沒有增加,多放一些請求進來,可能只是讓更多請求一起等。
先讓請求不要一直堆進來
我先把 EMQX 的 authentication pipelining 從 100 改成 1,減少單條 HTTP 連線上的積壓。這不代表整個 EMQX 一次只能認證一台裝置,其他 HTTP 連線仍然可以工作。它減少的是同一條連線上尚未收到回應的請求。
這個調整沒有增加 DIS 的 CPU,但當時讓更多認證答案能在 timeout 前回去。
接著在 DIS 限制 bcrypt 併發和等待佇列、限制 DB pool,把 hash 移到 transaction 外面,再加上 4 秒 request deadline,讓等待有時間上限。已經開始的 bcrypt 工作不能靠 context 直接中止,所以我也調整 HTTP write timeout,預設留到 8 秒,讓服務仍有時間把回應送出去。
滿載或暫時碰不到 DB 時,DIS 回 503,並在紀錄裡區分 overload、deadline 和 backend unavailable。真正的程式錯誤仍然要看,裝置不存在也不能一律當成過載。503 本身不保證裝置會重試,還要看 EMQX 認證鏈和裝置端的處理。
Pool 設了上限,還有另一個問題
最初 open pool 設 40,idle 只留 10。Open 包含正在使用和閒置的連線;idle 是其中已經連好、暫時沒工作的那一部分。
流量高的時候開到 40,降下來又關掉多餘連線,下次流量來再重開。PgBouncer 清理舊連線有時間差,反覆開關,還是可能短暫撞到 client limit。
後來我讓 idle 上限跟著 open 上限,讓已經建立的連線留下來重用,安靜 5 分鐘後再回收。這不會在啟動時就開滿 40 條,只是允許已建立的連線留著待命。
另外,我也修了 Android 註冊流程裡已經拿著 transaction 連線,卻又去 pool 等第二條連線的問題。這在 pool 沒上限時不容易看出來,但加上限制後,就可能所有請求都拿著一條、等另一條,最後互相卡住。交易內的查詢改用既有 transaction,才不會一邊限制資源,一邊讓程式自己卡住。
測試與現場恢復,要分開看
當時 DEV 的 200 VUs 測試紀錄裡,HTTP 500 次數從 5,481 降到 29,timeouts 從 33 降到 0。這是開發環境的測試結果,不能直接當成 production 錯誤率。
Production 恢復也包含 DevOps 擴大 CPU 和 PgBouncer pool 的工作。裝置端的 backoff、重連與憑證恢復,則是需要另外協作的部分,不能只看後端改完就說整件事結束了。
這次讓我學到,查這類問題真的不能只看一個服務。EMQX、DIS 和 PgBouncer 各自都有容量和 timeout,接起來之後,一層變慢就可能拖住下一層。我後來做的,就是把這些等待拆開:CPU 忙的時候別占著 DB,處理不了就早點回應,讓每一層都有機會恢復。