錯誤處理
鉤子執行在它所觀察的請求內部,所以它們的失敗如何處理很重要。這一頁是確切的策略。
兩條策略
hooks_raise 控制鉤子拋異常時會發生什麼。
hooks_raise |
行為 |
|---|---|
True(預設) |
鉤子的異常從呼叫中向外傳播。 |
False |
鉤子被跳過並給一個 RuntimeWarning,呼叫繼續。 |
它按例項設定,也可以逐呼叫覆蓋(predict_batch、system_one、Router.route、Router.predict
上的 hooks_raise=)。逐呼叫的 None 表示「用例項的值」。
# strict: a broken audit hook fails the request
laya.load("convaiinnovations/laya", on_predict_end=audit, hooks_raise=True)
# lenient: telemetry must never take down a served request
laya.load("convaiinnovations/laya", on_predict_end=metrics, hooks_raise=False)
dispatch 捕獲 Exception。任何不是 Exception 的東西(見 BaseException)
永遠不會被吞掉,即便 hooks_raise=False 也一樣。
某個東西失敗時還會執行什麼
predict 生命週期被包在 try / except / finally 裡,所以清理鉤子在失敗時也會執行。
try:
on_predict_start
inference
except BaseException as exc:
ctx.error = exc
on_error (best effort; cannot mask exc)
raise
finally:
elapsed_ms, usage
on_predict_end (best effort; cannot mask exc on the failure path)
失敗矩陣,按事件和執行時:
| 事件 | 執行時 | 如果它拋異常 |
|---|---|---|
on_predict_start |
Agent / Router | hooks_raise=True:on_error 和 on_predict_end 仍然執行,然後異常傳播。False:警告並繼續(丟擲之前做的改動保留)。 |
| inference | Agent / Router | 設定 ctx.error,執行 on_error,執行 on_predict_end,異常傳播。 |
on_error |
Agent / Router | 永遠不會遮住原來的異常;以 __context__ 連結。 |
on_predict_end(成功路徑) |
Agent / Router | hooks_raise=True:傳播(結果算出來了但呼叫失敗)。False:警告。 |
on_predict_end(失敗路徑) |
Agent / Router | 永遠不會遮住原來的異常;以 __context__ 連結。 |
on_route |
Router | 直接傳播;這時還沒有 predict 上下文。 |
on_load |
Router | 直接傳播;checkpoint 保持構建好並常駐。 |
on_evict |
Router | 直接傳播;checkpoint 已經被釋放。 |
值得知道的後果:
- 一個失敗的
on_load會讓模型留在快取裡,所以下一次load直接返回它,不會再觸發on_load。 - 成功路徑上一個失敗的
on_predict_end意味著呼叫方拿到的是一個異常而不是結果,即便推理成功 了。對於純副作用的 end 鉤子,用hooks_raise=False。
異常鏈
當一個鉤子在另一個異常已經在傳播時失敗,原來的異常被重新丟擲,鉤子的異常作為 __context__ 附上。
根因永遠不丟。
class BadTelemetry:
def on_error(self, ctx):
raise RuntimeError("telemetry down")
try:
agent.system_one(state, questions, hooks=[BadTelemetry()])
except RuntimeError as exc:
assert exc.__context__ is not None # the telemetry failure
同樣的規則適用於失敗路徑上一個失敗的 on_predict_end。
BaseException
dispatch 捕獲 Exception,不是 BaseException,所以 KeyboardInterrupt 和 SystemExit 總會
傳播。它們仍然觸發 predict 生命週期的 except BaseException 分支,這意味著 on_error 和
on_predict_end 在程序展開之前執行。如果你在意中斷延遲,就讓這些鉤子快且非阻塞。
配置錯誤
壞的配置在任何推理之前就以 TypeError 快速失敗:
| 情況 | 丟擲時機 | 例子 |
|---|---|---|
| 類而不是例項 | 構造時 | hooks=[MyHook] |
| 沒有生命週期方法 | 構造時 | hooks=[object()] |
| 事件不可呼叫 | 構造時 | on_predict_start = 5 |
| 便捷鉤子不可呼叫 | 構造時 | on_predict_start=123 |
hooks= 裡是普通可呼叫物件 |
構造時 | hooks=[lambda ctx: None] |
逐呼叫鉤子在呼叫發生時校驗,所以一個壞的逐呼叫鉤子從 predict/system_one 拋 TypeError,而
不是構造時。
警告
hooks_raise=False 時,每個失敗的鉤子發出一個 RuntimeWarning,點名鉤子型別和事件:
laya: hook Metrics.on_predict_end failed: connection reset
警告每次失敗發出一次,而不是每個鉤子定義一次,所以一個在高負載下時好時壞的鉤子可能很吵。如果 這要緊,就在鉤子內部聚合並限流。
超時
hooks_timeout 以秒為單位給每次鉤子呼叫設上界。超過限制後仍在執行的鉤子被當作鉤子失敗:
hooks_raise=True 時是 TimeoutError,False 時是一個 RuntimeWarning。None(預設)表示
不限。
laya.load("convaiinnovations/laya", on_predict_end=metrics, hooks_timeout=2.0)
它可以按例項設定,也可以在 predict_batch、system_one、Router.route、Router.predict 和
ONNXAgent.system_one 上逐呼叫覆蓋。這個值必須是正數;0 或負數在設定它的那個點拋
ValueError,而不是在一個零長度的 join 上競爭。
帶超時的鉤子在一個 worker 執行緒上執行,用的是呼叫方 contextvars 上下文的一份副本,所以呼叫方
設定的請求 id 或 tracing span 對鉤子可見。
一個誠實的提醒:Python 無法中斷執行緒,所以超時的鉤子會在後臺繼續執行。超時給的是請求等待多久的
上界,不是鉤子活多久的上界。用它來保持一個正在服務的請求響應,而不是用來回收那部分工作。對於一個
可能掛住的鉤子,也給底層呼叫它自己的超時(socket 或 HTTP 超時)。因為執行緒無法回收,一個每次呼叫
都掛住的鉤子會每次呼叫多長一個執行緒;給一個可能掛住的鉤子它自己的上界,而不是依賴 hooks_timeout
來讓它停下。
對於非同步鉤子,協程在事件迴圈上執行;呼叫方的超時到點仍然返回,而協程繼續在迴圈上執行。
超時還會釋放 hooks_concurrent=False 的鎖:dispatch 只等鉤子到限制為止,然後繼續,而超時的鉤子
在鎖之外繼續執行。所以這把鎖序列化的是那些按時完成的鉤子,不是每一個曾經啟動過的鉤子;一個超時的
鉤子不再擋住它後面的鉤子。
選一條策略
| 鉤子種類 | 建議 | 為什麼 |
|---|---|---|
| 策略 / 防護欄 / 脫敏 | hooks_raise=True |
一條靜默失敗的策略是一個安全漏洞。 |
| 審計 / 日誌 | 測試裡 hooks_raise=True,生產裡常常 False |
丟掉一條審計記錄應該響亮,但不一定致命。 |
| 指標 / 鏈路追蹤 | hooks_raise=False |
可觀測性絕不能讓請求失敗。 |
| 快取讀/寫 | hooks_raise=True |
一個壞掉的快取應該暴露出來,而不是靜默漏掉。 |
你可以混搭:用例項預設值安裝一個防護欄,給一個遙測鉤子它自己的 try/except,或者逐呼叫用單獨的
hooks_raise。