某百萬DAU遊戲的服務端優化工作

水風發表於2021-01-13
某百萬DAU遊戲的服務端優化工作

作者:水風 本文首發知乎
https://zhuanlan.zhihu.com/p/341855913

現在在一款百玩DAU的遊戲專案中工作,由於怕玩家找上來聊一些非技術的工作,就不報名字了,只討論技術問題。

我們遊戲是一款基於skynet的通服遊戲,開房間的遊戲架構,預計單服可承載60w同時線上。

遊戲目前已經上線兩年了,上線後我們做了不少服務端優化的工作。

這篇文章主要介紹遊戲服務端優化的一些方法,主要以介紹思想為主,為了舉例更容易理解,很多實現思路並不是我們遊戲的,這裡也是作為例子說明。

1、所有的程式碼都要能熱更

所有的程式設計師都寫過bug,bug是難免的,但是我們可以儘量降低bug對玩家的影響。

而當程式碼上線後,若發現bug,為了不影響玩家體驗,不能通過重啟只能通過熱更去修復。

我們在skynet的基礎上,做了多種熱更方案,基本上能保證所有的程式碼都能被熱更。而且,可熱更,是我們新開發功能時的一個必須考慮的事情。

我們基於skynet、lua和服務端架構,做了以下熱更方案:

  • sharedata.update,熱更策劃配置表。(skynet支援)
  • lua模組的熱更:我們在玩家邏輯中,將狀態統一放在一個資料模組中,將邏輯放在其他模組。這樣,邏輯模組就可以被隨時替換。在skynet中通過cache.clear()清理快取程式碼,然後將所有的玩家服務的指定邏輯模組替換即可。
  • service滾動更新:若一個service是不斷銷燬並且不斷建立新的,就可以使用這種方案。比如一場戰鬥的房間,或者玩家service在玩家登陸時建立玩家登出後銷燬(我們其實是有記憶體池的,但是可以通過清空記憶體池並且回收現有的服務)。在skynet中通過cache.clear()清理快取程式碼即可,新的service會自動使用新的程式碼。這個更新方案有個壞處,就是無法實時的立刻更新已建立的service,而新的service會立刻生效。
  • 程式級別的重啟或滾動更新:若程式重啟不會影響到整體遊戲叢集,那麼可以通過重啟程式來更新程式碼。比如我們的登陸伺服器,可以先通過負載均衡器將流量從某一個登陸服轉給其他的,然後等沒有流量後重啟,然後依次重啟其他的。玩家邏輯程式也是如此,可以先不在分配新玩家登陸某玩家程式,然後將當前程式玩家遷移到其他程式,然後重啟之,依次執行滾動更新即可。
  • inject:skynet也支援了替換某函式,所以可以通過寫一個新的函式替換老的函式。此方案理論上可以熱更所有程式碼,但inject程式碼寫起來麻煩一些,尤其是很長的函式。

通過以上熱更方案,基本能覆蓋全部情況。

一般只有我們發現只能通過inject但想修改的函式非常長或者修改非常複雜覺得不穩妥的時候,我們才會去通過重啟修復線上問題(此情況在我們遊戲中極少)。

2、儘量詳盡的日誌

完善的日誌非常重要,為處理線上問題定位線上bug提供基礎,也為運營查問題提供支援。此外,也可以將日誌用於下文要說的監控和報警。

日誌這東西,不用時沒感覺,用的時候就後悔:為啥不在這裡加一條日誌,為啥不把這個資訊列印出來。

如何記錄日誌,沒有一個統一的標準,我這裡說一下我的經驗和思考,這裡主要介紹非戰鬥邏輯的日誌。

哪些地方寫日誌:

  • 玩家一個行為對玩家狀態產生改變,這個邏輯期間至少一條日誌。
  • 玩家某些行為投放物品,一般投放前。
  • 一些重要的狀態切換,比如一個戰鬥房間由準備進入開戰狀態。
  • 任何錯誤和異常,都需要加一條錯誤日誌,防禦性程式設計檢測到錯誤也需要。

日誌資訊,日誌需要記錄什麼:

  • 日誌的基本業務描述,一般幾個關鍵字。比如投放武器
  • 邏輯涉及的上下文關鍵資訊。比如投放武器的 武器資訊,比如武器所屬的玩家ID
  • 其他資訊,比如服務端程式資訊、時間戳、日誌所屬entity等。

日誌等級常見的又debug、info、error、fatal。一般表示的含義是:

  • debug:開發環境列印,線上環境不列印。一般用於程式開發。(這裡有個坑,debug日誌一般的實現是線上進行過濾,但這個函式的引數還是會執行,所以如果是超級長的字串操作要注意效能問題)
  • info:線上環境列印,我們說的日誌大部分都是這個級別。
  • error:和log類似,只是是錯誤資訊。一般是業務預期內的錯誤,不會影響系統正常執行。
  • fatal:業務預期外的錯誤,表示這類錯誤資訊需要開發人員去關注和排查。

對於戰鬥,最好的方案是回放錄影,日誌一般是次選項。

3、監控和報警

通過監控,我們要對線上伺服器的情況儘量的瞭解,並且能提前發現問題,防止問題擴大化後才知道。報警和監控相輔相成,監控到異常後,馬上報警,我們就能立刻對線上問題進行處理。

我們用到的監控/報警有:

  • 慢響應:監控客戶端請求的平均響應速度,一般能整體評估遊戲伺服器是否有卡頓。
  • FATAL日誌:有些特殊情況需要馬上通知開發人員,比如伺服器開服失敗等,服務端會列印FATAL日誌。這時候就需要報警通知。
  • 線上traceback:一般用指令碼寫的服務端邏輯,都會有一些線上traceback,這個是需要重點關注的問題。發現有trace就需要去確認traceback的影響,若影響玩家功能需要熱更修復。
  • 投放監控:我們遊戲對物品投放也有監控,防止玩家因為某些bug可以刷某類物品。由策劃對每類物品設定一個報警上限,玩家當日獲取此類物品超過閾值會通知到我們。我們會去檢查玩家行為看是否符合預期。
  • 玩家行為監控:除了物品獲取,也有些玩家行為需要監控。比如我們遊戲中每天打懸賞令的次數,按照策劃的設計每天不會太多。超過某個數量說明可能有問題,需要人工審查。
  • 服務端效能監控:我們會監控單點服務cpu使用率,skynet中一個服務只使用一個執行緒,單點服務cpu佔用上限是一個核,所以比較容易出現問題。我們若發現單點服務的cpu使用率超過閾值,就會報警,可以提前進行效能優化。
  • 服務端機器監控:監控機器、資料庫的硬體資訊,比如cpu、記憶體等。超過閾值進行報警。
  • 阿里雲費用監控:對按量付費的專案,我們會對其進行監控,若某一天的消費超過某一閾值,則進行報警。

總之,通過監控以及與之對應的報警,能提前發現線上問題,降低影響。大事化小,小事化無。

4、容錯:保底邏輯

大型分散式叢集必須要考慮容錯問題,容錯分為幾個層面:

  • 架構容錯(機器當機、程式crash):主要通過消除單點等方式,後文會介紹。
  • DB等Saas服務卡頓/閃斷:做好斷線重連、重試等容錯邏輯。
  • 邏輯容錯:有些邏輯系統中難免出現異常、bug或者超出預期的情況,在服務端中,對於這些問題儘量寫一些保底邏輯,當出現問題時,能降低問題造成的影響。

下面主要介紹一下邏輯容錯的相關情況:

  • 系統異常:某些非核心服務出現異常(卡死、crash、網路中斷)可以之關閉異常服務,保證系統整體正常執行。
  • bug:比如某款遊戲中的反外掛邏輯特別複雜,有時候會因為改動了某些戰鬥機製造成外掛的誤判,把正常玩家判為外掛玩家,這種bug比較難完全避免並且會造成大規模玩家封號等較大的影響。因此,可以增加了一個保底邏輯,每小時因外掛封號的玩家數量超過一個閾值以後,就不再直接封號,而是報警並且把這些玩家記錄下來,去人工檢查後確認是否有問題,有問題再人工操作封號。這個保底邏輯一方面可以報警讓我們快速發現外掛檢測bug,另一方面即使出現了bug也能降低影響人數。
  • 玩家行為超出預期:比如,有些遊戲會記錄玩家的歷史好友(刪掉的),這個記錄會隨著玩家行為變得越來越長。若完全沒有限制,當客戶端請求或者邏輯遍歷歷史好友列表時,會造成卡頓。因此,這種情況最好設一個上限,歷史好友數量超過一個閾值就刪掉之前的。
  • 功能開關:開發新功能一定要做好開關,可以隨時線上關閉功能。若出了問題,可以關閉功能後慢慢修復,不影響玩家玩其他功能。

5、非同步提交

玩家有些操作不需要等待等待邏輯執行完成返回響應,這種操作可以將任務提交到佇列中然後非同步執行。這種方案的好處是即使任務處理能力不足,不會影響到玩家造成玩家卡頓。

我們曾經開發過一個副本成績排行榜,排行榜的上榜規則比較複雜,當玩家打完副本後會將戰鬥成績提交到排行榜,排行榜通過一系列的邏輯將成績插入到排行榜中。當我們開服後,玩家大量湧入這個新玩法,此外,由於排行榜是空的,大量成績都會進入排行榜中,造成排行榜卡頓,導致玩家完成戰鬥後提交成績時卡死。

後來,我們將其改為玩家打完副本後將成績提交到排行榜中,但不等待排行榜的響應。這樣,當排行榜邏輯卡頓的時候,只是有可能成績上榜會延遲,但不影響玩家體驗。

以skynet為例,儘量用skynet.send替代skynet.call。若發現skynet.call沒有返回值時,就去判斷一下call的邏輯和下文邏輯是否有順序依賴關係,若沒有依賴關係就可以改為send。

這其實就是訊息佇列的思想,通過非同步處理提高系統效能和削峰、降低系統耦合性,大家可以去百度“訊息佇列”詳細瞭解。

6、消除單點和水平擴充套件

一個遊戲伺服器叢集的承載上限,就是叢集中的邏輯單點的承載上限。所以,在遊戲伺服器架構設計中,要儘量的消除單點,改為支援水平擴充套件。

伺服器叢集中存在單點的常見原因是因為資料需要統一管理,比如玩家管理器、家族管理器等,需要管理所有玩家或者所有家族。

這種情況有兩種解決方案:

  • 加一層分發邏輯:比如我們之前家族管理器管理所有家族的資訊以及相關的邏輯,後來就扛不住了。然後我們抽象出來了familymaster和familynode,每個familynode管理部分家族,familynode可以無限的水平擴充套件。familymaster依然是單點,但是他只是記錄每個家族在哪個familynode上面,所以承載上限很高。
  • 使用無狀態:將資料和邏輯分離,資料放在redis/db中,邏輯執行都去讀寫db。這種方案理論上是可以無限擴充套件的,因為db是支援無限擴充套件的,但是要求狀態(資料)相對比較簡單,容易存在db中並且可以高效讀寫。比如上文提到的familymaster依然是單點,但只管理familyid到familynode的資訊,這個資訊我們就可以存在redis中,然後每次讀寫都去操作redis,這樣就達到了理論上的無限擴充套件。

一般來說,遊戲伺服器並不要求完全的消除單點,因為需要做很多額外的事情,要麼增加開發成本,要麼增加運維成本。所以,只要我們的單點承載上限超出遊戲玩家量的需求,就可以了。不要過度優化。

消除單點一方面可以帶來承載量的提升(高併發),另一方面可以提高可用性(高可用)。通過消除單點,一個功能可能分佈在多個程式/機器上,即使某個程式掛了,其他程式也可以使用,仍然可以提供服務。當然,寫程式碼時需要處理這種異常才可以獲得高可用性。

我們遊戲的服務端簡化版架構如下圖所示,我們的玩家邏輯、戰鬥邏輯和家族邏輯都是可以水平擴充套件的。而只有一些管理全服資訊的邏輯(比如維護玩家再哪個程式上)才會放在管理器裡,管理器是伺服器的單點,也是伺服器承載量的瓶頸。

某百萬DAU遊戲的服務端優化工作
伺服器架構(簡化版)

7、功能解耦和隔離

根據KISS(Keep It Stupid Simple)原則,應該將功能儘量的拆分成小的程式碼模組。這個原則對應到遊戲伺服器就是要將功能儘量的拆分成一個個服務,每個服務都只負責一小塊功能。

Skynet提供了比較好的模組解耦模式:service模式,skynet中每個service就可以對應一個物理意義上的服務,而每個service就是一個執行緒,同程式service之間具有一定的隔離。而不同service可以放在一個程式,也可以放在不同的程式,提供了不同的隔離級別。

KISS原則我是基本贊成的,但是我認為遊戲的玩家個人邏輯應該放在一個服務中,若拆為多個服務會造成服務間耦合嚴重。比如玩家升級,往往涉及到揹包、屬性、代幣等不同模組。這個地方更合適用程式碼模組來區分開,但執行時屬於一個服務。

除了玩家個人邏輯,其他功能可以適當的拆分,比如好友服務、聊天服務、排行榜服務等。

將功能拆為一個個服務以後,就需要考慮如何隔離。隔離方式skynet支援執行緒隔離和程式隔離,有的單執行緒伺服器可能只支援程式隔離。

  • 執行緒隔離的優點在於不同服務執行在同一程式,呼叫是函式呼叫,不存在失敗的概念,缺點是一定程度上違反了KISS原則,並且服務之間隔離度低某些情況仍會互相影響
  • 程式隔離優點在於程式功能更單一明確,隔離度高不會互相影響,但服務間通訊變為網路通訊更復雜,此外,每個服務一個程式,會造成程式數量龐大,管理和維護成本高。

以前我曾基於python寫過遊戲微服務,因為python只支援單執行緒,所以每個程式只能承載一個服務。這種模式主要存在兩個問題,一、服務間的呼叫請求都是網路rpc,都存在失敗的可能,給業務開發造成了很大的成本。二、程式數量很多,因為一類服務往往又多個例項,每個例項都是一個程式,程式數量為N*M,程式數量多造成治理困難。

skynet這種模式就比較好,一個程式可以承載很多服務例項,每個服務例項一個執行緒,服務之間基於執行緒進行隔離。不同的服務可以放在一個程式中,一個程式也可以承載多個相同或者不同型別的服務例項。

那麼,在skynet模式中,什麼情況使用執行緒隔離,什麼情況使用程式隔離呢?

  • 首先根據物理含義,將服務進行分組,同組服務放在同一程式。比如玩家服務、家族服務、登陸服務等。這個主要是將不同的核心服務進行隔離,也考慮容易管理。
  • 對於效能消耗高的服務,進行隔離。防止打滿CPU影響其他服務。
  • 對於不穩定的服務,進行隔離。比如某服務使用了沒有被廣泛驗證的C擴充套件,crash概率就會高很多。

8、引入超時

通過上文介紹的服務拆分和隔離,我們將服務端進行了拆分,拆分後我們希望對某些服務中的異常進行進一步的隔離。

skynet把叢集看作一個整體,所以通過skynet.call呼叫其他程式函式並等待返回預設是無限等待的,沒有timeout。

這樣就導致若某個模組卡頓或者出現了異常,就會導致叢集雪崩,影響到所有的功能。

比如我們遊戲的chat模組,曾因為某些問題導致程式卡頓,而玩家登入都會去註冊和拉取聊天訊息,進而導致玩家無法登入,也無法正常遊戲。

我們的聊天功能在前期設計的時侯設計的比較複雜,所以實現方案比較複雜,我看了一遍程式碼後覺得重構的成本和風險都太高。於是,我們希望即使chat卡頓或異常,也不要影響玩家的正常遊戲,只是讓玩家不能聊天而已。

因此,我們在skynet中增加了timeout機制,支援skynet.call超時。

引入了超時後,也需要增加超時後的邏輯處理。超時可能有三種情況,1.接收方沒有收到請求。2.接收方收到了但是出trace沒有返回響應。3.請求方沒有收到接收方發出的響應。

業務需要處理超時問題,一般有兩種方案:重試或忽略。對於有些關鍵邏輯,需要寫重試邏輯,重試要保證冪等性。對於不重要的邏輯,可以忽略,比如發一個聊天訊息。建議儘量忽略,重試邏輯寫起來很麻煩,而且容易出問題。具體可以參考“分散式事務”相關資訊。

在遊戲的大部分的模組間耦合還是比較重的,所以skynet將叢集認為是一個整體,我覺得是合理的,所以不應該過份解耦。只有一些相對獨立的模組,可以通過解耦防止問題擴散和雪崩。

引入超時後,應該將遊戲系統進行分割,核心業務不使用超時,不然寫超時處理邏輯會非常麻煩。非核心業務加入超時,將核心業務和非核心業務進行解耦。

9、部分資料轉存redis

大部分遊戲都把持久化資料存在mysql或者mongo中。而redis常用於cache等場景,比較少用於持久化儲存。

但redis本身支援RDB和AOF持久化,其實有作為持久化儲存的能力。而有些遊戲資料很小,但存在mysql裡面麻煩。

比如玩家的好友關係資料,一個好友關係涉及兩個玩家,存在任何一個玩家身上都不合理。而如果存在mysql裡面,如果設計不好,可能載入時需要訪問很多次mysql。

這類資料存在redis就很方便,佔用不了多少空間,而且大大提高了訪問速度。我們遊戲千萬量級的註冊玩家,玩家的好友關係資料也不過小几十G。

一般來說,業務上存mysql/Mongo覺得比較麻煩,資料量又不大,訪問頻率很高的,都可以存在redis中。

將Redis作為持久化儲存其實是沒有資料可靠性保證的,所以需要考慮異常問題對遊戲系統的影響。若系統不能接受任何的異常情況,建議還是使用mysql。

此外,還需要考慮回檔問題(雖然永遠不希望遇到)。因為一個玩家的資料分散在了不同的地方,有的在mysql,有的在redis,所以回檔的時候要想辦法回檔到一個點。(阿里雲的企業版Redis也就是Tair,支援精準時間點恢復資料)

10、灰度測試環境

對於一個線上專案,任何的修改都是有風險的,而有些底層的修改(比如資料儲存相關程式碼)可能會涉及到所有的業務邏輯。這種情況若只是讓QA測試某些情況其實是非常不穩的。

因此,我們將某些玩家邏輯程式設為灰度環境,只有指定的玩家可以進入。這樣,我們就可以將某些涉及範圍較大的改動,先在灰度環境中上線,選取某些玩家進入。即使出現問題,也隻影響選區的測試玩家。測試一段時間後,若測試玩家沒有反饋問題,就可以將改動正式上線了。

灰度環境是線上環境,和測試服具有本質區別。因為直接承載線上玩家,所以應用場景和測試服相比限制更多,比如我們只應用於玩家個人邏輯節點,也只測試底層程式碼邏輯,不測試業務邏輯。和測試服比起來優點是比較靈活,不需要部署測試服並且安排玩家進來測試。

我們的灰度環境可以分為多級,比如第一級灰度只能公司內部測試人員進入,新功能剛開始上線時就先放到這個環境。第二級灰度我們線上隨機選取幾百到幾千的玩家進入,一般是經過第一級灰度驗證過的功能。

某百萬DAU遊戲的服務端優化工作
灰度測試

第一級灰度環境的業務邏輯可以和線上有些許差別,但是第二級灰度因為直接面向外部玩家,所以要求業務上完全一致,一般都是底層的修改。

一級灰度因為只有內部玩家,所以理論上來說可以隨時重啟更新程式碼,所以可以隨時將程式碼上線測試,不用等周版本,比較靈活。

一級灰度還有一些特殊用法,比如線上某個活動出了問題暫時關閉了入口,然後通過熱更修復了。為了驗證線上的修復結果,可以先在灰度環境開啟入口,驗證修復結果。

總之,有了灰度測試環境,可以相對大範圍的驗證一些底層修改,對於線上專案非常重要。而且,可以比較靈活的線上上做一些事情。

11、壓測

一款遊戲上線前應該經過比較詳細的壓測,並且在後續的開發新功能和架構迭代過程中需要持續的進行壓測。

  • 壓測主要是為了評估三個內容:
  • 驗證在大規模併發請求的環境下邏輯執行的正確性。
  • 查詢在大規模併發請求的環境下功能的效能瓶頸和效能熱點。

評估遊戲或功能的承載能力和需求,規劃機器部署需求。

壓測中需要關注的功能點(常出現效能問題的場景):

  • 開服:關注登陸和建立賬號,這兩塊邏輯一般都比較複雜。可以增加排隊系統處理這個問題。
  • 廣播:比如全服聊天。可以分頻道,也可以服務降級。
  • MMO遊戲中玩家聚集:比如國戰類遊戲中的同屏大量玩家聚集。可以優化同步策略,也可以邏輯分線。
  • 定時(同時)功能:比如某個活動會同時拉大量玩家進入某個場景。
  • 單點服務:最多隻能跑滿一個CPU的服務。
  • 資料上限:比如某遊戲曾經因為大量玩家申請某頭部主播好友,導致主播好友申請列表增加了近10w,導致機器直接卡死。
  • 資料庫相關:考慮資料庫的承載。
  • 全服玩家操作:比如通過命令給全服玩家發郵件。

為了方便壓測,我們做了一套壓測工具,可以支援在容器中快速部署壓測叢集、執行壓測任務並彙總壓測結果。

12、動態擴容和縮容

對於大部分遊戲,都會有玩家線上人數的波動,比如某些活動期間人數很多,但每日凌晨都人數比較少。

我們遊戲週末晚上會搞一些活動,週末晚上活動期間和平時相同時間段相比同時線上上升一倍。如果我們按照最大同時線上部署機器,會造成較大的浪費。

比如下圖,常駐機器承載可以滿足平時的需求,但是到了某些活動期間,就無法滿足需求。這時候,如果支援動態擴容,就可以將機器在活動前增加,活動後回收,既節省了成本,又給玩家更流暢的遊戲體驗。

某百萬DAU遊戲的服務端優化工作
動態擴容縮容

我們遊戲可以將玩家個人邏輯和戰鬥邏輯程式做到了動態擴容縮容,這類程式佔比最大價效比最高,其他程式沒有支援。

動態擴容縮容需要注意一些點:

  • 對於我們這種大DAU遊戲,阿里雲在某些可用區的備用庫存不夠,導致無法啟動動態機器。所以需要考慮跨可用區的支援。
  • 動態擴容比較容易,動態縮容需要做一些邏輯處理,需要達到優雅退出的效果。戰鬥服比較容易,戰鬥結束後關閉程式即可,對於我們這種玩家個人邏輯程式需要處理的事情多一些。我們關程式時會分步執行,先將此程式標記為新玩家不可進入,過段時間後再將非戰鬥狀態的玩家踢下線(此步驟玩家無感知),最後強制踢下線所有玩家(此時玩家已經極少),基本做到了玩家無感知。
  • 需要有較好的運維流程支援自動化,手動做的話人力成本太高而且容易出錯。
  • 最佳的方案是根據線上的情況(比如線上玩家)自動化擴容縮容。
  • 這個方法不適合用於自建機房,對於阿里雲/AWS這種按量付費機器支援的較好的雲提供商比較適合。

13、cache

大部分效能問題都可以通過cache來解決,空間換時間,多買點記憶體,讓玩家玩的爽一點,很值。

增加cache,需要考慮兩個點:cache存放位置和cache更新策略。

13.1 cache存放位置

常見的存放cache的位置有:

  • 貼近讀取資料的實體(消費者)
  • 貼近生產資料的實體(生產者)
  • 生產者和消費者之間
  • 第三方,比如redis

假設一個場景:玩家需要去拉取全服的一個排行榜,而這個排行榜的計算可能是很重度的計算,所以每次拉取都重新計算不可取。

服務端架構如下圖所示,全服排行榜負責計算生成排行榜,每個玩家程式中管理很多個玩家entity,每個玩家都會去全服排行榜中請求排行榜資訊。

某百萬DAU遊戲的服務端優化工作

上面說的四種位置,在這個場景下的對應關係如下:

  • 貼近消費者:存在玩家entity中,每個玩家都有自己的cache。
  • 貼近生產者:存在全服排行榜,cache全服有效,所有的玩家共享cache。
  • 消費者和生產者之間:存在玩家程式中,每個玩家程式中的所有玩家共享cache。不同玩家程式之間的玩家不共享。
  • redis:將生成的排行榜資料存在redis,全服玩家共享。

說一下四種存放位置的優缺點和應用場景:

  • 貼近消費者:若消費者消費頻率特別高,且不同消費者資料不同,可以存在消費者這邊。這種情況其實比較少。
  • 貼近生產者:這種情況比較多,一般是為了通過空間換事件,是常見的方案。
  • 消費者和生產者之間:這種情況一般是全部消費者的整體消費頻率特別高,為了防止給單點壓力太大,所以存在中間,降低壓力。
  • redis:這個和貼近生產者差不多,最大的區別在於,redis可以與伺服器解耦,伺服器重啟,redis的資料也存在。常見的情況比如存玩家的簡要資訊(供其他玩家檢視)。

當然,cache也可以在不同的地方同時存在,也就是多級cache。這種情況一般可以獲得更好的效率,但需要針對每一級cache定義維護和更新策略,邏輯更加複雜,bug更難查。

13.2 cache更新/失效策略

cache的引入一般是為了解決效能問題,但也並不是沒有成本。成本就在於需要管理cache,也就是決定cache什麼時侯失效和更新,增加了程式設計的複雜性。

生存時間(ttl,time to live)

cache最常見的更新策略是使用生存時間ttl,即快取超過一定的時間後自動失效,然後重新計算或者去資料來源拉取。比如域名解析中就是用ttl控制DNS伺服器中域名解析資訊快取失效。

這種策略最簡單,建議優先使用這種策略。

主動更新cache

這種策略是cache的生產者主動去更新cache,這種更新策略思想類似寫擴散。

比如遊戲常見的玩家簡要資訊cache,這種cache一般是玩家更新自己的資訊時,就去更新自己的簡要資訊。(當然,不一定完全實時)

這種策略一般是要求cache的實時性要求比較高,但是又不希望所有的請求都打到資料生產者中執行。

關於這類思想,大家可以去搜尋“讀擴散/寫擴散”來了解更多的內容。

固定cache空間

某些場景下,cache可用的空間是有限的, 在有限空間的前提下,我們希望儘量的提升cache空間的利用效率。當可用空間沒有用盡時,cache一直不會失效,當可用空間用盡後,以一定的策略去將某些cache失效,以獲得空間給新的cache。最常見的是LRU策略。

因為硬體資源是有限的,這種策略也常見於硬體和系統層,比如虛擬記憶體的管理,比如mysql等資料庫將部分資訊快取在記憶體中以提高查詢效率,比如Redis記憶體空間用盡後記憶體淘汰。

這種cache的管理方式業務邏輯中用的比較少,偶爾配合其他策略一起使用,增加保底機制防止cache所佔用的記憶體空間過大。

常見的策略有LRU和LFU。比如若redis佔用記憶體接近記憶體上限時,會使用類LRU策略淘汰資料。

其他各類策略

cache也可以根據不同的業務場景設定更新和失效策略,比如可以在一個副本中將某些cache設為永不失效,只有在副本結束時才去統一清理。

具體策略根據具體需求可以使用各種花式方案。

後記

一款DAU百萬級的遊戲,而且是已經上線的遊戲,其實優化起來非常困難,真*為一輛高速行駛的汽車換零件。

為了給玩家帶來更好的遊戲體驗,我們做優化計劃時並不保守,但非常謹慎的執行。

如臨深淵,如履薄冰。

附:

公司招人,在杭州,一線薪水,不輸任何其他遊戲公司。
公司快速發展中,機會多多!
服務端、客戶端都要,技術專家、主程都要~


相關文章