懂得取捨才是快取設計的真諦

uu365發表於2021-05-24

Previously

前兩篇文章(快取穩定性快取正確性)跟大家討論了快取的『穩定性』和『正確性』,快取常見問題還剩下『可觀測性』和『規範落地&工具建設』

  • 穩定性
  • 正確性
  • 可觀測性
  • 規範落地和工具建設

上週文章發完之後,很多同學對我留的問題進行了深入的討論,我相信經過深度的思考,會讓你對快取一致性的理解更加深刻!

首先,各個 Go 群和 go-zero 群裡有很多的討論,但是大家也都沒有找到非常滿意的答案。

讓我們來一起分析一下這個問題的幾種可能解法:

  • 利用分散式鎖讓每次的更新變成一個原子操作。這種方法最不可取,就相當於自廢武功,放棄了高併發能力,去追求強一致性,別忘了我之前文章強調過『這個系列文章只針對非追求強一致性要求的高併發場景,金融支付等同學自行判斷』,所以這種解法我們首先放棄。

  • A刪除快取 加上延遲,比如過1秒再執行此操作。這樣的壞處是為了解決這種概率極低的情況,而讓所有的更新在1秒內都只能獲取舊資料。這種方法也不是很理想,我們也不希望使用。

  • A刪除快取 這裡改成設定一個特殊佔位符,並讓 B設定快取 用 redis 的 setnx 指令,然後後續請求遇到這個特殊佔位符時重新請求快取。這個方法相當於在刪除快取時加了一種新的狀態,我們來看下圖的情況

    是不是又繞回來了,因為A請求在遇到佔位符時必須強行設定快取或者判斷是不是內容為佔位符。所以這也解決不了問題。

那我們看看 go-zero 是怎麼應對這種情況的,我們選擇對這種情況不做處理,是不是很吃驚?那麼我們回到原點來分析這種情況是怎麼發生的:

  • 對讀請求的資料沒有快取(壓根沒載入到快取或者快取已失效),觸發了DB讀取
  • 此時來了一個對該資料的更新操作
  • 需要滿足這樣的順序:B請求讀DB -> A請求寫DB -> A請求刪除快取 -> B請求設定快取

我們都知道DB的寫操作需要鎖行記錄,是個慢操作,而讀操作不需要,所以此類情況相對發生的概率比較低。而且我們有設定過期時間,現實場景遇到此類情況概率極低,要真正解決這類問題,我們就需要通過 2PC 或是 Paxos 協議保證一致性,我想這都不是大家想用的方法,太複雜了!

做架構最難的我認為是懂得取捨(trade-off),尋找最佳收益的平衡點是非常考驗綜合能力的。當然,如果大家有什麼好的想法,可以通過群或者公眾號聯絡我,感謝!

本文作為系列文章第三篇,主要跟大家探討『快取監控和程式碼自動化』

快取可觀測性

前面兩篇文章我們解決了快取的穩定性和資料一致性問題,此時我們的系統已經充分享受到了快取帶來的價值,解決了從零到一的問題,那麼我們接下來要考慮的是如何進一步降低使用成本,判斷哪些快取帶來了實際的業務價值,哪些可以去掉,從而降低伺服器成本,哪些快取我需要增加伺服器資源,各個快取的 qps 是多少,命中率多少,有沒有需要進一步調優等。

上圖是一個服務的快取監控日誌,可以看出這個快取服務的每分鐘有5057個請求,其中99.7%的請求都命中了快取,只有13個落到DB了,DB都成功返回了。從這個監控可以看到這個快取服務把DB壓力降低了三個數量級(90%命中是一個數量級,99%命中是兩個數量級,99.7%差不多三個數量級了),可以看出這個快取的收益是相當可以的。

但如果反過來,快取命中率只有0.3%的話就沒什麼收益了,那麼我們就應該把這個快取去掉,一是可以降低系統複雜度(如非必要,勿增實體嘛),二是可以降低伺服器成本。

如果這個服務的 qps 特別高(足以對DB造成較大壓力),那麼如果快取命中率只有50%,就是說我們降低了一半的壓力,我們應該根據業務情況考慮增加過期時間來增加快取命中率。

如果這個服務的 qps 特別高(足以對快取造成較大壓力),快取命中率也很高,那麼我們可以考慮增加快取能夠承載的 qps 或者加上程式內快取來降低快取的壓力。

所有這些都是基於快取監控的,只有可觀測了,我們才能做進一步有針對性的調優和簡化,我也一直強調『沒有度量,就沒有優化』。

如何讓快取被規範使用?

瞭解 go-zero 設計思路或者看過我的分享視訊的同學可能對我經常講的『工具大於約定和文件』有印象。

對於快取來說,知識點是非常繁多的,每個人寫出的快取程式碼一定會風格迥異,而且所有知識點都寫對是非常難的,就像我這種寫了那麼多年程式的老鳥來說,一次讓我把所有知識點都寫對,依然是非常困難的。那麼 go-zero 是怎麼解決這個問題的呢?

  • 儘可能把抽象出來的通用解決方法封裝到框架裡。這樣整個快取的控制流程就不需要大家來操心了,只要你呼叫正確的方法,就沒有出錯的可能性。
  • 把從建表 sql 到 CRUD + Cache 的程式碼都通過工具一鍵生成。避免了大家去根據表結構寫一堆結構和控制邏輯。

這是從 go-zero 的官方示例 bookstore 裡截的一個 CRUD + Cache 的生成說明。我們可以通過指定的建表 sql 檔案或者 datasource 來提供給 goctl 所需的 schema,然後 goctlmodel 子命令可以一鍵生成所需的 CRUD + Cache 程式碼。

這樣就確保了所有人寫的快取程式碼都是一樣的,工具生成能不一樣嗎?:P

未完待續

本文跟大家一起討論了快取的可觀測性和程式碼自動化,下一篇我來跟大家分享一下我們是怎麼提煉和抽象快取的通用解決方法的,大家可以預先了解一下聚族索引的設計,自己先思考一下快取該如何做,畢竟經過深度思考,你的理解會更加深刻嘛!

所有這些問題的解決方法都已包含在 go-zero 微服務框架裡,如果你想要更好的瞭解 go-zero 專案,歡迎前往官方網站上學習具體的示例。

視訊回放地址

ArchSummit架構師峰會-海量併發下的快取架構設計

專案地址

github.com/tal-tech/go-zero

歡迎使用 go-zero 並 star 支援我們!

微信交流群

關注『微服務實踐』公眾號並點選 交流群 獲取社群群二維碼。

本作品採用《CC 協議》,轉載必須註明作者和本文連結

相關文章