上一篇把 Sandbox 的 architecture 和 lifecycle 整理完之後,我們知道一個 sandbox 大概會經過:
create
→ assigned
→ busy
→ idle
→ delete
但這裡有一個假設:
One conversation
→ One sandbox
conversation 少的時候沒什麼問題,但如果同時進來很多 request,sandbox 不可能無限開。
所以接下來要處理的是:
有限的 execution capacity 到底要怎麼分?
現在還是 POC,所以不會真的去跑 100 個 sandbox,而是在 Daytona 現有額度裡看看怎麼解決 scheduling 的問題。
最直覺的限制可能是:
max_sandboxes: 10
但在 Daytona 中還會受到 total memory 的限制,所以 provider 真正在意的不只有 sandbox 數量,也包含總共用了多少 memory。
目前同時設定:
server:
max_sandboxes: 10
max_memory_gb: 10
例如:
Sandbox A → 1 GB
Sandbox B → 1 GB
Sandbox C → 4 GB
Sandbox D → 4 GB
雖然只有 4 個 sandbox,但 memory 已經用完 10 GB。
所以 manager 必須同時追蹤:
Provider 算什麼 resource,我們就要跟著算什麼 resource。
這裡把可以拿來建立 sandbox 的 capacity 稱為 slot。
概念上就是:
take capacity
→ create sandbox
→ use
→ delete
→ return capacity
不管是正常 delete、create failed 還是 provider error,最後都必須把 capacity 還回去。
不然可能出現:
Real sandboxes: 6
Manager thinks: 10 / 10 used
所以 resource manager 麻煩的地方不是 create,而是:
不管 sandbox 是正常刪除、建立失敗,還是 provider error,capacity 都要正確釋放。
只有 global limit 還不夠。
目前設定:
server:
max_sandboxes: 10
max_sandboxes_per_account: 3
borrow_keep_free: 2
max_sandboxes_per_account: 3 比較像正常情況下的 limit,而不是絕對不能超過。
如果使用者不多資源還足夠,一個用戶可以暫時使用更多 sandbox。
例如:
10 total slots
3 already used
7 free
即使已經有 3 個 sandbox在使用,還是可以多使用5個,原因如下:
borrow_keep_free: 2
代表至少保留 2 個 free slots 給其他 request。
這樣做是取平衡:
所以 max_sandboxes_per_account 比較像 normal share,而 borrow_keep_free 則控制一個 account 最多可以借到什麼程度。
Capacity 處理完之後,下一個問題是 latency。
目前 Daytona 的 cold start 加上 prepare 大約需要11秒左右:
如果每次都等 sandbox ready 才開始處理 request,使用者會明顯感受到延遲。
所以現在會先保留:
warm_sandboxes: 1
也就是預先準備一個 ready sandbox:
Warm Pool
└── Ready Sandbox
新的 conversation 需要時可以直接拿來用,拿走之後如果還有 capacity,再從 background 補一個。
但 warm sandbox 一樣會:
因此這是一個 trade-off 的問題:
較低的 startup latency
vs
較高的 idle cost
而且 warm sandbox 一樣算在 global capacity 裡。
例如:
max_sandboxes = 10
9 assigned
1 warm
→ 10 / 10
前面 Day 18 有提到,Agent 拿的是 LazySandbox,而不是直接拿 real Daytona sandbox。
當 request 進來時,Agent 和 sandbox startup 可以同時開始:
User Request
↓
┌────────┴────────┐
↓ ↓
Agent starts Sandbox starts
immediately in background
│ │
└────────┬────────┘
↓
First code execution
↓
Sandbox ready?
├─ yes → execute
└─ no → wait
如果問題只是:
How many videos are in Gaming?
Agent 可能只需要 query_database → answer,完全不需要 Python。
這種情況下,如果一定要先等 sandbox ready 才能繼續後續的執行,那就只是在浪費時間。
所以現在讓 sandbox creation 在 background 執行。真的第一次碰到 execute(...) 時,LazySandbox 才確認 real sandbox 是否 ready。
這樣不是把 cold start 消失掉,而是盡量把它藏在 Agent 前面的 reasoning、SQL query 或其他工作後面。
當然代價是有可能這個 sandbox 最後根本沒用到。
所以這裡也是一個 trade-off:
較低的 perceived latency
vs
較高的 resource cost
目前選擇讓 sandbox 在 background 先啟動,主要是希望換到比較好的使用者體驗。
如果 sandbox count 或 memory 都已經滿了,為了讓新的 request 不會無限等帶,目前設定最多等待 30 秒:
sandbox_wait_seconds: 30
這段時間可能發生:
如果最後還是沒有 capacity,LazySandbox 會拿到一個正常的 error result,例如:
Sandbox unavailable:
all sandboxes are in use
而不是直接讓整個 Agent execution crash。
做到這裡,manager 實際上要一起考慮:
所以這裡做的不是傳統的:
Request
→ Server A / B / C
而比較像:
Execution Capacity Scheduling
決定有限的 sandbox resource 要怎麼分給不同 conversation。
現在我們處理的是:
但到這裡,我們還是把每個 sandbox 當成差不多的 execution resource。
例如:
1 vCPU
1 GB Memory
那如果 Agent 跑了一段 code 結果 memory 爆掉,可能有幾種情況:
所以下一篇想開始測量每個 execution step 用掉多少資源,再決定應該調整 execution strategy,還是真的需要更大的 Sandbox。