AgEnD 如何從三十多個 agent 就不穩,到穩定跑四十二個以上
三個會讓整個 fleet 卡住好幾秒的熱點、會拖垮所有 agent 的幾種故障,以及我們改了什麼——附上一個約 45 個 instance 的 fleet 改善前後的實測數字。
- 效能
- 穩定性
在 AgEnD 裡,一個管理程序負責協調 fleet 的所有 agent:替它們送訊息、提供網頁儀表板、和聊天平台溝通。只有幾個 agent 的時候,沒有人會注意到這個程序的負擔;過了三十個左右,問題就浮現了:fleet 會一次卡住好幾秒,偶爾還會整個停掉。這篇文章說明我們量了什麼、改了什麼,以及現在的數字。
1. 背景:三十多個 agent 就不穩
出問題的有兩件事。
fleet 會一次卡住好幾秒。 管理程序在單一個事件迴圈上做事。只要迴圈上有一段同步的工作跑得太久,其他所有事情都得等:訊息、網頁儀表板、tmux,還有 agent 的工具呼叫。十月初,一個約 39 個 instance 的 fleet,連續三天(10 月 7 日到 9 日)分別寫下 30、64、77 筆卡頓警告,最嚴重的一次延遲 7.2 秒(#1956)。一筆警告代表什麼,見第 2 節。
一個故障就拖垮所有 agent。
- 某個 agent 臨時跑的腳本長到 19 GB,被核心砍掉,systemd 連帶把整個服務都停了(#2015)。
- 一次網路中斷讓 fleet 結束執行,之後它的 Discord 連線放棄重試,一直沒有恢復(#2191)。
/login的瀏覽器終端機和它的公開連結,有兩個錯誤會沒被接住而讓 fleet 當掉。這兩個錯誤是在調查一則使用者的/login回報(#2243)時發現並重現的,但那則回報本身並不是它們造成的,見第 6 節。
2. 怎麼量的
AgEnD 會監看自己的事件迴圈。每 30 秒,它會看這段期間迴圈最嚴重的延遲;只要達到 一秒以上,就為這段期間寫下 一筆警告,寫明期間跑了哪些同步工作、各花了多久。由此可知兩件事:
- 一筆警告代表一段 30 秒的期間,而不是單一一次卡頓:同一段期間裡的多次卡頓只會留下一筆,記的是其中最嚴重的那次。
- 同一項工作連續的呼叫(彼此相隔不到 10 毫秒)會合在一起記成一段,時間是加總的。
第 5 節線上的數字,都是以唯讀方式從 fleet 日誌裡的這些警告計算出來的。整個 fleet 的重啟次數則另外計算,依據是日誌裡啟動和停止的紀錄。
這能告訴我們在真實的 fleet 上時間花在 哪裡,但線上的 fleet 變數很多:負載取決於開了幾個儀表板分頁、agent 多常重啟。所以每項修正也在 實驗環境 裡量過:一個 40 個 instance(部分檢查用 80 個)的暫用 fleet,搭配跑在私有 socket 上的真 tmux,一次只改一件事。實驗環境的數字說明一項修正省下多少;線上的數字說明使用者實際看到什麼。下文凡是實驗環境的數字,都會明確標示。
3. 熱點與修正
幾乎每一筆警告點名的都是同樣三件事(#1956)。
儀表板的狀態,每個分頁各重建一次。 每個開著的儀表板分頁每 10 秒會收到一次 fleet 的狀態,而 fleet 每次都為每個分頁從頭建一份。建立時會同步從磁碟讀每個 instance 的檔案,有些檔案還讀了三次。
- #2225:每一拍只建一次,所有開著的分頁共用。每個觀看者看到的仍然只有自己能看的部分;訪客看到的內容是從同一份結果裁出來的,絕不會拿別人的那份。
- #2226:每個 instance 的狀態列檔案,每次建立只讀一次,不再最多讀三次。
Codex 的模型清單,每次建立都重讀。 每個 Codex instance 在每次建立狀態時,都會重新讀取並解析 Codex 整份模型快取(約 400 KB)和它的設定檔。在實驗環境裡,這佔了狀態建立時間的 71%。
- #2224:結果依那些檔案的版本保存,只有檔案變了才重讀。
啟動 tmux 指令。 每一則送進 agent 的訊息都要跑 tmux 指令。Node 啟動子程序時會同步複製父程序,父程序用的記憶體越多就越慢:在實驗環境裡大約每 100 MB 要 3 毫秒。管理程序大約用 550 MB;在 10 月 19 日的警告裡(見第 5 節),一段記下來的 tmux 工作中位數是 111 毫秒,最長 747 毫秒;遇到重啟或大量同時啟動時還會疊在一起。
- #2230:改由一個小的輔助程序替 fleet 啟動 tmux 指令。fleet 只要傳一則訊息給它,啟動指令時就不必再複製 fleet 的記憶體。輔助程序不在時,每次呼叫都會退回原本直接啟動的方式。
實驗環境的數字。 每一列都是該 PR 自己單獨量測的改善前後,不是同一次合併的量測,也不能相加。
| 修正(實驗環境) | 量測項目 | 改善前 | 改善後 |
|---|---|---|---|
| #2224 | 狀態建立中位數,40 個 instance | 9.0 毫秒 | 2.2 毫秒 |
| #2226 | 每次建立的檔案系統呼叫 | 247 次 | 167 次 |
| #2225 | 開 5 個分頁時的一拍 | 42.0 毫秒 | 10.9 毫秒 |
| #2230 | tmux 指令佔用迴圈,551 MB | 26.2 毫秒 | 0.38 毫秒 |
4. 穩定性:網路中斷和記憶體不足不再拖垮 fleet
一個失控的指令不再讓 fleet 停掉。 systemd 對服務的預設做法是:只要核心因為記憶體不足砍掉服務裡的任何一個程序,就把整個服務都停掉。agent 執行的所有東西都在 fleet 的服務裡,所以一個 agent 的 19 GB 腳本就把所有 agent 一起拖下去了。
- #2018:服務現在會寫入
OOMPolicy=continue。只有被砍的那個程序結束,管理程序和其他 agent 照常運作。已經安裝的服務,會在下一次agend update時加上這個設定。 - 這適用於 Linux 上 AgEnD 的 systemd 服務,以及 agent 執行的指令被砍的情況。如果核心砍的是管理程序本身,服務仍然會停,然後由 systemd 重新啟動。
- #2036:主機記憶體吃緊時,日誌、
/health和儀表板會列出最大的幾個程序,以及它們屬於哪個 agent,讓你在核心出手之前就看得到是誰在吃記憶體。
網路中斷不再讓 fleet 停掉,Discord 也會自己恢復。
- #2195:網路中斷期間,Discord 連線在被替換時丟出的錯誤,一路跑到 fleet 的最後一道錯誤處理,而那裡會把一切都停掉。現在這個錯誤在發生的地方就處理掉了。啟動失敗的連線會以有上限的間隔持續重試,不再試十次就放棄;設定 → 連線 → 立即重試 可以要求馬上再試一次。不需要聊天平台的 fleet 工具(傳訊給其他 agent、查任務狀態之類)在連線不在時照常可用。萬一 fleet 真的因為意外的錯誤而停止,現在會先把原因寫進
fleet-fatal.log。 - #2204:已經被替換掉的 Discord 連線,不能再晚一步建立新的 socket;否則它之後可能把同樣的錯誤丟進那道錯誤處理。
/login 的兩個錯誤,現在只會結束那一次登入。
- #2245:瀏覽器終端機的伺服器晚一步丟出的錯誤,或通道輸出串流的錯誤,原本沒被接住,就把 fleet 拖垮了。現在只會結束那一次登入,並在聊天裡說明原因和下一步可以怎麼做。
- 這兩個錯誤是單獨重現的。它們不是使用者回報中那次卡死的原因,也沒有修好它:那則回報後來證實是第 6 節的 WSL 卡死,目前仍未解決。
5. 改善前後的實測數字
以下數字來自一個約 45 個 instance 的實際運作中的 fleet,計算方式是第 2 節說的警告。時間是當地時間(UTC+8)。主要的計數也寫在 #1956 的總結裡。
| 改善前 | 改善後 | |
|---|---|---|
| 時間範圍 | 10 月 19 日整天 | 10/20 15:30 → 10/21 15:30 |
| AgEnD 2.3.0 | beta.9 – .11(未修正) | beta.16 – .18(已修正) |
| 警告(一秒以上) | 16 筆(每小時 0.67 筆) | 0 筆 |
| 最嚴重延遲:中位數/p90/最大 | 1.47/2.74/5.75 秒 | — |
| 最嚴重延遲的加總 | 28.7 秒 | 0 秒 |
| 整個 fleet 重啟 | 5 次 | 2 次(都是更新) |
改善前的 16 筆警告全部落在 14:01 到 16:18 之間;改善後 24 個小時裡每一小時都是 0 筆。「最嚴重延遲」是每一筆警告在它那段期間裡最嚴重的延遲,加總起來並不等於 fleet 實際卡住的總時間。改善前的警告點名的,正是第 3 節那些工作;每個數字是一段記下來的工作,連續的呼叫算在一起(中位數/p90/最長):
- Codex 模型清單,16 筆中有 15 筆;
- 狀態建立,14 筆(566/1,634/2,250 毫秒);
- 儀表板串流,13 筆(651/1,567/2,279 毫秒);
- tmux 指令,10 筆(111/266/747 毫秒)。
閱讀這些數字時請留意:
- 這是實際運作的 fleet,不是實驗室。 卡頓和負載有關,尤其是開著的儀表板分頁數和重啟次數。改善前那段期間重啟比較多(5 次對 2 次),而當時開了幾個儀表板分頁並沒有紀錄。
- 改善後的期間橫跨三個版本(beta.16、.17、.18),三個都包含這些修正。
- 0 筆的意思是沒有任何一段期間的最嚴重延遲達到一秒。 更短的卡頓不會被記錄,所以這不代表 fleet 從來不會延遲。
- 第 3 節的實驗環境數字才是在受控條件下量的; 這張表呈現的是使用者實際看到的效果。
6. 還沒解決的問題
WSL 上 /login 之後的卡死(#1880)。在一台 WSL 主機上,/login 開啟瀏覽器終端機大約一分鐘後,管理程序停止回應,連正常的停止都停不下來:有一個執行緒卡在核心裡等待。它沒有在用 CPU,這條路徑上也沒有找到 AgEnD 會卡住的程式碼,所以目前還沒有修正。前面提到的使用者回報(#2243)也是同一類卡死。這個 issue 附了一段簡短的唯讀擷取指令,如果再發生,請在 重啟之前 先執行它,我們就能看到它卡在哪裡。
每個指令的記憶體上限(#2015)。OOMPolicy=continue 能在一個指令記憶體不足時讓 fleet 繼續運作,但不能一開始就阻止指令用掉太多記憶體。替每個 agent 的指令設上限,目前還在設計階段,還沒有實作。
如果你的 fleet 卡住或停掉,第 2 節說的那些警告就在 fleet 的日誌裡,而且會寫出當時在跑什麼。把它們附在 issue 裡:上面的每一項修正,都是這樣開始的。