2013年5月3日 星期五

[idea] 反思快取機制


觀察著 Administor 忙碌地處理系統問題,時時監測 Server Loading、httpd 數量、總體記憶體用量,還有久久無法消退的 PENDING REQUEST。配合 Log 的狀態,使我反思一個問題:我們的快取,真的是我們要的嗎?

實作 WebService API 服務時,有些需要耗費大量資源的運算,在結果不太需要頻率更新的時候,我們通常會導入『快取機制』。當採用快取時,程式的流程會變成:


  1. 檢查快取是否有計算結果可用
  2. 如果有快取的結果,則馬上回傳。若沒有則進行 3.
  3. 進行耗費大量資源的運算
  4. 將運算結果放進快取
  5. 回傳結果


快取系統的概念,就是將一個計算結果放進一個具有計時器的儲存空間,當時間一到就會讓儲存的內容過期。在步驟 2. 取不到結果時,就是『計算結果』已經過期所致。因為沒有可用快取,而需走到步驟 3. 進行實際的資料存取。這概念說起來簡單,但別忘了,我們的程式類型是 WebService API。這表示同時間內,可能具有許多的 HTTP REQUEST 進來。若同時間有 N 個 REQUEST 走到步驟 3. 那就會同時有 N 個『耗費大量資源的運算』。

一般來說會使用 Guard Object(或 Barrier) 實作成 Future Pattern 來改善同時間 N 個『耗費大量資源的運算』的問題,在實作正確的情況下,即使 N 個 HTTP REQUEST 進來,也只會有其中一個進行真實的運算。不過因為 Guard Object 的效果,在 Server Administrator 看起來會像是有 N 個 HTTP REQUEST 處於 PENDING 的狀態。

每一個 PENDING 的 HTTP REQUEST 都會佔用系統資源。當 Server 需要持有更多的 HTTP REQUEST 時,那麼就有可能拖慢他的效能。至少 Guard Object 擋住的是真實的 Thread,這個 Thread 綁定的相關資源就佔在那了,至少在 Java Web 來說,Session 跟它的內容物就佔了一些空間。若沒有將 Guard Object 設計好,那可能這 PENDING 會持續到永遠,並且一直累積新的 PENDING REUQEST。

沒設計好不代表有 Bug 實際存在那裡,而是有隱含的『強假設』沒有被處理。像是對 DB 做 SQL Query,『一定』會有資料回來。若 DB 的 Connection Pool 耗盡的行為是『無限等待』,那麼 Guard Object 就會連帶著『甲你攬牢牢』緊抓不放,這就是典型的『強假設』。Guard Object 應該有個停損點來釋放抓住的 HTTP REQUEST,但是實在無法精確預設這個耗時的運算要多久,怎麼設定 timed out 都會成為魔術數字。最後,它可能是一個巨大的 timed out。

這樣的設計,可能對於避免過於耗費 CPU,或短其大量取用記憶體能有較好的效果,但對於耗時的運算可能不是個好方法。在這種架構下,要處理耗時運算的 work around 方法就是定期地發出 HTTP REQUEST 讓失去的快取被補上,並『祈禱』每次等待的都是『機器人』而不是真的 API 使用者。

一切的根源來自於快取模型的限制:時間到了,資料就會消失。而我們的程式就是依賴這個特性去實作的!當資料不見了就執行運算得到新的結果,將它放入快取之內。辜且稱之為:『被動式快取模型』。

理想的情況是:只有系統啟動的第一次,使用者會等待需要快取的運算。快取的資料過期了,仍可繼續使用。在背景快取服務會利用資料存取的 Callback 主動更新內容。相對於我們原先對快取機制的認知,稱作『主動式快取』。

若我們的快取模型是這樣的,那麼只會有第一回合的使用者在等待時間較久,而我們對於快取的 timed out 設定也更有彈性。這樣只需真實地考慮一份資料的更新週期,不太需要在乎使用者是否會等待過久的問題。一來快取的更新不再由使用者發出的 HTTP REQUEST 觸發,這能減輕不必要的 PENDING REQUEST。同時,讓總體的連線等待時間下降,理論上來說,系統整體的負載也會降低。

(此文撰寫於 2011/12/29 日,由於 posterous 要結束了,重貼回 blog)

沒有留言:

張貼留言