在系統架構的實作裡,常為了減輕後端資料庫的負擔,通常會在程式跟資料庫之間隔一層快取層(例:Redis or mongoDB...)。
常見的實作方式之一就是 request 打進來的時候,程式先查詢 Redis,若命中快取則直接回傳值;如果沒命中(Cache Miss),才進入資料庫進行查詢,然後將結果放一份在 Redis。
在一般「正常」情境下它可以運作良好,但在某些惡意攻擊下,可能就會被 client 端直接 DDos 到後端資料庫,也就是所謂的「緩存穿透」。
所有 request 打進來請求的 key 在快取層裡面全部都找不到,都會繞過快取層直接打到後端的資料庫,造成資料庫的負擔變得很重。
這些所謂找不到的 key,有可能就是 client 端「刻意」的動作,故意打一些預期不會有的 key 值進來。
做法:當資料庫也查不到資料時,依然把 Key 存入 Redis,但值設定為一個空的結果物件,讓一樣的 key 在下一次的 request 進來時是打到快取層。
優點:實作簡單,能直接擋掉相同 Key 的重複攻擊。
缺點:
做法:在還沒打到快取層之前,先用邏輯來判斷 key 值是不是合法的。例如:ID 必須是 between 0 and Max(ID) + 10000 之類的。讓一些一看就是不合法的 key 值,沒有機會往後流。
優點:可以在「更前面」的地方就將這種 dirty request 直接攔掉。
缺點:
做法:在快取層之前再擋一層。將所有可能存在的合法 Key (不用內容)再存一份快取。 request 打進來時先用這份 key 清單來判斷是否合法。
優點:空間換時間,要多存一份快取。
缺點:又要再多維護一份快取清單。當資料有異動時,這份 key 清單快取都有更新到。
實作:可以用 IP or token 之類的唯一身份識別,來卡同一個身份在某段時間區間內可以打進來的 request 量。
缺點:要再實作一套 throttle 機制來達到這個效果
如果這條路確定只會有內部系統的「網內互打」,那只要源頭使用的 client 端設計沒有問題,基本上上面的問題不太會遇到才對。
但如果是會「對外」,上面這些解法就可以考慮補一下,避免後端的資料庫被打爆。
但這些解法都沒有所謂的最佳解,也都只能依當時的情境跟實際「可行性」來決定要用哪個方法。
比如說時間不允許、影響的 scope 過大、影響的功能太敏感 or 主管不允許……等,各種不同的阻力。這時候就都是在做一個所謂的 trade-off 了。