十年網(wǎng)站開發(fā)經(jīng)驗(yàn) + 多家企業(yè)客戶 + 靠譜的建站團(tuán)隊(duì)
量身定制 + 運(yùn)營維護(hù)+專業(yè)推廣+無憂售后,網(wǎng)站問題一站解決
注:陌陌爭霸的數(shù)據(jù)庫部分我沒有參與具體設(shè)計(jì),只是參與了一些討論和提出一些意見。 在出現(xiàn)問題的時候,也都是由肥龍、曉靖、Aply 同學(xué)判斷研究解決的。所以我對 Redis 的判斷大多也從他們的討論中聽來,加上自己的一些猜測,并沒有去仔細(xì)閱讀 Redis 文檔和閱讀 Redis 代碼。

10余年的壽縣網(wǎng)站建設(shè)經(jīng)驗(yàn),針對設(shè)計(jì)、前端、開發(fā)、售后、文案、推廣等六對一服務(wù),響應(yīng)快,48小時及時工作處理。成都營銷網(wǎng)站建設(shè)的優(yōu)勢是能夠根據(jù)用戶設(shè)備顯示端的尺寸不同,自動調(diào)整壽縣建站的顯示方式,使網(wǎng)站能夠適用不同顯示終端,在瀏覽器中調(diào)整網(wǎng)站的寬度,無論在任何一種瀏覽器上瀏覽網(wǎng)站,都能展現(xiàn)優(yōu)雅布局與設(shè)計(jì),從而大程度地提升瀏覽體驗(yàn)。成都創(chuàng)新互聯(lián)公司從事“壽縣網(wǎng)站設(shè)計(jì)”,“壽縣網(wǎng)站推廣”以來,每個客戶項(xiàng)目都認(rèn)真落實(shí)執(zhí)行。
雖然我們最終都解決了問題,但本文中說描述的技術(shù)細(xì)節(jié)還是很有可能與事實(shí)相悖,請閱讀的同學(xué)自行甄別。
在陌陌爭霸之前,我們并沒有大規(guī)模使用過 Redis 。只是直覺上感覺 Redis 很適合我們的架構(gòu):我們這個游戲不依賴數(shù)據(jù)庫幫我們處理任何數(shù)據(jù),總的數(shù)據(jù)量雖然較大,但增長速度有限。由于單臺服務(wù)機(jī)處理能力有限,而游戲又不能分服, 玩家在任何時間地點(diǎn)登陸,都只會看到一個世界。
所以我們需要有一個數(shù)據(jù)中心獨(dú)立于游戲系統(tǒng)。而這個數(shù)據(jù)中心只負(fù)責(zé)數(shù)據(jù)中轉(zhuǎn)和數(shù)據(jù)落地就可以了。Redis 看起來就是最佳選擇,游戲系統(tǒng)對它只有按玩家 ID 索引出玩家的數(shù)據(jù)這一個需求。
我們將數(shù)據(jù)中心分為 32 個庫,按玩家 ID 分開。不同的玩家之間數(shù)據(jù)是完全獨(dú)立的。在設(shè)計(jì)時,我堅(jiān)決反對了從一個單點(diǎn)訪問數(shù)據(jù)中心的做法,堅(jiān)持每個游戲服務(wù)器節(jié)點(diǎn)都要多每個數(shù)據(jù)倉庫直接連接。因?yàn)樵谶@里制造一個單點(diǎn)毫無必要。
根據(jù)我們事前對游戲數(shù)據(jù)量的估算,前期我們只需要把 32 個數(shù)據(jù)倉庫部署到 4 臺物理機(jī)上即可,每臺機(jī)器上啟動 8 個 Redis 進(jìn)程。一開始我們使用 64G 內(nèi)存的機(jī)器,后來增加到了 96G 內(nèi)存。實(shí)測每個 Redis 服務(wù)會占到 4~5 G 內(nèi)存,看起來是綽綽有余的。
由于我們僅僅是從文檔上了解的 Redis 數(shù)據(jù)落地機(jī)制,不清楚會踏上什么坑,為了保險起見,還配備了 4 臺物理機(jī)做為從機(jī),對主機(jī)進(jìn)行數(shù)據(jù)同步備份。
Redis 支持兩種 BGSAVE 的策略,一種是快照方式,在發(fā)起落地指令時,fork 出一個進(jìn)程把整個內(nèi)存 dump 到硬盤上;另一種喚作 AOF 方式,把所有對數(shù)據(jù)庫的寫操作記錄下來。我們的游戲不適合用 AOF 方式,因?yàn)槲覀兊膶懭氩僮鲗?shí)在的太頻繁了,且數(shù)據(jù)量巨大。
第一次事故出在 2 月 3 日,新年假期還沒有過去。由于整個假期都相安無事,運(yùn)維也相對懈怠。
中午的時候,有一臺數(shù)據(jù)服務(wù)主機(jī)無法被游戲服務(wù)器訪問到,影響了部分用戶登陸。在線嘗試修復(fù)連接無果,只好開始了長達(dá) 2 個小時的停機(jī)維護(hù)。
在維護(hù)期間,初步確定了問題。是由于上午一臺從機(jī)的內(nèi)存耗盡,導(dǎo)致了從機(jī)的數(shù)據(jù)庫服務(wù)重啟。在從機(jī)重新對主機(jī)連接,8 個 Redis 同時發(fā)送 SYNC 的沖擊下,把主機(jī)擊毀了。
這里存在兩個問題,我們需要分別討論:
問題一:從機(jī)的硬件配置和主機(jī)是相同的,為什么從機(jī)會先出現(xiàn)內(nèi)存不足。
問題二:為何重新進(jìn)行 SYNC 操作會導(dǎo)致主機(jī)過載。
問題一當(dāng)時我們沒有深究,因?yàn)槲覀儧]有估算準(zhǔn)確過年期間用戶增長的速度,而正確部署數(shù)據(jù)庫。數(shù)據(jù)庫的內(nèi)存需求增加到了一個臨界點(diǎn),所以感覺內(nèi)存不足 的意外發(fā)生在主機(jī)還是從機(jī)都是很有可能的。從機(jī)先掛掉或許只是碰巧而已(現(xiàn)在反思恐怕不是這樣, 冷備腳本很可能是罪魁禍?zhǔn)祝T缙谖覀兪嵌〞r輪流 BGSAVE 的,當(dāng)數(shù)據(jù)量增長時,應(yīng)該適當(dāng)調(diào)大 BGSAVE 間隔,避免同一臺物理機(jī)上的 redis 服務(wù)同時做 BGSAVE ,而導(dǎo)致 fork 多個進(jìn)程需要消耗太多內(nèi)存。由于過年期間都回家過年去了,這件事情也被忽略了。
問題二是因?yàn)槲覀儗χ鲝耐降臋C(jī)制了解不足:
仔細(xì)想想,如果你來實(shí)現(xiàn)同步會怎么做?由于達(dá)到同步狀態(tài)需要一定的時間。同步最好不要干涉正常服務(wù),那么保證同步的一致性用鎖肯定是不好的。所以 Redis 在同步時也觸發(fā)了 fork 來保證從機(jī)連上來發(fā)出 SYNC 后,能夠順利到達(dá)一個正確的同步點(diǎn)。當(dāng)我們的從機(jī)重啟后,8 個 slave redis 同時開啟同步,等于瞬間在主機(jī)上 fork 出 8 個 redis 進(jìn)程,這使得主機(jī) redis 進(jìn)程進(jìn)入交換分區(qū)的概率大大提高了。
在這次事故后,我們?nèi)∠?slave 機(jī)。因?yàn)檫@使系統(tǒng)部署更復(fù)雜了,增加了許多不穩(wěn)定因素,且未必提高了數(shù)據(jù)安全性。同時,我們改進(jìn)了 bgsave 的機(jī)制,不再用定時器觸發(fā),而是由一個腳本去保證同一臺物理機(jī)上的多個 redis 的 bgsave 可以輪流進(jìn)行。另外,以前在從機(jī)上做冷備的機(jī)制也移到了主機(jī)上。好在我們可以用腳本控制冷備的時間,以及錯開 BGSAVE 的 IO 高峰期。
第二次事故最出現(xiàn)在最近( 2 月 27 日)。
我們已經(jīng)多次調(diào)整了 Redis 數(shù)據(jù)庫的部署,保證數(shù)據(jù)服務(wù)器有足夠的內(nèi)存。但還是出了次事故。事故最終的發(fā)生還是因?yàn)閮?nèi)存不足而導(dǎo)致某個 Redis 進(jìn)程使用了交換分區(qū)而處理能力大大下降。在大量數(shù)據(jù)擁入的情況下,發(fā)生了雪崩效應(yīng):曉靖在原來控制 BGSAVE 的腳本中加了行保底規(guī)則,如果 30 分鐘沒有收到 BGSAVE 指令,就強(qiáng)制執(zhí)行一次保障數(shù)據(jù)最終可以落地(對這條規(guī)則我個人是有異議的)。結(jié)果數(shù)據(jù)服務(wù)器在對外部失去響應(yīng)之后的半小時,多個 redis 服務(wù)同時進(jìn)入 BGSAVE 狀態(tài),吃光了內(nèi)存。
花了一天時間追查事故的元兇。我們發(fā)現(xiàn)是冷備機(jī)制惹的禍。我們會定期把 redis 數(shù)據(jù)庫文件復(fù)制一份打包備份。而操作系統(tǒng)在拷貝文件時,似乎利用了大量的內(nèi)存做文件 cache 而沒有及時釋放。這導(dǎo)致在一次 BGSAVE 發(fā)生的時候,系統(tǒng)內(nèi)存使用量大大超過了我們原先預(yù)期的上限。
這次我們調(diào)整了操作系統(tǒng)的內(nèi)核參數(shù),關(guān)掉了 cache ,暫時解決了問題。
經(jīng)過這次事故之后,我反思了數(shù)據(jù)落地策略。我覺得定期做 BGSAVE 似乎并不是好的方案。至少它是浪費(fèi)的。因?yàn)槊看?BGSAVE 都會把所有的數(shù)據(jù)存盤,而實(shí)際上,內(nèi)存數(shù)據(jù)庫中大量的數(shù)據(jù)是沒有變更過的。一目前 10 到 20 分鐘的保存周期,數(shù)據(jù)變更的只有這個時間段內(nèi)上線的玩家以及他們攻擊過的玩家(每 20 分鐘大約發(fā)生 1 到 2 次攻擊),這個數(shù)字遠(yuǎn)遠(yuǎn)少于全部玩家數(shù)量。
我希望可以只備份變更的數(shù)據(jù),但又不希望用內(nèi)建的 AOF 機(jī)制,因?yàn)?AOF 會不斷追加同一份數(shù)據(jù),導(dǎo)致硬盤空間太快增長。
我們也不希望給游戲服務(wù)和數(shù)據(jù)庫服務(wù)之間增加一個中間層,這白白犧牲了讀性能,而讀性能是整個系統(tǒng)中至關(guān)重要的。僅僅對寫指令做轉(zhuǎn)發(fā)也是不可靠的。因?yàn)槭ズ妥x指令的時序,有可能使數(shù)據(jù)版本錯亂。
如果在游戲服務(wù)器要寫數(shù)據(jù)時同時向 Redis 和另一個數(shù)據(jù)落地服務(wù)同時各發(fā)一份數(shù)據(jù)怎樣?首先,我們需要增加版本機(jī)制,保證能識別出不同位置收到的寫操作的先后(我記得在狂刃中,就發(fā)生過數(shù)據(jù)版本錯 亂的 Bug );其次,這會使游戲服務(wù)器和數(shù)據(jù)服務(wù)器間的寫帶寬加倍。
最后我想了一個簡單的方法:在數(shù)據(jù)服務(wù)器的物理機(jī)上啟動一個監(jiān)護(hù)服務(wù)。當(dāng)游戲服務(wù)器向數(shù)據(jù)服務(wù)推送數(shù)據(jù)并確認(rèn)成功后,再把這組數(shù)據(jù)的 ID 同時發(fā)送給這個監(jiān)護(hù)服務(wù)。它再從 Redis 中把數(shù)據(jù)讀回來,并保存在本地。
因?yàn)檫@個監(jiān)護(hù)服務(wù)和 Redis 1 比 1 配置在同一臺機(jī)器上,而硬盤寫速度是大于網(wǎng)絡(luò)帶寬的,它一定不會過載。至于 Redis ,就成了一個純粹的內(nèi)存數(shù)據(jù)庫,不再運(yùn)行 BGSAVE 。
這個監(jiān)護(hù)進(jìn)程同時也做數(shù)據(jù)落地。對于數(shù)據(jù)落地,我選擇的是 unqlite ,幾行代碼就可以做好它的 Lua 封裝。它的數(shù)據(jù)庫文件只有一個,更方便做冷備。當(dāng)然 levelDB 也是個不錯的選擇,如果它是用 C 而不是 C++ 實(shí)現(xiàn)的話,我會考慮后者的。
和游戲服務(wù)器的對接,我在數(shù)據(jù)庫機(jī)器上啟動了一個獨(dú)立的 skynet 進(jìn)程,監(jiān)聽同步 ID 的請求。因?yàn)樗恍枰幚砗芎唵螏讉€ Redis 操作,我特地手寫了 Redis 指令。最終這個服務(wù) 只有一個 lua 腳本 ,其實(shí)它是由三個 skynet 服務(wù)構(gòu)成的,一個監(jiān)聽外部端口,一個處理連接上的 Redis 同步指令,一個單點(diǎn)寫入數(shù)據(jù)到 unqlite 。為了使得數(shù)據(jù)恢復(fù)高效,我特地在保存玩家數(shù)據(jù)的時候,把恢復(fù)用的 Redis 指令拼好。這樣一旦需要恢復(fù),只用從 unqlite 中讀出玩家數(shù)據(jù),直接發(fā)送給 Redis 即可。
有了這個東西,就一并把 Redis 中的冷熱數(shù)據(jù)解決了。長期不登陸的玩家,我們可以定期從 Redis 中清掉,萬一這個玩家登陸回來,只需要讓它幫忙恢復(fù)。
曉靖不喜歡我依賴 skynet 的實(shí)現(xiàn)。他一開始想用 python 實(shí)現(xiàn)一個同樣的東西,后來他又對 Go 語言產(chǎn)生了興趣,想借這個需求玩一下 Go 語言。所以到今天,我們還沒有把這套新機(jī)制部署到生產(chǎn)環(huán)境。
原文地址。獲作者授權(quán)轉(zhuǎn)載。