題幹與適用場景
外掛宿主同時執行多種語言編寫的元件。舊元件依賴 WASI 0.2 的 wasi:io 資源,新元件希望使用 WASI 0.3 的 async func、stream 與 future。請設計處理日誌串流、遠端呼叫與取消的元件介面,說明宿主如何排程、限流並相容舊版本。
這題考察非同步語意與 ABI 邊界,不是把所有函式都標成 async。WASI.dev 將 0.3 描述為加入原生非同步並重構介面;Component Model FAQ 說明 wasi:io 被移除,就緒狀態由 Component Model 原語跨邊界傳播。答案必須區分介面契約、執行階段排程與業務副作用。
面試官考察點
強回答會先確認呼叫是單值結果、持續串流還是可取消的長操作,再選擇 future、stream 或普通回傳值。它能解釋為什麼 0.2 的 pollable 資源在元件邊界形成 sandwich problem,以及 0.3 如何把等待與喚醒交給執行階段。
面試官也會看背壓、資源上限、錯誤分類與版本遷移。只說「非同步提高效能」不夠;需要說明 stream 的消費速度、future 的完成語意、取消後副作用狀態,以及舊元件如何透過 adapter 執行。
回答前需要澄清的問題
資料是單值還是持續串流
確認遠端呼叫只回傳一個回應,還是持續輸出日誌、檔案區塊或事件。單值結果用 future;持續資料用有明確結束訊號與容量策略的 stream。若消費者可能暫停,必須定義緩衝上限與生產者行為。
取消是否必須阻止副作用
確認取消發生在排隊、執行中還是外部寫入之後。元件邊界能傳播取消請求,但不能替外部系統回滾已提交的副作用;需要冪等鍵、查詢狀態或補償流程。
遷移是否允許雙版本執行
確認宿主是否能同時載入 0.2 與 0.3、舊元件是否必須零修改,以及執行階段是否支援邊界 adapter。答案不同會決定採用 side-by-side 執行階段、WASI 0.2 到 0.3 adapter,還是一次切換。
30 秒回答框架
「我先依語意選原語:一次性的遠端結果用 future,連續日誌用有容量與結束標記的 stream,同步純計算維持普通函式。WASI 0.3 把就緒與喚醒放進 Component Model 的原生 ABI,避免每個元件各自持有 pollable。宿主仍要設定並發、緩衝、逾時與取消策略;取消只保證停止後續工作,外部副作用用冪等狀態確認。遷移時保留 0.2 world,透過 adapter 與相容矩陣逐步切換。」
分步驟深入解答
第一步:把非同步語意寫進 WIT world
先定義最小介面,不把所有函式都變成非同步。示例使用 WIT 風格偽程式碼:
package acme:plugin@0.3.0;
interface logs {
record chunk { bytes: list<u8>, end: bool }
export next: func() -> future<chunk>
}
world worker {
import logs;
export run: async func(input: string) -> result<string, failure>;
}future 代表一個最終值,不等於永遠在背景執行的工作;stream 更適合由執行階段逐步交付的序列。介面還要寫清錯誤、結束與取消後的狀態,否則不同語言產生的 binding 會有不同解讀。
第二步:解釋 0.2 到 0.3 的邊界變化
WASI 0.2 透過 pollable、input-stream 與 output-stream 表達 I/O 就緒。它們作為元件資源時,等待訊號要穿過呼叫者與被呼叫者各自的非同步層,容易出現資源只在一側可見的 sandwich problem。WASI 0.3 直接使用 async func、future 與 stream 原語,執行階段負責跨元件的就緒傳播。
這不是把舊二進位強行改寫成新 ABI。官方 FAQ 說明 0.3 執行階段可以在 host boundary 將 0.2 imports 映射到 0.3 原語;遷移應保留舊 world,先讓 adapter 承擔轉換,再逐個升級元件。
第三步:為 stream 建立背壓與記憶體界線
生產者不能無限寫入。宿主為每個 stream 設定元素數、位元組數與等待時間上限;消費者落後時暫停讀取或回傳明確的過載錯誤。中間緩衝應按租戶與元件隔離,避免一個慢消費者占滿執行階段記憶體。對檔案與日誌,優先傳遞區塊或受控 handle,不把整個物件一次複製到邊界。
第四步:設計 future 的取消與錯誤
future 完成前,宿主保存操作 ID、截止時間與取消訊號。取消應阻止尚未開始的工作,並向執行中的元件送出合作式停止;如果外部服務可能已寫入,狀態標成 UNKNOWN,透過冪等鍵查詢或補償,而不是直接重試。錯誤要區分逾時、取消、業務拒絕、依賴失敗與協定不相容,讓重試策略分別處理。
第五步:把排程責任留給執行階段
元件內部不應各自啟動隱藏事件迴圈來等待同一類 I/O。宿主執行階段統一管理喚醒、並發與公平性;元件只宣告介面與回傳結果。同步熱路徑可以保持普通函式,避免不必要的 task 建立。Bytecode Alliance 路線文章指出,非同步基礎設施若侵入同步呼叫會產生額外開銷,因此要量測同步 adapter、非同步邊界與業務計算三部分。
第六步:制定漸進遷移與驗證矩陣
發布清單記錄 WIT 套件版本、WASI 版本、執行階段、adapter 與元件雜湊。矩陣至少涵蓋 0.2 元件在 0.3 host、0.3 元件在舊 host 的失敗方式、stream 中途取消、逾時後未知副作用、慢消費者與重複重連。先影子執行並比較完成率、端到端延遲、緩衝峰值、取消延遲與重試次數,再按元件版本灰度。
高品質示範回答
我不會先把所有介面改成 async。我先確認每個操作的語意:單次遠端結果用 future,持續日誌用 stream,純本地計算保持同步。WASI 0.3 的關鍵變化是把 async func、future 與 stream 放進 Component Model 的原生 ABI,取代 0.2 的 wasi:io pollable 資源;執行階段因此能在元件邊界傳播 ready 與 wake-up。
實作上,我給 stream 設元素、位元組與等待上限,消費者變慢就暫停生產或回傳過載,不讓一個租戶耗盡共享記憶體。future 保存操作 ID 與截止時間;取消能停止未開始工作,執行中的外部寫入則進入 UNKNOWN,透過冪等鍵查詢或補償,不能盲目重試。
遷移保留 0.2 world,用 adapter 在 host boundary 做映射,先影子執行再按版本灰度。驗證矩陣涵蓋舊元件、新元件、取消、背壓、錯誤與重複重連;如果執行階段或工具鏈還不支援 0.3,我會延後 world 切換,而不是偽造相容。
常見錯誤
- 錯誤表現 → 把
future當成任意背景工作 → 失敗原因 → future 代表可完成的結果,生命週期與取消仍需契約 → 修正方法 → 記錄操作 ID、截止時間與取消狀態。 - 錯誤表現 → 用無限佇列吸收 stream 峰值 → 失敗原因 → 慢消費者會把記憶體壓力變成執行階段故障 → 修正方法 → 設定位元組與元素上限,傳播背壓或明確拒絕。
- 錯誤表現 → 說 0.3 會自動回滾外部寫入 → 失敗原因 → ABI 取消不等於跨系統交易 → 修正方法 → 使用冪等鍵、狀態查詢與補償流程。
- 錯誤表現 → 直接刪除 0.2 world → 失敗原因 → 舊元件與工具鏈可能仍依賴 pollable 資源 → 修正方法 → 透過 adapter 與相容矩陣分階段遷移。
- 錯誤表現 → 每個元件自建事件迴圈 → 失敗原因 → 排程、取消與公平性無法統一 → 修正方法 → 讓執行階段擁有 wake-up 與並發預算,元件只宣告介面。
追問及應對
追問一:stream 的生產者比消費者快,應該丟資料還是阻塞?
先按資料價值分類。日誌可按等級丟棄並記錄丟棄計數,財務事件不能靜默丟棄,應持久化到有界外部佇列並回傳背壓。無論選哪種策略,都要把租戶、元件與流 ID 寫入指標,避免只看到總吞吐。
追問二:future 逾時後如何判斷是否已完成?
逾時只代表呼叫方停止等待。宿主保留操作 ID,向依賴查詢狀態;若依賴沒有查詢 API,就把結果標成 UNKNOWN,交給人工或補償任務處理。只有服務契約明確冪等且確認未完成時才重試,不能用網路連線關閉推斷交易回滾。
追問三:0.2 元件使用 pollable,0.3 host 如何暴露同樣語意?
adapter 把 0.2 資源事件映射成 0.3 future 或 stream,但要保留資源關閉、錯誤與取消語意。先在單元件測試中比較 ready 順序與 EOF,再驗證跨元件組合;如果 adapter 只能模擬部分行為,應在部署門禁拒絕依賴缺失的介面。
追問四:如何證明非同步改造真的降低成本?
把指標拆成邊界 adapter 時間、執行階段喚醒次數、緩衝峰值、CPU、記憶體、端到端延遲與業務完成率,並與同步基線對照。若呼叫很短且沒有並發等待,task 管理和 ABI adapter 可能抵銷收益,應保留同步介面或合併批量呼叫。